<?php

/**
 * ---------------------------------------------------------------------
 *
 * GLPI - Gestionnaire Libre de Parc Informatique
 *
 * http://glpi-project.org
 *
 * @copyright 2015-2023 Teclib' and contributors.
 * @copyright 2003-2014 by the INDEPNET Development Team.
 * @licence   https://www.gnu.org/licenses/gpl-3.0.html
 *
 * ---------------------------------------------------------------------
 *
 * LICENSE
 *
 * This file is part of GLPI.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 *
 * ---------------------------------------------------------------------
 */

namespace Glpi\Console\Migration;

use CommonDBTM;
use Computer;
use ComputerModel;
use Datacenter;
use DB;
use DCRoom;
use Glpi\Console\AbstractCommand;
use Glpi\Toolbox\Sanitizer;
use Item_Rack;
use Monitor;
use MonitorModel;
use NetworkEquipment;
use NetworkEquipmentModel;
use PassiveDCEquipment;
use PassiveDCEquipmentModel;
use PDU;
use PDUModel;
use Peripheral;
use PeripheralModel;
use Plugin;
use Rack;
use RackModel;
use RackType;
use State;
use Symfony\Component\Console\Exception\LogicException;
use Symfony\Component\Console\Exception\RuntimeException;
use Symfony\Component\Console\Helper\ProgressBar;
use Symfony\Component\Console\Helper\QuestionHelper;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\ChoiceQuestion;

class RacksPluginToCoreCommand extends AbstractCommand
{
    /**
     * Error code returned if plugin version or plugin data is invalid.
     *
     * @var integer
     */
    const ERROR_PLUGIN_VERSION_OR_DATA_INVALID = 1;

    /**
     * Error code returned if import failed.
     *
     * @var integer
     */
    const ERROR_PLUGIN_IMPORT_FAILED = 1;

    /**
     * Version of Racks plugin required for this migration.
     * @var string
     */
    const RACKS_REQUIRED_VERSION = '1.8.0';

    /**
     * Choice value for other type: ignore.
     * @var string
     */
    const OTHER_TYPE_CHOICE_IGNORE = 'i';

    /**
     * Choice value for other type: computer.
     * @var string
     */
    const OTHER_TYPE_CHOICE_COMPUTER = 'c';

    /**
     * Choice value for other type: network equipment.
     * @var string
     */
    const OTHER_TYPE_CHOICE_NETWORKEQUIPEMENT = 'n';

    /**
     * Choice value for other type: peripheral.
     * @var string
     */
    const OTHER_TYPE_CHOICE_PERIPHERAL = 'p';

    /**
     * Choice value for other type: pdu.
     * @var string
     */
    const OTHER_TYPE_CHOICE_PDU = 'u';

    /**
     * Choice value for other type: monitor.
     * @var string
     */
    const OTHER_TYPE_CHOICE_MONITOR = 'm';

    /**
     * Choice value for other type: passive device.
     * @var string
     */
    const OTHER_TYPE_CHOICE_PASSIVEDCEQUIPEMENT = 'd';

    /**
     * Datacenter on which rooms will be created.
     *
     * @var integer
     */
    private $datacenter_id;

    /**
     * Room on which racks will be placed if no corresponding room found.
     *
     * @var integer
     */
    private $fallback_room_id;

    /**
     * Imported elements mapping.
     *
     * @var array
     */
    private $elements_mapping;

    protected function configure()
    {
        parent::configure();

        $this->setName('migration:racks_plugin_to_core');
        $this->setDescription(__('Migrate Racks plugin data into GLPI core tables'));

        $this->addOption(
            'ignore-other-elements',
            'i',
            InputOption::VALUE_NONE,
            __('Ignore "PluginRacksOther" models and elements')
        );

        $this->addOption(
            'skip-errors',
            's',
            InputOption::VALUE_NONE,
            __('Do not exit on import errors')
        );

        $this->addOption(
            'truncate',
            't',
            InputOption::VALUE_NONE,
            __('Remove existing core data')
        );

        $this->addOption(
            'update-plugin',
            'u',
            InputOption::VALUE_NONE,
            sprintf(
                __('Run Racks plugin update (you need version %s files to do this)'),
                self::RACKS_REQUIRED_VERSION
            )
        );

        $this->addOption(
            'without-plugin',
            'w',
            InputOption::VALUE_NONE,
            sprintf(
                __('Enable migration without plugin files (we cannot validate that plugin data are compatible with supported %s version)'),
                self::RACKS_REQUIRED_VERSION
            )
        );
    }

    protected function execute(InputInterface $input, OutputInterface $output)
    {

        $this->elements_mapping = []; // Clear elements mapping

        $no_interaction = $input->getOption('no-interaction');

        if (!$no_interaction) {
           // Ask for confirmation (unless --no-interaction)
            $output->writeln(
                [
                    __('You are about to launch migration of Racks plugin data into GLPI core tables.'),
                    __('It is better to make a backup of your existing data before continuing.')
                ]
            );

            $this->askForConfirmation(false);
        }

        if (!$this->checkPlugin()) {
            return self::ERROR_PLUGIN_VERSION_OR_DATA_INVALID;
        }

        if ($input->getOption('truncate')) {
            $this->cleanCoreTables();
        }

        if (!$this->migratePlugin()) {
            return self::ERROR_PLUGIN_IMPORT_FAILED;
        }

        $output->writeln('<info>' . __('Migration done.') . '</info>');

        return 0; // Success
    }

