<?php
/**
 * Copyright since 2007 PrestaShop SA and Contributors
 * PrestaShop is an International Registered Trademark & Property of PrestaShop SA
 *
 * NOTICE OF LICENSE
 *
 * This source file is subject to the Open Software License (OSL 3.0)
 * that is bundled with this package in the file LICENSE.md.
 * It is also available through the world-wide-web at this URL:
 * https://opensource.org/licenses/OSL-3.0
 * If you did not receive a copy of the license and are unable to
 * obtain it through the world-wide-web, please send an email
 * to license@prestashop.com so we can send you a copy immediately.
 *
 * DISCLAIMER
 *
 * Do not edit or add to this file if you wish to upgrade PrestaShop to newer
 * versions in the future. If you wish to customize PrestaShop for your
 * needs please refer to https://devdocs.prestashop.com/ for more information.
 *
 * @author    PrestaShop SA and Contributors <contact@prestashop.com>
 * @copyright Since 2007 PrestaShop SA and Contributors
 * @license   https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0)
 */

namespace PrestaShopBundle\Install;

use PrestaShop\PrestaShop\Adapter\Entity\Db;
use PrestaShop\PrestaShop\Adapter\Entity\DbQuery;
use PrestaShop\PrestaShop\Adapter\Entity\Image;
use PrestaShop\PrestaShop\Adapter\Entity\ImageManager;
use PrestaShop\PrestaShop\Adapter\Entity\ImageType;
use PrestaShop\PrestaShop\Adapter\Entity\Pack;
use PrestaShop\PrestaShop\Adapter\Entity\Shop;
use PrestaShop\PrestaShop\Adapter\Entity\StockAvailable;
use PrestaShop\PrestaShop\Adapter\Entity\Tag;
use PrestaShop\PrestaShop\Adapter\Entity\Tools;
use PrestaShop\PrestaShop\Core\Foundation\Filesystem\FileSystem;
use PrestaShopBundle\Install\EntityLoader\FileLoader;
use PrestaShopDatabaseException;
use PrestashopInstallerException;
use SimpleXMLElement;

class XmlLoader
{
    /**
     * @var LanguageList
     */
    protected $language;

    /**
     * @var \Symfony\Bundle\FrameworkBundle\Translation\Translator
     */
    protected $translator;

    /**
     * @var array List of languages stored as array(id_lang => iso)
     */
    protected $languages = [];

    /**
     * @var array List of errors
     */
    protected $errors = [];

    protected $data_path;
    protected $lang_path;
    protected $img_path;
    public $path_type;

    protected $ids = [];

    protected $primaries = [];

    protected $delayed_inserts = [];

    /**
     * @var FileLoader
     */
    private $fileLoader;

    public function __construct()
    {
        $this->language = LanguageList::getInstance();
        $this->setDefaultPath();
    }

    /**
     * Set list of installed languages.
     *
     * @param array $languages array(id_lang => iso)
     */
    public function setLanguages(array $languages)
    {
        $this->languages = $languages;
    }

    public function setTranslator($translator)
    {
        $this->translator = $translator;

        return $translator;
    }

    public function setDefaultPath()
    {
        $this->path_type = 'common';
        $this->data_path = _PS_INSTALL_DATA_PATH_ . 'xml/';
        $this->lang_path = _PS_INSTALL_LANGS_PATH_;
        $this->img_path = _PS_INSTALL_DATA_PATH_ . 'img/';
        $this->fileLoader = new FileLoader($this->data_path, $this->lang_path);
    }

    public function setFixturesPath($path = null)
    {
        if ($path === null) {
            $path = _PS_INSTALL_FIXTURES_PATH_ . 'fashion/';
        }

        $this->path_type = 'fixture';
        $this->data_path = $path . 'data/';
        $this->lang_path = $path . 'langs/';
        $this->img_path = $path . 'img/';
        $this->fileLoader = new FileLoader($this->data_path, $this->lang_path);
    }

    /**
     * Get list of errors.
     *
     * @return array
     */
    public function getErrors()
    {
        return $this->errors;
    }

    /**
     * Add an error.
     *
     * @param string $error
     */
    public function setError($error)
    {
        $this->errors[] = $error;
    }

    /**
     * Store an ID related to an entity and its identifier (E.g. we want to save that product with ID "ipod_nano" has the ID 1).
     *
     * @param string $entity
     * @param string $identifier
     * @param int $id
     */
    public function storeId($entity, $identifier, $id)
    {
        $this->ids[$entity . ':' . $identifier] = $id;
    }

    /**
     * Retrieve an ID related to an entity and its identifier.
     *
     * @param string $entity
     * @param string $identifier
     */
    public function retrieveId($entity, $identifier)
    {
        return isset($this->ids[$entity . ':' . $identifier]) ? $this->ids[$entity . ':' . $identifier] : 0;
    }

    public function getIds()
    {
        return $this->ids;
    }

    public function setIds($ids)
    {
        $this->ids = $ids;
    }

    /**
     * @return string[] Entity names
     *
     * @throws PrestashopInstallerException
     */
    public function getSortedEntities()
    {
        // Browse all XML files from data/xml directory
        $entities = [];
        $dependencies = [];
        foreach (scandir($this->data_path) as $file) {
            if (preg_match('#^(.+)\.xml$#', $file, $m)) {
                $entity = $m[1];
                $xml = $this->fileLoader->load($entity);

                // Store entities dependencies (with field type="relation")
                if ($xml instanceof \SimpleXMLElement && isset($xml->fields, $xml->fields->field)) {
                    foreach ($xml->fields->field as $field) {
                        if ($field['relation'] && $field['relation'] != $entity) {
                            if (!isset($dependencies[(string) $field['relation']])) {
                                $dependencies[(string) $field['relation']] = [];
                            }
                            $dependencies[(string) $field['relation']][] = $entity;
                        }
                    }
                }
                $entities[] = $entity;
            }
        }

        // Sort entities to populate database in good order (E.g. zones before countries)
        do {
            $current = (isset($sort_entities)) ? $sort_entities : [];
            $sort_entities = [];
            foreach ($entities as $key => $entity) {
                if (isset($dependencies[$entity])) {
                    $min = count($entities) - 1;
                    foreach ($dependencies[$entity] as $item) {
                        if (($key = array_search($item, $sort_entities)) !== false) {
                            $min = min($min, $key);
                        }
                    }
                    if ($min == 0) {
                        array_unshift($sort_entities, $entity);
                    } else {
                        array_splice($sort_entities, $min, 0, [$entity]);
                    }
                } else {
                    $sort_entities[] = $entity;
                }
            }
            $entities = $sort_entities;
        } while ($current != $sort_entities);

        return $sort_entities;
    }

