<?php
/**
 * @author      Laurent Jouanneau
 * @copyright   2009-2023 Laurent Jouanneau
 *
 * @see        http://jelix.org
 * @licence     GNU Lesser General Public Licence see LICENCE file or http://www.gnu.org/licenses/lgpl.html
 */

namespace Jelix\Installer;

use Jelix\Core\Infos\ModuleInfos;
use Jelix\Dependencies\Item;
use Jelix\Dependencies\Resolver;
use Jelix\IniFile\IniModifierInterface;
use Jelix\Version\VersionComparator;

/**
 * container for module properties, according to a specific entry point configuration.
 *
 * It represents the state of the module, as known by the application:
 * installation status, the module version known during the last installer launch
 * etc.
 */
class ModuleStatus
{
    /**
     * @var string
     */
    public $name;

    /**
     * indicate if the module is enabled into the application or not.
     *
     * @var bool
     */
    public $isEnabled = false;
    /**
     * @var string
     */
    public $dbProfile = '';

    /**
     * indicate if the module is marked as installed.
     *
     * @var bool true/false or 0/1
     */
    public $isInstalled = false;

    /**
     * The version of the module that has been installed.
     *
     * @var string
     */
    public $version;

    /**
     * @var string[] parameters for installation
     */
    public $parameters = array();

    public $skipInstaller = false;

    /**
     * the module is configured for any instance.
     */
    const CONFIG_SCOPE_APP = 0;

    /**
     * the module is configured only at the instance level
     * (installed by the user, not by the developer).
     */
    const CONFIG_SCOPE_LOCAL = 1;

    /**
     * indicate if the module is configured into the app, or only for
     * the instance, so only into local configuration.
     *
     * @var int one of CONFIG_SCOPE_* constants
     */
    public $configurationScope = 0;

    protected $path;

    /**
     * @param string $name   the name of the module
     * @param string $path   the path to the module
     * @param array  $config configuration of modules ([modules] section),
     *                       generated by the configuration compiler for a specific
     *                       entry point
     * @param boolean $isNativeModule true if this is a module installed natively into the application
     *   (versus a module installed into an instance of the application)
     */
    public function __construct($name, $path, $config, $isNativeModule)
    {
        $this->name = $name;
        $this->path = $path;
        $this->isEnabled = $config[$name.'.enabled'];
        $this->isInstalled = $config[$name.'.installed'];
        $this->version = (string) $config[$name.'.version'];

        if (isset($config[$name.'.dbprofile'])) {
            $this->dbProfile = $config[$name.'.dbprofile'];
        }

        if (isset($config[$name.'.installparam'])) {
            $this->parameters = self::unserializeParameters($config[$name.'.installparam']);
        }

        if (isset($config[$name.'.skipinstaller']) && $config[$name.'.skipinstaller'] == 'skip') {
            $this->skipInstaller = true;
        }

        $deprecatedLocally = isset($config[$name.'.localconf']) && $config[$name.'.localconf'];
        $this->configurationScope = (!$isNativeModule || $deprecatedLocally) ? self::CONFIG_SCOPE_LOCAL : self::CONFIG_SCOPE_APP;
    }

    public function getPath()
    {
        return $this->path;
    }

    public function getName()
    {
        return $this->name;
    }

    public function saveInfos(IniModifierInterface $configIni, $defaultParameters = array())
    {
        $previous = $configIni->getValue($this->name.'.enabled', 'modules');
        if ($previous === null || $previous != $this->isEnabled) {
            $configIni->setValue($this->name.'.enabled', $this->isEnabled, 'modules');
        }

        $this->setConfigInfo($configIni, 'dbprofile', ($this->dbProfile != 'default' ? $this->dbProfile : ''), '');
        $this->setConfigInfo($configIni, 'installparam', self::serializeParametersAsArray($this->parameters, $defaultParameters), '');
        $this->setConfigInfo($configIni, 'skipinstaller', ($this->skipInstaller ? 'skip' : ''), '');
        $this->setConfigInfo(
            $configIni,
            'localconf',
            ($this->configurationScope == self::CONFIG_SCOPE_LOCAL ? self::CONFIG_SCOPE_LOCAL : 0),
            self::CONFIG_SCOPE_APP
        );
    }

