<?php
/**
 * value store for versions for the deployment tool
 * 
 * EXAMPLES:
 * 
 * get versions of a phase 
 *     $oVersion = new valuestore();
 *     $oVersion->setProject("", $this->_aPrjConfig["fileprefix"], $sPhase, $sPlace);
 *     $aVersions=$oVersion->getVersion();
 * 
 * set versions of a host
 *     $oVersion = new valuestore();
 *     $oVersion->setProject("", $$sPackage, $sPhase, $sPlace);
 *     $oVersion->updateVar($sVarname, $sValue)
 *
 * delete
 *     $oVersion = new valuestore();
 * 
 *     to delete values for a single place:
 *     $oVersion->setProject("", $this->_aPrjConfig["fileprefix"], $sPhase, $sPlace);
 *     $aVersions=$oVersion->deleteValues();
 * 
 *     to delete all version items for a whole whole project (packages) in all phases
 *     $oVersion->setProject("", $this->_aPrjConfig["fileprefix"]);
 *     $aVersions=$oVersion->deleteValues("version");
 * 
 * @author hahn
 */
class valuestore {

    public $sProject = false;
    public $sPackage = false;
    public $sPhase = false;
    public $sPlace = false;
    public $sHost = false;
    public $sVariable = false;
    public $sData = false;
    
    protected $_bDebug = true;

    /**
     * filename of sqlite database file
     * @var type 
     */
    private $_dbfile = false;

    /**
     * database connection
     * @var object
     */
    protected $_oDB = false;

    /**
     * TTL vor stored values - 1 day [sec]
     * @var integer
     */
    protected $_iTTL = 86400;

    /**
     * create statement for the database
     * @var type 
     */
    private $_sCreate = '
        CREATE TABLE "values" (
          `id` INTEGER PRIMARY KEY  AUTOINCREMENT  NOT NULL  UNIQUE ,
          `time` DATETIME,
          `project` TEXT,
          `package` TEXT,
          `phase` TEXT,
          `place` TEXT,
          `host` TEXT,
          `variable` TEXT,
          `data` TEXT,
          UNIQUE (project, package, phase, place, host, variable) ON CONFLICT REPLACE
        );'
    ;
    
    // hardcoded
    protected $_allowedPhases=array('preview', 'stage', 'live');
    protected $_allowedPlaces=array('onhold', 'ready2install', 'deployed');
    
    // ----------------------------------------------------------------------
    // CONSTRUCTOR
    // ----------------------------------------------------------------------
    
    /**
     * constructor ... no params
     */
    public function __construct(){
        
        // cache dir is hardcoded to versions directory :-/
        $this->_dbfile = __DIR__ . '/../data/versioncache.db';
        
        $this->_oDB = new PDO("sqlite:" . $this->_dbfile);
        if (!file_exists($this->_dbfile) || !filesize($this->_dbfile)) {
            $this->_createDb();
        }
    }
    /**
     * add a log messsage
     * @global object $oLog
     * @param  string $sMessage  messeage text
     * @param  string $sLevel    warnlevel of the given message
     * @return bool
     */
    private function log($sMessage, $sLevel = "info") {
        global $oCLog;
        if($oCLog){
            return $oCLog->add(basename(__FILE__) . " class " . __CLASS__ . " - " . $sMessage, $sLevel);
        }
        return false;
    }
    
    // ----------------------------------------------------------------------
    // PRIVATE 
    // ----------------------------------------------------------------------

    /**
     * create sqlite database - called in constructor if the file does not exist
     */
    private function _createDb() {
        if (file_exists($this->_dbfile)) {
            echo $this->_bDebug ? "removing existing file $this->_dbfile ...<br>\n" : '';
            unlink($this->_dbfile);
        }
        // after deleting the db file we need to re instantiate
        echo $this->_bDebug ? "create database as file $this->_dbfile ...<br>\n" : '';
        $this->_oDB = new PDO("sqlite:" . $this->_dbfile);
        $this->_makeQuery($this->_sCreate);
        if (!file_exists($this->_dbfile)) {
            $this->_quit(__FUNCTION__ , "ERROR: unable to create sqlite database " . $this->_dbfile);
        }
        return true;
    }

    /**
     * execute a sql statement
     * @param string $sSql   sql statement
     * @param array  $aVars  array with values (uses PDO::prepare(); $sSql must contain placeholders :key)
     * @return database object
     */
    private function _makeQuery($sSql, $aVars=false) {
        // $this->_log(__FUNCTION__."($sSql)");
        // echo "DEBUG: executing SQL<pre>$sSql</pre>";
        $this->log("start query");
        if ($aVars && is_array($aVars)){
            $oStatement = $this->_oDB->prepare($sSql);
            $result = $oStatement->execute($aVars);
        } else {
            $result = $this->_oDB->query($sSql);
        }
        if(!$result){
            $this->log('<pre>'.print_r($this->_oDB->errorInfo()).'</pre>', 'error');
        }
        $this->log("end query - ".$sSql);
        return $result;
    }
    