    /**
     * Read all XML files from data folder and populate tables.
     *
     * @throws PrestashopInstallerException
     */
    public function populateFromXmlFiles()
    {
        $entities = $this->getSortedEntities();

        // Populate entities
        foreach ($entities as $entity) {
            $this->populateEntity($entity);
        }
    }

    /**
     * Populate an entity.
     *
     * @param string $entity Entity name to populate
     *
     * @throws PrestashopInstallerException
     */
    public function populateEntity($entity)
    {
        $populateEntityMethod = 'populateEntity' . Tools::toCamelCase($entity);
        if (method_exists($this, $populateEntityMethod)) {
            $this->$populateEntityMethod();

            return;
        }

        if (substr($entity, 0, 1) == '.' || substr($entity, 0, 1) == '_') {
            return;
        }

        $xml = $this->fileLoader->load($entity);

        // Read list of fields
        if (!$xml instanceof \SimpleXMLElement && !empty($xml->fields)) {
            throw new PrestashopInstallerException('List of fields not found for entity ' . $entity);
        }

        $is_multi_lang_entity = $this->isMultilang($entity);
        $xml_langs = $multilang_columns = [];
        $default_lang = null;

        if ($is_multi_lang_entity) {
            $multilang_columns = $this->getColumns($entity, true);
            foreach ($this->languages as $id_lang => $iso) {
                if ($iso == $this->language->getLanguageIso()) {
                    $default_lang = $id_lang;
                }

                try {
                    $xml_langs[$id_lang] = $this->fileLoader->load($entity, $iso);
                } catch (PrestashopInstallerException $e) {
                    $xml_langs[$id_lang] = null;
                }
            }
        }

        // Load all row for current entity and prepare data to be populated
        $i = 0;
        foreach ($xml->entities->$entity as $node) {
            $data = [];
            $identifier = (string) $node['id'];

            // Read attributes
            foreach ($node->attributes() as $k => $v) {
                if ($k != 'id') {
                    $data[$k] = (string) $v;
                }
            }

            // Read cdatas
            foreach ($node->children() as $child) {
                $data[$child->getName()] = (string) $child;
            }

            // Load multilang data
            $data_lang = [];
            if ($is_multi_lang_entity) {
                $xpath_query = $entity . '[@id="' . $identifier . '"]';
                foreach ($xml_langs as $id_lang => $xml_lang) {
                    if (!$xml_lang) {
                        continue;
                    }

                    if (($node_lang = $xml_lang->xpath($xpath_query)) || ($node_lang = $xml_langs[$default_lang]->xpath($xpath_query))) {
                        $node_lang = $node_lang[0];
                        foreach ($multilang_columns as $column => $is_text) {
                            $value = '';
                            if ($node_lang[$column]) {
                                $value = (string) $node_lang[$column];
                            }

                            if ($node_lang->$column) {
                                $value = (string) $node_lang->$column;
                            }
                            $data_lang[$column][$id_lang] = $value;
                        }
                    }
                }
            }

            $data = $this->rewriteRelationedData($entity, $data);
            $createEntityMethod = 'createEntity' . Tools::toCamelCase($entity);
            if (method_exists($this, $createEntityMethod)) {
                // Create entity with custom method in current class
                $this->$createEntityMethod($identifier, $data, $data_lang);
            } else {
                $this->createEntity($entity, $identifier, (string) $xml->fields['class'], $data, $data_lang);
            }

            if ($xml->fields['image']) {
                $copyImagesMethod = 'copyImages' . Tools::toCamelCase($entity);
                if (method_exists($this, $copyImagesMethod)) {
                    $this->{$copyImagesMethod}($identifier, $data);
                } else {
                    $this->copyImages($entity, $identifier, (string) $xml->fields['image'], $data);
                }
            }
            ++$i;

            if ($i >= 100) {
                $this->flushDelayedInserts();
                $i = 0;
            }
        }

        $this->flushDelayedInserts();
        $this->fileLoader->flushCache($entity);
    }

    protected function getFallBackToDefaultLanguage($iso)
    {
        return file_exists($this->lang_path . $iso . '/data/') ? $iso : 'en';
    }

    protected function getFallBackToDefaultEntityLanguage($iso, $entity)
    {
        if ($this->getFallBackToDefaultLanguage($iso) === 'en') {
            return 'en';
        }

        return file_exists($this->lang_path . $this->getFallBackToDefaultLanguage($iso) . '/data/' . $entity . '.xml') ? $iso : 'en';
    }

