<?php
/* Copyright (C) 2017 Icinga Development Team <info@icinga.com> */

namespace Icinga\Module\Toplevelview;

use Icinga\Application\Benchmark;
use Icinga\Application\Icinga;
use Icinga\Exception\InvalidPropertyException;
use Icinga\Exception\NotImplementedError;
use Icinga\Exception\NotReadableError;
use Icinga\Exception\NotWritableError;
use Icinga\Exception\ProgrammingError;
use Icinga\Module\Toplevelview\Tree\TLVTree;
use Icinga\Util\DirectoryIterator;
use Icinga\Web\Session;

class ViewConfig
{
    const FORMAT_YAML = 'yml';
    const SESSION_PREFIX = 'toplevelview_view_';

    protected $config_dir;

    protected $name;

    protected $format;

    protected $file_path;

    protected $view;

    protected $raw;

    protected $tree;

    protected $hasBeenLoaded = false;
    protected $hasBeenLoadedFromSession = false;

    /**
     * Content of the file
     *
     * @var string
     */
    protected $text;

    protected $textChecksum;

    /**
     * @param             $name
     * @param string|null $config_dir
     * @param string      $format
     *
     * @return static
     */
    public static function loadByName($name, $config_dir = null, $format = self::FORMAT_YAML)
    {
        $object = new static;
        $object
            ->setName($name)
            ->setConfigDir($config_dir)
            ->setFormat($format)
            ->load();

        return $object;
    }

    /**
     * @param string|null $config_dir
     * @param string      $format
     *
     * @return static[]
     */
    public static function loadAll($config_dir = null, $format = self::FORMAT_YAML)
    {
        $suffix = '.' . $format;

        $config_dir = static::configDir($config_dir);
        $directory = new DirectoryIterator($config_dir, $suffix);

        $views = array();
        foreach ($directory as $name => $path) {
            if (is_dir($path)) {
                // no not descend and ignore directories
                continue;
            }
            $name = basename($name, $suffix);
            $views[$name] = static::loadByName($name, $config_dir, $format);
        }

        // try to load from session
        $len = strlen(self::SESSION_PREFIX);
        foreach (static::session()->getAll() as $k => $v) {
            if (substr($k, 0, $len) === self::SESSION_PREFIX) {
                $name = substr($k, $len);
                if (! array_key_exists($name, $views)) {
                    $views[$name] = static::loadByName($name, $config_dir, $format);
                }
            }
        }

        ksort($views);

        return $views;
    }

    /**
     * @return string
     */
    public function getFilePath()
    {
        if ($this->file_path === null) {
            if ($this->format === null) {
                throw new ProgrammingError('format not set!');
            }
            $this->file_path = $this->getConfigDir() . DIRECTORY_SEPARATOR . $this->name . '.' . $this->format;
        }
        return $this->file_path;
    }

    /**
     * @param string $file_path
     *
     * @return $this
     */
    public function setFilePath($file_path)
    {
        $this->file_path = $file_path;
        return $this;
    }

    /**
     * @return $this
     */
    public function load()
    {
        if ($this->text === null) {
            $this->loadFromSession();
        }
        if ($this->text === null) {
            $this->loadFromFile();
        }
        return $this;
    }

    public function loadFromFile()
    {
        $file_path = $this->getFilePath();
        $this->text = file_get_contents($file_path);
        if ($this->text === false) {
            throw new NotReadableError('Could not read file %s', $file_path);
        }
        $this->view = null;
        $this->hasBeenLoadedFromSession = false;
        $this->hasBeenLoaded = true;
        return $this;
    }

    /**
     * @return string
     */
    public function getText()
    {
        return $this->text;
    }

    public function getTextChecksum()
    {
        if ($this->textChecksum === null) {
            $this->textChecksum = sha1($this->text);
        }
        return $this->textChecksum;
    }

    /**
     * @param $text
     *
     * @return $this
     */
    public function setText($text)
    {
        $this->text = $text;
        $this->textChecksum = null;
        $this->raw = null;
        $this->tree = null;
        return $this;
    }

    protected function writeFile($path, $content, $mode = '0660')
    {
        $existing = file_exists($path);
        if (file_put_contents($path, $content) === false) {
            throw new NotWritableError('Could not save to %s', $path);
        }

        if ($existing === false) {
            $octalMode = intval($mode, 8);
            if ($mode !== null && false === @chmod($path, $octalMode)) {
                throw new NotWritableError('Failed to set file mode "%s" on file "%s"', $mode, $path);
            }
        }
    }

    protected function storeBackup($force = false)
    {
        $backupDir = $this->getConfigBackupDir();

        $this->ensureConfigDir($backupDir);

        $ts = (string) time();
        $backup = $backupDir . DIRECTORY_SEPARATOR . $ts . '.' . $this->format;

        if (file_exists($backup)) {
            throw new ProgrammingError('History file with timestamp already present: %s', $backup);
        }

        $existingFile = $this->getFilePath();
        $oldText = file_get_contents($existingFile);
        if ($oldText === false) {
            throw new NotReadableError('Could not read file %s', $existingFile);
        }

        // only save backup if changed or forced
        if ($force || $oldText !== $this->text) {
            $this->writeFile($backup, $oldText);
        }
    }

