<?php
/**
 * 2007-2018 PrestaShop
 *
 * 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.txt.
 * 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 http://www.prestashop.com for more information.
 *
 * @author    PrestaShop SA <contact@prestashop.com>
 * @copyright 2007-2018 PrestaShop SA
 * @license   https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0)
 * International Registered Trademark & Property of PrestaShop SA
 */

namespace PrestaShopBundle\Install;

use PrestaShop\PrestaShop\Adapter\Entity\Pack;
use PrestaShop\PrestaShop\Adapter\Entity\Tools;
use PrestashopInstallerException;
use PrestaShopDatabaseException;
use PrestaShop\PrestaShop\Adapter\Entity\Tag;
use PrestaShop\PrestaShop\Adapter\Entity\Shop;
use PrestaShop\PrestaShop\Adapter\Entity\Db;
use PrestaShop\PrestaShop\Adapter\Entity\StockAvailable;
use PrestaShop\PrestaShop\Adapter\Entity\ImageType;
use PrestaShop\PrestaShop\Adapter\Entity\Image;
use PrestaShop\PrestaShop\Adapter\Entity\ImageManager;
use PrestaShop\PrestaShop\Adapter\Entity\DbQuery;

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 = array();

    /**
     * @var array Store in cache all loaded XML files
     */
    protected $cache_xml_entity = array();

    /**
     * @var array List of errors
     */
    protected $errors = array();

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

    protected $ids = array();

    protected $primaries = array();

    protected $delayed_inserts = array();

    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/';
    }

    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/';
    }

    /**
     * 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;
    }

    public function getSortedEntities()
    {
        // Browse all XML files from data/xml directory
        $entities = array();
        $dependencies = array();
        $fd = opendir($this->data_path);
        while ($file = readdir($fd)) {
            if (preg_match('#^(.+)\.xml$#', $file, $m)) {
                $entity = $m[1];
                $xml = $this->loadEntity($entity);

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

        // Sort entities to populate database in good order (E.g. zones before countries)
        do {
            $current = (isset($sort_entities)) ? $sort_entities : array();
            $sort_entities = array();
            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, array($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
     */
    public function populateFromXmlFiles()
    {
        $entities = $this->getSortedEntities();

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

    /**
     * Populate an entity
     *
     * @param string $entity
     */
    public function populateEntity($entity)
    {
        if (method_exists($this, 'populateEntity'.Tools::toCamelCase($entity))) {
            $this->{'populateEntity'.Tools::toCamelCase($entity)}();
            return;
        }

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

        $xml = $this->loadEntity($entity);

        // Read list of fields
        if (!is_object($xml) || !$xml->fields) {
            throw new PrestashopInstallerException('List of fields not found for entity '.$entity);
        }

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

                try {
                    $xml_langs[$id_lang] = $this->loadEntity($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 = array();
            $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 = array();
            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);
            if (method_exists($this, 'createEntity'.Tools::toCamelCase($entity))) {
                // Create entity with custom method in current class
                $method = 'createEntity'.Tools::toCamelCase($entity);
                $this->$method($identifier, $data, $data_lang);
            } else {
                $this->createEntity($entity, $identifier, (string)$xml->fields['class'], $data, $data_lang);
            }

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

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

        $this->flushDelayedInserts();
        unset($this->cache_xml_entity[$this->path_type][$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 "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->loadEntity('tag', $this->getFallBackToDefaultLanguage($iso));
            $tags = array();
            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] = array();
                    }
                    $tags[$product_id][] = trim((string)$tag_node['name']);
                }
            }

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

    /**
     * Load an entity XML file
     *
     * @param string $entity
     * @return \SimpleXMLElement
     */
    protected function loadEntity($entity, $iso = null)
    {
        if (!isset($this->cache_xml_entity[$this->path_type][$entity][$iso])) {
            if (substr($entity, 0, 1) == '.' || substr($entity, 0, 1) == '_') {
                return;
            }

            $path = $this->data_path.$entity.'.xml';
            if ($iso) {
                $path = $this->lang_path.$this->getFallBackToDefaultEntityLanguage($iso, $entity).'/data/'.$entity.'.xml';
            }

            if (!file_exists($path)) {
                throw new PrestashopInstallerException('XML data file '.$entity.'.xml not found');
            }

            $this->cache_xml_entity[$this->path_type][$entity][$iso] = @simplexml_load_file($path, 'SimplexmlElement');
            if (!$this->cache_xml_entity[$this->path_type][$entity][$iso]) {
                throw new PrestashopInstallerException('XML data file '.$entity.'.xml invalid');
            }
        }

        return $this->cache_xml_entity[$this->path_type][$entity][$iso];
    }

    /**
     * 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->loadEntity($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>', array('%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 = array())
    {
        $xml = $this->loadEntity($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'])) ? true : false);
            $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 = array();
                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'][] = array(
                        'id_shop' => 1,
                        'id_'.$entity => $entity_id,
                    );
                }
            }
        }

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

    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 = array();
            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']);
    }

    public function createEntityTab($identifier, array $data, array $data_lang)
    {
        static $position = array();

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

        if (!isset($position[$data['id_parent']])) {
            $position[$data['id_parent']] = 0;
        }
        $data['position'] = $position[$data['id_parent']]++;

        // 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'];
        }

        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 = array();
            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 = array(
            'product' => 'products',
            'category' => 'categories',
            'manufacturer' => 'manufacturers',
            'supplier' => 'suppliers',
            'store' => 'stores',
        );

        $types = array();
        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%"',
                        array('%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%")',
                            array('%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%"',
                                array('%identifier%' => $identifier.'-'.$type['name']),
                                'Install'
                            )
                        );
                    }
                    @chmod($target_file, 0644);
                } 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%"',
                            array('%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%"', array('%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%"',
                    array('%identifier%' => $identifier, '%entity%' => 'product'),
                    'Install'
                )
            );
            return;
        }
        @chmod($dst_path.'.'.$image->image_format, 0644);

        $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%")',
                        array('%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"',
                            array('%identifier%' => $identifier.'-'.$type['name'], '%entity%' => 'product'),
                            'Install'
                        )
                    );
                }
                @chmod($target_file, 0644);
            } 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%"',
                        array('%identifier%' => $identifier.'-'.$type['name'], '%entity%' => 'product'),
                        'Install'
                    )
                );
            }
        }
    }

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

        if (is_null($tables)) {
            $tables = array();
            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 = array())
    {
        static $columns = array();

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

        if (!isset($columns[$table])) {
            $columns[$table] = array();
            $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(array('id_'.$table, 'date_add', 'date_upd', 'deleted', 'id_lang'), $exclude);

        $list = array();
        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 (!is_null($cache)) {
            return $cache;
        }

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

        $classes = array();
        foreach (scandir($dir) 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 (is_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 intval($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 = array();
        foreach (scandir($this->data_path) as $file) {
            if ($file[0] != '.' && preg_match('#^(.+)\.xml$#', $file, $m)) {
                $entities[] = $m[1];
            }
        }
        return $entities;
    }

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

        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] = array();
            if (isset($field['relation'])) {
                $info['fields'][$column]['relation'] = (string)$field['relation'];
            }
        }
        return $info;
    }

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

        $dependencies = array();
        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']] = array();
                    }
                    $dependencies[$info_field['relation']][] = $entity;
                }
            }
        }

        return $dependencies;
    }

    public function generateEntitySchema($entity, array $fields, array $config)
    {
        if ($this->entityExists($entity)) {
            $xml = $this->loadEntity($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 = array();
        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 : array();
            $sort_entities = array();
            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, array($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->loadEntity($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->loadEntity($entity);
        $primary = (isset($xml->fields['primary']) && $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 (isset($xml->fields['sql']) && $xml->fields['sql']) {
            $sql->where((string)$xml->fields['sql']);
        }

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

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

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

        // Get multilang columns
        $alias_multilang = array();
        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 = array();
        $results = Db::getInstance()->executeS($sql);
        if (Db::getInstance()->getNumberError()) {
            $this->setError($this->translator->trans('SQL error on query <i>%query%</i>', array('%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, (isset($xml->fields['id']) && $xml->fields['id']) ? (string)$xml->fields['id'] : null);
                }

                if (!isset($nodes[$id])) {
                    $node = array();
                    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 = array();
                    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 array(
            'nodes' =>        $nodes,
            'nodes_lang' =>    $nodes_lang,
        );
    }

    public function getEntityContentsTag()
    {
        $nodes_lang = array();

        $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']] = array();
            }

            if (!isset($nodes_lang[$row['id_lang']][$identifier])) {
                $nodes_lang[$row['id_lang']][$identifier] = array(
                    '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 array(
            'nodes' =>        array(),
            'nodes_lang' => $nodes_lang,
        );
    }

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

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

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

        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 (isset($types[$k]) && $types[$k]) {
                    $entity_node->addChild($k, $v);
                } else {
                    $entity_node[$k] = $v;
                }
            }
        }
    }

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

        $types = array();
        if (isset($reference[$entity])) {
            $types = array();
            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) 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 = array();
        foreach (ImageType::getImagesTypes('products') as $type) {
            $types[] = $type['name'];
        }

        $backup_path = $this->img_path.'p/';
        $from_path = _PS_PROD_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->loadEntity('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');
            }
        }
    }
}
