<?php
/********************************************************************************
 * (c)1337 aheadware.com - All rights reserved
 ********************************************************************************/

/**
 * Class Francis_Db_Row
 *
 * @property integer $id
 */
abstract class Francis_Db_Row extends Francis_Core_EventDispatcher {
    ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
    // Const
    ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////

    const DEFAULT_LIMIT = 10;
    const DEFAULT_OFFSET = 0;

    ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
    // Statics
    ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////

    /**
     * Get instance by given id (ie. primary key)
     *
     * @param integer $id
     * @return NULL | static
     */
    public static function get( $id ) {
        // Try to get from cache

        // CACHE DISABLED FOR NOW
        /*
        $cache = Francis_Core_Cache::get();
        $cacheKey = self::getCacheKey( $id );
        $retEnt = $cache->get( $cacheKey );
        */

        $retEnt = NULL;
        if ( is_null ( $retEnt ) ) {
            // Get one instance
            $db = static::getDbh();
            $sql = "SELECT * FROM " . static::getTable() . " WHERE id = :id LIMIT 1";
            $params = array( ':id' => $id );
            $stmt = $db->prepare( $sql );
            $stmt->execute( $params );
            $ent = new static();
            $stmt->setFetchMode( PDO::FETCH_INTO, $ent );
            $stmt->fetch();
            $ent->afterLoad();

            if ( $ent->id ) {
                $retEnt = $ent;
                // CACHE DISABLED FOR NOW
                //$cache->set( $cacheKey,$retEnt );
            } else {
                $retEnt = NULL;
            }

        }

        return $retEnt;
    }

    /**
     * Find one instance by given param
     *
     * @param string $sql
     * @param array $params
     * @return NULL | static
     */
    public static function findOne( $sql, array $params = array() ) {
        // Find instance by given params
        $db = static::getDbh();
        $stmt = $db->prepare( $sql );
        $stmt->execute( $params );
        $ent = new static();
        $stmt->setFetchMode( PDO::FETCH_INTO, $ent );
        $stmt->fetch();
        $ent->afterLoad();

        if ( $ent->id ) {
            return $ent;
        } else {
            return NULL;
        }
    }

    /**
     * Find 0 to X entities
     *
     * @param string $sql
     * @param array $params (optional) Params to bind
     *
     * @return array
     */
    public static function findMany( $sql, array $params = array() ) {
        // Find instance by given params
        $db = static::getDbh();
        $stmt = $db->prepare( $sql );
        $stmt->execute( $params );
        $arr = $stmt->fetchAll( PDO::FETCH_CLASS, get_called_class() );
        /** @var Francis_Db_Row $ent */
        foreach( $arr as $ent ) {
            $ent->afterLoad();
        }
        return $arr;
    }

    /**
     * Executes given statement on current DB
     *
     * @param $sql
     * @param array $params
     *
     * @return bool
     */
    public static function execute( $sql, array $params = array() ) {
        $db = static::getDbh();
        $stmt = $db->prepare( $sql );
        return $stmt->execute( $params );
    }
    /**
     * Returns table name
     * Note:
     * Should acutally be abstract - but PHP strict mode prohibits it
     *
     * @return null|string
     */
    public static function getTable() {
        return NULL;
    }

    /**
     * @return null|Francis_Db_MySql
     */
    public static function getDbh() {
        return NULL;
    }

    public static function getCacheKey( $id ) {
        return self::getTable() . '_' . $id;
    }

    /**
     * Convert certain classes to DB types before storing them
     *
     * @param string $key
     * @param mixed $value
     * @return string
     */
    public static function convertToDB( $key, $value ) {
        $suffix = substr( $key, -3 );
        if ( '_sr' === $suffix  ) {
            return base64_encode( serialize($value) );
        } else if ( '_dt' === $suffix && !is_null( $value ) ) {
            //return $value->format( DateTime::ISO8601 );
            return $value->format('Y-m-d H:i:s');
        } else if ( '_js' === $suffix && is_array( $value ) ) {
            return json_encode( $value, true );
        } else {
            return $value;
        }
    }

    /**
     * Convert certain db type fields into corresponding PHP objects
     *
     * @param string $key
     * @param string $value
     * @return DateTime|mixed
     */
    public static function convertFromDB( $key, $value ) {
        $suffix = substr( $key, -3 );
        if ( '_sr' === $suffix  )
        {
            return unserialize( base64_decode( $value ) );

        }
        else if ( '_dt' === $suffix && !is_null( $value ) )
        {
            return DateTime::createFromFormat(
                'Y-m-d H:i:s',
                '' . $value,
                new \DateTimeZone( "UTC" )
            );
        }
        else if ( '_js' === $suffix && !is_null( $value ) )
        {
            return json_decode( $value, true );
        }
        else if ( 'last_update' === $key)
        {
            return strtotime( $value );
        }
        else
        {
            return $value;
        }
    }

    ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
    // Members
    ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////

    protected $fields = array();
    protected $touched = array();

    /**
     * Construct DB Row record
     *
     * @param array $values
     */
    public function __construct() {
    }

    /**
     * Dynamically populate entity with given properties
     *
     * @param array $props
     */
    public function populate( array $props ) {
        foreach( $props as $prop => $value ) {
            $this->$prop = $value;
        }
    }

