<?php

/**
 * Object that represents the configuration file.
 *
 * @category Serenity
 * @package  Config
 */
class Serenity_Config_Config
{
    /**
     * @var string Absolute path to the configuration file.
     */
    private $filePath = null;

    /**
     * @var mixed Content parser. Must be a valid callback or null.
     *            If null then the default one is used.
     */
    private $parser;

    /**
     * @var stdClass|null Configuration data.
     */
    private $data = null;

    /**
     * Constructor.
     *
     * @param string $filePath Path to the configuration file.
     * @param mixed  $parser   Content parser. Must be a valid callback or null.
     *                         If null then the default one is used.
     *
     * @throws Serenity_Config_ConfigException If file does not exist.
     */
    public function __construct($filePath = null, $parser = null)
    {
        if ($filePath !== null) {
			$this->filePath = realpath((string) $filePath);
			if ($this->filePath === false) {
				$message = "Configuration file '$filePath' not found.";
				throw new Serenity_Config_ConfigException($message);
			}
		}

        if ($parser !== null && !is_callable(array($parser, 'parse'))) {
            $message = "Specified content parser is not a valid callback.";
            throw new Serenity_Config_ConfigException($message);
        }

        $this->parser = $parser;
    }

    /**
     * Parse contents of the configuration file.
     *
     * @param string $contents The configuration file contents.
     *
     * @return stdClass|null Configuration data or null if parsing failed.
     */
    protected function _parse($contents)
    {
        if ($this->parser !== null) {
            return (object) $this->parser->parse($contents, $this->filePath);
        }

        preg_match_all('/".[^"]*"/', $contents, $strings);
        $contents = preg_replace('/".[^"]*"/', '%s', $contents);

        $patterns = array('/\/\/.*/', '/\/\*.*?\*\//s', '/([0-9a-zA-Z]+):/');
        $replacements = array('', '', '"\1":');

        $contents = preg_replace($patterns, $replacements, $contents);

        return json_decode('{' . vsprintf($contents, $strings[0]) . '}');
    }

    /**
     * Load the configuration file.
     *
     * @return stdClass Configuration data.
     *
     * @throws Serenity_Config_ConfigException If configuration data are not valid.
     */
    protected function _loadData()
    {
        $data = $this->_parse(file_get_contents($this->filePath));

        if ($data === null) {
            $message = 'Configuration data are not valid.';
            throw new Serenity_Config_ConfigException($message);
        }

        return $data;
    }

    /**
     * Get value stored under specified key.
     *
     * @param string $key Key.
     *
     * @return mixed Value.
     *
     * @throws Serenity_Config_ConfigException If specified key does not exist.
     */
    public function __get($key)
    {
        $data = $this->getData();
        $key = (string) $key;

        if (!property_exists($data, $key)) {
            $message = "Key '$key' does not exist.";
            throw new Serenity_Config_ConfigException($message);
        }

        return $data->$key;
    }

    /**
     * Set value under specified key.
     *
     * @param string $key   Key.
     * @param mixed  $value Value.
     */
    public function __set($key, $value)
    {
        $data = $this->getData();
        $key = (string) $key;

        $data->$key = $value;
    }

    /**
     * Check if specified key exists.
     *
     * @param string $key A key.
     *
     * @return bool True if specified key exists, false otherwise.
     */
    public function __isset($key)
    {
        return property_exists($this->getData(), (string) $key);
    }

	/**
	 * Append another configuration data to current ones.
	 * Appended object can implement getData method to be used
	 * for configuration data obtaining.
	 *
	 * @param object|array $config Configuration data.
	 *
	 * @return Serenity_Config_Config Self instance.
	 */
	public function appendConfig($config)
	{
		if (method_exists($config, 'getData')) {
			$config = $config->getData();
		}

		$this->data = (object) array_merge((array) $this->getData(), (array) $config);

		return $this;
	}

    /**
     * Get all configuration data.
     *
     * @return stdClass All configuration data.
     */
    public function getData()
    {
        if ($this->data === null) {
            $this->data = ($this->filePath !== null)
				? $this->_loadData()
				: new stdClass();
        }

        return $this->data;
    }
}
