<?php
namespace DynCom\mysyde\common\classes;
use DynCom\mysyde\common\interfaces\GenericDBQueryWrapperInterface;
use PDO;
use PDOException;

/**
 * Created by PhpStorm.
 * User: Micha
 * Date: 16.01.2015
 * Time: 23:38
 */
class PDOQueryWrapper implements GenericDBQueryWrapperInterface
{

    protected $hostName = 'localhost';
    protected $portNumber = '3306';
    protected $dbName = 'dcshop';
    protected $userName = 'root';
    protected $password = '';
    protected $optionsArray = array(\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION);
    protected $dsn;

    protected $allowedComparisonOperators = array('IS NULL', 'IS NOT NULL', '=', '!=', '<', '>', '>=', '<=', 'IN', 'LIKE');
    protected $comparisonValidation;

    protected $unfinishedQuery = [];
    protected $unfinishedQueryGroups = array();
    protected $currQueryGroup;

    protected $connectionObject;

    protected $query = '';
    /**
     * @var \PDOStatement
     */
    protected $stmt;

    /**
     * @return mixed
     */
    public function getStmt()
    {
        return $this->stmt;
    }

    /**
     * @return mixed
     */
    public function getResult()
    {
        return $this->result;
    }

    protected $result;
    protected $resultIndex = 0;

    protected $errorState = TRUE;
    protected $errorMessage = 'No connection';
    protected $errorCode = '-1';


    /**
     * @param string $hostName
     * @param int $portNumber
     * @param string $dbName
     * @param string $userName
     * @param string $password
     * @param array $optionsArray
     */
    public function __construct($hostName, $portNumber, $dbName, $userName, $password, array $optionsArray = NULL)
    {

        $this->hostName = ($hostName ? (string)$hostName : 'localhost');
        $this->portNumber = ($portNumber ? (int)$portNumber : 3306);
        $this->dbName = ($dbName ? (string)$dbName : 'dcshop');
        $this->userName = ($userName ? (string)$userName : 'root');
        $this->password = ($password ? (string)$password : '123');
        $this->optionsArray = ((is_array($optionsArray) && count($optionsArray) > 0) ? $optionsArray : array(\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION));

        $this->dsn = 'mysql:dbname=' . $this->dbName . ';host=' . $this->hostName . ';port=' . (string)$this->portNumber . ';charset=utf8mb4';

        $pdo = new \PDO($this->dsn, $this->userName, $this->password, $this->optionsArray);

        if (!$pdo->exec('SET NAMES utf8mb4') || !$pdo->exec('SET CHARACTER SET utf8mb4')) {
            $pdoErrorInfo = $pdo->errorInfo();
            $this->errorState = TRUE;
            $this->errorMessage = print_r($pdoErrorInfo, 1);
        }

        $this->connectionObject = $pdo;
        $this->errorState = FALSE;
        $this->errorMessage = '';

        $this->_setComparisonValidationArray();

    }

    /**
     * @param $hostName
     * @return $this
     */
    public function setHostName($hostName)
    {
        $this->hostName = $hostName;
        return $this;
    }

    /**
     * @param $portNumber
     * @return $this
     */
    public function setPortNumber($portNumber)
    {
        $this->portNumber = $portNumber;
        return $this;
    }

    /**
     * @param $dbName
     * @return $this
     */
    public function setDbName($dbName)
    {
        $this->dbName = $dbName;
        return $this;
    }

    /**
     * @param $userName
     * @return $this
     */
    public function setUserName($userName)
    {
        $this->userName = $userName;
        return $this;
    }

    /**
     * @param $password
     * @return $this
     */
    public function setPassword($password)
    {
        $this->password = $password;
        return $this;
    }

    /**
     * @param array $optionsArray
     * @return $this
     */
    public function setOptionsArray(array $optionsArray)
    {
        $this->optionsArray = $optionsArray;
        return $this;
    }

    /**
     * @param string $query
     * @return $this
     */
    public function setQuery($query)
    {
        $this->query = $query;
        return $this;
    }

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

    /**
     * @return int
     */
    public function getPortNumber()
    {
        return $this->portNumber;
    }

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

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

    /**
     * @return array
     */
    public function getOptionsArray()
    {
        return $this->optionsArray;
    }

    /**
     * @return mixed
     */
    public function getQuery()
    {
        return $this->query;
    }

    /**
     * @return boolean
     */
    public function isErrorState()
    {
        return $this->errorState;
    }

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

    /**
     * @return bool
     */
    public function reconnect()
    {

        $pdo = new \PDO($this->dsn, $this->userName, $this->password, $this->optionsArray);

        if (!$pdo->exec('SET NAMES utf8') || !$pdo->exec('SET CHARACTER SET utf8')) {
            $pdoErrorInfo = $pdo->errorInfo();
            $this->query = '';
            $this->stmt = NULL;
            $this->result = NULL;
            $this->errorState = TRUE;
            $this->errorMessage = print_r($pdoErrorInfo, 1) . ' || ';
            return FALSE;
        }

        $this->connectionObject = $pdo;

        $this->errorState = FALSE;
        $this->errorMessage = '';

        $this->query = '';
        $this->stmt = NULL;
        $this->result = NULL;
        $this->errorState = FALSE;
        $this->errorMessage = '';
        return TRUE;
    }

    /**
     * @return bool
     * @throws \ErrorException
     */
    public function doQuery()
    {

        if (!($this->connectionObject instanceof \PDO)) {
            throw new \ErrorException('No PDO Connection-object set');
        }

        if (empty($this->query)) {
            throw new \ErrorException('Query is not set');
        }


        //Check env for logging directives
        $logQueries	=	(bool)getenv('LOG_QUERIES');
        $tableFilters	=	(string)getenv('QUERY_LOG_TABLES');
        $methodFilters	=	(string)getenv('QUERY_LOG_METHODS');

        $tablesToLog = explode(',',$tableFilters);
        $methodsToLog = explode(',',$methodFilters);

        if (count($tablesToLog) > 0) {
            $logTable = false;
            foreach($tablesToLog as $tableName) {

                if ($tableName && strpos($this->query,$tableName) !== false) {
                    $logTable = true;
                }
            }
        } else {
            $logTable = true;
        }

        if (count($methodsToLog) > 0) {
            $logMethod = false;
            foreach($methodsToLog as $methodName) {
                if ($methodName && strpos($this->query,$methodName) !== false) {
                    $logMethod = true;
                }
            }
        } else {
            $logMethod = true;
        }

        $logQuery = $logQueries && $logTable && $logMethod;
        if ($logQuery) {
            $filePath = rtrim($_SERVER['DOCUMENT_ROOT'],'/') . '/logs/query_log.txt';
            $newContent = time() . ' - QUERY: ' . $this->query . PHP_EOL . generateCallTrace() . PHP_EOL . PHP_EOL;
            file_put_contents($filePath,$newContent,FILE_APPEND);
        }

        try {
            $pdoStatement = $this->connectionObject->query($this->query);
        } catch (PDOException $e) {
            $this->stmt = NULL;
            $this->result = NULL;
            $this->errorState = TRUE;
            //echo "... QUERY ERROR!!! ...QUERY : $this->query";
            $errorInfo = print_r($this->connectionObject->errorInfo(), 1);
            $this->errorMessage .= $errorInfo . ' || ';
            return FALSE;
        }

        $this->result = NULL;
        $this->stmt = $pdoStatement;
        $this->unfinishedQuery = [];
        $this->errorMessage = '';
        $this->errorState = false;
        $this->errorCode = '0';
        return TRUE;

    }