    /**
     * @param $key
     */
    public function touch( $key )
    {
        if ( isset( $this->fields[ $key ] ) )
        {
            $this->touched[ $key ] = true;
        }
    }

    /**
     * Magic method for property acces
     *
     * @param string $key
     * @param mixed $value
     */
    public function __set( $key, $newValue )
    {
        if ( !isset( $this->fields[ $key ] ) || $this->fields[ $key ] != $newValue ) {
            $this->fields[ $key ] = $newValue;
            $this->touched[ $key ] = true;
        }
    }

    /**
     * Magic method for property access
     *
     * @param string $key
     * @return mixed
     */
    public function __get( $key ) {
        if ( isset( $this->fields[ $key ] ) ) {
            return $this->fields[ $key ];
        } else {
            return NULL;
        }
    }

    /**
     * Converts special DB types and
     * sets all fields to untouch state
     */
    public function afterLoad() {
        foreach( $this->fields as $k => $v ) {
            $this->fields[ $k ] = self::convertFromDB( $k, $v );
            $this->touched[ $k ] = false;
        }
    }

    /**
     * Returns array of touch key/value pairs
     *
     * @return array [ (string) 'key' => (mixed) 'value', ... ]
     */
    public function getTouched() {
        $touched = array();
        foreach( $this->touched as $k => $v ) {
            if ( true === $v ) {
                $touched[ $k ] = $this->fields[ $k ];
            }
        }
        return $touched;
    }

    public function isTouched( $key ) {
        return ( array_key_exists( $key, $this->getTouched() ) );
    }

    /**
     *
     * @return array Assoc array of fields (ie. properties)
     */
    public function asArray() {
        return $this->fields;
    }
    /**
     * Save tocuhed values of row entity
     *
     * @throws PDOException e.g. duplicate key etc.
     */
    public function save() {
        // Call hook
        $this->beforeSave();

        // If no values has been changed
        $touched = $this->getTouched();
        if ( 0 === count( $touched ) ) {
            return;
        }

        // Insert | Update
        $db = static::getDbh();
        if ( 0 < (int)$this->id ) {
            // Update
            $sql = 'UPDATE ' . static::getTable() . ' SET ';
            $params = array( ':id' => $this->id );
            foreach( $touched as $k => $v ) {
                $sql .= $k . ' = :' . $k . ',';
                $params[ ':' . $k ] = self::convertToDB( $k, $v );
            }
            $sql = rtrim( $sql, ',' ) . ' ';
            $sql .= 'WHERE id = :id LIMIT 1';
            $stmt = $db->prepare( $sql );
            $stmt->execute( $params );

            // Fire update event
            Francis_Core_EventHandler::get()->trigger(
                strtolower( static::getTable() ) . '_updated',
                array( 'entity' => $this )
            );

        } else {
            // Insert
            $sql = 'INSERT INTO `' . static::getTable() . '` ';
            $fields = '(';
            $values = '(';
            $params = array();
            foreach( $touched as $k => $v ) {
                $fields .= '`' . $k . '`,';
                $values .= ':' . $k . ',';
                $params[ ':' . $k ] = self::convertToDB( $k, $v );
            }
            $fields = rtrim( $fields, ',' ) . ')';
            $values = rtrim( $values, ',' ) . ')';

            $sql .= $fields . ' VALUES ' . $values;
            $stmt = $db->prepare( $sql );
            $stmt->execute( $params );
            $this->id = $db->lastInsertId();

            // Fire created event
            Francis_Core_EventHandler::get()->trigger(
                strtolower( static::getTable() ) . '_created',
                array( 'entity' => $this )
            );
        }

        // CACHE DISABLED FOR NOW
        // Clean cache
        // Francis_Core_Cache::get()->delete( self::getCacheKey( $this->id ) );

        // Call hook
        $this->afterSave();
    }

    /**
     * Delete current row
     */
    public function delete() {
        // Call hook
        $this->beforeSave();

        // Delete instance
        $db = static::getDbh();

        $isNewTransaction = false;
        if ( !$db->inTransaction() ) {
            $db->beginTransaction();
            $isNewTransaction = true;
        }

        try {
            // Delete appointment itself
            $sql = "DELETE FROM " . static::getTable() . " WHERE id = :id LIMIT 1";
            $params = array( ':id' => $this->id );
            $stmt = $db->prepare( $sql );
            $stmt->execute( $params );

            $this->deleteReferences();

            if ( $isNewTransaction ) {
                $db->commit();
            }

            // Fire deleted event
            Francis_Core_EventHandler::get()->trigger(
                strtolower( static::getTable() ) . '_deleted',
                array( 'entity' => $this )
            );

            $success = true;
        } catch ( Exception $e ) {
            if ( $isNewTransaction ) {
                $db->rollBack();
            }
            throw $e;
        }


        // CACHE DISABLED FOR NOW
        // Clean cache
        // Francis_Core_Cache::get()->delete( self::getCacheKey( $this->id ) );

        // Call hook
        $this->afterDelete();
    }

    // Hooks
    // Intended to be owerwritten
    public function beforeSave() {}
    public function afterSave() {}
    public function beforeDelete() {}
    // Called within an transaction - no need to open a transaction of current dbh
    public function deleteReferences() {}
    public function afterDelete() {}
}