    /**
     * execute a sql statement
     * @param string $sSql sql statement
     * @return array of resultset
     */
    private function _makeSelectQuery($sSql, $aKey=false) {
        // $this->_log(__FUNCTION__."($sSql)");
        // echo "DEBUG: executing select SQL<pre>$sSql</pre>";
        $this->log("start query");
        $oStatement = $this->_oDB->prepare($sSql);
        $oStatement->execute();
        $aReturn=array();
        while ($row = $oStatement->fetch(PDO::FETCH_ASSOC)) {
          if ($aKey && array_key_exists($aKey, $row)){
            $aReturn[] = $row[$aKey];
          } else {
            $aReturn[] = $row;
          }
        }        
        $this->log("end query - ".$sSql);
        return $aReturn;
    }

    /**
     * log error and quit. it echoes the error message on screen if debug 
     * is enabled.
     * @param string  $sFunction  name of method that throws the error
     * @param string  $sMessage   error message
     * @return boolean
     */
    private function _quit($sFunction, $sMessage){
        error_log(__CLASS__ . "::$sFunction - $sMessage " . "whereiam: " . print_r($this->whereiam(), 1));
        if ($this->_bDebug){
            echo __CLASS__ . "::$sFunction stopped.<br>\n"
                    . "whereiam: <pre>" . print_r($this->whereiam(), 1)."</pre>"
                    ;
            die($sMessage);
        } else {
            die("ERROR ... wrong usage of class ". __CLASS__);
        }
        return false;
    }

    // ----------------------------------------------------------------------
    // PUBLIC GETTER
    // ----------------------------------------------------------------------
    
    
    /**
     * get list of current projects
     * @return type
     */
    public function getProjects(){
        $sSql="select distinct(project) from `values`";
        return $this->_makeSelectQuery($sSql, 'project');
    }
    
    /**
     * get list of current projects
     * @return type
     */
    public function getPackages(){
        $sSql="select distinct(package) from `values`";
        return $this->_makeSelectQuery($sSql, 'package');
    }
    
    /**
     * get phases of the current project; a project must be set be set before
     * @return type
     */
    public function getPhases(){
        if (!$this->sProject && !$this->sPackage){
            $this->_quit(__FUNCTION__ , "you need to set a project first. Use the setProject() method to do so.");
        }
        $sWhere='1=1 '
            . ($this->sProject ? "AND project='"  . $this->sProject . "' " : "")
            . ($this->sPackage ? "AND package='"  . $this->sPackage . "' " : "")
            ;
        $sSql="select distinct(phase) from `values` WHERE $sWhere";
        return $this->_makeSelectQuery($sSql,'phase');
    }
    
    /**
     * get places of the current project; a project and must be set be set before
     * @return type
    public function getPlaces(){
        if (!$this->sProject || !$this->sPhase){
            $this->_quit(__FUNCTION__ , "ERROR: you need to set a project, and phase first. Use the setProject() method to do so.");
        }
        $sWhere='1=1 '
            . ($this->sProject ? "AND project='"  . $this->sProject . "' " : "")
            . ($this->sPackage ? "AND phase='"    . $this->sPackage . "' " : "")
            ;
        $sSql="select distinct(phase) from `values` WHERE $sWhere";
        $sSql="select distinct(place) from `values`
            WHERE
                project='" . $this->sProject . "'
                AND phase='" . $this->sPhase . "'
                ";
        return $this->_makeSelectQuery($sSql, 'place');
    }
     */
    
    /**
     * get hosts that have installed a project
     * @return type
     */
    public function getHosts(){
        $sWhere='1=1 '
            . ($this->sProject ? "AND project='"  . $this->sProject . "' " : "")
            . ($this->sPackage ? "AND package='"  . $this->sPackage . "' " : "")
            . ($this->sPhase   ? "AND phase='"  . $this->sPhase . "' "   : "")
            . "AND place='deployed' "
            //. "AND host>'' "
            ;
        $sSql="select distinct(host) from `values` WHERE $sWhere";
        return $this->_makeSelectQuery($sSql, 'host');
    }

