<?php
// Copyright 1999-2017. Plesk International GmbH. All rights reserved.

namespace Plesk\MigrationLibV8\Common\Backend\Files;

use Plesk\MigrationLibV8\SiteImport\Entity\SiteInfo\File;
use Plesk\MigrationLibV8\Utils\FileUtils;
use Plesk\MigrationLibV8\Plesk\PleskFunctions;

/**
 * @property mixed GLOBAL "[GLOBAL]" section of configuration file
 * @property mixed plesk "[plesk]" section of configuration file
 * @property mixed source "[source]" section of configuration file
 */
class Config extends \Zend_Config_Ini
{
    /** @var string */
    private $_path;
    private $_defaultSourceSectionName;

    /**
     * \Plesk\MigrationLibV8\Common\Backend\Files\Config constructor.
     *
     * @param string $path
     */
    public function __construct($path)
    {
        $this->_path = $path;
        if (!FileUtils::fileExists($this->_path)) {
            FileUtils::putFileContents($this->_path, '');
        }
        parent::__construct($this->getPath(), null, true);
    }

    /**
     * Get path to the configuration file.
     *
     * @return string
     */
    public function getPath()
    {
        return $this->_path;
    }

    /**
     * Save configuration file to file.
     * Do not forget to call that function if you performed changes - they are not written automatically.
     */
    public function save()
    {
        $writer = new \Zend_Config_Writer_Ini([
            'config' => $this,
            'filename' => $this->getPath()
        ]);
        $iniFileContents = $writer->render();
        if (!FileUtils::fileExists($this->getPath())) {
            FileUtils::putFileContents($this->getPath(), $iniFileContents);
        } else {
            # Overwrite only if content differs, to reduce probability of certain race conditions,
            # (race conditions are still possible!) when one process tries to read the config
            # while another one overwrites it even if the file was not changed.
            $oldIniFileContents = FileUtils::fileGetContents($this->getPath());
            if ($oldIniFileContents != $iniFileContents) {
                FileUtils::putFileContents($this->getPath(), $iniFileContents);
            }
        }
    }

    /**
     * Initialize default options, that do not depend on user's input and should be always presented
     */
    public function initDefaultOptions()
    {
        // do not check log priority of Plesk: that check stops migration if verbose logging is enabled
        $this->setGlobalValue('skip-log-priority-check', $this->_encodeBooleanValue(true));
        // do not set directory permissions - there were issues with them on Windows
        $this->setGlobalValue('skip-set-session-directory-permissions', $this->_encodeBooleanValue(true));
        // use per-session logs
        $this->setGlobalValue('use-separate-log', $this->_encodeBooleanValue(true));

        $this->setTargetValue('ip', PleskFunctions::getDefaultIPAddress());
    }

    /**
     * Set source panel type.
     *
     * @param $sourceType string
     */
    public function setSourceType($sourceType)
    {
        $this->setGlobalValue('source-type', $sourceType);
    }

    /**
     * Get source panel type.
     *
     * @return string
     */
    public function getSourceType()
    {
        return $this->_getGlobalValue('source-type');
    }

    /**
     * Set directory to store session files.
     *
     * @param $sessionDirectory string
     */
    public function setSessionDirectory($sessionDirectory)
    {
        $this->setGlobalValue('session-dir', $sessionDirectory);
    }

    /**
     * Set OS of the target server.
     *
     * @param $os string
     */
    public function setTargetOS($os)
    {
        $this->setTargetValue('os', $os);
    }

    /**
     * Set name of configuration file section where source server settings are stored.
     *
     * @param $sectionName string
     */
    public function setDefaultSourceSectionName($sectionName)
    {
        $this->_defaultSourceSectionName = $sectionName;
    }

    /**
     * Set global configuration option.
     *
     * @param $name string name of the option
     * @param $value string value of the option
     */
    public function setGlobalValue($name, $value)
    {
        if (is_null($this->GLOBAL)) {
            $this->GLOBAL = [];
        }
        $this->GLOBAL->$name = $value;
    }

    /**
     * Set configuration option for the target server.
     *
     * @param $name string name of the option
     * @param $value string value of the option
     */
    public function setTargetValue($name, $value)
    {
        if (is_null($this->plesk)) {
            $this->plesk = [];
        }
        $this->plesk->$name = $value;
    }

    /**
     * Get global configuration option.
     *
     * @param $name string
     * @param null $default
     * @return string
     */
    protected function _getGlobalValue($name, $default=null)
    {
        if (is_null($this->GLOBAL)) {
            return $default;
        } else {
            if (!is_null($this->GLOBAL->$name)) {
                return $this->GLOBAL->$name;
            } else {
                return $default;
            }
        }
    }