    /**
     * Special case for "country" entity.
     */
    public function populateEntityCountry()
    {
        $xml = $this->fileLoader->load('country');

        // Read list of fields
        if (empty($xml->fields)) {
            throw new PrestashopInstallerException('List of fields not found for entity country');
        }
        $langs = [];
        $languageList = LanguageList::getInstance();
        foreach ($this->languages as $id_lang => $iso) {
            $langs[$id_lang] = $languageList->getCountriesByLanguage($iso);
        }

        // Load all row for current entity and prepare data to be populated
        $i = 0;
        if ($xml->entities->country instanceof SimpleXMLElement) {
            foreach ($xml->entities->country as $node) {
                $data = [];

                // Read attributes
                $identifier = '';
                foreach ($node->attributes() as $k => $v) {
                    if ($k == 'id') {
                        $identifier = (string) $v;
                        continue;
                    }
                    $data[$k] = (string) $v;
                }

                // Load multilang data
                $data_lang = [];
                foreach ($langs as $id_lang => $countries) {
                    $data_lang['name'][$id_lang] = $countries[strtolower($identifier)] ?? '';
                }

                $data = $this->rewriteRelationedData('country', $data);
                $this->createEntity('country', $identifier, (string) $xml->fields['class'], $data, $data_lang);
                ++$i;

                if ($i >= 100) {
                    $this->flushDelayedInserts();
                    $i = 0;
                }
            }
        }

        $this->flushDelayedInserts();
    }

    /**
     * Special case for "tag" entity.
     */
    public function populateEntityTag()
    {
        foreach ($this->languages as $id_lang => $iso) {
            if (!file_exists($this->lang_path . $this->getFallBackToDefaultLanguage($iso) . '/data/tag.xml')) {
                continue;
            }

            $xml = $this->fileLoader->load('tag', $this->getFallBackToDefaultLanguage($iso));
            $tags = [];
            foreach ($xml->tag as $tag_node) {
                $products = trim((string) $tag_node['products']);
                if (!$products) {
                    continue;
                }

                foreach (explode(',', $products) as $product) {
                    $product = trim($product);
                    $product_id = $this->retrieveId('product', $product);
                    if (!isset($tags[$product_id])) {
                        $tags[$product_id] = [];
                    }
                    $tags[$product_id][] = trim((string) $tag_node['name']);
                }
            }

            foreach ($tags as $id_product => $tag_list) {
                Tag::addTags($id_lang, $id_product, $tag_list);
            }
        }
    }

    /**
     * Check fields related to an other entity, and replace their values by the ID created by the other entity.
     *
     * @param string $entity
     * @param array $data
     */
    protected function rewriteRelationedData($entity, array $data)
    {
        $xml = $this->fileLoader->load($entity);
        foreach ($xml->fields->field as $field) {
            if ($field['relation']) {
                $id = $this->retrieveId((string) $field['relation'], $data[(string) $field['name']]);
                if (!$id && $data[(string) $field['name']] && is_numeric($data[(string) $field['name']])) {
                    $id = $data[(string) $field['name']];
                }
                $data[(string) $field['name']] = $id;
            }
        }

        return $data;
    }

    public function flushDelayedInserts()
    {
        foreach ($this->delayed_inserts as $entity => $queries) {
            $type = Db::INSERT_IGNORE;
            if ($entity == 'access') {
                $type = Db::REPLACE;
            }

            if (!Db::getInstance()->insert($entity, $queries, false, true, $type)) {
                $this->setError($this->translator->trans('An SQL error occurred for entity <i>%entity%</i>: <i>%message%</i>', ['%entity%' => $entity, '%message%' => Db::getInstance()->getMsgError()], 'Install'));
            }
            unset($this->delayed_inserts[$entity]);
        }
    }

    /**
     * Create a simple entity with all its data and lang data
     * If a methode createEntity$entity exists, use it. Else if $classname is given, use it. Else do a simple insert in database.
     *
     * @param string $entity
     * @param string $identifier
     * @param string $classname
     * @param array $data
     * @param array $data_lang
     */
    public function createEntity($entity, $identifier, $classname, array $data, array $data_lang = [])
    {
        $xml = $this->fileLoader->load($entity);
        if ($classname) {
            $classname = '\\' . $classname;
            // Create entity with ObjectModel class
            $object = new $classname();
            $object->hydrate($data);
            if ($data_lang) {
                $object->hydrate($data_lang);
            }
            $object->add(true, isset($xml->fields['null']));
            $entity_id = $object->id;
            unset($object);
        } else {
            // Generate primary key manually
            $primary = '';
            $entity_id = 0;
            if (!$xml->fields['primary']) {
                $primary = 'id_' . $entity;
            } elseif (strpos((string) $xml->fields['primary'], ',') === false) {
                $primary = (string) $xml->fields['primary'];
            }
            unset($xml);

            if ($primary) {
                $entity_id = $this->generatePrimary($entity, $primary);
                $data[$primary] = $entity_id;
            }

            // Store INSERT queries in order to optimize install with grouped inserts
            $this->delayed_inserts[$entity][] = array_map('pSQL', $data);
            if ($data_lang) {
                $real_data_lang = [];
                foreach ($data_lang as $field => $list) {
                    foreach ($list as $id_lang => $value) {
                        $real_data_lang[$id_lang][$field] = $value;
                    }
                }

                foreach ($real_data_lang as $id_lang => $insert_data_lang) {
                    $insert_data_lang['id_' . $entity] = $entity_id;
                    $insert_data_lang['id_lang'] = $id_lang;
                    $this->delayed_inserts[$entity . '_lang'][] = array_map('pSQL', $insert_data_lang);
                }

                // Store INSERT queries for _shop associations
                $entity_asso = Shop::getAssoTable($entity);
                if ($entity_asso !== false && $entity_asso['type'] == 'shop') {
                    $this->delayed_inserts[$entity . '_shop'][] = [
                        'id_shop' => 1,
                        'id_' . $entity => $entity_id,
                    ];
                }
            }
        }

        $this->storeId($entity, $identifier, $entity_id);
    }

    public function createEntityAttribute($identifier, array $data, array $data_lang = [])
    {
        $this->createEntity('attribute', $identifier, 'ProductAttribute', $data, $data_lang);
    }

