<?php

  /**
   * Simple container for managing configuration options and storing values
   *
   * @package angie.library
   */
  final class ConfigOptions {
    
    /**
     * Return value by name
     * 
     * If $name is an array, system will get array of configuration option 
     * values and return them as associative array
     * 
     * Set $use_cache to false if you want this method to ignore cached values
     *
     * @param mixed $name
     * @param boolean $use_cache
     * @return mixed
     */
    static function getValue($name, $use_cache = true) {
      if(empty($name)) {
        throw new InvalidParamError('name', $name);
      } // if

      $find = (array) $name;
      
      $single = $find !== $name; // if we had conversion to array, we had scalar
      
      $cached_values = cache_get('config_values');
      $values = array();
      
      foreach($find as $option) {
        if($use_cache && is_array($cached_values) && isset($cached_values[$option])) {
          $values[$option] = $cached_values[$option];
          continue;
        } // if
        
        if($row = DB::executeFirstRow('SELECT value FROM ' . TABLE_PREFIX . 'config_options WHERE name = ?', $option)) {
          $values[$option] = $row['value'] ? unserialize($row['value']) : null;
          
          if(is_array($cached_values)) {
            $cached_values[$name] = $values[$option];
          } else {
            $cached_values = array($name => $values[$option]);
          } // if
        } else {
          throw new ConfigOptionDnxError($option);
        } // if
      } // foreach
      cache_set('config_values', $cached_values);
      
      return $single ? array_shift($values) : $values;
    } // getValue
    
    /**
     * Set value for a given object
     * 
     * This function can be called in following ways:
     * 
     * ConfigOptions::setValue('Option Name', 'Value');
     * 
     * as well as:
     * 
     * ConfigOptions::setValeu(array(
     *   'Option 1' => 'Value 1',
     *   'Option 2' => 'Value 2',
     * ));
     *
     * @param string $name
     * @param boolean $clear_cache
     * @param mixed $value
     * @param boolean $clear_for_cache
     * @return mixed
     */
    static function setValue($name, $value = null, $clear_for_cache = false) {
      try {
        DB::beginWork('Setting configuration values @ ' . __CLASS__);
        
        $cached_values = cache_get('config_values');
        $to_set = is_array($name) ? $name : array($name => $value);
        
        foreach($to_set as $k => $v) {
          if(self::exists($k, false)) {
            DB::execute('UPDATE ' . TABLE_PREFIX . 'config_options SET value = ? WHERE name = ?', serialize($v), $k);
            
            if(is_array($cached_values)) {
              $cached_values[$k] = $v;
            } else {
              $cached_values = array($k => $v);
            } // if
          } else {
            throw new ConfigOptionDnxError($k);
          } // if
        } // foreach
        
        cache_set('config_values', $cached_values);
        
        if($clear_for_cache) {
        	self::clearCacheFor();
        } // if
        
        DB::commit('Configuration values set @ ' . __CLASS__);
      } catch(Exception $e) {
        DB::rollback('Failed to set configuration values @ ' . __CLASS__);
        
        throw $e;
      } // try
      
      return $name === $to_set ? $to_set : $value; // Return what we have just set
    } // setValue
    
    /**
     * Get value for a given parent object
     *
     * @param string $name
     * @param IConfigContext $for
     * @param boolean $use_cache
     * @return mixed
     */
    static function getValueFor($name, IConfigContext $for, $use_cache = true) {
      $find = (array) $name;
      
      $cache_id = self::getCacheIdFor($for);
      
      $cached_values = cache_get($cache_id);
      $values = array();
      
      foreach($find as $option) {
        if($use_cache && is_array($cached_values) && isset($cached_values[$option])) {
          $values[$option] = $cached_values[$option];
          continue;
        } // if
        
        if($row = DB::executeFirstRow('SELECT value FROM ' . TABLE_PREFIX . 'config_option_values WHERE name = ? AND parent_type = ? AND parent_id = ?', $option, get_class($for), $for->getId())) {
          $values[$option] = $row['value'] ? unserialize($row['value']) : null;
        } else {
          $values[$option] = self::getValue($option, $use_cache);
        } // if
        
        if(is_array($cached_values)) {
          $cached_values[$name] = $values[$option];
        } else {
          $cached_values = array($name => $values[$option]);
        } // if
      } // foreach
      cache_set($cache_id, $cached_values);
      
      return $find === $name ? $values : array_shift($values);
    } // getValueFor
    
    /**
     * Returns true if there is a value for given config options
     *
     * @param string $name
     * @param IConfigContext $for
     * @return boolean
     */
    static function hasValueFor($name, IConfigContext $for) {
      if(is_array($name)) {
        $options = array_unique($name);
      } else {
        $options = array($name);
      } // if
      
      return ((integer) DB::executeFirstCell("SELECT COUNT(*) AS 'row_count' FROM " . TABLE_PREFIX . 'config_option_values WHERE name = ? AND parent_type = ? AND parent_id = ?', $options, get_class($for), $for->getId())) == count($name);
    } // hasValueFor
    
    /**
     * Set value for a given parent object
     *
     * @param string $name
     * @param IConfigContext $for
     * @param mixed $value
     */
    static function setValueFor($name, IConfigContext $for, $value = null) {
      $config_option_values_table = TABLE_PREFIX . 'config_option_values';
      
      try {
        DB::beginWork('Setting configuration option for object @ ' . __CLASS__);
        
        $cache_id = self::getCacheIdFor($for);
        
        $cached_values = cache_get($cache_id);
        
        $to_set = is_array($name) ? $name : array($name => $value);
        foreach($to_set as $k => $v) {
          if(self::exists($k, false)) {
            if((integer) DB::executeFirstCell("SELECT COUNT(*) AS 'row_count' FROM $config_option_values_table WHERE name = ? AND parent_type = ? AND parent_id = ?", $k, get_class($for), $for->getId())) {
            	if($v === null) {
            		DB::execute("DELETE FROM $config_option_values_table WHERE name = ? AND parent_type = ? AND parent_id = ?", $v, get_class($for), $for->getId());
            	} else {
            		DB::execute("UPDATE $config_option_values_table SET value = ? WHERE name = ? AND parent_type = ? AND parent_id = ?", serialize($v), $k, get_class($for), $for->getId());
            	} // if
            } else {
              DB::execute("INSERT INTO $config_option_values_table (name, parent_type, parent_id, value) VALUES (?, ?, ?, ?)", $k, get_class($for), $for->getId(), serialize($v));
            } // if
            
            if($v === null) {
            	if(is_array($cached_values) && isset($cached_values[$k])) {
            		unset($cached_values[$k]);
            	} // if
            } else {
            	if(is_array($cached_values)) {
	              $cached_values[$k] = $v;
	            } else {
	              $cached_values = array($k => $v);
	            } // if
            } // if
          } else {
            throw new ConfigOptionDnxError($name);
          } // if
        } // foreach
        
        cache_set($cache_id, $cached_values);
        
        DB::commit('Configuration option values set for object @ ' . __CLASS__);
      } catch(Exception $e) {
        DB::rollback('Failed to set configuration option values for object @ ' . __CLASS__);
        
        throw $e;
      } // try
      
      return $name === $to_set ? $to_set : $value; // Return what we have just set
    } // setValueFor
    
    /**
     * Remove all custom values for given option
     *
     * @param string $name
     */
    static function removeValues($name) {
      DB::execute('DELETE FROM ' . TABLE_PREFIX . 'config_option_values WHERE name IN (?)', $name);
      cache_remove_by_pattern('config_values_for_*');
    } // removeValues
    
    /**
     * Remove all values for a given parent object
     *
     * @param IConfigContext $for
     * @param mixed $specific
     */
    static function removeValuesFor(IConfigContext $for, $specific = null) {
      if($specific) {
        DB::execute('DELETE FROM ' . TABLE_PREFIX . 'config_option_values WHERE name IN (?) AND parent_type = ? AND parent_id = ?', (array) $specific, get_class($for), $for->getId());
      } else {
        DB::execute('DELETE FROM ' . TABLE_PREFIX . 'config_option_values WHERE parent_type = ? AND parent_id = ?', get_class($for), $for->getId());
      } // if
      
      cache_remove(self::getCacheIdFor($for));
    } // removeValuesFor
    
    /**
     * Return number of custom values for given option
     *
     * @param string $name
     * @param mixed $value
     * @param array $exclude
     * @return integer
     */
    static function countByValue($name, $value, $exclude = null) {
      $exclude_filter = '';
      
      if(is_foreachable($exclude)) {
        $exclude_filter = array();
        
        foreach($exclude as $exclude_object) {
          $exclude_filter[] = DB::prepare('(parent_type = ? AND parent_id = ?)', array(get_class($exclude_object), $exclude_object->getId()));
        } // foreach
        
        $exclude_filter = ' AND NOT (' . implode(' OR ', $exclude_filter) . ')';
      } // if
      
      return (integer) DB::executeFirstCell("SELECT COUNT(*) AS 'row_count' FROM " . TABLE_PREFIX . 'config_option_values WHERE name = ? AND value = ? ' . $exclude_filter, $name, serialize($value));
    } // countByValue
    
    /**
     * Remove all custom values by $name and $value
     * 
     * This method is useful when we need to clean up custom values when 
     * something system wide is changed (language or filter is removed etc)
     *
     * @param string $name
     * @param mixed $value
     */
    static function removeByValue($name, $value) {
      DB::execute("DELETE FROM " . TABLE_PREFIX . 'config_option_values WHERE name = ? AND value = ?', $name, serialize($value));
      cache_remove_by_pattern('config_values_for_*');
    } // removeByValue
    
    /**
     * Clear cache for given type and ID
     * 
     * If ID is not set, all values for given type will be removed
     * 
     * @param string $for_type
     * @param integer $for_id
     */
    static function clearCacheFor($for_type = null, $for_id = null) {
    	if($for_type && $for_id) {
    		cache_remove('config_values_for_' . strtolower($for_type) . '_' . $for_id);
    	} elseif($for_type) {
    		cache_remove_by_pattern('config_values_for_' . strtolower($for_type) . '_*');
    	} elseif($for_id) {
    		cache_remove_by_pattern('config_values_for_*_' . $for_id);
    	} else {
    		cache_remove_by_pattern('config_values_for_*');
    	} // if
    } // clearCacheFor
    
    /**
     * Return cache ID for a given object
     *
     * @param IConfigContext $for
     * @return string
     */
    private static function getCacheIdFor(IConfigContext $for) {
      if($for instanceof IConfigContext) {
        return 'config_values_for_' . strtolower(get_class($for)) . '_' . $for->getId();
      } else {
        throw new InvalidInstanceError('for', $for, 'IConfigContext');
      } // if
    } // getCacheIdFor
    
    // ---------------------------------------------------
    //  Management
    // ---------------------------------------------------
    
    /**
     * Cached array of exists value
     *
     * @var array
     */
    static private $exists_cache = array();
    
    /**
     * Check if specific configuration option exists
     *
     * @param string $name
     * @param boolean $use_cache
     * @return boolean
     */
    static function exists($name, $use_cache = true) {
      if(!isset(self::$exists_cache[$name]) || !$use_cache) {
        self::$exists_cache[$name] = (boolean) DB::executeFirstCell("SELECT COUNT(*) AS 'row_count' FROM " . TABLE_PREFIX . 'config_options WHERE name = ?', $name);
      } // if
      
      return self::$exists_cache[$name];
    } // exists
    
    /**
     * Define new option
     *
     * @param string $name
     * @param string $module
     * @param mixed $default_value
     */
    static function addOption($name, $module, $default_value = null) {
      if(empty($name)) {
        throw new Error('Configuration option name is required');
      } // if
      if(empty($module)) {
        throw new Error('Configuration option needs to be associated with a module');
      } // if
      
      DB::execute('INSERT INTO ' . TABLE_PREFIX . 'config_options (name, module, value) VALUES (?, ?, ?)', $name, $module, serialize($default_value));
    } // addOption
    
    /**
     * Remove option definition
     *
     * @param string $name
     */
    function removeOption($name) {
      try {
        DB::beginWork();
        
        DB::execute('DELETE FROM ' . TABLE_PREFIX . 'config_options WHERE name = ?', $name);
        DB::execute('DELETE FROM ' . TABLE_PREFIX . 'config_option_values WHERE name = ?', $name);
        
        cache_remove('config_values');
        cache_remove_by_pattern('config_values_for_*');
        
        DB::commit();
      } catch(Exception $e) {
        DB::rollback();
        
        throw $e;
      } // try
    } // removeOption
    
    /**
     * Remove all options by module
     *
     * @param Module $module
     */
    static function removeByModule($module) {
      try {
        DB::beginWork('Removing options by module @ ' . __CLASS__);
        
        $rows = DB::executeFirstColumn('SELECT name FROM ' . TABLE_PREFIX . 'config_options WHERE module = ?', $module);
        if($rows) {
          DB::execute('DELETE FROM ' . TABLE_PREFIX . 'config_options WHERE name IN (?)', $rows);
          DB::execute('DELETE FROM ' . TABLE_PREFIX . 'config_option_values WHERE name IN (?)', $rows);
        } // if
        
        cache_remove('config_values');
        cache_remove_by_pattern('config_values_for_*');
        
        DB::commit('Module options removed @ ' . __CLASS__);
      } catch(Exception $e) {
        DB::rollback('Failed to remove module options @ ' . __CLASS__);
        
        throw $e;
      } // try
    } // removeByModule
    
  }