<?php
/**
 * CodeIgniter library
 *
 * @category CodeIgniter
 * @license /_licenses/untermStrich/agb_normal.pdf
 * @author Christian Koller
 * @copyright 2009 untermStrich
 * @package us_libraries
 * @version $Id$
*/

if (!defined('BASEPATH')) { exit('No direct script access allowed'); }

require_once APPPATH.'us_libraries/US_model.php';

/**
 * Allows to create an object with any amount of values in it
 *
 * It works basically like an array, accept:
 * You can access values using $name->getKey and
 * If the key does not exist, it returns null
 */
abstract class Data_Object extends US_model implements Iterator, Serializable
{
    /**
     * This array holds the information
     *
     * It keeps the information for this object on serialize
     *
     * @var array
     */
    protected $data_array = array();

    /**
     * Array to translate keys
     * @var array
     */
    protected $translate = array();

    private $_last_refresh = 0;

    /**
     * What vars shall be serialized for every model
     */
    private static $_vars_to_serialize = array(
        'data_array',
        '_last_refresh',
        'translate',
    );
    
    /**
     * Init the Data_Object
     *
     */
    public function __construct()
    {
        parent::__construct();

        $this->startup();
    }
    
    /**
     * How often (in minutes) shall the data object reload it's data from the database (force_init)
     *
     * A value of 0 means never
     *
     * @return  integer
     */
    abstract protected function refresh_rate();
    
    /**
     * This function will load the objects data from e.g. the database
     *
     * Also load $translate within this function!
     *
     * @return  void
     */
    abstract public function force_init();
    
    /**
     * Some init functionality used by all data objects
     *
     * Also checks if we have to refresh, and if so call force_init
     *
     * @return  void
     */
    protected function init()
    {
        if( empty($this->data_array) ) {
            $this->force_init();
            return;
        }
        if ($this->refresh_rate()==0) {
            return;
        }
        $wait = $this->refresh_rate() * 60;
        if ( $this->_last_refresh < (time() - $wait) ) {
            $this->force_init();
            $this->_last_refresh = time();
        }
    }
    
    /**
     * Runs an insert or update
     * 
     * On error, retry once
     *
     * @param   string  $table  The table to save to
     * @param   array   $merge  An associative array of merge values
     * @param   array   $set    An associative array of insert values
     * @return  object
     */
    protected function insert_or_update_delayed($table, array $merge, array $set)
    {
        $ok = $this->_insert_or_update_do($table, $merge, $set);

        if (!$ok) {
            $sleep_for = random_int(1000, 100000);
            
            log_message('error', $this->CI->lang->line('basic_db_w_fail').' for '.$table.' - Retry in '.$sleep_for);
            
            usleep($sleep_for);
            
            $ok = $this->_insert_or_update_do($table, $merge, $set);
            if (!$ok) {
                basic_error($this->CI->lang->line('basic_db_w_fail').' for '.$table.' (2nd try)', null, __CLASS__.'::'.__FUNCTION__);
            }
        }
    }
    
    /**
     * Runs an insert or update - once
     * 
     * @param   string  $table  The table to save to
     * @param   array   $merge  An associative array of merge values
     * @param   array   $set    An associative array of insert values
     * @return  object
     */
    private function _insert_or_update_do($table, $merge, $set)
    {
        $this->db->trans_start(); //Save and Persist
        try {
            $ok = $this->db->insert_or_update($table, $merge, $set);
            $this->db->trans_complete();
        } catch (Exception $exc) {
            $this->db->trans_rollback();
            log_message('error', $this->CI->lang->line('basic_db_w_fail').' for '.$table.': '.$exc->getTraceAsString());
        }
        return $ok;
    }
    
    
    /**
     * For foreach - Rewind the Iterator to the first element
     *
     * @return  mixed   The first element, or FALSE if the array is empty.
     */
    public function rewind(): void
    {
        $this->init();
        reset($this->data_array);
    }
    