    public function store()
    {
        $config_dir = $this->getConfigDir();
        $file_path = $this->getFilePath();

        $this->ensureConfigDir($config_dir);

        // ensure to save history
        if (file_exists($file_path)) {
            $this->storeBackup();
        }

        $this->writeFile($file_path, $this->text);

        $this->clearSession();
        return $this;
    }

    /**
     * @return string
     * @throws ProgrammingError When dir is not yet set
     */
    public function getConfigDir()
    {
        if ($this->config_dir === null) {
            throw new ProgrammingError('config_dir not yet set!');
        }
        return $this->config_dir;
    }

    /**
     * @return string
     */
    public function getConfigBackupDir()
    {
        return $this->getConfigDir() . DIRECTORY_SEPARATOR . $this->name;
    }

    /**
     * @param string $config_dir
     *
     * @return $this
     * @throws NotReadableError
     */
    public function setConfigDir($config_dir = null)
    {
        $this->config_dir = static::configDir($config_dir);
        $this->file_path = null;
        return $this;
    }

    protected static function ensureConfigDir($path, $mode = '2770')
    {
        if (! file_exists($path)) {
            if (mkdir($path) !== true) {
                throw new NotWritableError(
                    'Config path did not exit, and it could not be created: %s',
                    $path
                );
            }

            $octalMode = intval($mode, 8);
            if ($mode !== null && false === @chmod($path, $octalMode)) {
                throw new NotWritableError('Failed to set file mode "%s" on file "%s"', $mode, $path);
            }
        }
    }

    public static function configDir($config_dir = null)
    {
        $config_dir_module = Icinga::app()->getModuleManager()->getModule('toplevelview')->getConfigDir();
        if ($config_dir === null) {
            $config_dir = $config_dir_module . DIRECTORY_SEPARATOR . 'views';
        }

        static::ensureConfigDir($config_dir_module);
        static::ensureConfigDir($config_dir);

        return $config_dir;
    }

    /**
     * @return mixed
     */
    public function getName()
    {
        return $this->name;
    }

    /**
     * @param mixed $name
     *
     * @return $this
     */
    public function setName($name)
    {
        $this->name = $name;
        $this->file_path = null;
        return $this;
    }

    /**
     * @return string
     */
    public function getFormat()
    {
        return $this->format;
    }

    /**
     * @param string $format
     *
     * @return $this
     */
    public function setFormat($format)
    {
        $this->format = $format;
        $this->file_path = null;
        return $this;
    }

    public function getMeta($key)
    {
        $this->ensureParsed();
        if ($key !== 'children' && array_key_exists($key, $this->raw)) {
            return $this->raw[$key];
        } else {
            return null;
        }
    }

    public function setMeta($key, $value)
    {
        if ($key === 'children') {
            throw new ProgrammingError('You can not edit children here!');
        }
        $this->raw[$key] = $value;
        return $this;
    }

    public function getMetaData()
    {
        $this->ensureParsed();
        $data = array();
        foreach ($this->raw as $key => $value) {
            if ($key !== 'children') {
                $data[$key] = $value;
            }
        }
        return $data;
    }

    protected function ensureParsed()
    {
        if ($this->raw === null) {
            Benchmark::measure('Begin parsing YAML document');

            $text = $this->getText();
            if ($text === null) {
                // new ViewConfig
                $this->raw = array();
            } elseif ($this->format == self::FORMAT_YAML) {
                // TODO: use stdClass instead of Array?
                $this->raw = yaml_parse($text);
                if (! is_array($this->raw)) {
                    throw new InvalidPropertyException('Could not parse YAML config!');
                }
            } else {
                throw new NotImplementedError("Unknown format '%s'", $this->format);
            }

            Benchmark::measure('Finished parsing YAML document');
        }
    }

    /**
     * Loads the Tree for this configuration
     *
     * @return TLVTree
     */
    public function getTree()
    {
        if ($this->tree === null) {
            $this->ensureParsed();
            $this->tree = $tree = TLVTree::fromArray($this->raw);
            $tree->setConfig($this);
        }
        return $this->tree;
    }

    protected function getSessionVarName()
    {
        return self::SESSION_PREFIX . $this->name;
    }

    public static function session()
    {
        // TODO: is this CLI safe?
        return Session::getSession();
    }

    public function loadFromSession()
    {
        if (($sessionConfig = $this->session()->get($this->getSessionVarName())) !== null) {
            $this->text = $sessionConfig;
            $this->hasBeenLoadedFromSession = true;
            $this->hasBeenLoaded = true;
        }
        return $this;
    }

    public function clearSession()
    {
        $this->session()->delete($this->getSessionVarName());
    }

    public function storeToSession()
    {
        $this->session()->set($this->getSessionVarName(), $this->text);
    }

    /**
     * @return bool
     */
    public function hasBeenLoadedFromSession()
    {
        return $this->hasBeenLoadedFromSession;
    }

    /**
     * @return bool
     */
    public function hasBeenLoaded()
    {
        return $this->hasBeenLoaded;
    }

    public function __clone()
    {
        $this->name = null;
        $this->raw = null;
        $this->tree = null;

        $this->hasBeenLoaded = false;
        $this->hasBeenLoadedFromSession = false;
    }

    public function delete()
    {
        $file_path = $this->getFilePath();

        $this->clearSession();

        if (file_exists($file_path)) {
            $this->storeBackup(true);
            unlink($file_path);
        }

        return $this;
    }
}