    /**
     * Check that plugin state and existing data are OK for migration.
     *
     * @throws LogicException
     *
     * @return boolean
     */
    private function checkPlugin()
    {

        $check_version = !$this->input->getOption('without-plugin');

        if ($check_version) {
            $this->output->writeln(
                '<comment>' . __('Checking plugin version...') . '</comment>',
                OutputInterface::VERBOSITY_VERBOSE
            );

            $plugin = new Plugin();
            $plugin->checkPluginState('racks');

            if (!$plugin->getFromDBbyDir('racks')) {
                 $message  = __('Racks plugin is not part of GLPI plugin list. It has never been installed or has been cleaned.')
                  . ' '
                  . sprintf(
                      __('You have to install Racks plugin files in version %s to be able to continue.'),
                      self::RACKS_REQUIRED_VERSION
                  );
                 $this->output->writeln(
                     [
                         '<error>' . $message . '</error>',
                     ],
                     OutputInterface::VERBOSITY_QUIET
                 );
                 return false;
            }

            $is_version_ok = '1.8.0' === $plugin->fields['version'];
            if (!$is_version_ok) {
                $message  = sprintf(
                    __('You have to install Racks plugin files in version %s to be able to continue.'),
                    self::RACKS_REQUIRED_VERSION
                );
                $this->output->writeln(
                    '<error>' . $message . '</error>',
                    OutputInterface::VERBOSITY_QUIET
                );
                return false;
            }

            $is_installable = in_array(
                $plugin->fields['state'],
                [
                    Plugin::TOBECLEANED, // Can be in this state if check was done without the plugin dir
                    Plugin::NOTINSTALLED, // Can be not installed if plugin has been cleaned in plugin list
                    Plugin::NOTUPDATED, // Plugin 1.8.0 version has never been installed
                ]
            );
            if ($is_installable) {
                if ($this->input->getOption('update-plugin')) {
                    $message  = sprintf(
                        __('Migrating plugin to %s version...'),
                        self::RACKS_REQUIRED_VERSION
                    );
                    $this->output->writeln(
                        '<info>' . $message . '</info>',
                        OutputInterface::VERBOSITY_NORMAL
                    );

                    ob_start();
                    $plugin->install($plugin->fields['id']);
                    ob_end_clean();

                   // Reload and check migration result
                    $plugin->getFromDB($plugin->fields['id']);
                    if (!in_array($plugin->fields['state'], [Plugin::TOBECONFIGURED, Plugin::NOTACTIVATED])) {
                            $message  = sprintf(
                                __('Plugin migration to %s version failed.'),
                                self::RACKS_REQUIRED_VERSION
                            );
                            $this->output->writeln(
                                '<error>' . $message . '</error>',
                                OutputInterface::VERBOSITY_QUIET
                            );
                            return false;
                    }
                } else {
                    $message = sprintf(
                        __('Racks plugin data has to be updated to %s version. It can be done using the --update-plugin option.'),
                        self::RACKS_REQUIRED_VERSION
                    );
                    $this->output->writeln(
                        '<comment>' . $message . '</comment>',
                        OutputInterface::VERBOSITY_QUIET
                    );
                    return false;
                }
            }

            $is_state_ok   = in_array(
                $plugin->fields['state'],
                [
                    Plugin::ACTIVATED, // Should not be possible as 1.8.0 is not compatible with 9.3
                    Plugin::TOBECONFIGURED, // Should not be possible as check_config of plugin returns always true
                    Plugin::NOTACTIVATED,
                ]
            );
            if (!$is_state_ok) {
                 // Should not happens as installation should put plugin in awaited state
                 throw new \Symfony\Component\Console\Exception\LogicException('Unexpected plugin state.');
            }
        }

        $rack_tables = [
            'glpi_plugin_racks_itemspecifications',
            'glpi_plugin_racks_others',
            'glpi_plugin_racks_othermodels',
            'glpi_plugin_racks_racks',
            'glpi_plugin_racks_racks_items',
            'glpi_plugin_racks_rackmodels',
            'glpi_plugin_racks_racktypes',
            'glpi_plugin_racks_rackstates',
            'glpi_plugin_racks_roomlocations',
        ];
        $missing_tables = false;
        foreach ($rack_tables as $table) {
            if (!$this->db->tableExists($table)) {
                $this->output->writeln(
                    '<error>' . sprintf(__('Racks plugin table "%s" is missing.'), $table) . '</error>',
                    OutputInterface::VERBOSITY_QUIET
                );
                $missing_tables = true;
            }
        }
        if ($missing_tables) {
            $this->output->writeln(
                '<error>' . __('Migration cannot be done.') . '</error>',
                OutputInterface::VERBOSITY_QUIET
            );
            return false;
        }

        return true;
    }