    /**
     * For foreach - Return the current element
     *
     * @return  mixed   The element
     */
    #[\ReturnTypeWillChange]
    public function current()
    {
        $this->init();
        return current($this->data_array);
    }
    
    /**
     * For foreach - Return the key of the current element
     *
     * @return  mixed   The key of the current element Returns NULL if invalid element is selected.
     */
    #[\ReturnTypeWillChange]
    public function key()
    {
        $this->init();
        return key($this->data_array);
    }
    
    /**
     * For foreach - Move forward to next element
     *
     * @return  The next element, or FALSE if there are no more elements.
     */
    public function next(): void
    {
        $this->init();
        next($this->data_array);
    }
    
    /**
     * For foreach - Checks if current position is valid
     *
     * @return  boolean
     */
    public function valid(): bool
    {
        $this->init();
        return key($this->data_array) !== null;
    }
    
    /**
     * Returns if the data for a key is set
     * 
     * This methode is triggered by calling isset() or empty()
     *
     * @param   string  $key    The key name
     * @return  boolean
     */
    public function __isset($key)
    {
        $this->init();
        $this->translate($key);
        return isset($this->data_array[$key]);
    }
    
    /**
     * Unsets a data field
     *
     * This methode is invoked when unset() is used
     *
     * @param   string  $key    The key name
     * @return  void
     */
    public function __unset($key)
    {
        $this->init();
        $this->translate($key);
        unset($this->data_array[$key]);
    }
    
    
    /**
     * Allows to call getter and setter methodes
     *
     * Supports:
     *  <ul>
     *      <li>setKEY  To set a key
     *          <ul>
     *              <li>mixed   Value
     *          </ul>
     *      </li>
     *      <li>getKEY  To get a key
     *      <li>setboolKEY  To set a key as bool
     *          <ul>
     *              <li>bool   Value
     *          </ul>
     *      </li>
     *      <li>getboolKEY  To get a key as bool
     *  </ul>
     *
     * @param   string  $name   Name of the called function
     * @param   array   $args   All parameters
     * @return  mixed   Whatever the methode called wants to return
     */
    public function __call($name, array $args)
    {
        if ( preg_match('/^getbooljs(\w+)/', strtolower($name), $match) ) {
            return $this->getbooljs($match[1]);
        } elseif ( preg_match('/^(setbool|getbool)(\w+)/', strtolower($name), $match) ) {
            $key = $match[2];
            if ('getbool' == $match[1]) {
                if (is_array($args) && count($args) > 0 && isset($args[0])) {
                    return $this->getbool($key, $args[0]);
                } else {
                    return $this->getbool($key);
                }
                
            } else {
                $this->setbool($key, $args[0]);
                return;
            }
        } elseif ( preg_match('/^(setoldfloat|getoldfloat)(\w+)/', strtolower($name), $match) ) {
            $key = $match[2];
            if ('getoldfloat' == $match[1]) {
                return $this->getoldfloat($key);
            } else {
                $this->setoldfloat($key, $args[0]);
                return;
            }
        } elseif ( preg_match('/^(set|get)(\w+)/', strtolower($name), $match) ) {
            $key = $match[2];
            if ('get' == $match[1]) {
                return $this->get($key);
            } else {
                $this->set($key, $args[0]);
                return;
            }
        } else {
            throw new Exception('Call to undefined method Data_Object::'.$name.'().');
        }
    }
    
    /**
     * The getter
     *
     * Overwrite for e.g. persistence calls
     *
     * @param   string  $key    The key name
     * @return  mixed Element
     */
    public function get($key)
    {
        $this->init();
        $this->translate($key);
        if ( array_key_exists($key, $this->data_array) )
            return $this->data_array[$key];
        return null;
    }
    
    /**
     * The getter for bool values
     *
     * @param   string  $key        The key name
     * @param   string  $default    Optional an default value if var is not set in db
     * @return  boolean
     */
    public function getbool($key, $default=false)
    {
        $val = $this->get($key);
        $val = ($val === null) ? $default : $this->string_to_bool($val);
        return $val;
    }