    /**
     * Get configuration option for the target server.
     *
     * @param $name string name of the option
     * @param null $default
     * @return string
     */
    protected function _getTargetValue($name, $default=null)
    {
        if (!is_null($this->plesk->$name)) {
            return $this->plesk->$name;
        } else {
            return $default;
        }
    }

    /**
     * Set configuration option for the main source server.
     *
     * @param $name string name of the option
     * @param $value string value of the option
     * @throws \Exception
     */
    public function setSourceValue($name, $value)
    {
        $sourceSection = $this->_getSourceSection();
        $sourceSection->$name = $value;
    }

    /**
     * Get configuration option for the main source server.
     *
     * @param $name string
     * @param null $default
     * @return string
     */
    public function getSourceValue($name, $default=null)
    {
        $sourceSection = $this->_getSourceSection();
        if (!is_null($sourceSection->$name)) {
            return $sourceSection->$name;
        } else {
            return $default;
        }
    }

    protected function _getSourceSection()
    {
        $sourceSectionName = $this->_getSourceSectionName();

        if (is_null($sourceSectionName)) {
            throw new \pm_Exception(
                "Internal error: the source section name is not set. " .
                "Call the 'setSourceSectionName' function before setting any source parameters."
            );
        }

        if (is_null($this->$sourceSectionName)) {
            $this->$sourceSectionName = [];
        }

        return $this->$sourceSectionName;
    }

    protected function _getSourceSectionName()
    {
        // First, try to read source section name from config file.
        $configSourceSections = $this->_getGlobalValue('sources');
        if ($configSourceSections !== null) {
            $sources = explode(",", $configSourceSections);
            foreach ($sources as $source) {
                $source = trim($source);
                if ($source !== '') {
                    return $source;
                }
            }
        }

        // Then, fallback to default section name.
        return $this->_defaultSourceSectionName;
    }

    /**
     * Parse boolean configuration value from string.
     *
     * @param $value string
     * @return bool
     */
    protected function _parseBooleanValue($value)
    {
        return in_array($value, ['true', '1', 'on']);
    }

    /**
     * Encode boolean value to a string that could be put to configuration file.
     *
     * @param $value bool
     * @return string
     */
    protected function _encodeBooleanValue($value)
    {
        if ($value) {
            return 'true';
        } else {
            return 'false';
        }
    }

    /**
     * Parse simple list value from string.
     *
     * @param $value string
     * @return array
     */
    protected function _parseListValue($value)
    {
        $result = [];
        foreach (explode(',', $value) as $item) {
            $result[] = trim($item);
        }
        return $result;
    }

    /**
     * Encode list value to a string that could be put to configuration file.
     *
     * @param $value
     * @return string
     */
    protected function _encodeListValue($value)
    {
        return implode(', ', $value);
    }

    /**
     * Overridden parse INI file function to allow other INI syntax supported by backend.
     *
     * The original function was taken and modified in the following way: instead of plain parse_ini_file,
     * we read file contents as string, then perform replacements, and call parse_ini_string function.
     * Other aspects (like error handling) are left as they were in the original function.
     *
     * @param string $filename
     * @return array
     * @throws \Zend_Config_Exception
     */
    protected function _parseIniFile($filename)
    {
        set_error_handler(array($this, '_loadFileErrorHandler'));

        // MODIFICATIONS START
        $iniFileContents = FileUtils::fileGetContents($filename);

        // Replace 'param: value' with 'param = "value"', as 'param: value' syntax is not supported by PHP
        $iniFileContents = preg_replace('/^\s*([-a-zA-Z0-9]+)\s*:\s*(.*?)(\s*)$/m', '$1 = "$2"$3', $iniFileContents);
        // Remove comments starting with "#", which are not supported by PHP
        $iniFileContents = preg_replace('/^\s*#.*$/m', '', $iniFileContents);

        // Raw read mode is used instead of normal to avoid converting boolean 'true' -> string '1' and boolean 'false' -> empty string ''
        $iniArray = parse_ini_string($iniFileContents, true, INI_SCANNER_RAW); // Warnings and errors are suppressed
        // MODIFICATIONS END

        restore_error_handler();

        // Check if there was a error while loading file
        if ($this->_loadFileErrorStr !== null) {
            /**
             * @see Zend_Config_Exception
             */
            /** @noinspection PhpIncludeInspection */
            require_once 'Zend/Config/Exception.php';
            throw new \Zend_Config_Exception($this->_loadFileErrorStr);
        }

        return $iniArray;
    }
}
