<?php

namespace apexl\Config;

use apexl\Utils\Arrays\Merge;

/**
 * Class Config
 * A class to provide a single persisting store for all loaded configurations. Allows the overriding, temporary loading or locking of config
 * @package apexl\Config
 */
class Singleton
{
    /** @var Singleton */
    private static $instance;
    private $data;
    private $loadedFiles = []; /** List of loaded files, and associated options. */

    public static $cacheDir;
    public static $cacheFilename = 'configCache.json';
    /**
     * config constructor. Set private to prevent the class being instantiated
     */
    private function __construct()
    {
    }

    /**
     * Function to get the instance of this config. Singleton.
     * @return mixed
     * @throws \Exception
     */
    public static function getInstance()
    {
        if (!self::$instance) {
            self::$instance = new Singleton();
        }
        return self::$instance;
    }

    /**
     * @param $file
     * @param string $key
     * @param bool $loadFromCache
     * @param array $options
     * @return Singleton|bool
     * @throws \Exception
     */
    public static function load($file, $key = '', $loadFromCache = true, $options = [])
    {
        self::getInstance();
        if (!is_file($file)) {
            trigger_error('Cannot load the provided configuration file: '.$file);
            return false;
        }

        //load from an amalgamated config file. If one doesnt exist, create it.
        if ($loadFromCache) {
            $loaded = self::$instance->loadFromCache($file, $key = '', $options = []);
        } else {
            $loaded = self::$instance->loadFile($file, $key, $options, false);
        }
        return $loaded === false ? $loaded : self::$instance;
    }

    /**
     * @param $dir
     */
    public static function setCacheDirectory($dir)
    {
        self::$cacheDir = $dir;
    }

    /**
     * @param $dir
     */
    public static function setCacheFilename($dir)
    {
        self::$cacheDir = $dir;
    }

    /**
     * @return mixed
     */
    public static function getCacheDirectory()
    {
        return self::$cacheDir;
    }

    /**
     * Function to temporarily load the provided configuration file, do not store the result in the singleton.
     * @param $file
     * @param string $key
     * @return bool|mixed|static
     * @throws \Exception
     */
    public static function tempLoad($file, $key = '')
    {
        self::getInstance();
        return self::$instance->loadFile($file, $key, [], true);
    }

    /**
     * @param $file
     * @param string $key
     * @param array $options
     * @return $this|bool|mixed
     */
    private function loadFromCache($file, $key = '', $options = [])
    {
        if ($this->cacheable()) {
            $success = true;
            if (!file_exists($this::$cacheDir.'/'.$this::$cacheFilename)) {
                $success = $this->writeConfigDataToCacheFile();
            }
            if ($success !== false) {
                return $this->loadFile($this::$cacheDir . '/' . $this::$cacheFilename, $key, $options, false);
            }
        }
        return $this->loadFile($file, $key, $options, false);
    }

    /**
     * @return bool
     */
    private function cacheable()
    {
        if ($this::$cacheDir) {
            return true;
        }
        return false;
    }

    public function reloadConfig()
    {
        //cache the config files
        $cachedFiles = self::$instance->loadedFiles;
        //reset the loaded config
        self::$instance->loadedFiles = [];
        //loop over the file cache and reload the files.
        foreach ($cachedFiles as $key => $files) {
            foreach ($files as $file) {
                self::$instance->loadFile($file['file'], '', $files['options'] ?? [], false);
            }
        }
    }

    /**
     * @param $file
     * @param $key
     * @param $options
     * @param $temp
     * @return $this|bool|mixed
     */
    private function loadFile($file, $key, $options, $temp)
    {
        $extension = strtolower(pathinfo($file, PATHINFO_EXTENSION));

        $configData = $this->convertFileToArray($file, $extension);
        if (empty($configData)) {
            trigger_error('The provided configuration file is empty or is not readable: '. $file);
            return false;
        }

        if ($temp) {
            if (empty($key)) {
                return $configData;
            } else {
                return isset($configData[$key]) ? $configData[$key] : false;
            }
        }

        //no defined Key? assume all root elements are keys.
        if (empty($key)) {
            foreach ($configData as $key => $keyData) {
                $this->storeConfigData($key, $keyData);
            }
        } else {
            $this->storeConfigData($key, $configData);
        }

        //mark the file as loaded;
        $this->loadedFiles[$key][] = ['file' => $file, 'options' => $this->setFileOptions($options)];
        //only store the data in this instance if we're not temp loading.
        return $this;
    }

