<?php

Namespace EesyPHP;


Class Config {

  /**
   * Configuration file path
   * @var string|null
   */
  private static $filepath = null;

  /**
   * Loaded configuration data
   * @var array|null
   */
  private static $config = null;

  /**
   * Extra registered variables usable in configuration file
   * @var array
   */
  private static $extra_variables = array();

  /**
   * Load configuration file
   * @param string $filepath The configuration file path
   * @param bool $extra Load as an extra configuration file (default: false)
   * @return bool
   */
  public static function load($filepath, $extra=false) {
    if (!file_exists($filepath)) {
      Log :: error("Configuration file not found ($filepath)");
      return false;
    }
    $config = yaml_parse_file($filepath);
    if (!is_array($config) || empty($config)) {
      Log :: error("Fail to load %sconfiguration file (%s)", $extra?'extra ':'', $filepath);
      return false;
    }
    if (!$extra || !is_array(self :: $config))
      self :: $config = $config;
    else
      self :: $config = array_replace_recursive(self :: $config, $config);
    if (!$extra)
      self :: $filepath = $filepath;
    return true;
  }

  /**
   * Check if configuration is loaded
   * @return bool
   */
  public static function loaded() {
    return is_array(self :: $config);
  }

  /**
   * Save configuration
   * @param string|null $filepath The configuration file path
   * @param bool $overwrite Overwrite existing file (optional, default: true)
   * @return bool
   */
  public static function save($filepath=null, $overwrite=true) {
    if (!self :: loaded()) {
      Log :: error("Configuration not loaded, can't save it.");
      return false;
    }
    if (!$filepath) $filepath = self :: $filepath;
    if (!$filepath) {
      Log :: error("No configuration file set or pass as parameter");
      return false;
    }
    if (file_exists($filepath)) {
      if (!$overwrite) {
        Log :: error("Configuration file already exist (%s)", $filepath);
        return false;
      }
      if (!is_writable($filepath)) {
        Log :: error(
          "Configuration file already exist and is not writable (%s)",
          $filepath);
        return false;
      }
    }
    if (!yaml_emit_file($filepath, self :: $config)) {
      Log :: error("Fail to save configuration in '%s'", $filepath);
      return false;
    }
    Log :: info("Configuration saved in '%s'", $filepath);
    return true;
  }

  /**
   * Register extra variable usable in configuration file
   * @param string $key The variable key
   * @param mixed $value The variable value
   */
  public static function register_extra_variable($key, $value) {
    self :: $extra_variables[$key] = $value;
  }

  /**
   * Check if a specific configuration variable is set
   *
   * @param string $key The configuration variable key
   * @param array|null $config Optional configuration to use instead of current loaded configuration
   * @return bool
   **/
  public static function isset($key, &$config=null) {
    if (array_key_exists($key, self :: $extra_variables))
      return true;
    if (!is_array($config) && !self :: loaded())
      Log :: fatal('Configuration not loaded (on checking if %s is set)', $key);
    $exploded_key = explode('.', $key);
    if (!is_array($exploded_key)) return false;
    $value = is_array($config)?$config:self :: $config;
    foreach ($exploded_key as $k) {
      if (!is_array($value) || !isset($value[$k]))
        return false;
      $value = $value[$k];
    }
    return true;
  }

  /**
   * Get a specific configuration variable value
   *
   * @param string $key The configuration variable key
   * @param mixed $default The default value to return if configuration variable
   *                       is not set (Default : null)
   * @param string $cast   The type of expected value. The configuration variable
   *                       value will be cast as this type. Could be : bool, int,
   *                       float or string. (Optional, default : raw value)
   * @param bool $split If true, $cast=='array' and $value is a string, split
   *                    the value by comma (optional, default: false)
   * @param array|null $config Optional configuration to use instead of current loaded configuration
   * @param callable|null $get Optional callable to get another value that will be used to replace
   *                           variables references found in retreived value.
   * @return mixed The configuration variable value
   **/
  public static function get($key, $default=null, $cast=null, $split=false, &$config=null, $get=null) {
    if (array_key_exists($key, self :: $extra_variables)) {
      $value = self :: $extra_variables[$key];
    }
    else if (!is_array($config) && !self :: loaded()) {
      Log :: fatal(
        "Configuration not loaded (on getting %s):\n%s",
        $key, Log :: get_debug_backtrace_context()
      );
    }
    else {
      $exploded_key = explode('.', $key);
      if (!is_array($exploded_key)) return self :: replace_variables($default, $get?$get:$config);
      $value = is_array($config)?$config:self :: $config;
      foreach ($exploded_key as $k) {
        if (!is_array($value) || !isset($value[$k]))
          return self :: replace_variables($default, $get?$get:$config);
        $value = $value[$k];
      }
    }
    return self :: replace_variables(cast($value, $cast, $split), $get?$get:$config);
  }

  /**
   * Replace variable in specified value
   * @param mixed $value
   * @param array|callable|null $config Optional configuration to use to retreive variable reference
   *                                    value instead of current loaded configuration. Could be an
   *                                    array of configuration data or a callable that could be used
   *                                    as Config::get() to retreive the desired variable reference.
   * @return mixed
   */
  public static function replace_variables($value, $config=null) {
    if (is_array($value)) {
      foreach(array_keys($value) as $key) {
        if (is_string($value[$key]) || is_array($value[$key]))
          $value[$key] = self :: replace_variables($value[$key], $config);
      }
    }
    else if (is_string($value)) {
      $config = is_null($config)?self :: $config:$config;
      $iteration = 0;
      while (preg_match('/\$\{([^\}]+)\}/', $value, $m)) {
        if ($iteration > 20)
          Log::fatal('Config::replace_variables(%s): max iteration reached');
        if (is_callable($config))
          $replace_by = call_user_func($config, $m[1], '', 'string', false);
        else
          $replace_by = self :: get($m[1], '', 'string', false, $config);
        $value = str_replace($m[0], $replace_by, $value);
        $iteration++;
      }
    }
    return $value;
  }

  /**
   * Get list of keys of a specific configuration variable
   *
   * @param string $key The configuration variable key
   *
   * @return array An array of the keys of a specific configuration variable
   **/
  public static function keys($key) {
    $value = self :: get($key);
    return (is_array($value)?array_keys($value):array());
  }

  /**
   * Set a configuration variable
   *
   * @param string $key The configuration variable key
   * @param mixed $value The configuration variable value
   * @param array|null &$config Optional configuration to use instead of current loaded configuration
   *
   * @return boolean
   **/
  public static function set($key, $value, &$config=null) {
    $exploded_key = explode('.', $key);
    if (!is_array($exploded_key)) return false;
    if (is_array($config)) {
      $parent = &$config;
    }
    else {
      if (!is_array(self :: $config))
        self :: $config = array();
      $parent = &self :: $config;
    }
    for ($i=0; $i < count($exploded_key) - 1; $i++) {
      $k = $exploded_key[$i];
      if (!array_key_exists($k, $parent))
        $parent[$k] = array();
      $parent = &$parent[$k];
    }
    $parent[array_pop($exploded_key)] = $value;
    return true;
  }

  /**
   * Helper to set PHP INI option and log error
   * @param string $option
   * @param string|int|float|bool|null $value
   * @return void
   */
  public static function ini_set($option, $value) {
    Log :: trace('Set PHP INI option "%s" to "%s"', $option, $value);
    if (ini_set($option, $value) === false)
      Log::warning('Fail to set PHP INI option "%s" to "%s"', $option, $value);
    Log :: trace(
      'PHP INI option "%s" after setting it to "%s": "%s"',
      $option, $value, vardump(ini_get($option)));
  }

}

# vim: tabstop=4 shiftwidth=4 softtabstop=4 expandtab