    /**
     * get entries of the current place (project, phase and place can be
     * set before)
     * @see setProject()
     * @param string  $sVariable  variable for filtering column "variable"
     * @return array
     */
    public function getVar($sVariable=''){
        $sWhere='1=1 '
            . ($this->sProject ? "AND project='"  . $this->sProject . "' " : "")
            . ($this->sPackage ? "AND package='"  . $this->sPackage . "' " : "")
            . ($this->sPhase   ? "AND phase='"    . $this->sPhase   . "' " : "")
            . ($this->sPlace   ? "AND place='"    . $this->sPlace   . "' " : "")
            . ($this->sHost    ? "AND host='"     . $this->sHost    . "' " : "")
            . ($sVariable      ? "AND variable='" . $sVariable      . "' " : "")
            ;
        $sSql="select * from `values` WHERE $sWhere";
        return $this->_makeSelectQuery($sSql);
    }
    
    /**
     * get versions of the current place (project, phase and place can be
     * set before)
     * @see setProject()
     * @return array
     */
    public function getVersion(){
        $aData=$this->getVar('version');
        
        // get json in subkey host -> data and store keys in host -> _data
        if($aData && is_array($aData) && count($aData)){
            foreach($aData as $iKey=>$aHostinfos){
                $aJson=json_decode($aHostinfos['data'], 1);
                $aData[$iKey]['_data']=$aJson;
            }
        }
        return $aData;
    }
    
    /**
     * return currebntly set project, phase, place and host
     * @return array
     */
    public function whereiam(){
        return array(
            'project'=>$this->sProject,
            'package'=>$this->sPackage,
            'phase'=>$this->sPhase,
            'place'=>$this->sPlace,
            'host'=>$this->sHost,
        );
    }
    /**
     * return currebntly set project, phase, place and host
     * @return type
     */
    public function dumpdb(){
        $sSql="select * from `values`";
        return $this->_makeSelectQuery($sSql);
    }
    
    
    // ----------------------------------------------------------------------
    // PUBLIC SETTER
    // ----------------------------------------------------------------------
    
    /**
     * init a "position" before getting or updating deleting a value
     * 
     * @param string  $sProject  project id
     * @param string  $sPackage  package id
     * @param string  $sPhase    phase (preview|stage|live)
     * @param string  $sPlace    place (onhold|ready2install|deployed)
     * @param string  $sHost     hostname (for place deployed)
     * @return type string
     */
    public function setProject($sProject, $sPackage, $sPhase=false, $sPlace=false, $sHost=false){
        $this->sProject=preg_replace('/[^a-z\-\_0-9]/', '' ,$sProject);
        $this->setPackage($sPackage, $sPhase, $sPlace, $sHost);
        return $this->sProject;
    }

    /**
     * set a package (and more granular items)
     * @see setProject()
     * 
     * @param string  $sPackage  package id
     * @param string  $sPhase    phase (preview|stage|live)
     * @param string  $sPlace    place (onhold|ready2install|deployed)
     * @param string  $sHost     hostname (for place deployed)
     * @return type string
     */
    public function setPackage($sPackage, $sPhase, $sPlace=false, $sHost=false){
        $this->sPackage=preg_replace('/[^a-z\-\_0-9]/', '' ,$sPackage);
        $this->setPhase($sPhase, $sPlace, $sHost);
        return $this->sPackage;
    }
    
    /**
     * set a phase (and more granular items)
     * @see setProject()
     * 
     * @param string  $sPhase    phase (preview|stage|live)
     * @param string  $sPlace    place (onhold|ready2install|deployed)
     * @param string  $sHost     hostname (for place deployed)
     * @return type string
     */
    public function setPhase($sPhase, $sPlace=false, $sHost=false){
        if (!$this->sProject && !$this->sPackage){
            $this->_quit(__FUNCTION__ , "ERROR: you need to set a project. Use the setProject() method to do so.");
            return false;
        }
        if ($sPhase && array_search($sPhase, $this->_allowedPhases)===false){
            $this->_quit(__FUNCTION__ , "ERROR: you set a wrong phase [$sPhase]");
        }
        $this->sPhase=$sPhase;
        $this->setPlace($sPlace, $sHost);
        return $this->sPhase;
    }

    /**
     * set a place (and host)
     * @see setProject()
     * 
     * @param string  $sPlace    place (onhold|ready2install|deployed)
     * @param string  $sHost     hostname (for place deployed)
     * @return type string
     */
    public function setPlace($sPlace, $sHost=false){
        if ((!$this->sProject && !$this->sPackage)){
            $this->_quit(__FUNCTION__ , "ERROR: you need to set a project and phase. Use the setProject() method to do so.");
            return false;
        }
        if ($sPlace && !$this->sPhase){
            $this->_quit(__FUNCTION__ , "ERROR: you cannot set place [$sPlace] with leaving phase empty.");
        }
        if ($sPlace && array_search($sPlace, $this->_allowedPlaces)===false){
            $this->_quit(__FUNCTION__ , "ERROR: you set a wrong place [$sPlace]");
        }
        $this->sPlace=$sPlace;
        $this->setHost($sHost);
        return $this->sPlace;
    }
    