    public function createEntityConfiguration($identifier, array $data, array $data_lang)
    {
        if (Db::getInstance()->getValue('SELECT id_configuration FROM ' . _DB_PREFIX_ . 'configuration WHERE name = \'' . pSQL($data['name']) . '\'')) {
            return;
        }

        $entity = 'configuration';
        $entity_id = $this->generatePrimary($entity, 'id_configuration');
        $data['id_configuration'] = $entity_id;

        // Store INSERT queries in order to optimize install with grouped inserts
        $this->delayed_inserts[$entity][] = array_map('pSQL', $data);
        if ($data_lang) {
            $real_data_lang = [];
            foreach ($data_lang as $field => $list) {
                foreach ($list as $id_lang => $value) {
                    $real_data_lang[$id_lang][$field] = $value;
                }
            }

            foreach ($real_data_lang as $id_lang => $insert_data_lang) {
                $insert_data_lang['id_' . $entity] = $entity_id;
                $insert_data_lang['id_lang'] = $id_lang;
                $this->delayed_inserts[$entity . '_lang'][] = array_map('pSQL', $insert_data_lang);
            }
        }

        $this->storeId($entity, $identifier, $entity_id);
    }

    /**
     * @param string $identifier
     * @param array $data
     * @param array $data_lang
     *
     * @return $this
     *
     * @throws PrestaShopDatabaseException
     */
    public function createEntityPack($identifier, array $data, array $data_lang)
    {
        Pack::addItem($data['id_product_pack'], $data['id_product_item'], $data['quantity']);

        return $this;
    }

    public function createEntityStockAvailable($identifier, array $data, array $data_lang)
    {
        $stock_available = new StockAvailable();
        $stock_available->updateQuantity($data['id_product'], $data['id_product_attribute'], $data['quantity'], $data['id_shop']);
    }

    /**
     * Called from self::populateEntity
     *
     * @param string $identifier Tab id
     * @param array $data Attributes + children of tab element
     * @param array $data_lang Translated attributes
     *
     * @throws PrestashopInstallerException
     */
    public function createEntityTab($identifier, array $data, array $data_lang)
    {
        static $position = [];

        $entity = 'tab';
        $xml = $this->fileLoader->load($entity);

        if (!isset($position[$data['id_parent']])) {
            $position[$data['id_parent']] = 0;
        }
        $data['position'] = $position[$data['id_parent']]++;
        $data['icon'] = $data['icon'] ?? '';
        $data['wording'] = $data['wording'] ?? '';
        $data['wording_domain'] = $data['wording_domain'] ?? '';
        $data['route_name'] = $data['route_name'] ?? '';

        // Generate primary key manually
        if (!$xml->fields['primary']) {
            $primary = 'id_' . $entity;
        } elseif (strpos((string) $xml->fields['primary'], ',') === false) {
            $primary = (string) $xml->fields['primary'];
        } else {
            $primary = '';
        }

        if ($primary) {
            $entity_id = $this->generatePrimary($entity, $primary);
            $data[$primary] = $entity_id;
        } else {
            $entity_id = 0;
        }

        // Make sure data are correctly ordered because some attributes are optional
        // and Db::insert needs to have all data keys in the same order when using multiple insert
        ksort($data);

        // Store INSERT queries in order to optimize install with grouped inserts
        $this->delayed_inserts[$entity][] = array_map('pSQL', $data);
        if ($data_lang) {
            $real_data_lang = [];
            foreach ($data_lang as $field => $list) {
                foreach ($list as $id_lang => $value) {
                    $real_data_lang[$id_lang][$field] = $value;
                }
            }

            foreach ($real_data_lang as $id_lang => $insert_data_lang) {
                $insert_data_lang['id_' . $entity] = $entity_id;
                $insert_data_lang['id_lang'] = $id_lang;
                $this->delayed_inserts[$entity . '_lang'][] = array_map('pSQL', $insert_data_lang);
            }
        }

        $this->storeId($entity, $identifier, $entity_id);
    }

    public function generatePrimary($entity, $primary)
    {
        if (!isset($this->primaries[$entity])) {
            $entity = Db::getInstance()->escape($entity, false, true);
            $primary = Db::getInstance()->escape($primary, false, true);
            $this->primaries[$entity] = (int) Db::getInstance()->getValue(
                'SELECT ' . $primary . ' FROM `' . _DB_PREFIX_ . $entity . '` ORDER BY `' . $primary . '` DESC'
            );
        }

        return ++$this->primaries[$entity];
    }

    public function copyImages($entity, $identifier, $path, array $data, $extension = 'jpg')
    {
        // Get list of image types
        $reference = [
            'product' => 'products',
            'category' => 'categories',
            'manufacturer' => 'manufacturers',
            'supplier' => 'suppliers',
            'store' => 'stores',
        ];

        $types = [];
        if (isset($reference[$entity])) {
            $types = ImageType::getImagesTypes($reference[$entity]);
        }

        // For each path copy images
        $path = array_map('trim', explode(',', $path));
        foreach ($path as $p) {
            $from_path = $this->img_path . $p . '/';
            $dst_path = _PS_IMG_DIR_ . $p . '/';
            $entity_id = $this->retrieveId($entity, $identifier);

            if (!@copy($from_path . $identifier . '.' . $extension, $dst_path . $entity_id . '.' . $extension)) {
                $this->setError(
                    $this->translator->trans(
                        'Cannot create image "%identifier%" for entity "%entity%"',
                        ['%entity%' => $entity, '%identifier%' => $identifier],
                        'Install'
                    )
                );

                return;
            }

            foreach ($types as $type) {
                $origin_file = $from_path . $identifier . '-' . $type['name'] . '.' . $extension;
                $target_file = $dst_path . $entity_id . '-' . $type['name'] . '.' . $extension;

                // Test if dest folder is writable
                if (!is_writable(dirname($target_file))) {
                    $this->setError(
                        $this->translator->trans(
                            'Cannot create image "%identifier%" (bad permissions on folder "%folder%")',
                            ['%identifier%' => $identifier . '-' . $type['name'], '%folder%' => dirname($target_file)],
                            'Install'
                        )
                    );
                } elseif (file_exists($origin_file)) {
                    // If a file named folder/entity-type.extension exists just copy it
                    // this is an optimisation in order to prevent to much resize
                    if (!@copy($origin_file, $target_file)) {
                        $this->setError(
                            $this->translator->trans(
                                'Cannot create image "%identifier%"',
                                ['%identifier%' => $identifier . '-' . $type['name']],
                                'Install'
                            )
                        );
                    }
                    @chmod($target_file, FileSystem::DEFAULT_MODE_FILE);
                } elseif (!ImageManager::resize(
                    $from_path . $identifier . '.' . $extension,
                    $target_file,
                    $type['width'],
                    $type['height']
                )) {
                    // Resize the image if no cache was prepared in fixtures
                    $this->setError(
                        $this->translator->trans(
                            'Cannot create image "%identifier%" for entity "%entity%"',
                            ['%identifier%' => $identifier . '-' . $type['name'], '%entity%' => $entity],
                            'Install'
                        )
                    );
                }
            }
        }
        Image::moveToNewFileSystem();
    }