    /**
     * @param IniModifierInterface $configIni
     * @param string               $name
     * @param mixed                $value
     * @param mixed                $defaultValue
     */
    private function setConfigInfo($configIni, $name, $value, $defaultValue)
    {
        // only modify the file when the value is not already set
        // to avoid to have to save the ini file  #perfs
        $previous = $configIni->getValue($this->name.'.'.$name, 'modules');
        if ($value !== $defaultValue) {
            if ($previous != $value) {
                $configIni->setValue($this->name.'.'.$name, $value, 'modules');
            }
        } elseif ($previous !== null) {
            // if the value is the default one, and there was a previous value
            // be sure to remove the key from the configuration file to
            // slim the configuration file
            $configIni->removeValue($this->name.'.'.$name, 'modules');
        }
    }

    public function clearInfos(IniModifierInterface $configIni)
    {
        foreach (array('enabled', 'dbprofile', 'installparam',
            'skipinstaller', 'localconf', ) as $param) {
            $configIni->removeValue($this->name.'.'.$param, 'modules');
        }
    }

    /**
     * Unserialize parameters coming from the ini file.
     *
     * Parameters could be fully serialized into a single string, or
     * could be as an associative array where only values are serialized
     *
     * @param array|string $parameters
     *
     * @return array
     */
    public static function unserializeParameters($parameters)
    {
        $trueParams = array();
        if (!is_array($parameters)) {
            $parameters = trim($parameters);
            if ($parameters == '') {
                return $trueParams;
            }
            $params = array();
            foreach (explode(';', $parameters) as $param) {
                $kp = explode('=', $param);
                if (count($kp) > 1) {
                    $params[$kp[0]] = $kp[1];
                } else {
                    $params[$kp[0]] = true;
                }
            }
        } else {
            $params = $parameters;
        }

        foreach ($params as $key => $v) {
            if (is_string($v) && (strpos($v, ',') !== false || (strlen($v) && $v[0] == '['))) {
                $trueParams[$key] = explode(',', trim($v, '[]'));
            } elseif ($v === 'false') {
                $trueParams[$key] = false;
            } elseif ($v === 'true') {
                $trueParams[$key] = true;
            } else {
                $trueParams[$key] = $v;
            }
        }

        return $trueParams;
    }

    /**
     * Serialize parameters to be stores into an ini file.
     *
     * The result is a single string with fully serialized array as found
     * in Jelix 1.6 or lower.
     *
     * @param array $parameters
     * @param array $defaultParameters
     *
     * @return string
     */
    public static function serializeParametersAsString($parameters, $defaultParameters = array())
    {
        $p = array();
        foreach ($parameters as $name => $v) {
            if (is_array($v)) {
                if (!count($v)) {
                    continue;
                }
                $v = '['.implode(',', $v).']';
            }
            if (isset($defaultParameters[$name]) && $defaultParameters[$name] === $v && $v !== true) {
                // don't write values that equals to default ones except for
                // true values else we could not known into the installer if
                // the absence of the parameter means the default value or
                // it if means false
                continue;
            }
            if ($v === true || $v === 'true') {
                $p[] = $name;
            } elseif ($v === false || $v === 'false') {
                if (isset($defaultParameters[$name]) && is_bool($defaultParameters[$name])) {
                    continue;
                }
                $p[] = $name.'=false';
            } else {
                $p[] = $name.'='.$v;
            }
        }

        foreach ($defaultParameters as $name => $v) {
            if ($v === true && !isset($parameters[$name])) {
                $p[] = $name;
            }
        }

        return implode(';', $p);
    }

    /**
     * Serialize parameters to be stores into an ini file.
     *
     * The result is an array with serialized values. It does not contain
     * parameters that have same value in both given parameters and default parameters
     * or parameters that are only into default parameters.
     *
     * @param array $parameters
     * @param array $defaultParameters
     *
     * @return array
     */
    public static function serializeParametersAsArray($parameters, $defaultParameters = array())
    {
        $p = array();
        foreach ($parameters as $name => $v) {
            if (is_array($v)) {
                if (!count($v)) {
                    continue;
                }
                $v = '['.implode(',', $v).']';
            }
            if (isset($defaultParameters[$name]) && $defaultParameters[$name] === $v) {
                // don't write values that equals to default ones
                continue;
            }
            $p[$name] = $v;
        }
        return $p;
    }