    /**
     * Clean data from core tables.
     *
     * @throws RuntimeException
     */
    private function cleanCoreTables()
    {

        $core_tables = [
            'glpi_datacenters',
            'glpi_dcrooms',
            'glpi_items_racks',
            'glpi_pdus',
            'glpi_racks',
            'glpi_rackmodels',
            'glpi_racktypes',
            'glpi_passivedcequipments',
            'glpi_passivedcequipmenttypes',
            'glpi_passivedcequipmentmodels',
        ];

        foreach ($core_tables as $table) {
            $result = $this->db->query('TRUNCATE ' . $this->db->quoteName($table));

            if (!$result) {
                throw new \Symfony\Component\Console\Exception\RuntimeException(
                    sprintf('Unable to truncate table "%s"', $table)
                );
            }
        }
    }


    private function migratePlugin()
    {

        $no_interaction = $this->input->getOption('no-interaction');

        $skip_errors = $this->input->getOption('skip-errors');

       // Create datacenter
        $this->datacenter_id = $this->createDatacenter();
        if (null === $this->datacenter_id && !$skip_errors) {
            return false;
        }

        if (!$this->input->getOption('ignore-other-elements')) {
            if ($no_interaction) {
                $this->output->writeln(
                    '<comment>' . __('Other models and items cannot be migrated when --no-interaction option is used.') . '</comment>',
                    OutputInterface::VERBOSITY_NORMAL
                );
            } else {
                if (!$this->importOtherElements() && !$skip_errors) {
                    return false;
                }
            }
        }

        $failure = (!$this->importItemsSpecifications() && !$skip_errors)
         || (!$this->importRackModels() && !$skip_errors)
         || (!$this->importRackTypes() && !$skip_errors)
         || (!$this->importRackStates() && !$skip_errors)
         || (!$this->importRooms() && !$skip_errors)
         || (!$this->importRacks() && !$skip_errors)
         || (!$this->importRackItems() && !$skip_errors);

        return !$failure;
    }

    /**
     * Create temporary datacenter.
     *
     * @return null|integer Datacenter id, or null in case of failure
     */
    private function createDatacenter()
    {

        $this->output->writeln(
            '<comment>' . __('Creating datacenter...') . '</comment>',
            OutputInterface::VERBOSITY_VERBOSE
        );

        $dc = new Datacenter();
        $dc_fields = [
            'name' => 'Temp Datacenter (from racks plugin migration script)',

        ];

        if (!($dc_id = $dc->getFromDBByCrit($dc_fields))) {
            $dc_id = $dc->add($dc_fields);
        }

        if (false === $dc_id) {
            $this->outputImportError(
                '<error>' . __('Unable to create datacenter.') . '</error>'
            );
            return null;
        }

        return $dc_id;
    }