    public function copyImagesOrderState($identifier, array $data)
    {
        $this->copyImages('order_state', $identifier, 'os', $data, 'gif');
    }

    public function copyImagesTab($identifier, array $data)
    {
        $from_path = $this->img_path . 't/';
        $dst_path = _PS_IMG_DIR_ . 't/';
        if (file_exists($from_path . $data['class_name'] . '.gif') && !file_exists($dst_path . $data['class_name'] . '.gif')) {
            //test if file exist in install dir and if do not exist in dest folder.
            if (!@copy($from_path . $data['class_name'] . '.gif', $dst_path . $data['class_name'] . '.gif')) {
                $this->setError($this->translator->trans('Cannot create image "%identifier%" for entity "%entity%"', ['%identifier%' => $identifier, '%tab%' => 'tab'], 'Install'));

                return;
            }
        }
    }

    public function copyImagesImage($identifier)
    {
        $path = $this->img_path . 'p/';
        $image = new Image($this->retrieveId('image', $identifier));
        $dst_path = $image->getPathForCreation();
        if (!@copy($path . $identifier . '.jpg', $dst_path . '.' . $image->image_format)) {
            $this->setError(
                $this->translator->trans(
                    'Cannot create image "%identifier%" for entity "%entity%"',
                    ['%identifier%' => $identifier, '%entity%' => 'product'],
                    'Install'
                )
            );

            return;
        }
        @chmod($dst_path . '.' . $image->image_format, FileSystem::DEFAULT_MODE_FILE);

        $types = ImageType::getImagesTypes('products');
        foreach ($types as $type) {
            $origin_file = $path . $identifier . '-' . $type['name'] . '.jpg';
            $target_file = $dst_path . '-' . $type['name'] . '.' . $image->image_format;

            // Test if dest folder is writable
            if (!is_writable(dirname($target_file))) {
                $this->setError(
                    $this->translator->trans(
                        'Cannot create image "%identifier%" (bad permissions on folder "%folder%")',
                        ['%identifier%' => $identifier . '-' . $type['name'], '%folder%' => dirname($target_file)],
                        'Install'
                    )
                );
            } elseif (file_exists($origin_file)) {
                // If a file named folder/entity-type.jpg exists just copy it
                // this is an optimisation in order to prevent to much resize
                if (!@copy($origin_file, $target_file)) {
                    $this->setError(
                        $this->translator->trans(
                            'Cannot create image "%1$s" for entity "%2$s"',
                            ['%identifier%' => $identifier . '-' . $type['name'], '%entity%' => 'product'],
                            'Install'
                        )
                    );
                }
                @chmod($target_file, FileSystem::DEFAULT_MODE_FILE);
            } elseif (!ImageManager::resize($path . $identifier . '.jpg', $target_file, $type['width'], $type['height'])) {
                // Resize the image if no cache was prepared in fixtures
                $this->setError(
                    $this->translator->trans(
                        'Cannot create image "%identifier%" for entity "%entity%"',
                        ['%identifier%' => $identifier . '-' . $type['name'], '%entity%' => 'product'],
                        'Install'
                    )
                );
            }
        }
    }

    public function getTables()
    {
        static $tables = null;

        if (null === $tables) {
            $tables = [];
            foreach (Db::getInstance()->executeS('SHOW TABLES') as $row) {
                $table = current($row);
                if (preg_match('#^' . _DB_PREFIX_ . '(.+?)(_lang)?$#i', $table, $m)) {
                    $tables[$m[1]] = (isset($m[2]) && $m[2]) ? true : false;
                }
            }
        }

        return $tables;
    }

    public function hasElements($table)
    {
        $table = Db::getInstance()->escape($table, false, true);

        return (bool) Db::getInstance()->getValue('SELECT COUNT(*) FROM `' . _DB_PREFIX_ . $table . '`');
    }

    public function getColumns($table, $multilang = false, array $exclude = [])
    {
        static $columns = [];

        if ($multilang) {
            return ($this->isMultilang($table)) ? $this->getColumns($table . '_lang', false, ['id_' . $table]) : [];
        }

        if (!isset($columns[$table])) {
            $columns[$table] = [];
            $sql = 'SHOW COLUMNS FROM `' . _DB_PREFIX_ . bqSQL($table) . '`';
            foreach (Db::getInstance()->executeS($sql) as $row) {
                $columns[$table][$row['Field']] = $this->checkIfTypeIsText($row['Type']);
            }
        }

        $exclude = array_merge(['id_' . $table, 'date_add', 'date_upd', 'deleted', 'id_lang'], $exclude);

        $list = [];
        foreach ($columns[$table] as $k => $v) {
            if (!in_array($k, $exclude)) {
                $list[$k] = $v;
            }
        }

        return $list;
    }