    /**
     * set a host for place "deployed"
     * @see setProject()
     * 
     * @param string  $sHost     hostname
     * @return type string
     */
    public function setHost($sHost=false){
        if ((!$this->sProject && !$this->sPackage)){
            $this->_quit(__FUNCTION__ , "ERROR: you need to set a project, phase and place. Use the setProject() method to do so.");
            return false;
        }
        if($sHost && $this->sPlace!=='deployed'){
            $this->_quit(__FUNCTION__ , "ERROR: a host can be set on place [deployed] only.");
        }
        /*
        if(!$sHost && $this->sPlace==='deployed'){
            $this->_quit(__FUNCTION__ , "ERROR: on place [deployed] a host MUST be set.");
        }
         */
        $this->sHost=preg_replace('/[^a-z\.\-0-9]/', '', $sHost);
        return $this->sHost;
    }
    
    // ----------------------------------------------------------------------
    // DATABASE FUNCTIONS
    // ----------------------------------------------------------------------
    
    /**
     * cleanup value store
     * @param integer  $iTtl  optional: max age in seconds of items to keep; 0 to delete all; default is 1 day
     * @return boolean
     */
    public function cleanup($iTtl=false){
        if ($iTtl===false){
            $iTtl=$this->_iTTL;
        }
        $sTtlDate=date("Y-m-d H:i:s", date("U") - (int)$iTtl );
        $sSql="DELETE FROM `values` WHERE `time` < '$sTtlDate' "
            . ($this->sProject ? "AND project='"  . $this->sProject . "' " : "")
            . ($this->sPackage ? "AND package='"  . $this->sPackage . "' " : "")
            . ($this->sPhase   ? "AND phase='"    . $this->sPhase   . "' " : "")
            . ($this->sPlace   ? "AND place='"    . $this->sPlace   . "' " : "")
            . ($this->sHost    ? "AND host='"     . $this->sHost    . "' " : "")
            ;
        // die("$iTtl ... $sSql ... ABORT");
        $this->_makeQuery($sSql);
        $this->_makeQuery("vacuum;");
        return true;
    }
    
    /**
     * delete values from value store. You need to call setProject() to set
     * project or package and optional phase, place, host. Then call this delete
     * method.
     * @param string $sVariable  optional: limit deletion to a given variable
     * @return boolean
     */
    public function deleteValues($sVariable){
        if ((!$this->sProject && !$this->sPackage) ){
            $this->_quit(__FUNCTION__ , "ERROR: you need to set a project, phase and place. use the setProject() method to do so.");
        }
        $sWhere='1=1 '
            . ($this->sProject ? "AND project='"  . $this->sProject . "' " : "")
            . ($this->sPackage ? "AND package='"  . $this->sPackage . "' " : "")
            . ($this->sPhase   ? "AND phase='"    . $this->sPhase   . "' " : "")
            . ($this->sPlace   ? "AND place='"    . $this->sPlace   . "' " : "")
            . ($this->sHost    ? "AND host='"     . $this->sHost    . "' " : "")
            . ($sVariable      ? "AND variable='" . $sVariable      . "' " : "")
            ;
        $sSql="DELETE FROM `values` WHERE $sWhere";
        // echo $sSql; 
        // return true;
        return $this->_makeQuery($sSql);
    }
    
    /**
     * update a version
     * @return boolean
     */
    public function updateVar($sVarname,$sValue){
        if ((!$this->sProject && !$this->sPackage) || !$this->sPhase || !$this->sPlace ){
            $this->_quit(__FUNCTION__ , "ERROR: you need to set a project, phase and place. use the setProject() method to do so.");
        }

        $this->cleanup();
        $sSql="
        INSERT into `values` (`time`, `project`, `package`, `phase`, `place`, `host`, `variable`, `data` )
        VALUES (
            '" . date("Y-m-d H:i:s") . "',
            '" . $this->sProject . "',
            '" . $this->sPackage . "',
            '" . $this->sPhase . "',
            '" . $this->sPlace . "',
            '" . $this->sHost . "',
            :variable,
            :value
        );
        ";
        return $this->_makeQuery(
                $sSql,
                array(
                    'variable'=>$sVarname,
                    'value'=>$sValue,
                )
        );
    }
    /**
     * update a version value
     * @param type $sVersioninfos
     * @return boolean
     */
    public function updateVersion($sVersioninfos){
        return $this->updateVar('version', $sVersioninfos);
    }
    
}