    /**
     * Import other models and items.
     *
     * @throws LogicException
     *
     * @return boolean True in case of success, false in case of errors.
     */
    private function importOtherElements()
    {

        $has_errors = false;

       // Import other models
        $this->output->writeln(
            '<comment>' . __('Importing other models...') . '</comment>',
            OutputInterface::VERBOSITY_VERBOSE
        );

        $othermodels_iterator = $this->db->request(
            [
                'FROM' => 'glpi_plugin_racks_othermodels'
            ]
        );

        if ($count_othermodels = $othermodels_iterator->count()) {
            $this->output->writeln(
                [
                    '<comment>' . __('Other items do not exist in GLPI core.') . '</comment>',
                    sprintf(
                        __('We found %d models for other items. For each, we will ask you where you want to import it.'),
                        $count_othermodels
                    ),
                ],
                OutputInterface::VERBOSITY_QUIET
            );

            foreach ($othermodels_iterator as $othermodel) {
                 $model_label = $othermodel['name'];
                if (strlen($othermodel['comment'])) {
                    $model_label .= ' (' . $othermodel['comment'] . ')';
                }

                 /** @var QuestionHelper $question_helper */
                 $question_helper = $this->getHelper('question');
                 $answer = $question_helper->ask(
                     $this->input,
                     $this->output,
                     new ChoiceQuestion(
                         sprintf(__('Where do you want to import "%s"?'), $model_label),
                         [
                             self::OTHER_TYPE_CHOICE_COMPUTER            => Computer::getTypeName(1),
                             self::OTHER_TYPE_CHOICE_NETWORKEQUIPEMENT   => NetworkEquipment::getTypeName(1),
                             self::OTHER_TYPE_CHOICE_PERIPHERAL          => Peripheral::getTypeName(1),
                             self::OTHER_TYPE_CHOICE_PDU                 => PDU::getTypeName(1),
                             self::OTHER_TYPE_CHOICE_MONITOR             => Monitor::getTypeName(1),
                             self::OTHER_TYPE_CHOICE_PASSIVEDCEQUIPEMENT => PassiveDCEquipment::getTypeName(1),
                             self::OTHER_TYPE_CHOICE_IGNORE              => __('Ignore (default)'),
                         ],
                         self::OTHER_TYPE_CHOICE_IGNORE
                     )
                 );

                if (self::OTHER_TYPE_CHOICE_IGNORE === $answer) {
                     continue;
                }

                 $new_itemtype       = null;
                 $new_model_itemtype = null;
                switch ($answer) {
                    case self::OTHER_TYPE_CHOICE_COMPUTER:
                         $new_itemtype       = Computer::class;
                         $new_model_itemtype = ComputerModel::class;
                        break;
                    case self::OTHER_TYPE_CHOICE_NETWORKEQUIPEMENT:
                        $new_itemtype       = NetworkEquipment::class;
                        $new_model_itemtype = NetworkEquipmentModel::class;
                        break;
                    case self::OTHER_TYPE_CHOICE_PERIPHERAL:
                        $new_itemtype       = Peripheral::class;
                        $new_model_itemtype = PeripheralModel::class;
                        break;
                    case self::OTHER_TYPE_CHOICE_PDU:
                        $new_itemtype       = PDU::class;
                        $new_model_itemtype = PDUModel::class;
                        break;
                    case self::OTHER_TYPE_CHOICE_MONITOR:
                        $new_itemtype       = Monitor::class;
                        $new_model_itemtype = MonitorModel::class;
                        break;
                    case self::OTHER_TYPE_CHOICE_PASSIVEDCEQUIPEMENT:
                        $new_itemtype       = PassiveDCEquipment::class;
                        $new_model_itemtype = PassiveDCEquipmentModel::class;
                        break;
                }

                if (null === $new_model_itemtype) {
                    throw new \Symfony\Component\Console\Exception\LogicException(
                        sprintf('Answer "%s" has no corresponding itemtype.', $answer)
                    );
                }

                $new_model = new $new_model_itemtype();
                $new_model_fields = Sanitizer::sanitize([
                    'name'    => $othermodel['name'],
                    'comment' => $othermodel['comment'],
                ]);

                if (
                    !($new_model_id = $new_model->getFromDBByCrit($new_model_fields))
                     && !($new_model_id = $new_model->add($new_model_fields))
                ) {
                     $has_errors = true;

                     $message = sprintf(__('Unable to import other model "%s".'), $model_label);
                     $this->outputImportError($message);
                    if ($this->input->getOption('skip-errors')) {
                        continue;
                    } else {
                        return false;
                    }
                }

                 $this->addElementToMapping(
                     'PluginRacksOtherModel',
                     $othermodel['id'],
                     $new_model_itemtype,
                     $new_model_id
                 );

                 // Import items from model
                 $message = sprintf(__('Importing items from model "%s"...'), $model_label);
                 $this->output->writeln(
                     '<comment>' . $message . '</comment>',
                     OutputInterface::VERBOSITY_NORMAL
                 );

                 $otheritems_iterator = $this->db->request(
                     [
                         'FROM'  => 'glpi_plugin_racks_others',
                         'WHERE' => [
                             'plugin_racks_othermodels_id' => $othermodel['id'],
                         ],
                     ]
                 );

                if ($otheritems_iterator->count()) {
                     $progress_bar = new ProgressBar($this->output, $otheritems_iterator->count());
                     $progress_bar->start();

                     $fk_new_model = getForeignKeyFieldForItemType($new_model_itemtype);

                    foreach ($otheritems_iterator as $otheritem) {
                        $progress_bar->advance(1);

                        $new_item_fields = Sanitizer::sanitize([
                            'name'        => strlen($otheritem['name'])
                                     ? $otheritem['name']
                                     : $otheritem['id'],
                            'entities_id' => $otheritem['entities_id'],
                            $fk_new_model => $new_model_id
                        ]);

                        $new_item = new $new_itemtype();

                        if (!($new_item_id = $new_item->add($new_item_fields))) {
                                   $has_errors = true;

                                   $message = sprintf(
                                       __('Unable to import other item "%s".'),
                                       $new_item_fields['name']
                                   );
                                   $this->outputImportError($message, $progress_bar);
                            if ($this->input->getOption('skip-errors')) {
                                continue;
                            } else {
                                return false;
                            }
                        }

                        $this->addElementToMapping(
                            'PluginRacksOther',
                            $otheritem['id'],
                            $new_itemtype,
                            $new_item_id
                        );
                    }

                     $progress_bar->finish();
                     $this->output->write(PHP_EOL);
                } else {
                    $this->output->writeln(
                        '<comment>' . __('No items found.') . '</comment>',
                        OutputInterface::VERBOSITY_NORMAL
                    );
                }
            }
        }

        return !$has_errors;
    }

    /**
     * Import items specifications.
     *
     * @return boolean True in case of success, false in case of errors.
     */
    private function importItemsSpecifications()
    {

        $has_errors = false;

        $this->output->writeln(
            '<comment>' . __('Importing items specifications...') . '</comment>',
            OutputInterface::VERBOSITY_NORMAL
        );

        $specs_iterator = $this->db->request(
            [
                'FROM'  => 'glpi_plugin_racks_itemspecifications',
                'ORDER' => 'id ASC'
            ]
        );

        if ($specs_iterator->count()) {
            $progress_bar = new ProgressBar($this->output, $specs_iterator->count());
            $progress_bar->start();

            foreach ($specs_iterator as $spec) {
                $progress_bar->advance(1);

                $message = sprintf(
                    __('Importing specifications for model %s (%s)...'),
                    $spec['itemtype'],
                    $spec['model_id']
                );
                $this->writelnOutputWithProgressBar(
                    $message,
                    $progress_bar,
                    OutputInterface::VERBOSITY_VERY_VERBOSE
                );

                $model = $this->getCorrespondingItem($spec['itemtype'], $spec['model_id']);

                if (null === $model) {
                     $message = sprintf(
                         __('Model %s (%s) not found.'),
                         $spec['itemtype'],
                         $spec['model_id']
                     );
                     $this->writelnOutputWithProgressBar(
                         $message,
                         $progress_bar,
                         OutputInterface::VERBOSITY_VERBOSE
                     );
                     continue;
                }

                $model_input = [
                    'id'                => $model->fields['id'],
                    'required_units'    => $spec['size'],
                    'depth'             => ($spec['length'] == 1 ? 1 : 0.5),
                    'weight'            => $spec['weight'],
                    'is_half_rack'      => 0,
                    'power_connections' => $spec['nb_alim'],
                ];

                if (!$model->update($model_input)) {
                    $has_errors = true;

                    $message = sprintf(
                        __('Unable to update model %s (%s).'),
                        $spec['itemtype'],
                        $spec['model_id']
                    );
                    $this->outputImportError($message, $progress_bar);
                    if ($this->input->getOption('skip-errors')) {
                         continue;
                    } else {
                        return false;
                    }
                }
            }

            $progress_bar->finish();
            $this->output->write(PHP_EOL);
        } else {
            $this->output->writeln(
                '<comment>' . __('No items specifications found.') . '</comment>',
                OutputInterface::VERBOSITY_NORMAL
            );
        }

        return !$has_errors;
    }

