<?php

/*
Licensed under GPLv2 or above

Version 1.2.2
*/

class ConfigWriter {
    
    /**
     * Skip updating the index if the index acctually exist
     * 
     * This will also create new index if not exist
     */
    const SKIP_IF_EXIST = 0;
    /**
     * Skip creating new index if the specified index is not found
     */
    const SKIP_CREATE_IF_NOT_EXIST = 1;
    /**
     * Update or create the index
     */
    const CREATE_OR_UPDATE = 2;

    /**
     * The config source file
     * 
     * set during with class construct
     * @var string 
     */
    protected $_file ;

    /**
     * The config file to write
     * 
     * Default to source file
     * @var string 
     */
    protected $_destinationFile ;
    
    /**
     * Content read from file
     * @var string 
     */
    protected $_fileContent;

    
    /**
     * The variable to search for in the file
     * 
     * eg if we can to update 'option_name' value in a file with
     * $config['option_name'] array, the variable is 'config'
     * 
     * @note we assume each option index is on seperate line like 
     *  $config['option_name_one']  = 'one' ;
     *  $config['option_name_two']  = 'two' ;
     * 
     * if you have array like $config['options'] = array(
     *                                      'option_name_one' => 'one',
     *                                      'option_name_two' => 'two'
     *                                 );
     * You have to first get $config['options'] in variable then manually update 
     * the elements, after which you will can use this config writer to update the
     * $config['options'] element
     * 
     * @var string 
     */
    protected $_variable ;

    /**
     * The target index of the variable
     * 
     * @var string 
     */
    protected $_index ;
    
    /**
     *
     * @var string 
     */
    protected $_replacement;
    
    /**
     *
     * @var string 
     */
    protected $_lastError;
    
    
    /**
     * This option determines whether the file should be updated automatically
     * 
     * if set to false, you will manually call save() method after you 
     * have done your writing with write() method
     * 
     * @since 1.1.0
     * @var boolean 
     */
    protected $_autoSave = true ;


    // --------------------------------------------------------------------

    /**
     * CI Singleton
     *
     * @var object
     */
    protected $CI;

    // --------------------------------------------------------------------

    /**
     * Class constructor
     *
     * Loads the calendar language file and sets the default time reference.
     *
     * @uses	CI_Lang::$is_loaded
     *
     * @param	array	$config	Calendar options
     * @return	void
     */
    public function __construct($config = NULL) 
    {

	$this->CI =& get_instance();

	empty($config) OR $this->initialize($config);

	log_message('info', 'ConfigWriter Class Initialized');

    }
    
    // --------------------------------------------------------------------