    public function getClasses($path = null)
    {
        static $cache = null;

        if (null !== $cache) {
            return $cache;
        }

        $dir = $path;
        if (null === $dir) {
            $dir = _PS_CLASS_DIR_;
        }

        $classes = [];
        foreach (scandir($dir, SCANDIR_SORT_NONE) as $file) {
            if ($file[0] != '.' && $file != 'index.php') {
                if (is_dir($dir . $file)) {
                    $classes = array_merge($classes, $this->getClasses($dir . $file . '/'));
                } elseif (preg_match('#^(.+)\.php$#', $file, $m)) {
                    $classes[] = $m[1];
                }
            }
        }

        sort($classes);
        if (null === $path) {
            $cache = $classes;
        }

        return $classes;
    }

    public function checkIfTypeIsText($type)
    {
        if (preg_match('#^(longtext|text|tinytext)#i', $type)) {
            return true;
        }

        if (preg_match('#^varchar\(([0-9]+)\)$#i', $type, $m)) {
            return (int) ($m[1]) >= 64 ? true : false;
        }

        return false;
    }

    public function isMultilang($entity)
    {
        $tables = $this->getTables();

        return isset($tables[$entity]) && $tables[$entity];
    }

    public function entityExists($entity)
    {
        return file_exists($this->data_path . $entity . '.xml');
    }

    public function getEntitiesList()
    {
        $entities = [];
        foreach (scandir($this->data_path, SCANDIR_SORT_NONE) as $file) {
            if ($file[0] != '.' && preg_match('#^(.+)\.xml$#', $file, $m)) {
                $entities[] = $m[1];
            }
        }

        return $entities;
    }

    public function getEntityInfo($entity)
    {
        $info = [
            'config' => [
                'id' => '',
                'primary' => '',
                'class' => '',
                'sql' => '',
                'ordersql' => '',
                'image' => '',
                'null' => '',
            ],
            'fields' => [],
        ];

        if (!$this->entityExists($entity)) {
            return $info;
        }

        $xml = @simplexml_load_file($this->data_path . $entity . '.xml', 'SimplexmlElement');
        if (!$xml) {
            return $info;
        }

        if ($xml->fields['id']) {
            $info['config']['id'] = (string) $xml->fields['id'];
        }

        if ($xml->fields['primary']) {
            $info['config']['primary'] = (string) $xml->fields['primary'];
        }

        if ($xml->fields['class']) {
            $info['config']['class'] = (string) $xml->fields['class'];
        }

        if ($xml->fields['sql']) {
            $info['config']['sql'] = (string) $xml->fields['sql'];
        }

        if ($xml->fields['ordersql']) {
            $info['config']['ordersql'] = (string) $xml->fields['ordersql'];
        }

        if ($xml->fields['null']) {
            $info['config']['null'] = (string) $xml->fields['null'];
        }

        if ($xml->fields['image']) {
            $info['config']['image'] = (string) $xml->fields['image'];
        }

        foreach ($xml->fields->field as $field) {
            $column = (string) $field['name'];
            $info['fields'][$column] = [];
            if (isset($field['relation'])) {
                $info['fields'][$column]['relation'] = (string) $field['relation'];
            }
        }

        return $info;
    }

    public function getDependencies()
    {
        $entities = [];
        foreach ($this->getEntitiesList() as $entity) {
            $entities[$entity] = $this->getEntityInfo($entity);
        }

        $dependencies = [];
        foreach ($entities as $entity => $info) {
            foreach ($info['fields'] as $field => $info_field) {
                if (isset($info_field['relation']) && $info_field['relation'] != $entity) {
                    if (!isset($dependencies[$info_field['relation']])) {
                        $dependencies[$info_field['relation']] = [];
                    }
                    $dependencies[$info_field['relation']][] = $entity;
                }
            }
        }

        return $dependencies;
    }

    public function generateEntitySchema($entity, array $fields, array $config)
    {
        if ($this->entityExists($entity)) {
            $xml = $this->fileLoader->load($entity);
        } else {
            $xml = new SimplexmlElement('<entity_' . $entity . ' />');
        }
        unset($xml->fields);

        // Fill <fields> attributes (config)
        $xml_fields = $xml->addChild('fields');
        foreach ($config as $k => $v) {
            if ($v) {
                $xml_fields[$k] = $v;
            }
        }

        // Create list of fields
        foreach ($fields as $column => $info) {
            $field = $xml_fields->addChild('field');
            $field->name = $column;
            if (isset($info['relation'])) {
                $field->relation = $info['relation'];
            }
        }

        // Recreate entities nodes, in order to have the <entities> node after the <fields> node
        $store_entities = clone $xml->entities;
        unset($xml->entities);
        $xml->addChild('entities', $store_entities);

        $xml->asXML($this->data_path . $entity . '.xml');
    }

    /**
     * ONLY FOR DEVELOPMENT PURPOSE.
     */
    public function generateAllEntityFiles()
    {
        $entities = [];
        foreach ($this->getEntitiesList() as $entity) {
            $entities[$entity] = $this->getEntityInfo($entity);
        }
        $this->generateEntityFiles($entities);
    }

    /**
     * ONLY FOR DEVELOPMENT PURPOSE.
     */
    public function generateEntityFiles($entities)
    {
        $dependencies = $this->getDependencies();

        // Sort entities to populate database in good order (E.g. zones before countries)
        do {
            $current = (isset($sort_entities)) ? $sort_entities : [];
            $sort_entities = [];
            foreach ($entities as $entity) {
                if (isset($dependencies[$entity])) {
                    $min = count($entities) - 1;
                    foreach ($dependencies[$entity] as $item) {
                        if (($key = array_search($item, $sort_entities)) !== false) {
                            $min = min($min, $key);
                        }
                    }
                    if ($min == 0) {
                        array_unshift($sort_entities, $entity);
                    } else {
                        array_splice($sort_entities, $min, 0, [$entity]);
                    }
                } else {
                    $sort_entities[] = $entity;
                }
            }
            $entities = $sort_entities;
        } while ($current != $sort_entities);

        foreach ($sort_entities as $entity) {
            $this->generateEntityContent($entity);
        }
    }