    /**
     * The getter for bool values
     *
     * @param   string  $key    The key name
     * @return  boolean
     */
    public function getoldfloat($key)
    {
        $value_str = $this->get($key);
        return (float)str_replace(
                array('.', ','),
                array('', '.'),
                $value_str ?? ''
            );
    }

    /**
     * The getter for bool values or null if missing
     *
     * @param   string  $key    The key name
     * @return  boolean
     */
    public function getbool_ornull($key)
    {
        $val = $this->get($key);
        if ($val===null) {
            return null;
        }
        return $this->string_to_bool($val);
    }
    
    /**
     * The getter for bool values as javascript
     *
     * @param   string  $key    The key name
     * @return  string  Bool value as string for javascript
     */
    public function getbooljs($key)
    {
        return $this->getbool($key) ? 'true' : 'false';
    }
    
    /**
     * The setter
     *
     * Overwrite for e.g. persistence calls
     *
     * @param   string  $key    The key name
     * @param   mixed   $value  The value to save
     * @return  void
     */
    public function set($key, $value)
    {
        $this->init();
        $this->translate($key);
        $this->data_array[$key] = $value;
    }
    
    /**
     * The setter for bool values
     *
     * @param   string  $key    The key name
     * @param   boolean $value  The value to save
     * @return  void
     */
    public function setbool($key, $value)
    {
        if( $value ) {
            $this->set($key, 'TRUE');
        } else {
            $this->set($key, 'FALSE');
        }
    }

    /**
     * The setter for old float values
     *
     * @param   string  $key        The key name
     * @param   boolean $float_val  The value to save
     * @return  void
     */
    public function setoldfloat($key, $float_val)
    {
        $matches = explode('.', abs($float_val)); //Split into parts
        $float_past = (count($matches)>=2) ? strlen($matches[1]) : 0; //Get lenght of 2nd part, if any
        if ($float_past>6) $float_past=6;
        
        $value_str = number_format($float_val, $float_past, ',', '');
        $this->set($key, $value_str);
    }
    
    /**
     * Call this methode on every key, before you use the key
     *
     * @param   string  &$key   Reference Key to translate
     * @return  void
     */
    protected function translate(&$key)
    {
        $this->init();
        $key = strtolower($key);
        if( array_key_exists($key, $this->translate) ) {
            $key = $this->translate[$key];
        }
    }
    
    /**
     * Makes a bool out of a string
     * 
     * @param   string  $val    The value
     * @return  boolean
     */
    public function string_to_bool($val)
    {
        if ( !empty($val) )
            return ( strtolower($val)==='true' || $val==='1' || $val===1 || $val===true );
        else
            return false;
    }
    
    
    /**
     * String representation of object
     *
     * @return  string
     */
    public function serialize()
    {
        return serialize($this->__serialize());
    }
    
    /**
     * Serialize all vars_to_serialize
     * 
     * @return array
     */
    public function __serialize(): array
    {
        $data = array();
        
        foreach (self::$_vars_to_serialize as $var) {
            $data[$var] = $this->$var;
        }
        
        return $data;
    }
    
    /**
     * Constructs the object
     *
     * @param   string  $datas_array_ser    The data array
     * @return  void
     */
    public function unserialize($datas_array_ser)
    {
        $this->__unserialize(@unserialize($datas_array_ser));
    }
    
    /**
     * Unserialize all vars_to_serialize
     * 
     * @param   array   $data   Data array
     * @return  void
     */
    public function __unserialize(array $data): void
    {
        foreach (self::$_vars_to_serialize as $var) {
            if (!isset($data[$var])) {
                log_message( 'error', 'Can not unserialize the Data_Object "'.get_class($this).'" - Missing "'.$var.'"' );
            }
            $this->$var = $data[$var];
        }
        
        $this->startup();
    }
    

    /**
     * Assign libraries
     *
     * @return  void
     */
    public function startup()
    {
        $this->CI = CI_Controller::get_instance();
    }
    
} // END class Data_object