    /**
     * Initialize the user preferences
     *
     * Accepts an associative array as input, containing display preferences
     *
     * @param	array	config preferences
     * @return	CI_Calendar
     */
    public function initialize($config = array())
    {
        $this->_file = APPPATH .'config/config.php';
        $this->_autoSave = true ;
        $this->setVariableName('config');

        if( is_array($config) )
        {
            if (array_key_exists('file', $config))
                $this->_file = APPPATH .'config/'.$config['file'];
            if (array_key_exists('auto_save', $config))
                $this->_autoSave = $config['auto_save'];
            if (array_key_exists('variable_name', $config))
                $this->setVariableName($config['variable_name'] );
        }
        else
        {
            if( isset($config) )
                $this->_file = APPPATH .'config/config.php';
            else
                $this->_file = APPPATH .'config/'.$config;
        }
        $this->_destinationFile = $this->_file ;
        
        if ( ! file_exists($this->_file))
        {
            //throw new Exception('Config Write Error: Config file doesnt exists ' . $this->_file);
            $this->_lastError = 'Config Write Error: Config file doesnt exists ' . $this->_file ;
            log_message('warning', 'ConfigWriter error: config file not exist, using default' . $this->_file);
            $this->_file = APPPATH .'config/config.php';
            return $this;
        }
        $this->_fileContent =  file_get_contents( $this->_file ) ;
        return $this;
    }

    
    /**
     * Wite or update an item of the config array
     * 
     * @param string|array $index To update 'language' index  $config['language']
     *  This will be string 'language' 
     *  To update hostname' of $db['default']['hostname'] , then index
     *  will be array( 'default' , 'hostname' )
     *
     * @param mixed $replacement
     * @param boolean $write_method Skip updating item if already exists
     * @param null|array $comments Comment to add to the top of item (new item), each element
     *  will be placed oon a new line. *  is added before each line , meaning
     *  you dont have to put /** or *  unless you want it show 
     * @throws Exception for invalid write method
     * 
    * @note you can not update existing item comment
     *  
     * 
     * @return boolean
     * 
     */
    public function write( $index  = null, $replacement = null , $write_method = self::CREATE_OR_UPDATE , $comments = null )
    {
        // error exists in the constructor?
        if($this->_lastError)
        {
            return $this;
        }
        
        $this->_fileContent = trim($this->_fileContent); 
        
        if(!$index)
        {
            $this->_lastError = 'You must set index you want to update' ;
            return $this;
        }
        $prefix = $this->_variable  ;
        
        $regex = '#(' . $prefix  . ')(';
        // add a mark in case config item doesnt exists
        $mark = "{$prefix}" ;
        // we can update multi dimentional
        $indices = is_array($index)? $index : array( $index ) ;
        $comment_str = '' ;
        
        foreach ( $indices as  $i)
        {
            $is_int = is_int($i) ;
             // we make sure we dont change the index type if its numeric
            $new_item_index = $is_int? $i : "'$i'" ;
            $regex .= '\[\s*';
            // if the index is int, we dont need ' or "" to be checked in the regex
            $regex .= $is_int? '' : '(\'|\")' ;
            $regex .=  '('.$i.')*';
            $regex .= $is_int? '' : '(\'|\")';
            $regex .= '\s*\]' ;
            // Used before we seperated numeric index from string
            //$regex .= '\[\s*(\'|\")(' . $i . ')*(\'|\")\s*\]' ;
            
            //placed in file as new index if doesn't exist
            $mark .= "[$new_item_index]" ;
        }
       
        // value
        $regex .= ')\s*=\s?' ; 
        $regex .= "({$this->getStringRegex()}|{$this->getIntRegex()}|{$this->getArrayRegex()})" ;
        //allow ; within a string @issue 7
        // $regex .= '|.*".*".*|' ; 
        // $regex .= "|.*'.*'.*|"; 
        //close value match
        $regex .= ';\s*$#m' ; 
        $mark .= " = ";
        $exists = preg_match($regex, $this->_fileContent, $m);
        
        if($exists)
        {
            // well config aleady exists 
            // may be is application upgrade :) we wouldnt wana overide user settings 
            if( $write_method == self::SKIP_IF_EXIST )
            {
                return $this;
            }
            // update the content
            $this->_fileContent = preg_replace(   $regex ,  
                '$1$2 = ' .  var_export( $replacement , true ).';' , $this->_fileContent   ) ;
        }
        // config item doesnt exist yet create new index if reqyuired
        else
        {
            switch ($write_method)
            {
                case self::SKIP_CREATE_IF_NOT_EXIST:
                    return $this;
                case self::CREATE_OR_UPDATE:
                case self::SKIP_IF_EXIST:
            
                    // new item here 
                    $mark .= var_export( $replacement , true ) . ' ;' ;
                    $mark .= "\n" ;
                    $mark .= "\n" ;
                    
                    // add comment if provided
                    if(is_array($comments) && count($comments) > 0 )
                    {
                        //open comment 
                        $comment_str .= '/**' ;
                        $comment_str .= "\n" ;
                        foreach ($comments as $line)
                        {
                            $comment_str .= ' * ' . $line . "\n" ;
                        }
                        // close the comment
                        $comment_str .= '*/';
                    }

                     // lets try remove traling slash from the variable name since 
                    // we are writing php here 
                    if ( substr($mark, 0 , 1) == '\\' )
                    {
                        $mark = substr($mark, 1 );
                    }
                    
                     // we have updated the index, the file will save automatically in the class shutdwon
                    // we did this allow update multiple index by call write() method as manay times 
                    // as required
                    // 
                   
                    // check if the file has php closing tag
                    if(substr($this->_fileContent, -2) === '?>' )
                    {
                        // remove the closing tag before adding our new item
                        $this->_fileContent = substr($this->_fileContent, 0 , -2). "\n" . $comment_str . "\n"  . $mark . ' ?>' ;
                    }
                    else
                    {
                        $this->_fileContent = $this->_fileContent . "\n" . $comment_str . "\n"  . $mark . "\n" ;
                    }
                    break;
                case self::SKIP_CREATE_IF_NOT_EXIST:
                    return $this;
                    default :
                        throw new Exception('Class: '.__CLASS__ .' Method :'.__METHOD__.'third parameter is invalid. Use the class constant');
            }
        }
        return $this;
    }
    