    public function generateEntityContent($entity)
    {
        $xml = $this->fileLoader->load($entity);
        if (method_exists($this, 'getEntityContents' . Tools::toCamelCase($entity))) {
            $content = $this->{'getEntityContents' . Tools::toCamelCase($entity)}($entity);
        } else {
            $content = $this->getEntityContents($entity);
        }

        unset($xml->entities);
        $entities = $xml->addChild('entities');
        $this->createXmlEntityNodes($entity, $content['nodes'], $entities);
        $xml->asXML($this->data_path . $entity . '.xml');

        // Generate multilang XML files
        if ($content['nodes_lang']) {
            foreach ($content['nodes_lang'] as $id_lang => $nodes) {
                if (!isset($this->languages[$id_lang])) {
                    continue;
                }

                $iso = $this->languages[$id_lang];
                if (!is_dir($this->lang_path . $this->getFallBackToDefaultLanguage($iso) . '/data')) {
                    mkdir($this->lang_path . $this->getFallBackToDefaultLanguage($iso) . '/data');
                }

                $xml_node = new SimplexmlElement('<entity_' . $entity . ' />');
                $this->createXmlEntityNodes($entity, $nodes, $xml_node);
                $xml_node->asXML($this->lang_path . $this->getFallBackToDefaultEntityLanguage($iso, $entity) . '/data/' . $entity . '.xml');
            }
        }

        if ($xml->fields['image']) {
            if (method_exists($this, 'backupImage' . Tools::toCamelCase($entity))) {
                $this->{'backupImage' . Tools::toCamelCase($entity)}((string) $xml->fields['image']);
            } else {
                $this->backupImage($entity, (string) $xml->fields['image']);
            }
        }
    }

    /**
     * ONLY FOR DEVELOPMENT PURPOSE.
     */
    public function getEntityContents($entity)
    {
        $xml = $this->fileLoader->load($entity);
        $primary = !empty($xml->fields['primary']) ? (string) $xml->fields['primary'] : 'id_' . $entity;
        $is_multilang = $this->isMultilang($entity);

        // Check if current table is an association table (if multiple primary keys)
        $is_association = false;
        if (strpos($primary, ',') !== false) {
            $is_association = true;
            $primary = array_map('trim', explode(',', $primary));
        }

        // Build query
        $sql = new DbQuery();
        $sql->select('a.*');
        $sql->from($entity, 'a');
        if ($is_multilang) {
            $sql->select('b.*');
            $sql->leftJoin($entity . '_lang', 'b', 'a.' . $primary . ' = b.' . $primary);
        }

        if (!empty($xml->fields['sql'])) {
            $sql->where((string) $xml->fields['sql']);
        }

        if (!$is_association) {
            $sql->select('a.' . $primary);
            if (empty($xml->fields['ordersql'])) {
                $sql->orderBy('a.' . $primary);
            }
        }

        if ($is_multilang && empty($xml->fields['ordersql'])) {
            $sql->orderBy('b.id_lang');
        }

        if (!empty($xml->fields['ordersql'])) {
            $sql->orderBy((string) $xml->fields['ordersql']);
        }

        // Get multilang columns
        $alias_multilang = $multilang_columns = [];
        if ($is_multilang) {
            $columns = $this->getColumns($entity);
            $multilang_columns = $this->getColumns($entity, true);

            // If some columns from _lang table have same name than original table, rename them (E.g. value in configuration)
            foreach ($multilang_columns as $c => $is_text) {
                if (isset($columns[$c])) {
                    $alias = $c . '_alias';
                    $alias_multilang[$c] = $alias;
                    $sql->select('a.' . $c . ' as ' . $c . ', b.' . $c . ' as ' . $alias);
                }
            }
        }

        // Get all results
        $nodes = $nodes_lang = [];
        $results = Db::getInstance()->executeS($sql);
        if (Db::getInstance()->getNumberError()) {
            $this->setError($this->translator->trans('SQL error on query <i>%query%</i>', ['%sql%' => $sql], 'Install'));
        } else {
            foreach ($results as $row) {
                // Store common columns
                if ($is_association) {
                    $id = $entity;
                    foreach ($primary as $key) {
                        $id .= '_' . $row[$key];
                    }
                } else {
                    $id = $this->generateId($entity, $row[$primary], $row, (!empty($xml->fields['id'])) ? (string) $xml->fields['id'] : null);
                }

                if (!isset($nodes[$id])) {
                    $node = [];
                    foreach ($xml->fields->field as $field) {
                        $column = (string) $field['name'];
                        if (isset($field['relation'])) {
                            $sql = 'SELECT `id_' . bqSQL($field['relation']) . '`
									FROM `' . bqSQL(_DB_PREFIX_ . $field['relation']) . '`
									WHERE `id_' . bqSQL($field['relation']) . '` = ' . (int) $row[$column];
                            $node[$column] = $this->generateId((string) $field['relation'], Db::getInstance()->getValue($sql));

                            // A little trick to allow storage of some hard values, like '-1' for tab.id_parent
                            if (!$node[$column] && $row[$column]) {
                                $node[$column] = $row[$column];
                            }
                        } else {
                            $node[$column] = $row[$column];
                        }
                    }
                    $nodes[$id] = $node;
                }

                // Store multilang columns
                if ($is_multilang && $row['id_lang']) {
                    $node = [];
                    foreach ($multilang_columns as $column => $is_text) {
                        $node[$column] = $row[isset($alias_multilang[$column]) ? $alias_multilang[$column] : $column];
                    }
                    $nodes_lang[$row['id_lang']][$id] = $node;
                }
            }
        }

        return [
            'nodes' => $nodes,
            'nodes_lang' => $nodes_lang,
        ];
    }

