<?php

namespace dokuwiki\plugin\config\core;

use dokuwiki\plugin\config\core\Setting\Setting;
use dokuwiki\plugin\config\core\Setting\SettingNoClass;
use dokuwiki\plugin\config\core\Setting\SettingNoDefault;
use dokuwiki\plugin\config\core\Setting\SettingNoKnownClass;
use dokuwiki\plugin\config\core\Setting\SettingUndefined;

/**
 * Holds all the current settings and proxies the Loader and Writer
 *
 * @author Chris Smith <chris@jalakai.co.uk>
 * @author Ben Coburn <btcoburn@silicodon.net>
 * @author Andreas Gohr <andi@splitbrain.org>
 */
class Configuration
{
    public const KEYMARKER = '____';

    /** @var Setting[] metadata as array of Settings objects */
    protected $settings = [];
    /** @var Setting[] undefined and problematic settings */
    protected $undefined = [];

    /** @var array all metadata */
    protected $metadata;
    /** @var array all default settings */
    protected $default;
    /** @var array all local settings */
    protected $local;
    /** @var array all protected settings */
    protected $protected;

    /** @var bool have the settings been changed since loading from disk? */
    protected $changed = false;

    /** @var Loader */
    protected $loader;
    /** @var Writer */
    protected $writer;

    /**
     * ConfigSettings constructor.
     */
    public function __construct()
    {
        $this->loader = new Loader(new ConfigParser());
        $this->writer = new Writer();

        $this->metadata = $this->loader->loadMeta();
        $this->default = $this->loader->loadDefaults();
        $this->local = $this->loader->loadLocal();
        $this->protected = $this->loader->loadProtected();

        $this->initSettings();
    }

    /**
     * Get all settings
     *
     * @return Setting[]
     */
    public function getSettings()
    {
        return $this->settings;
    }

    /**
     * Get all unknown or problematic settings
     *
     * @return Setting[]
     */
    public function getUndefined()
    {
        return $this->undefined;
    }

    /**
     * Have the settings been changed since loading from disk?
     *
     * @return bool
     */
    public function hasChanged()
    {
        return $this->changed;
    }

    /**
     * Check if the config can be written
     *
     * @return bool
     */
    public function isLocked()
    {
        return $this->writer->isLocked();
    }

    /**
     * Update the settings using the data provided
     *
     * @param array $input as posted
     * @return bool true if all updates went through, false on errors
     */
    public function updateSettings($input)
    {
        $ok = true;

        foreach ($this->settings as $key => $obj) {
            $value = $input[$key] ?? null;
            if ($obj->update($value)) {
                $this->changed = true;
            }
            if ($obj->hasError()) $ok = false;
        }

        return $ok;
    }

    /**
     * Save the settings
     *
     * This save the current state as defined in this object, including the
     * undefined settings
     *
     * @throws \Exception
     */
    public function save()
    {
        // only save the undefined settings that have not been handled in settings
        $undefined = array_diff_key($this->undefined, $this->settings);
        $this->writer->save(array_merge($this->settings, $undefined));
    }

    /**
     * Touch the settings
     *
     * @throws \Exception
     */
    public function touch()
    {
        $this->writer->touch();
    }

    /**
     * Load the extension language strings
     *
     * @return array
     */
    public function getLangs()
    {
        return $this->loader->loadLangs();
    }

    /**
     * Initalizes the $settings and $undefined properties
     */
    protected function initSettings()
    {
        $keys = [
            ...array_keys($this->metadata),
            ...array_keys($this->default),
            ...array_keys($this->local),
            ...array_keys($this->protected)
        ];
        $keys = array_unique($keys);

        foreach ($keys as $key) {
            $obj = $this->instantiateClass($key);

            if ($obj->shouldHaveDefault() && !isset($this->default[$key])) {
                $this->undefined[$key] = new SettingNoDefault($key);
            }

            $d = $this->default[$key] ?? null;
            $l = $this->local[$key] ?? null;
            $p = $this->protected[$key] ?? null;

            $obj->initialize($d, $l, $p);
        }
    }

    /**
     * Instantiates the proper class for the given config key
     *
     * The class is added to the $settings or $undefined arrays and returned
     *
     * @param string $key
     * @return Setting
     */
    protected function instantiateClass($key)
    {
        if (isset($this->metadata[$key])) {
            $param = $this->metadata[$key];
            $class = $this->determineClassName(array_shift($param), $key); // first param is class
            $obj = new $class($key, $param);
            $this->settings[$key] = $obj;
        } else {
            $obj = new SettingUndefined($key);
            $this->undefined[$key] = $obj;
        }
        return $obj;
    }

    /**
     * Return the class to load
     *
     * @param string $class the class name as given in the meta file
     * @param string $key the settings key
     * @return string
     */
    protected function determineClassName($class, $key)
    {
        // try namespaced class first
        if (is_string($class)) {
            $modern = str_replace('_', '', ucwords($class, '_'));
            $modern = '\\dokuwiki\\plugin\\config\\core\\Setting\\Setting' . $modern;
            if ($modern && class_exists($modern)) return $modern;
            // try class as given
            if (class_exists($class)) return $class;
            // class wasn't found add to errors
            $this->undefined[$key] = new SettingNoKnownClass($key);
        } else {
            // no class given, add to errors
            $this->undefined[$key] = new SettingNoClass($key);
        }
        return '\\dokuwiki\\plugin\\config\\core\\Setting\\Setting';
    }
}