    /**
     * Update the file name of the array config
     * 
     * You will olnly do this to transfer the php file content to another file
     * 
     * @param string $name
     * @deprecated since 1.3.0 use setDestinationFile()  method
     * 
     * @return ConfigWriter for method chaining
     */
    public function setFilename($name = null)
    {
        return $this->setDestinationFile($name);
    }
    

    /**
     * Set the file to save updated configuration
     * @since 1.3.0
     * 
     * @param string $name
     * 
     * @return ConfigWriter for method chaining
     */
    public function setDestinationFile($path)
    {
        $this->_destinationFile = $path;

        return $this;
    }

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

    
   
    /**
     *  Set the config varible name
     * // actually we expect someting like '\$config' but user migt just provide 'config'
     * 
     * @param string $name
     * @since 1.3.0
     * @return ConfigWriter
     */
    public function setVariableName($name = null)
    {
       
        if( ! is_string($name))
        {
            $this->_lastError = 'Variable name not string: '. $name;
            return $this;
        }
        
        if(substr($name, 0, 1 ) != '$' && substr($name, 1, 1 ) != '$')
        {
            $name = '$' . $name ;
        }
        if(substr($name, 0 , 1 ) != '\\')
        {
            $name = '\\' . $name ;
        }
        
        $this->_variable = $name;
        
        return $this;
    }


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

    /**
     * We can now update the file content
     * 
     * if the _autosave property is true, we auto update file
     * 
     */
    public function __destruct() 
    {
        if($this->_autoSave)
        {
            $this->save();
        }
    }
    
    /**
     * Save the new content to file 
     * 
     * You can call ConfigWriter::write() as many times as required
     * before calling this method
     * 
     * @return \ConfigWriter
     */
    public function save()
    {
        if( ! $this->_lastError)
        {
            file_put_contents( $this->_destinationFile , $this->_fileContent ) ;
        }
        return $this;
    }
    
    /**
     * Check if any error has occured
     * 
     * @return boolean
     */
    public function hasError()
    {
        return ! empty($this->_lastError);
    }
    /**
     * Get last error 
     * 
     * @return string Last error by the library if any
     */
    public function getLastError()
    {
        return $this->_lastError;
    }
    
    /**
     * Set auto save option
     * 
     * @since 1.1.1
     * @param boolean $option
     */
    public function setAutoSave($option = true)
    {
        $this->_autoSave = $option;

        return $this;
    }
    /**
     * Set auto save option
     * @since 1.3.0
     * 
     * @return boolean Auto save option
     */
    public function getAutoSave()
    {
        return $this->_autoSave;
    }

    /**
     * Get confil file content
     */
    public function getContent()
    {
      return $this->_fileContent;   
    }


    protected function getStringRegex()
    {
        return '(\'|").*(\'|")';
    }

    protected function getIntRegex()
    {
        return '[^;]+';
    }

    protected function getArrayRegex()
    {
        return '(:?array|\[)\s*\(.*(:?\)|\])*';
    }
}