    /**
     * @return bool
     * @throws \ErrorException
     */
    public function prepareQuery()
    {

        if (empty($this->query)) {
            throw new \ErrorException('Query is not set.');
        }
        //FOR VALIDATION of calls, uncomment this:
        // $this->connectionObject = new \PDO($this->dsn,$this->userName,$this->password,$this->optionsArray);
        if (!($stmt = $this->connectionObject->prepare($this->query))) {
            $this->stmt = NULL;
            $this->result = NULL;
            return FALSE;
        }

        $this->result = NULL;
        $this->stmt = $stmt;
        return TRUE;
    }

    /**
     * @param array $array Parameter-Value-Type-Array
     *                      array(
     *                          array(ParameterName1,ParameterValue1,ParameterType1),
     *                          array(ParameterName2,ParameterValue2,ParameterType2)
     *                          ...
     *                      )
     *
     * @return bool
     * @throws \ErrorException
     */
    public function bindParameters(array $array)
    {

        if(!($this->stmt instanceof \PDOStatement)) {
            throw new \ErrorException('No Statement is set.');
        }
        if ($this->stmt->errorCode() > 0) {
            throw new \ErrorException('Statement is in error. ' . print_r($this->stmt->errorInfo(),1));
        }

        $nameTestPattern = '/^(?:\:[a-zA-Z0-9_\-]+)|(?:[0-9]+)$/';
        foreach ($array as $singleParameterArray) {
            if (!is_array($singleParameterArray) || (count($singleParameterArray) !== 3)) {
                throw new \ErrorException('At least one entry in the paramter-array does not contain an array of count 3
                the required format [colon]parameterName');
            }
            $paramName = (string)$singleParameterArray[0];
            $paramValue = $singleParameterArray[1];
            $paramType = $singleParameterArray[2];
            if (!preg_match($nameTestPattern, $paramName)) {
                throw new \ErrorException('At least one parameterName is neither a question-mark nor a string with pattern [colon]parameterName');
            }
            if (!is_int($paramType)) {
                throw new \ErrorException('At least one parameterType is not an integer');
            }
            if (is_numeric($paramName)) {
                $paramIndex = (int)$paramName;
                if (!$this->stmt->bindValue($paramIndex, $paramValue, $paramType)) {
                    throw new \ErrorException('Parameter \'' . $paramName . '\', Value \'' . $paramValue . '\', Type \'' . $paramType . ' could not be bound');
                }
            } else {
                if (!$this->stmt->bindValue($paramName, $paramValue, $paramType)) {
                    throw new \ErrorException('Parameter \'' . $paramName . '\', Value \'' . $paramValue . '\', Type \'' . $paramType . ' could not be bound');
                }
            }
        }
        return TRUE;
    }


    /**
     * @return bool
     * @throws \ErrorException
     */
    public function executePreparedStatement()
    {
        if (!$this->stmt instanceof \PDOStatement) {
            throw new \ErrorException('No prepared statement set');
        }
        try {
            $this->stmt->execute();
        } catch (PDOException $e) {
            $this->errorState = TRUE;
            $this->errorMessage .= $e->getMessage() . ' || ';
            return FALSE;
        }

        return TRUE;
    }


    /**
     * @param array $params
     * @return bool
     * @throws \ErrorException
     */
    public function bindResult(array $params)
    {

        if (!($this->stmt instanceof \PDOStatement) || ($this->stmt->errorCode() > 0)) {
            throw new \ErrorException('Statement is not prepared or in error');
        }

        $numberMapping = FALSE;
        foreach ($params as $key => $resultBinding) {
            if (!is_array($resultBinding)) {
                $numberMapping = TRUE;
                if (!$this->stmt->bindColumn($key, $resultBinding)) {
                    throw new \ErrorException('At least one entry in the numeric column-binding array could not be bound');
                }
            } elseif ($numberMapping) {
                throw new \ErrorException('Column-binding array is of mixed type. Array has to either contain sub-arrays with column-name - binding-reference structure or numeric array with only binding-reference');
            } else {
                if (count($resultBinding) !== 2) {
                    throw new \ErrorException('at least one sub-array of column-binding array does not have count 2');
                }
                if (!$this->stmt->bindColumn($resultBinding[0], $resultBinding[1])) {
                    throw new \ErrorException('At least one sub-array in column-binding array could not be bound');
                }
            }
        }
        return TRUE;

    }


    /**
     * @return bool
     */
    public function startTransaction()
    {

        if (!$this->connectionObject->beginTransaction()) {
            $this->errorState = TRUE;
            $this->errorMessage .= print_r($this->connectionObject->errorInfo, 1) . ' || ';
            return FALSE;
        }

        $this->stmt = NULL;
        $this->result = NULL;
        $this->errorState = FALSE;
        $this->errorMessage = '';
        return TRUE;
    }

    /**
     * @return bool
     */
    public function commitTransaction()
    {

        if (!$this->connectionObject->commit()) {
            $this->errorState = TRUE;
            $this->errorMessage .= print_r($this->connectionObject->errorInfo, 1) . ' || ';
            return FALSE;
        }

        $this->stmt = NULL;
        $this->result = NULL;
        $this->errorState = FALSE;
        $this->errorMessage = '';
        return TRUE;

    }

    /**
     * @return bool
     */
    public function rollbackTransaction()
    {

        if (!$this->connectionObject->rollBack()) {
            $this->errorState = TRUE;
            $this->errorMessage .= print_r($this->connectionObject->errorInfo, 1) . ' || ';
            return FALSE;
        }

        $this->stmt = NULL;
        $this->result = NULL;
        $this->errorState = FALSE;
        $this->errorMessage = '';
        return TRUE;

    }

    /**
     * @return int
     */
    public function getLastInsertId()
    {
        return $this->connectionObject->lastInsertId();
    }

    /**
     * @return int
     * @throws \ErrorException
     */
    public function getNoOfAffectedRows()
    {

        if (!($this->stmt instanceof \PDOStatement) || ($this->stmt->errorCode() > 0)) {
            throw new \ErrorException('Statement is not prepared or in error');
        }

        return $this->stmt->rowCount();

    }

    /**
     * @return int
     * @throws \ErrorException
     */
    public function getNoOfReturnedRows()
    {

        if (is_array($this->result)) {
            return count($this->result);
        }

        if(!($this->stmt instanceof \PDOStatement)) {
            throw new \ErrorException('No Statement is set.');
        }
        if ($this->stmt->errorCode() > 0) {
            throw new \ErrorException('Statement is in error. ' . print_r($this->stmt->errorInfo(),1));
        }

        if ($result = $this->stmt->fetchAll(PDO::FETCH_BOTH)) {
            $this->result = $result;
            return count($this->result);
        }

        if ($this->stmt->closeCursor() && $this->stmt->execute() && ($result = $this->stmt->fetchAll(PDO::FETCH_BOTH))) {
            $this->result = $result;
            return count($this->result);
        }

        return -1;

    }

    /**
     * @return array
     * @throws \ErrorException
     */
    public function getResultArray()
    {

        if (is_array($this->result) && (count($this->result) > 0)) {
            return $this->result;
        }

        if ($this->stmt instanceof \PDOStatement) {
            $result = $this->stmt->fetchAll(PDO::FETCH_BOTH);
            if (is_array($result)) {
                $this->result = $result;
                return $this->result;
            } else {
                $this->stmt->closeCursor();
                try {
                    $this->stmt->execute();
                    $result = $this->stmt->fetchAll();
                    if (count($result) > 0) {
                        $this->result = $result;
                        return $this->result;
                    }
                } catch (\Exception $e) {
                    throw new \ErrorException('Result was not already set, fetchAll on $this->stmt didn\'t return a rowset, and resetting cursor and trying fetchAll again did not work');
                }
            }
        }
        return array();
    }

    /**
     * @return array
     * @throws \ErrorException
     */
    public function getNextRow()
    {
        if (is_array($this->result) && is_array($this->result[$this->resultIndex])) {
            $returnValue = $this->result[$this->resultIndex];
            ++$this->resultIndex;
            return $returnValue;
        }
        if ($this->stmt instanceof \PDOStatement) {
            if ($result = $this->stmt->fetchAll()) {
                $this->result = $result;
                ++$this->resultIndex;
                return $this->result[0];
            }
            $this->stmt->closeCursor();
            try {
                $this->stmt->execute();
                $result = $this->stmt->fetchAll();
                if (is_array($result) && (count($result) > 0)) {
                    ++$this->resultIndex;
                    return $this->result[0];
                }
            } catch (\Exception $e) {
                throw new \ErrorException('Result was not already set, fetchAll on $this->stmt didn\'t return a rowset, and resetting cursor and trying fetchAll again did not work');
            }
        }
        return array();
    }

    /**
     * @param string $string
     *
     * @return bool|string
     */
    public function escapeString($string)
    {
        if (!($this->connectionObject instanceof \PDO)) {
            return FALSE;
        }

        return $this->connectionObject->quote($string);
    }

    /**
     * @return bool
     */
    public function closeStatement()
    {

        if (!($this->stmt instanceof \PDOStatement)) {
            return FALSE;
        }
        $this->stmt->closeCursor();
        return TRUE;

    }

    /**
     * @return \PDO
     */
    public function getConnectionObject()
    {
        return $this->connectionObject;
    }

    /**
     * @return array
     */
    public function __sleep()
    {
        return array_diff(array_keys(get_object_vars($this)), array('connectionObject'));
    }

    public function __wakeup()
    {
        $pdo = new \PDO($this->dsn, $this->userName, $this->password, $this->optionsArray);

        if (!$pdo->exec('SET NAMES utf8') || !$pdo->exec('SET CHARACTER SET utf8')) {
            $pdoErrorInfo = $pdo->errorInfo();
            $this->errorState = TRUE;
            $this->errorMessage = print_r($pdoErrorInfo, 1);
        }

        $this->connectionObject = $pdo;
        $this->errorState = FALSE;
        $this->errorMessage = '';
    }

    /**
     * @param $select
     * @return $this
     */
    public function select($select)
    {
        if (array_key_exists('lastElement',$this->unfinishedQuery) && null !== $this->unfinishedQuery['lastElement']) {
            throw new \BadMethodCallException("'select' cannot be called if a query was already initialized." . print_r($this->unfinishedQuery,1));
        }
        if (!is_scalar($select) && !is_array($select)) {
            throw new \InvalidArgumentException('The value entered is not a valid select expression. Only arrays and simple strings are allowed.');
        }
        if (is_array($select)) {
            foreach ($select as $key => $selectExpression) {
                if (!is_scalar($selectExpression)) {
                    throw new \InvalidArgumentException("Array-entry $key is not a valid select expression");
                }
            }
        }
        $this->unfinishedQuery['select'] = $select;
        $this->unfinishedQuery['lastElement'] = 'select';
        $this->unfinishedQuery['currJoinName'] = null;
        $this->unfinishedQuery['join'] = [];
        return $this;
    }

    /**
     * @param $fromName
     * @param string $fromAlias
     * @return $this
     */
    public function from($fromName, $fromAlias = '')
    {
        if ($this->unfinishedQuery['lastElement'] !== 'select') {
            throw new \BadMethodCallException("'from' can only be called immediately after calling 'select'.");
        }
        if (!array_key_exists('select', $this->unfinishedQuery)) {
            throw new \BadMethodCallException("'from' can only be called after 'select' has been called successfully.");
        }
        if (!is_scalar($fromName) || strip_tags($fromName) !== (string)$fromName) {
            throw new \InvalidArgumentException('The given name is invalid.');
        }
        if (!is_scalar($fromAlias) || strip_tags($fromAlias) !== (string)$fromAlias) {
            throw new \InvalidArgumentException('The given alias is invalid.');
        }
        $this->unfinishedQuery['from'][] = array('fromName' => $fromName, 'fromAlias' => $fromAlias);
        $this->unfinishedQuery['lastElement'] = 'from';
        $this->unfinishedQuery['currJoinName'] = null;
        return $this;
    }

    /**
     * @param $joinType
     * @param $joinName
     * @param $joinAlias
     * @return $this
     */
    public function join($joinType, $joinName, $joinAlias)
    {
        if (!in_array($joinType, array('left', 'right', 'inner', 'straight'))) {
            throw new \InvalidArgumentException("'joinType' must be either 'left', 'right', 'inner' or 'straight");
        }
        if (!in_array($this->unfinishedQuery['lastElement'], array('from', 'join', 'on', 'andOn', 'orOn'))) {
            throw new \BadMethodCallException("'from' can only be called immediately after calling either 'from', 'join','on','andOn' or 'orOn'.");
        }
        if (!array_key_exists('select', $this->unfinishedQuery)) {
            throw new \BadMethodCallException("'from' can only be called after 'select' has been called successfully.");
        }
        if (!is_scalar($joinName) || strip_tags($joinName) !== (string)$joinName) {
            throw new \InvalidArgumentException('The given name is invalid.');
        }
        if (!is_scalar($joinAlias) || strip_tags($joinAlias) !== (string)$joinAlias) {
            throw new \InvalidArgumentException('The given alias is invalid.');
        }
        $arr = array('joinType' => $joinType, 'joinName' => $joinName, 'joinAlias' => $joinAlias);
        $this->unfinishedQuery['join'][$joinName] = $arr;
        $this->unfinishedQuery['currJoinName'] = $joinName;
        $this->unfinishedQuery['lastElement'] = 'join';
        return $this;
    }

    /**
     * @param $fieldName
     * @param $operator
     * @param null $fieldValue
     * @param bool $fieldValueIsLiteral
     * @return $this
     */
    public function on($fieldName, $operator, $fieldValue = null, $fieldValueIsLiteral = false)
    {
        if (!isset($this->unfinishedQuery['currJoinName'])) {
            throw new \BadMethodCallException('There is no join currently open.');
        }
        if (!isset($this->unfinishedQuery['lastElement']) || (!in_array($this->unfinishedQuery['lastElement'], array('join', 'enterParen')))) {
            throw new \BadMethodCallException("'on' can only be called immediately after calling 'join'.");
        }
        if (!in_array($operator, $this->allowedComparisonOperators)) {
            throw new \InvalidArgumentException(strip_tags($operator) . " is not a valid comparison operator.");
        }
        if (!$this->comparisonValidation[$operator]($fieldName, $fieldValue)) {
            throw new \InvalidArgumentException('The given Name and/or Value are not allowed for the given operator.');
        }

        $this->unfinishedQuery['lastElement'] = 'on';
        if (!ctype_digit((string)$this->currQueryGroup)) {
            $currJoinName = $this->unfinishedQuery['currJoinName'];
            $currJoinRef = &$this->unfinishedQuery['join'][$currJoinName];
            $currJoinRef['on']['ungrouped'][] = array('precedingOperator' => '', 'fieldName' => $fieldName, 'operator' => $operator, 'fieldValue' => $fieldValue, 'valueIsLiteral' => (bool)$fieldValueIsLiteral);
        } else {
            $currJoinName = $this->unfinishedQuery['currJoinName'];
            $currJoinRef = &$this->unfinishedQuery['join'][$currJoinName];
            $currJoinRef['on']['grouped'][$this->currQueryGroup][] = array('precedingOperator' => '', 'fieldName' => $fieldName, 'operator' => $operator, 'fieldValue' => $fieldValue, 'valueIsLiteral' => (bool)$fieldValueIsLiteral);
        }
        return $this;
    }

    /**
     * @param $fieldName
     * @param $operator
     * @param null $fieldValue
     * @param bool $fieldValueIsLiteral
     * @return $this
     */
    public function andOn($fieldName, $operator, $fieldValue = null, $fieldValueIsLiteral = false)
    {
        if (!isset($this->unfinishedQuery['currJoinName'])) {
            throw new \BadMethodCallException('There is no join currently open.');
        }
        if (!isset($this->unfinishedQuery['lastElement']) || (!in_array($this->unfinishedQuery['lastElement'], array('on', 'andOn', 'orOn')))) {
            throw new \BadMethodCallException("'on' can only be called immediately after calling either 'on', 'andOn' or 'orOn'.");
        }
        if (!in_array($operator, $this->allowedComparisonOperators)) {
            throw new \InvalidArgumentException(strip_tags($operator) . "is not a valid comparison operator.");
        }
        if (!$this->comparisonValidation[$operator]($fieldName, $fieldValue)) {
            throw new \InvalidArgumentException('The given Name and/or Value are not allowed for the given operator.');
        }

        $this->unfinishedQuery['lastElement'] = 'andOn';
        if (!ctype_digit((string)$this->currQueryGroup)) {
            $currJoinName = $this->unfinishedQuery['currJoinName'];
            $currJoinRef = &$this->unfinishedQuery['join'][$currJoinName];
            $currJoinRef['on']['ungrouped'][] = array('precedingOperator' => 'AND', 'fieldName' => $fieldName, 'operator' => $operator, 'fieldValue' => $fieldValue, 'valueIsLiteral' => (bool)$fieldValueIsLiteral);
        } else {
            $currJoinName = $this->unfinishedQuery['currJoinName'];
            $currJoinRef = &$this->unfinishedQuery['join'][$currJoinName];
            $currJoinRef['on']['grouped'][$this->currQueryGroup][] = array('precedingOperator' => 'AND', 'fieldName' => $fieldName, 'operator' => $operator, 'fieldValue' => $fieldValue, 'valueIsLiteral' => (bool)$fieldValueIsLiteral);
        }
        return $this;
    }

    /**
     * @param $fieldName
     * @param $operator
     * @param null $fieldValue
     * @param bool $fieldValueIsLiteral
     * @return $this
     */
    public function orOn($fieldName, $operator, $fieldValue = null, $fieldValueIsLiteral = false)
    {
        if (!isset($this->unfinishedQuery['currJoinName'])) {
            throw new \BadMethodCallException('There is no join currently open.');
        }
        if (!isset($this->unfinishedQuery['lastElement']) || (!in_array($this->unfinishedQuery['lastElement'], array('on', 'andOn', 'orOn')))) {
            throw new \BadMethodCallException("'on' can only be called immediately after calling either 'on', 'andOn' or 'orOn'.");
        }
        if (!in_array($operator, $this->allowedComparisonOperators)) {
            throw new \InvalidArgumentException(strip_tags($operator) . "is not a valid comparison operator.");
        }
        if (!$this->comparisonValidation[$operator]($fieldName, $fieldValue)) {
            throw new \InvalidArgumentException('The given Name and/or Value are not allowed for the given operator.');
        }

        $this->unfinishedQuery['lastElement'] = 'orOn';
        if (!ctype_digit((string)$this->currQueryGroup)) {
            $currJoinName = $this->unfinishedQuery['currJoinName'];
            $currJoinRef = &$this->unfinishedQuery['join'][$currJoinName];
            $currJoinRef['on']['ungrouped'][] = array('precedingOperator' => 'OR', 'fieldName' => $fieldName, 'operator' => $operator, 'fieldValue' => $fieldValue, 'valueIsLiteral' => (bool)$fieldValueIsLiteral);
        } else {
            $currJoinName = $this->unfinishedQuery['currJoinName'];
            $currJoinRef = &$this->unfinishedQuery['join'][$currJoinName];
            $currJoinRef['on']['grouped'][$this->currQueryGroup][] = array('precedingOperator' => 'OR', 'fieldName' => $fieldName, 'operator' => $operator, 'fieldValue' => $fieldValue, 'valueIsLiteral' => (bool)$fieldValueIsLiteral);
        }
        return $this;
    }

    /**
     * @param $fieldName
     * @param $operator
     * @param null $fieldValue
     * @param bool $fieldValueIsLiteral
     * @return PDOQueryWrapper
     */
    public function where($fieldName, $operator, $fieldValue = null, $fieldValueIsLiteral = true)
    {
        /*if(isset($this->currQueryGroup)) {
            throw new \BadMethodCallException('A parenthesis needs to close before \'where\' can be called.');
        }*/
        if (!isset($this->unfinishedQuery['lastElement']) || (!in_array($this->unfinishedQuery['lastElement'], array('from', 'join', 'on', 'andOn', 'orOn', 'leaveParen', 'enterParen')))) {
            throw new \BadMethodCallException("'where' can only be called immediately after calling either 'from', 'join', 'on', 'andOn', 'orOn' or  'leaveParen'.");
        }
        if (!in_array($operator, $this->allowedComparisonOperators)) {
            throw new \InvalidArgumentException(strip_tags($operator) . "is not a valid comparison operator.");
        }
        if (!$this->comparisonValidation[$operator]($fieldName, $fieldValue)) {
            throw new \InvalidArgumentException('The given Name and/or Value are not allowed for the given operator.');
        }

        $this->unfinishedQuery['lastElement'] = 'where';
        if (!ctype_digit((string)$this->currQueryGroup)) {
            $this->unfinishedQuery['where']['ungrouped'][] = array('precedingOperator' => '', 'fieldName' => $fieldName, 'operator' => $operator, 'fieldValue' => $fieldValue, 'valueIsLiteral' => (bool)$fieldValueIsLiteral);
        } else {
            $this->unfinishedQuery['where']['grouped'][$this->currQueryGroup][] = array('precedingOperator' => '', 'fieldName' => $fieldName, 'operator' => $operator, 'fieldValue' => $fieldValue, 'valueIsLiteral' => (bool)$fieldValueIsLiteral);
        }
        $this->unfinishedQuery['currJoinName'] = null;
        return $this;
    }

    /**
     * @param $fieldName
     * @param $operator
     * @param null $fieldValue
     * @param bool $fieldValueIsLiteral
     * @return PDOQueryWrapper
     */
    public function andWhere($fieldName, $operator, $fieldValue = null, $fieldValueIsLiteral = true)
    {
        if (!isset($this->unfinishedQuery['lastElement']) || (!in_array($this->unfinishedQuery['lastElement'], array('where', 'andWhere', 'orWhere')))) {
            throw new \BadMethodCallException("'andWhere' can only be called immediately after calling either 'where', 'andWhere' or 'orWhere'.");
        }
        if (!in_array($operator, $this->allowedComparisonOperators)) {
            throw new \InvalidArgumentException(strip_tags($operator) . "is not a valid comparison operator.");
        }
        if (!$this->comparisonValidation[$operator]($fieldName, $fieldValue)) {
            throw new \InvalidArgumentException('The given Name and/or Value are not allowed for the given operator.');
        }

        $this->unfinishedQuery['lastElement'] = 'andWhere';
        if (!ctype_digit((string)$this->currQueryGroup)) {
            $this->unfinishedQuery['where']['ungrouped'][] = array('precedingOperator' => 'AND', 'fieldName' => $fieldName, 'operator' => $operator, 'fieldValue' => $fieldValue, 'valueIsLiteral' => (bool)$fieldValueIsLiteral);
        } else {
            $this->unfinishedQuery['where']['grouped'][$this->currQueryGroup][] = array('precedingOperator' => 'AND', 'fieldName' => $fieldName, 'operator' => $operator, 'fieldValue' => $fieldValue, 'valueIsLiteral' => (bool)$fieldValueIsLiteral);
        }
        $this->unfinishedQuery['currJoinName'] = null;
        return $this;
    }

    /**
     * @param $fieldName
     * @param $operator
     * @param null $fieldValue
     * @param bool $fieldValueIsLiteral
     * @return PDOQueryWrapper
     */
    public function orWhere($fieldName, $operator, $fieldValue = null, $fieldValueIsLiteral = true)
    {
        if (!isset($this->unfinishedQuery['lastElement']) || (!in_array($this->unfinishedQuery['lastElement'], array('where', 'andWhere', 'orWhere')))) {
            throw new \BadMethodCallException("'orWhere' can only be called immediately after calling either 'where', 'andWhere', 'on', 'andOn' or 'orOn' .");
        }
        if (!in_array($operator, $this->allowedComparisonOperators)) {
            throw new \InvalidArgumentException(strip_tags($operator) . "is not a valid comparison operator.");
        }
        if (!$this->comparisonValidation[$operator]($fieldName, $fieldValue)) {
            throw new \InvalidArgumentException('The given Name and/or Value are not allowed for the given operator.');
        }

        $this->unfinishedQuery['lastElement'] = 'orWhere';
        if (!ctype_digit((string)$this->currQueryGroup)) {
            $this->unfinishedQuery['where']['ungrouped'][] = array('precedingOperator' => 'OR', 'fieldName' => $fieldName, 'operator' => $operator, 'fieldValue' => $fieldValue, 'valueIsLiteral' => (bool)$fieldValueIsLiteral);
        } else {
            $this->unfinishedQuery['where']['grouped'][$this->currQueryGroup][] = array('precedingOperator' => 'OR', 'fieldName' => $fieldName, 'operator' => $operator, 'fieldValue' => $fieldValue, 'valueIsLiteral' => (bool)$fieldValueIsLiteral);
        }
        $this->unfinishedQuery['currJoinName'] = null;
        return $this;
    }

    /**
     * @param $expression
     * @return PDOQueryWrapper
     */
    public function groupBy($expression)
    {
        if (isset($this->currQueryGroup)) {
            throw new \BadMethodCallException('A parenthesis needs to close before \'groupBy\' can be called.');
        }
        if (!isset($this->unfinishedQuery['lastElement']) || (!in_array($this->unfinishedQuery['lastElement'], array('from', 'join', 'on', 'andOn', 'orOn', 'where', 'andWhere', 'orWhere', 'groupBy')))) {
            throw new \BadMethodCallException("'groupBy' can only be called immediately after calling either 'from', 'join', 'on', 'andOn', 'orOn', 'where', 'andWhere', 'orWhere' or 'groupBy' .");
        }
        if (!is_scalar($expression) || (strip_tags($expression) !== (string)$expression)) {
            throw new \InvalidArgumentException(strip_tags($expression) . ' is not a valid gorup-by expression.');
        }
        $this->unfinishedQuery['lastElement'] = 'groupBy';
        $this->unfinishedQuery['groupBy'][] = $expression;
        $this->unfinishedQuery['currJoinName'] = null;
        return $this;
    }

    /**
     * @param $fieldName
     * @param $operator
     * @param null $fieldValue
     * @return PDOQueryWrapper
     */
    public function having($fieldName, $operator, $fieldValue = null)
    {
        if (isset($this->currQueryGroup)) {
            throw new \BadMethodCallException('A parenthesis needs to close before \'having\' can be called.');
        }
        if (isset($this->currQueryGroup)) {
            throw new \BadMethodCallException('A parenthesis needs to close before \'having\' can be called.');
        }
        if (!isset($this->unfinishedQuery['lastElement']) || (!in_array($this->unfinishedQuery['lastElement'], array('groupBy', 'join', 'on', 'andOn', 'orOn')))) {
            throw new \BadMethodCallException("'having' can only be called immediately after calling either 'groupBy', 'join', 'on', 'andOn' or 'orOn' .");
        }
        if (!in_array($operator, $this->allowedComparisonOperators)) {
            throw new \InvalidArgumentException(strip_tags($operator) . "is not a valid comparison operator.");
        }
        if (!$this->comparisonValidation[$operator]($fieldName, $fieldValue)) {
            throw new \InvalidArgumentException('The given Name and/or Value are not allowed for the given operator.');
        }

        $this->unfinishedQuery['lastElement'] = 'having';
        if (!ctype_digit((string)$this->currQueryGroup)) {
            $this->unfinishedQuery['having']['ungrouped'][] = array('precedingOperator' => '', 'fieldName' => $fieldName, 'operator' => $operator, 'fieldValue' => $fieldValue);
        } else {
            $this->unfinishedQuery['having']['grouped'][$this->currQueryGroup][] = array('precedingOperator' => '', 'fieldName' => $fieldName, 'operator' => $operator, 'fieldValue' => $fieldValue);
        }
        $this->unfinishedQuery['currJoinName'] = null;
        return $this;
    }

    /**
     * @param $fieldName
     * @param $operator
     * @param null $fieldValue
     * @return PDOQueryWrapper
     */
    public function andHaving($fieldName, $operator, $fieldValue = null)
    {
        if (!isset($this->unfinishedQuery['lastElement']) || (!in_array($this->unfinishedQuery['lastElement'], array('having', 'andHaving', 'orHaving')))) {
            throw new \BadMethodCallException("'andHaving' can only be called immediately after calling either 'having', 'andHaving' or 'orHaving'.");
        }
        if (!in_array($operator, $this->allowedComparisonOperators)) {
            throw new \InvalidArgumentException(strip_tags($operator) . "is not a valid comparison operator.");
        }
        if (!$this->comparisonValidation[$operator]($fieldName, $fieldValue)) {
            throw new \InvalidArgumentException('The given Name and/or Value are not allowed for the given operator.');
        }

        $this->unfinishedQuery['lastElement'] = 'andHaving';
        if (!ctype_digit((string)$this->currQueryGroup)) {
            $this->unfinishedQuery['having']['ungrouped'][] = array('precedingOperator' => 'AND', 'fieldName' => $fieldName, 'operator' => $operator, 'fieldValue' => $fieldValue);
        } else {
            $this->unfinishedQuery['having']['grouped'][$this->currQueryGroup][] = array('precedingOperator' => 'AND', 'fieldName' => $fieldName, 'operator' => $operator, 'fieldValue' => $fieldValue);
        }
        $this->unfinishedQuery['currJoinName'] = null;
        return $this;
    }

    /**
     * @param $fieldName
     * @param $operator
     * @param null $fieldValue
     * @return PDOQueryWrapper
     */
    public function orHaving($fieldName, $operator, $fieldValue = null)
    {
        if (!isset($this->unfinishedQuery['lastElement']) || (!in_array($this->unfinishedQuery['lastElement'], array('having', 'andHaving', 'orHaving')))) {
            throw new \BadMethodCallException("'orHaving' can only be called immediately after calling either 'having', 'andHaving' or 'orHaving'.");
        }
        if (!in_array($operator, $this->allowedComparisonOperators)) {
            throw new \InvalidArgumentException(strip_tags($operator) . "is not a valid comparison operator.");
        }
        if (!$this->comparisonValidation[$operator]($fieldName, $fieldValue)) {
            throw new \InvalidArgumentException('The given Name and/or Value are not allowed for the given operator.');
        }

        $this->unfinishedQuery['lastElement'] = 'andHaving';
        if (!ctype_digit((string)$this->currQueryGroup)) {
            $this->unfinishedQuery['having']['ungrouped'][] = array('precedingOperator' => 'OR', 'fieldName' => $fieldName, 'operator' => $operator, 'fieldValue' => $fieldValue);
        } else {
            $this->unfinishedQuery['having']['grouped'][$this->currQueryGroup][] = array('precedingOperator' => 'OR', 'fieldName' => $fieldName, 'operator' => $operator, 'fieldValue' => $fieldValue);
        }
        $this->unfinishedQuery['currJoinName'] = null;
        return $this;
    }

    /**
     * @param $expression
     * @param $order
     * @return PDOQueryWrapper
     */
    public function orderBy($expression, $order)
    {
        if (!isset($this->unfinishedQuery['lastElement']) || (!in_array($this->unfinishedQuery['lastElement'], array('from', 'join', 'on', 'andOn', 'orOn', 'where', 'andWhere', 'orWhere', 'groupBy', 'orderBy','leaveParen')))) {
            throw new \BadMethodCallException("'orderBy' can only be called immediately after calling either 'from', 'join', 'on', 'andOn', 'orOn', 'where', 'andWhere', 'orWhere', 'groupBy','orderBy' or 'leaveParen'.");
        }
        if (!is_scalar($expression) || (strip_tags($expression) !== (string)$expression)) {
            throw new \InvalidArgumentException(strip_tags($expression) . ' is not a valid order-by expression.');
        }
        if (strcasecmp($order, 'asc') === 0) {
            $order = 'ASC';
        } elseif (strcasecmp($order, 'desc') === 0) {
            $order = 'DESC';
        } else {
            throw new \InvalidArgumentException('Paramter \'order\' must be \'asc\' or \'desc\' (not case sensitive).');
        }
        $this->unfinishedQuery['currJoinName'] = null;
        $this->unfinishedQuery['lastElement'] = 'orderBy';
        $this->unfinishedQuery['orderBy'][] = array('expression' => $expression, 'order' => $order);
        return $this;
    }

    /**
     * @param null $operator
     * @return $this
     */
    public function enterParentheses($operator = null)
    {
        if (isset($operator)) {
            if (strcasecmp($operator, 'AND') === 0) {
                $operator = 'AND';
            } elseif (strcasecmp($operator, 'OR') === 0) {
                $operator = 'OR';
            } else {
                throw new \InvalidArgumentException("Operator must be 'AND' or 'OR'.");
            }
        }
        if (!isset($this->unfinishedQuery['lastElement']) || !in_array($this->unfinishedQuery['lastElement'], array('join', 'on', 'andOn', 'orOn', 'where', 'andWhere', 'orWhere', 'leaveParen'))) {
            throw new \BadMethodCallException("Can only enter parantheses immediately after calling either 'join', 'on', 'andOn', 'orOn', 'where', 'andWhere','orWhere' or 'leaveParen.");
        }

        $newGroupIndex = 1;
        if (isset($this->unfinishedQueryGroups) && is_array($this->unfinishedQueryGroups) && count($this->unfinishedQueryGroups) > 0) {
            $newGroupIndex = (max(array_keys($this->unfinishedQueryGroups)) + 1);
        }
        $this->unfinishedQueryGroups[$newGroupIndex] = $operator;

        $this->currQueryGroup = $newGroupIndex;

        $this->unfinishedQuery['lastElement'] = 'enterParen';
        return $this;
    }

    /**
     * @return $this
     */
    public function leaveParentheses()
    {
        if (!ctype_digit((string)$this->currQueryGroup)) {
            throw new \BadMethodCallException("Can only leave parantheses if parentheses were set.");
        }
        $newGroupIndex = ($this->currQueryGroup - 1);
        $this->currQueryGroup = $newGroupIndex;
        if ($newGroupIndex <= 1) {
            $this->currQueryGroup = null;
        }
        $this->unfinishedQuery['lastElement'] = 'leaveParen';
        return $this;
    }

    /**
     * @return string
     */
    public function getConstructedQuery()
    {
        if (!isset($this->unfinishedQuery)) {
            throw new \BadMethodCallException('Query has not been constructed yet.');
        }

        //Evaluate Select-part
        $query = 'SELECT ';
        $query .= $this->unfinishedQuery['select'];
        $query .= ' FROM ';
        $i = 0;

        //Evaluate From-part
        foreach ($this->unfinishedQuery['from'] as $fromPart) {
            if ($i > 0) {
                $query .= ', ';
            }
            $query .= $fromPart['fromName'] . ' ';
            if (isset($fromPart['fromAlias'])) {
                $query .= $fromPart['fromAlias'] . ' ';
            }
            ++$i;
        }

        //Evaluate Join-part
        foreach ($this->unfinishedQuery['join'] as $joinPart) {
            $joinType = 'LEFT JOIN ';
            if ($joinPart['joinType'] === 'right') {
                $joinType = 'RIGHT JOIN ';
            } elseif ($joinPart['joinType'] === 'straight') {
                $joinType = 'STRAIGHT_JOIN ';
            }
            $query .= $joinType . $joinPart['joinName'] . ' ';
            if (isset($joinPart['joinAlias'])) {
                $query .= $joinPart['joinAlias'] . ' ';
            }
            if (is_array($joinPart['on'])) {
                $query .= 'ON (';
                if (isset($joinPart['on']['ungrouped']) && is_array($joinPart['on']['ungrouped'])) {
                    foreach ($joinPart['on']['ungrouped'] as $onCondition) {
                        $precedingOperator = $onCondition['precedingOperator'];
                        $fieldName = $onCondition['fieldName'];
                        $operator = $onCondition['operator'];
                        $valueIsLiteral = $onCondition['valueIsLiteral'];
                        $fieldValue = '';
                        if (isset($onCondition['fieldValue'])) {
                            $fieldValue = (($valueIsLiteral) ? $this->escapeString($onCondition['fieldValue']) : $onCondition['fieldValue']);
                        }
                        $query .= ' ' . $precedingOperator . ' (' . $fieldName . ' ' . $operator . ' ' . $fieldValue . ') ';
                        ++$i;
                    }
                }
                if (isset($joinPart['on']['grouped']) && is_array($joinPart['on']['grouped'])) {
                    $i = 0;
                    foreach ($joinPart['on']['grouped'] as $groupKey => $groupArr) {
                        $connector = (($i > 0) || (isset($joinPart['on']['ungrouped']) && is_array($joinPart['on']['ungrouped']))) ? $this->unfinishedQueryGroups[$groupKey] : '';
                        $query .= " $connector (";
                        foreach ($joinPart['on']['grouped'][$groupKey] as $onCondition) {
                            $precedingOperator = $onCondition['precedingOperator'];
                            $fieldName = $onCondition['fieldName'];
                            $operator = $onCondition['operator'];
                            $valueIsLiteral = $onCondition['valueIsLiteral'];
                            $fieldValue = '';
                            if (isset($onCondition['fieldValue'])) {
                                $fieldValue = (($valueIsLiteral) ? $this->escapeString($onCondition['fieldValue']) : $onCondition['fieldValue']);
                            }
                            $query .= ' ' . $precedingOperator . ' (' . $fieldName . ' ' . $operator . ' ' . $fieldValue . ') ';
                        }
                        $query .= ') ';
                        ++$i;
                    }
                }
                $query .= ') ';
            }
        }
        //Evaluate Where-part
        if (array_key_exists('where',$this->unfinishedQuery) && is_array($this->unfinishedQuery['where'])) {
            $query .= ' WHERE ';
            if (array_key_exists('ungrouped',$this->unfinishedQuery['where']) && is_array($this->unfinishedQuery['where']['ungrouped'])) {
                foreach ($this->unfinishedQuery['where']['ungrouped'] as $whereCondition) {
                    $precedingOperator = $whereCondition['precedingOperator'];
                    $fieldName = $whereCondition['fieldName'];
                    $operator = $whereCondition['operator'];
                    $valueIsLiteral = $whereCondition['valueIsLiteral'];
                    $fieldValue = '';
                    if (isset($whereCondition['fieldValue'])) {
                        $fieldValue = (($valueIsLiteral) ? $this->escapeString($whereCondition['fieldValue']) : $whereCondition['fieldValue']);
                    }
                    $query .= ' ' . $precedingOperator . ' (' . $fieldName . ' ' . $operator . ' ' . $fieldValue . ') ';
                }
            }
            if (isset($this->unfinishedQuery['where']['grouped']) && is_array($this->unfinishedQuery['where']['grouped'])) {
                $i = 0;
                $hasWhereUngrouped = array_key_exists('ungrouped',$this->unfinishedQuery['where']) && is_array($this->unfinishedQuery['where']['ungrouped']);
                foreach ($this->unfinishedQuery['where']['grouped'] as $groupKey => $groupArr) {
                    $connector = (($i > 0) || $hasWhereUngrouped) ? $this->unfinishedQueryGroups[$groupKey] : '';
                    $query .= " $connector (";
                    foreach ($this->unfinishedQuery['where']['grouped'][$groupKey] as $whereCondition) {
                        $precedingOperator = $whereCondition['precedingOperator'];
                        $fieldName = $whereCondition['fieldName'];
                        $operator = $whereCondition['operator'];
                        $valueIsLiteral = $whereCondition['valueIsLiteral'];
                        $fieldValue = '';
                        if (isset($whereCondition['fieldValue'])) {
                            $fieldValue = (($valueIsLiteral) ? $this->escapeString($whereCondition['fieldValue']) : $whereCondition['fieldValue']);
                        }
                        $query .= ' ' . $precedingOperator . ' (' . $fieldName . ' ' . $operator . ' ' . $fieldValue . ') ';
                    }
                    $query .= ')';
                    ++$i;
                }
            }
        }
        //Evaluate Group-By-part
        if (isset($this->unfinishedQuery['groupBy']) && is_array($this->unfinishedQuery['groupBy'])) {
            $query .= ' GROUP BY ';
            $i = 0;
            foreach ($this->unfinishedQuery['groupBy'] as $groupExp) {
                if ($i > 0) {
                    $query .= ', ';
                }
                $query .= $groupExp;
                ++$i;
            }
        }
        //Evaluate Having-part
        if (isset($this->unfinishedQuery['having']) && is_array($this->unfinishedQuery['having'])) {
            $query .= ' HAVING ';
            foreach ($this->unfinishedQuery['having']['ungrouped'] as $havingCondition) {
                $precedingOperator = $havingCondition['precedingOperator'];
                $fieldName = $havingCondition['fieldName'];
                $operator = $havingCondition['operator'];
                $valueIsLiteral = $havingCondition['valueIsLiteral'];
                $fieldValue = '';
                if (isset($havingCondition['fieldValue'])) {
                    $fieldValue = (($valueIsLiteral) ? $this->escapeString($havingCondition['fieldValue']) : $havingCondition['fieldValue']);
                }
                $fieldValue = (($havingCondition['fieldValue']) ? "'{$havingCondition['fieldValue']}'" : null);
                $query .= ' ' . $precedingOperator . ' (' . $fieldName . ' ' . $operator . ' ' . $fieldValue . ') ';
            }
            foreach ($this->unfinishedQuery['having']['grouped'] as $groupKey => $groupArr) {
                $connector = $this->unfinishedQueryGroups[$groupKey];
                $query .= " $connector (";
                foreach ($this->unfinishedQuery['having']['grouped'][$groupKey] as $havingCondition) {
                    $precedingOperator = $havingCondition['precedingOperator'];
                    $fieldName = $havingCondition['fieldName'];
                    $operator = $havingCondition['operator'];
                    $valueIsLiteral = $havingCondition['valueIsLiteral'];
                    $fieldValue = '';
                    if (isset($havingCondition['fieldValue'])) {
                        $fieldValue = (($valueIsLiteral) ? $this->escapeString($havingCondition['fieldValue']) : $havingCondition['fieldValue']);
                    }
                    $query .= ' ' . $precedingOperator . ' (' . $fieldName . ' ' . $operator . ' ' . $fieldValue . ') ';
                }
                $query .= ') ';
            }
            $query .= ') ';
        }

        //Evaluate Order-By-part
        if (array_key_exists('orderBy',$this->unfinishedQuery) && is_array($this->unfinishedQuery['orderBy'])) {
            $query .= ' ORDER BY ';
            $i = 0;
            foreach ($this->unfinishedQuery['orderBy'] as $groupExpArr) {
                $expression = $groupExpArr['expression'];
                $order = $groupExpArr['order'];
                if ($i > 0) {
                    $query .= ', ';
                }
                $query .= $expression . ' ' . $order;
                ++$i;
            }
        }
        return $query;
    }

    /**
     * @return $this
     */
    public function setConstructedQuery()
    {
        $this->setQuery($this->getConstructedQuery());
        unset($this->unfinishedQuery);
        return $this;
    }


    protected function _setComparisonValidationArray()
    {
        $arr = array(
            'IS NULL' => function ($name, $value = null) {
                return (is_scalar($name) && (strip_tags($name) === (string)$name));
            },
            'IS NOT NULL' => function ($name, $value = null) {
                return (is_scalar($name) && (strip_tags($name) === (string)$name));
            },
            '=' => function ($name, $value) {
                return (is_scalar($name) && is_scalar($value) && (strip_tags($name) === (string)$name) && (strip_tags($value) === (string)$value));
            },
            '!=' => function ($name, $value) {
                return (is_scalar($name) && is_scalar($value) && (strip_tags($name) === (string)$name) && (strip_tags($value) === (string)$value));
            },
            '<' => function ($name, $value) {
                return (is_scalar($name) && is_scalar($value) && (strip_tags($name) === (string)$name) && (strip_tags($value) === (string)$value));
            },
            '>' => function ($name, $value) {
                return (is_scalar($name) && is_scalar($value) && (strip_tags($name) === (string)$name) && (strip_tags($value) === (string)$value));
            },
            '>=' => function ($name, $value) {
                return (is_scalar($name) && is_scalar($value) && (strip_tags($name) === (string)$name) && (strip_tags($value) === (string)$value));
            },
            '<=' => function ($name, $value) {
                return (is_scalar($name) && is_scalar($value) && (strip_tags($name) === (string)$name) && (strip_tags($value) === (string)$value));
            },
            'IN' => function ($name, $value) {
                return (is_scalar($name) && (strip_tags($name) === (string)$name) && ((is_scalar($value) && (strip_tags($value) === (string)$value)) || (is_array($value) && array_depth($value) === 1)));
            },
            'LIKE' => function ($name, $value) {
                return (is_scalar($name) && is_scalar($value) && (strip_tags($name) === (string)$name) && (strip_tags($value) === (string)$value));
            }
        );
        $this->comparisonValidation = $arr;
    }

    /**
     * @return bool
     */
    public function inTransaction()
    {
        return $this->connectionObject->inTransaction();
    }

    /**
     * @param array $arr
     * @param $tableName
     * @param $updateOrInsert
     * @return string
     */
    public function getUpdateInsertQueryFromArray(array $arr, $tableName, $updateOrInsert)
    {
        $strIsUpdate = (strcasecmp($updateOrInsert, 'update') === 0);
        $strIsInsert = (strcasecmp($updateOrInsert, 'insert') === 0);
        if (!(strlen($tableName) > 0)) {
            throw new \InvalidArgumentException("Parameter 'tableName' must not be empty.");
        }
        if (!$strIsInsert && !$strIsUpdate) {
            throw new \InvalidArgumentException('Parameter \'updateOrInsert\' must be a either \'update\' or \'insert\'');
        }
        if ($strIsUpdate && (!array_key_exists('id', $arr) || !($arr['id'] > 0))) {
            throw new \InvalidArgumentException('id-field must be set in array for update operation');
        } elseif ($strIsInsert && array_key_exists('id', $arr) && $arr['id'] > 0) {
            throw new \InvalidArgumentException('id-field must not be set to a positive value for insert operation');
        }

        if ($strIsUpdate) {
            $id = (int)$arr['id'];
            $updateInsertSnippet = "UPDATE `$tableName` ";
            $whereIDSnippet = " WHERE (`id` = $id)";
        } else {
            $updateInsertSnippet = "INSERT INTO `$tableName` ";
            $whereIDSnippet = '';
        }
        $query = $updateInsertSnippet . 'SET ';
        $i = 0;
        foreach ($arr as $fieldName => $fieldValue) {
            if ($fieldName !== 'id') {
                if($i > 0) {
                    $query .= ',';
                }
                if(is_bool($fieldValue)) { $fieldValue = (int)$fieldValue;}
                elseif(is_float($fieldValue)) { $fieldValue = number_format($fieldValue,4,'.','');}
                elseif(is_string($fieldValue)) {$fieldValue = $this->connectionObject->quote($fieldValue);}
                $fieldValue = (string)$fieldValue;
                if('' === $fieldValue) { $fieldValue = '\'\'';}
                $query .= "`$fieldName` = $fieldValue";
                $i++;
            }
        }
        $query .= $whereIDSnippet;
        return $query;
    }

    /**
     * @param $tableName
     * @param $id
     * @return string
     */
    public function getDeleteQueryForID($tableName, $id)
    {
        if (!(strlen($tableName) > 0)) {
            throw new \InvalidArgumentException("Parameter 'tableName' must not be empty.");
        }
        $id = (int)$id;
        if (!($id > 0)) {
            throw new \InvalidArgumentException("Parameter 'id' must be a positive integer.");
        }
        $query = "DELETE FROM `$tableName` WHERE id = $id";
        return $query;
    }

    /**
     * @param $objName
     * @param array $constructorArgFields
     * @return mixed
     */
    public function getResultRowAsObject($objName, array $constructorArgFields = [])
    {
        if(!$this->stmt instanceof \PDOStatement) {
            throw new \RuntimeException('No PDOStatement for query. Cannot execute.');
        }

        $obj = $this->stmt->fetchObject($objName,$constructorArgFields);
        return $obj;
    }

    /**
     * @param $objName
     * @param array $constructorArgFields
     * @return array
     */
    public function getAllResultRowAsArrayOfObjects($objName, array $constructorArgFields = [])
    {
        if(!$this->stmt instanceof \PDOStatement) {
            throw new \RuntimeException('No PDOStatement for query. Cannot execute.');
        }
        $objArr = $this->stmt->fetchAll(PDO::FETCH_CLASS,$objName);
        return $objArr;
    }
}