    public function getEntityContentsTag()
    {
        $nodes_lang = [];

        $sql = 'SELECT t.id_tag, t.id_lang, t.name, pt.id_product
				FROM ' . _DB_PREFIX_ . 'tag t
				LEFT JOIN ' . _DB_PREFIX_ . 'product_tag pt ON t.id_tag = pt.id_tag
				ORDER BY id_lang';
        foreach (Db::getInstance()->executeS($sql) as $row) {
            $identifier = $this->generateId('tag', $row['id_tag']);
            if (!isset($nodes_lang[$row['id_lang']])) {
                $nodes_lang[$row['id_lang']] = [];
            }

            if (!isset($nodes_lang[$row['id_lang']][$identifier])) {
                $nodes_lang[$row['id_lang']][$identifier] = [
                    'name' => $row['name'],
                    'products' => '',
                ];
            }

            $nodes_lang[$row['id_lang']][$identifier]['products'] .= (($nodes_lang[$row['id_lang']][$identifier]['products']) ? ',' : '') . $this->generateId('product', $row['id_product']);
        }

        return [
            'nodes' => [],
            'nodes_lang' => $nodes_lang,
        ];
    }

    /**
     * ONLY FOR DEVELOPMENT PURPOSE.
     */
    public function generateId($entity, $primary, array $row = [], $id_format = null)
    {
        static $ids = [];

        if (isset($ids[$entity][$primary])) {
            return $ids[$entity][$primary];
        }

        if (!isset($ids[$entity])) {
            $ids[$entity] = [];
        }

        if (!$primary) {
            return '';
        }

        if (!$id_format || !$row || !$row[$id_format]) {
            $ids[$entity][$primary] = $entity . '_' . $primary;
        } else {
            $value = $row[$id_format];
            $value = preg_replace('#[^a-z0-9_-]#i', '_', $value);
            $value = preg_replace('#_+#', '_', $value);
            $value = preg_replace('#^_+#', '', $value);
            $value = preg_replace('#_+$#', '', $value);

            $store_identifier = $value;
            $i = 1;
            while (in_array($store_identifier, $ids[$entity])) {
                $store_identifier = $value . '_' . $i++;
            }
            $ids[$entity][$primary] = $store_identifier;
        }

        return $ids[$entity][$primary];
    }

    /**
     * ONLY FOR DEVELOPMENT PURPOSE.
     */
    public function createXmlEntityNodes($entity, array $nodes, SimpleXMLElement $entities)
    {
        $types = array_merge($this->getColumns($entity), $this->getColumns($entity, true));
        foreach ($nodes as $id => $node) {
            $entity_node = $entities->addChild($entity);
            $entity_node->id = $id;
            foreach ($node as $k => $v) {
                if (!empty($types[$k])) {
                    $entity_node->addChild($k, $v);
                } else {
                    $entity_node->{$k} = $v;
                }
            }
        }
    }

    /**
     * ONLY FOR DEVELOPMENT PURPOSE.
     */
    public function backupImage($entity, $path)
    {
        $reference = [
            'product' => 'products',
            'category' => 'categories',
            'manufacturer' => 'manufacturers',
            'supplier' => 'suppliers',
            'store' => 'stores',
        ];

        $types = [];
        if (isset($reference[$entity])) {
            $types = [];
            foreach (ImageType::getImagesTypes($reference[$entity]) as $type) {
                $types[] = $type['name'];
            }
        }

        $path_list = array_map('trim', explode(',', $path));
        foreach ($path_list as $p) {
            $backup_path = $this->img_path . $p . '/';
            $from_path = _PS_IMG_DIR_ . $p . '/';

            if (!is_dir($backup_path) && !mkdir($backup_path)) {
                $this->setError(sprintf('Cannot create directory <i>%s</i>', $backup_path));
            }

            foreach (scandir($from_path, SCANDIR_SORT_NONE) as $file) {
                if ($file[0] != '.' && preg_match('#^(([0-9]+)(-(' . implode('|', $types) . '))?)\.(gif|jpg|jpeg|png)$#i', $file, $m)) {
                    $file_id = $m[2];
                    $file_type = $m[3];
                    $file_extension = $m[5];
                    copy($from_path . $file, $backup_path . $this->generateId($entity, $file_id) . $file_type . '.' . $file_extension);
                }
            }
        }
    }

    /**
     * ONLY FOR DEVELOPMENT PURPOSE.
     */
    public function backupImageImage()
    {
        $types = [];
        foreach (ImageType::getImagesTypes('products') as $type) {
            $types[] = $type['name'];
        }

        $backup_path = $this->img_path . 'p/';
        $from_path = _PS_PRODUCT_IMG_DIR_;
        if (!is_dir($backup_path) && !mkdir($backup_path)) {
            $this->setError(sprintf('Cannot create directory <i>%s</i>', $backup_path));
        }

        foreach (Image::getAllImages() as $image) {
            $image = new Image($image['id_image']);
            $image_path = $image->getExistingImgPath();
            if (file_exists($from_path . $image_path . '.' . $image->image_format)) {
                copy($from_path . $image_path . '.' . $image->image_format, $backup_path . $this->generateId('image', $image->id) . '.' . $image->image_format);
            }

            foreach ($types as $type) {
                if (file_exists($from_path . $image_path . '-' . $type . '.' . $image->image_format)) {
                    copy($from_path . $image_path . '-' . $type . '.' . $image->image_format, $backup_path . $this->generateId('image', $image->id) . '-' . $type . '.' . $image->image_format);
                }
            }
        }
    }

    /**
     * ONLY FOR DEVELOPMENT PURPOSE.
     */
    public function backupImageTab()
    {
        $backup_path = $this->img_path . 't/';
        $from_path = _PS_IMG_DIR_ . 't/';
        if (!is_dir($backup_path) && !mkdir($backup_path)) {
            $this->setError(sprintf('Cannot create directory <i>%s</i>', $backup_path));
        }

        $xml = $this->fileLoader->load('tab');
        foreach ($xml->entities->tab as $tab) {
            if (file_exists($from_path . $tab->class_name . '.gif')) {
                copy($from_path . $tab->class_name . '.gif', $backup_path . $tab->class_name . '.gif');
            }
        }
    }
}