    /**
     * Import rack models.
     *
     * @return boolean True in case of success, false in case of errors.
     */
    private function importRackModels()
    {

        $has_errors = false;

        $this->output->writeln(
            '<comment>' . __('Importing rack models...') . '</comment>',
            OutputInterface::VERBOSITY_NORMAL
        );

        $models_iterator = $this->db->request(
            [
                'FROM' => 'glpi_plugin_racks_rackmodels',
            ]
        );

        if ($models_iterator->count()) {
            $progress_bar = new ProgressBar($this->output, $models_iterator->count());
            $progress_bar->start();

            foreach ($models_iterator as $old_model) {
                $progress_bar->advance(1);

                $message = sprintf(
                    __('Importing rack model "%s"...'),
                    $old_model['name']
                );
                $this->writelnOutputWithProgressBar(
                    $message,
                    $progress_bar,
                    OutputInterface::VERBOSITY_VERY_VERBOSE
                );

                $rackmodel = new RackModel();
                $rackmodel_fields = Sanitizer::sanitize(
                    [
                        'name'    => $old_model['name'],
                        'comment' => $old_model['comment'],
                    ]
                );

                if (
                    !($rackmodel_id = $rackmodel->getFromDBByCrit($rackmodel_fields))
                    && !($rackmodel_id = $rackmodel->add($rackmodel_fields))
                ) {
                     $has_errors = true;

                     $message = sprintf(__('Unable to import rack model "%s".'), $old_model['name']);
                     $this->outputImportError($message, $progress_bar);
                    if ($this->input->getOption('skip-errors')) {
                        continue;
                    } else {
                        return false;
                    }
                }

                $this->addElementToMapping(
                    'PluginRacksRackModel',
                    $old_model['id'],
                    RackModel::class,
                    $rackmodel_id
                );
            }

            $progress_bar->finish();
            $this->output->write(PHP_EOL);
        } else {
            $this->output->writeln(
                '<comment>' . __('No rack models found.') . '</comment>',
                OutputInterface::VERBOSITY_NORMAL
            );
        }

        return !$has_errors;
    }

    /**
     * Import rack types.
     *
     * @return boolean True in case of success, false in case of errors.
     */
    private function importRackTypes()
    {

        $has_errors = false;

        $this->output->writeln(
            '<comment>' . __('Importing rack types...') . '</comment>',
            OutputInterface::VERBOSITY_NORMAL
        );

        $types_iterator = $this->db->request(
            [
                'FROM' => 'glpi_plugin_racks_racktypes',
            ]
        );

        if ($types_iterator->count()) {
            $progress_bar = new ProgressBar($this->output, $types_iterator->count());
            $progress_bar->start();

            foreach ($types_iterator as $old_type) {
                $progress_bar->advance(1);

                $message = sprintf(
                    __('Importing rack type "%s"...'),
                    $old_type['name']
                );
                $this->writelnOutputWithProgressBar(
                    $message,
                    $progress_bar,
                    OutputInterface::VERBOSITY_VERY_VERBOSE
                );

                $racktype = new RackType();
                $racktype_fields = Sanitizer::sanitize(
                    [
                        'name'         => $old_type['name'],
                        'entities_id'  => $old_type['entities_id'],
                        'is_recursive' => $old_type['is_recursive'],
                        'comment'      => $old_type['comment'],
                    ]
                );

                if (
                    !($racktype_id = $racktype->getFromDBByCrit($racktype_fields))
                    && !($racktype_id = $racktype->add($racktype_fields))
                ) {
                     $has_errors = true;

                     $message = sprintf(__('Unable to import rack type "%s".'), $old_type['name']);
                     $this->outputImportError($message, $progress_bar);
                    if ($this->input->getOption('skip-errors')) {
                        continue;
                    } else {
                        return false;
                    }
                }

                $this->addElementToMapping(
                    'PluginRacksRackType',
                    $old_type['id'],
                    RackType::class,
                    $racktype_id
                );
            }

            $progress_bar->finish();
            $this->output->write(PHP_EOL);
        } else {
            $this->output->writeln(
                '<comment>' . __('No rack models found.') . '</comment>',
                OutputInterface::VERBOSITY_NORMAL
            );
        }

        return !$has_errors;
    }