    /**
     * @param $key
     * @param $configData
     * @return void
     */
    public function storeConfigData($key, $configData)
    {
        $exists = false;

        if (isset($this->loadedFiles[$key])) {
            $exists = true;
            //ok, so the file is already loaded, no point continuing unless we allow config to be overridden
            if ($this->loadedFiles[$key]['options']['lock']) {
                return;
            }
        }

        //set the data if new, merge the key if it exists and is not locked.
        $this->data[$key] = $exists ? array_replace_recursive($this->data[$key], $configData) : $configData;
    }

    /**
     * Function to take the current loaded config data and write it back to file
     */
    public function writeConfigDataToCacheFile()
    {
        $json = json_encode($this->data, JSON_PRETTY_PRINT);
        if (!is_dir(self::$cacheDir)) {
            if (mkdir(self::$cacheDir, 0755, true) !== false) {
                return file_put_contents(trim(self::$cacheDir, '/').'/'.self::$cacheFilename, $json);
            };
        } else {
            return file_put_contents(trim(self::$cacheDir, '/') . '/' . self::$cacheFilename, $json);
        }
        return false;
    }

    /**
     * @param $key
     * @param array $config
     * @return bool
     * @throws \Exception
     */
    public static function updateConfigFileByKey($key, array $config)
    {
        //get the last loaded config file for this key and update the values to ensure we're using them
        //@todo change this to update the last file that contains these values, or the first one if none do.
        if (!isset(self::$instance->loadedFiles[$key])) {
            $files = array_values(self::$instance->loadedFiles);
            $file = array_shift($files)[0]['file'];
        } else {
            $file = end(self::$instance->loadedFiles[$key])['file'];
        }
        //temp load the file and then merge recursive if the key exists.
        $tempLoad = self::tempLoad($file);
        $tempLoad[$key] = isset($tempLoad[$key]) ? Merge::arrayMergeRecursive([$tempLoad[$key], $config]) : $config;

        $write = file_put_contents($file, json_encode($tempLoad));
        if ($write !== false) {
            self::$instance->data[$key] = $tempLoad[$key];
            //if we're cacheable, we need to write the cache out again.
            if (self::$instance->cacheable()) {
                self::$instance->writeConfigDataToCacheFile();
            }
        }
        return $write !== false ? true : false; //just to make sure any checks are boolean safe.
    }

    /**
     * Function to convert the provided configuration to an array of data depending on file type.
     * @param $loadFile
     * @param $loadExtension
     * @return array|mixed
     */
    private function convertFileToArray($loadFile, $loadExtension)
    {
        switch($loadExtension) {
            case 'php':
                //if this is a php file, we need to include it.
                include $loadFile;
                //default to assuming the data is a $config array, allow this to be overridden with options.
                //@todo allow options config and if options provides more than one array, merge them before returning them.
                $configVars = get_defined_vars();
                $returnableConfig = [];
                foreach ($configVars as $var => $data) {
                    if ($var == 'loadFile' || $var == 'loadExtension' || $var == 'returnableConfig') {
                        continue;
                    }
                    $returnableConfig[$var] = $data;
                }
                return $returnableConfig;
                break;
            case 'json':
            default:
                $fileData = file_get_contents($loadFile);
                return json_decode($fileData, true);
                break;
        }
    }

    /**
     * Set loaded configuration file options.
     * @param $options
     * @return array
     */
    private function setFileOptions($options)
    {
        $default = [
            'lock' => true, //Prevent a file key from being overwritten once its loaded.
        ];
        if (!empty($options)) {
            $default = array_merge($default, $options);
        }
        return $default;
    }

    /**
     * Magic PHP __get function to get the requested configuration options by key. Response casted to object on return
     * @param $name
     * @return bool|mixed
     */
    public function __get($name)
    {
        return isset($this->data[$name]) ? json_decode(json_encode($this->data[$name]), false) : false;
    }

    public static function asArray($data)
    {
        return json_decode(json_encode($data), true);
    }

    /**
     * @param $name
     * @return bool
     */
    public function __isset($name)
    {
        return isset($this->data[$name]);
    }

    /**
     * @param $name
     */
    public function __unset($name)
    {
        unset($this->data[$name]);
    }

    /**
     *
     */
    public function __destruct()
    {
        $this->data = null;
        $this->loadedFiles = null;
    }
}