    /**
     * @param ModuleInfos $infos properties of the module
     * @param int|int[] $filter the status that the module should have to be selected
     *
     * @return Item
     */
    public function getResolverItem(ModuleInfos $infos, $filter, $forConfiguration = false)
    {
        $action = $this->getInstallAction($infos->version, $filter);
        if ($action == Resolver::ACTION_UPGRADE) {
            $item = new Item($this->name, $this->version, true);
            $item->setAction(Resolver::ACTION_UPGRADE, $infos->version);
        }
        else if ($action == Resolver::ACTION_REMOVE && !$forConfiguration) {
            $item = new Item($this->name, $this->version, true, false);
            $item->setAction(Resolver::ACTION_REMOVE);
        }
        else {
            $item = new Item($this->name, $this->version, $forConfiguration ? $this->isEnabled : $this->isInstalled);
            $item->setAction($action);
        }

        foreach ($infos->dependencies as $dep) {
            if ($dep['type'] == 'choice') {
                $list = array();
                foreach ($dep['choice'] as $choice) {
                    $list[$choice['name']] = $choice['version'];
                }
                $item->addAlternativeDependencies($list);
            } else {
                $item->addDependency($dep['name'], $dep['version']);
            }
        }

        foreach ($infos->incompatibilities as $dep) {
            $item->addIncompatibility($dep['name'], $dep['version']);
        }

        return $item;
    }

    const FILTER_DISABLED_UNINSTALLED = 37; // FILTER_VAL_NOTUPGRADED | FILTER_VAL_UNINSTALLED | FILTER_VAL_DISABLED
    const FILTER_DISABLED_INSTALLED = 25; // FILTER_VAL_UPGRADED | FILTER_VAL_INSTALLED | FILTER_VAL_DISABLED
    const FILTER_ENABLED_UNINSTALLED = 38; // FILTER_VAL_NOTUPGRADED | FILTER_VAL_UNINSTALLED | FILTER_VAL_ENABLED
    const FILTER_ENABLED_INSTALLED_UPGRADED = 26; // FILTER_VAL_UPGRADED | FILTER_VAL_INSTALLED | FILTER_VAL_ENABLED
    const FILTER_ENABLED_INSTALLED_NOT_UPGRADED = 42; // FILTER_VAL_NOTUPGRADED | FILTER_VAL_INSTALLED | FILTER_VAL_ENABLED

    const FILTER_VAL_DISABLED = 1;
    const FILTER_VAL_ENABLED = 2;
    const FILTER_VAL_UNINSTALLED = 4;
    const FILTER_VAL_INSTALLED = 8;
    const FILTER_VAL_UPGRADED = 16;
    const FILTER_VAL_NOTUPGRADED = 32;

    /**
     * @param string $newVersion the new version in case of the item can be upgraded
     * @param int|int[] $filter  one of FILTER_* const
     *
     * @return int
     * @throws Exception
     */
    protected function getInstallAction($newVersion, $filter)
    {
        if ($filter === false) {
            if ($this->isInstalled && $this->isEnabled ) {
                if (VersionComparator::compareVersion($newVersion, $this->version) != 0) {
                    return Resolver::ACTION_UPGRADE;
                }
            }
            return Resolver::ACTION_NONE;
        }
        $selected = Resolver::ACTION_INSTALL;
        $status = ($this->isEnabled ? self::FILTER_VAL_ENABLED : self::FILTER_VAL_DISABLED);
        if ($this->isInstalled) {
            $status |= self::FILTER_VAL_INSTALLED;
            if ($this->version != '') {
                if (VersionComparator::compareVersion($newVersion, $this->version) == 0) {
                    $status |= self::FILTER_VAL_UPGRADED;
                    if (!$this->isEnabled) {
                        $selected = Resolver::ACTION_REMOVE;
                    }
                }
                else {
                    $status |= self::FILTER_VAL_NOTUPGRADED;
                    $selected = Resolver::ACTION_UPGRADE;
                }
            }
            else {
                $status |= self::FILTER_VAL_UPGRADED;
            }
        }
        else {
            $status |= self::FILTER_VAL_UNINSTALLED | self::FILTER_VAL_NOTUPGRADED;
        }

        if (!is_array($filter)) {
            $filter = [ $filter ];
        }

        if (in_array($status, $filter)) {
            return $selected;
        }

        return Resolver::ACTION_NONE;
    }

}