    /**
     * Import rack states.
     *
     * @return boolean True in case of success, false in case of errors.
     */
    private function importRackStates()
    {

        $has_errors = false;

        $this->output->writeln(
            '<comment>' . __('Importing rack states...') . '</comment>',
            OutputInterface::VERBOSITY_NORMAL
        );

        $states_iterator = $this->db->request(
            [
                'FROM' => 'glpi_plugin_racks_rackstates',
            ]
        );

        if ($states_iterator->count()) {
            $progress_bar = new ProgressBar($this->output, $states_iterator->count());
            $progress_bar->start();

            foreach ($states_iterator as $old_state) {
                $progress_bar->advance(1);

                $message = sprintf(
                    __('Importing rack state "%s"...'),
                    $old_state['name']
                );
                $this->writelnOutputWithProgressBar(
                    $message,
                    $progress_bar,
                    OutputInterface::VERBOSITY_VERY_VERBOSE
                );

                $state = new State();
                $state_fields = Sanitizer::sanitize(
                    [
                        'name'      => $old_state['name'],
                        'states_id' => 0,
                    ]
                );

                if (!($state_id = $state->getFromDBByCrit($state_fields))) {
                     $state_fields['comment']      = $old_state['comment'];
                     $state_fields['entities_id']  = $old_state['entities_id'];
                     $state_fields['is_recursive'] = $old_state['is_recursive'];

                    if (!($state_id = $state->add($state_fields))) {
                        $has_errors = true;

                        $message = sprintf(__('Unable to import rack state "%s".'), $old_state['name']);
                        $this->outputImportError($message, $progress_bar);
                        if ($this->input->getOption('skip-errors')) {
                             continue;
                        } else {
                            return false;
                        }
                    }
                }

                $this->addElementToMapping(
                    'PluginRacksRackState',
                    $old_state['id'],
                    State::class,
                    $state_id
                );
            }

            $progress_bar->finish();
            $this->output->write(PHP_EOL);
        } else {
            $this->output->writeln(
                '<comment>' . __('No rack states found.') . '</comment>',
                OutputInterface::VERBOSITY_NORMAL
            );
        }

        return !$has_errors;
    }

    /**
     * Import rooms.
     *
     * @return boolean True in case of success, false in case of errors.
     */
    private function importRooms()
    {

        $has_errors = false;

        $this->output->writeln(
            '<comment>' . __('Importing rooms...') . '</comment>',
            OutputInterface::VERBOSITY_NORMAL
        );

        $rooms_iterator = $this->db->request(
            [
                'FROM' => 'glpi_plugin_racks_roomlocations',
            ]
        );

        if ($rooms_iterator->count()) {
            $progress_bar = new ProgressBar($this->output, $rooms_iterator->count());
            $progress_bar->start();

            foreach ($rooms_iterator as $old_room) {
                $progress_bar->advance(1);

                $message = sprintf(
                    __('Importing room "%s"...'),
                    $old_room['completename']
                );
                $this->writelnOutputWithProgressBar(
                    $message,
                    $progress_bar,
                    OutputInterface::VERBOSITY_VERY_VERBOSE
                );

                $room = new DCRoom();
                $room_fields = Sanitizer::sanitize(
                    [
                        'name'           => $old_room['completename'],
                        'entities_id'    => $old_room['entities_id'],
                        'is_recursive'   => 1,
                        'datacenters_id' => $this->datacenter_id,
                        'vis_cols'       => 10,
                        'vis_rows'       => 10,
                    ]
                );

                if (
                    !($room_id = $room->getFromDBByCrit($room_fields))
                    && !($room_id = $room->add($room_fields))
                ) {
                     $has_errors = true;

                     $message = sprintf(__('Unable to import room "%s".'), $old_room['completename']);
                     $this->outputImportError($message, $progress_bar);
                    if ($this->input->getOption('skip-errors')) {
                        continue;
                    } else {
                        return false;
                    }
                }

                $this->addElementToMapping(
                    'PluginRacksRoomLocation',
                    $old_room['id'],
                    DCRoom::class,
                    $room_id
                );
            }

            $progress_bar->finish();
            $this->output->write(PHP_EOL);
        } else {
            $this->output->writeln(
                '<comment>' . __('No rooms found.') . '</comment>',
                OutputInterface::VERBOSITY_NORMAL
            );
        }

        return !$has_errors;
    }

    /**
     * Import racks.
     *
     * @return boolean True in case of success, false in case of errors.
     */
    private function importRacks()
    {

        $has_errors = false;

        $this->output->writeln(
            '<comment>' . __('Importing racks...') . '</comment>',
            OutputInterface::VERBOSITY_NORMAL
        );

        $racks_iterator = $this->db->request(
            [
                'FROM' => 'glpi_plugin_racks_racks',
            ]
        );

        if ($racks_iterator->count()) {
            $i = 0;

            $progress_bar = new ProgressBar($this->output, $racks_iterator->count());
            $progress_bar->start();

            foreach ($racks_iterator as $old_rack) {
                $progress_bar->advance(1);

                $message = sprintf(
                    __('Importing rack "%s"...'),
                    $old_rack['name']
                );
                $this->writelnOutputWithProgressBar(
                    $message,
                    $progress_bar,
                    OutputInterface::VERBOSITY_VERY_VERBOSE
                );

                $rackmodel = $this->getCorrespondingItem(
                    'PluginRacksRackModel',
                    $old_rack['plugin_racks_rackmodels_id']
                );
                $racktype  = $this->getCorrespondingItem(
                    'PluginRacksRackType',
                    $old_rack['plugin_racks_racktypes_id']
                );
                $rackstate = $this->getCorrespondingItem(
                    'PluginRacksRackState',
                    $old_rack['plugin_racks_rackstates_id']
                );
                $room      = $this->getCorrespondingItem(
                    'PluginRacksRackState',
                    $old_rack['plugin_racks_rackstates_id']
                );
                if (null !== $room) {
                     $room_id = $room->fields['id'];
                } else {
                    $room_id = $this->getFallbackRoomId();
                    if (0 == $room_id && !$this->input->getOption('skip-errors')) {
                        return false;
                    }
                }

                $rack = new Rack();
                $rack_fields = Sanitizer::sanitize(
                    [
                        'name'             => $old_rack['name'],
                        'comment'          => "Imported from rack plugin",
                        'entities_id'      => $old_rack['entities_id'],
                        'is_recursive'     => $old_rack['is_recursive'],
                        'locations_id'     => $old_rack['locations_id'],
                        'serial'           => $old_rack['serial'],
                        'rackmodels_id'    => null !== $rackmodel ? $rackmodel->fields['id'] : 0,
                        'manufacturers_id' => $old_rack['manufacturers_id'],
                        'racktypes_id'     => null !== $racktype ? $racktype->fields['id'] : 0,
                        'states_id'        => null !== $rackstate ? $rackstate->fields['id'] : 0,
                        'users_id_tech'    => $old_rack['users_id_tech'],
                        'groups_id_tech'   => $old_rack['groups_id_tech'],
                        'width'            => (int) $old_rack['width'],
                        'height'           => (int) $old_rack['height'],
                        'depth'            => (int) $old_rack['depth'],
                        'max_weight'       => (int) $old_rack['weight'],
                        'number_units'     => $old_rack['rack_size'],
                        'is_template'      => $old_rack['is_template'],
                        'template_name'    => $old_rack['template_name'],
                        'is_deleted'       => $old_rack['is_deleted'],
                        'dcrooms_id'       => $room_id,
                        'bgcolor'          => "#FEC95C",
                    ]
                );

                if (!($rack_id = $rack->getFromDBByCrit($rack_fields))) {
                     $rack_fields['position'] = "9999999999999,-" . (++$i);

                    if (!($rack_id = $rack->add($rack_fields))) {
                        $has_errors = true;

                        $message = sprintf(__('Unable to import rack "%s".'), $old_rack['name']);
                        $this->outputImportError($message, $progress_bar);
                        if ($this->input->getOption('skip-errors')) {
                             continue;
                        } else {
                            return false;
                        }
                    }
                }

                $this->addElementToMapping(
                    'PluginRacksRack',
                    $old_rack['id'],
                    Rack::class,
                    $rack_id
                );
            }

            $progress_bar->finish();
            $this->output->write(PHP_EOL);
        } else {
            $this->output->writeln(
                '<comment>' . __('No racks found.') . '</comment>',
                OutputInterface::VERBOSITY_NORMAL
            );
        }

        return !$has_errors;
    }

    /**
     * Import rack items.
     *
     * @return boolean True in case of success, false in case of errors.
     */
    private function importRackItems()
    {

        $has_errors = false;

        $this->output->writeln(
            '<comment>' . __('Importing rack items...') . '</comment>',
            OutputInterface::VERBOSITY_NORMAL
        );

        $items_iterator = $this->db->request(
            [
                'FROM'  => 'glpi_plugin_racks_racks_items',
                'ORDER' => 'id'
            ]
        );

        if ($items_iterator->count()) {
            $progress_bar = new ProgressBar($this->output, $items_iterator->count());
            $progress_bar->start();

            foreach ($items_iterator as $old_item) {
                $progress_bar->advance(1);

                $itemtype = str_replace('Model', '', $old_item['itemtype']); // Plugin was storing model type as itemtype
                $items_id = $old_item['items_id'];

                $message = sprintf(
                    __('Importing rack item %s (%s)...'),
                    $itemtype,
                    $items_id
                );
                $this->writelnOutputWithProgressBar(
                    $message,
                    $progress_bar,
                    OutputInterface::VERBOSITY_VERY_VERBOSE
                );

                $item = $this->getCorrespondingItem($itemtype, $items_id);
                if (null === $item) {
                     $message = sprintf(__('Item %s (%s) not found.'), $itemtype, $items_id);
                     $this->writelnOutputWithProgressBar(
                         $message,
                         $progress_bar,
                         OutputInterface::VERBOSITY_VERBOSE
                     );
                      continue;
                }

                $item_input = [
                    'itemtype' => $item->getType(),
                    'items_id' => $item->fields['id'],
                ];

                $item_rack = new Item_Rack();
                if ($item_rack->getFromDBByCrit($item_input)) {
                    $message = sprintf(
                        __('Skipping item %s (%s) which is already linked to a rack.'),
                        $itemtype,
                        $items_id
                    );
                    $this->writelnOutputWithProgressBar(
                        $message,
                        $progress_bar,
                        OutputInterface::VERBOSITY_VERBOSE
                    );
                    continue;
                }

                $required_units = 1;
                $modeltype = $item->getType() . 'Model';
                if (class_exists($modeltype)) {
                    $model_fkey = getForeignKeyFieldForTable($modeltype::getTable());
                    if (
                        array_key_exists($model_fkey, $item->fields)
                        && null !== ($model = $this->getCorrespondingItem($modeltype, $item->fields[$model_fkey]))
                    ) {
                        $required_units = $model->fields['required_units'];
                    }
                }

                $position = $old_item['position'] - $required_units + 1;

                $rack = $this->getCorrespondingItem(
                    'PluginRacksRack',
                    $old_item['plugin_racks_racks_id']
                );

                $item_input = $item_input + [
                    'racks_id'    => null !== $rack ? $rack->fields['id'] : 0,
                    'position'    => $position,
                    'hpos'        => 0,
                    'bgcolor'     => '#69CEBA',
                    'orientation' => ($old_item['faces_id'] == 1 ? Rack::FRONT : Rack::REAR),
                ];

                if (!$item_rack->add($item_input)) {
                    $has_errors = true;

                    $message = sprintf(
                        __('Unable to import rack item %s (%s).'),
                        $itemtype,
                        $items_id
                    );
                    $this->outputImportError($message, $progress_bar);
                    if ($this->input->getOption('skip-errors')) {
                          continue;
                    } else {
                        return false;
                    }
                }
            }

            $progress_bar->finish();
            $this->output->write(PHP_EOL);
        } else {
            $this->output->writeln(
                '<comment>' . __('No rack items found.') . '</comment>',
                OutputInterface::VERBOSITY_NORMAL
            );
        }

        return !$has_errors;
    }

    /**
     * Add an element to mapping.
     *
     * @param string  $old_itemtype
     * @param integer $old_id
     * @param string  $new_itemtype
     * @param integer $new_id
     *
     * @return void
     */
    private function addElementToMapping($old_itemtype, $old_id, $new_itemtype, $new_id)
    {

        if (!array_key_exists($old_itemtype, $this->elements_mapping)) {
            $this->elements_mapping[$old_itemtype] = [];
        }
        $this->elements_mapping[$old_itemtype][$old_id] = [
            'itemtype' => $new_itemtype,
            'id'       => $new_id,
        ];
    }

    /**
     * Returns item corresponding to itemtype and id.
     * If item has been migrated to another itemtype, il will return the new item.
     *
     * @param string  $itemtype
     * @param integer $id
     *
     * @return null|CommonDBTM
     */
    private function getCorrespondingItem($itemtype, $id)
    {

        if (
            array_key_exists($itemtype, $this->elements_mapping)
            && array_key_exists($id, $this->elements_mapping[$itemtype])
        ) {
           // Element exists in mapping, get new element
            $mapping  = $this->elements_mapping[$itemtype][$id];
            $id       = $mapping['id'];
            $itemtype = $mapping['itemtype'];
        }

        if (!class_exists($itemtype)) {
            return null;
        }

        $item = new $itemtype();
        if (!$item->getFromDB($id)) {
            return null;
        }

        return $item;
    }

    /**
     * Returns fallback room id.
     *
     * @return number
     */
    private function getFallbackRoomId()
    {

        if (null === $this->fallback_room_id) {
            $room = new DCRoom();
            $room_fields = [
                'name'           => 'Temp room (from plugin racks migration script)',
                'entities_id'    => 0,
                'is_recursive'   => 1,
                'datacenters_id' => $this->datacenter_id,
                'vis_cols'       => 10,
                'vis_rows'       => 10,
            ];

            if (
                !($room_id = $room->getFromDBByCrit($room_fields))
                && !($room_id = $room->add($room_fields))
            ) {
                $this->outputImportError(__('Unable to create default room.'));

                $room_id = 0;
            }

            $this->fallback_room_id = $room_id;
        }

        return $this->fallback_room_id;
    }

    /**
     * Returns verbosity level for import errors.
     *
     * @return number
     */
    private function getImportErrorsVerbosity()
    {

        return $this->input->getOption('skip-errors')
         ? OutputInterface::VERBOSITY_NORMAL
         : OutputInterface::VERBOSITY_QUIET;
    }

    /**
     * Output import error message.
     *
     * @param string           $message
     * @param ProgressBar|null $progress_bar
     *
     * @return void
     */
    private function outputImportError($message, ProgressBar $progress_bar = null)
    {

        $skip_errors = $this->input->getOption('skip-errors');

        $verbosity = $skip_errors
         ? OutputInterface::VERBOSITY_NORMAL
         : OutputInterface::VERBOSITY_QUIET;

        $message = '<error>' . $message . '</error>';

        if ($skip_errors && $progress_bar instanceof ProgressBar) {
            $this->writelnOutputWithProgressBar(
                $message,
                $progress_bar,
                $verbosity
            );
        } else {
            if (!$skip_errors && $progress_bar instanceof ProgressBar) {
                $this->output->write(PHP_EOL); // Keep progress bar last state and go to next line
            }
            $this->output->writeln(
                $message,
                $verbosity
            );
        }
    }
}
