<?php

namespace b8;

use b8\Exception\HttpException;
use b8\Database;

abstract class Store
{
    protected $modelName  = null;
    protected $tableName  = null;
    protected $primaryKey = null;

    /**
     * @return \b8\Model
     */
    abstract public function getByPrimaryKey($key, $useConnection = 'read');

    public function getWhere(
        $where = [],
        $limit = 25,
        $offset = 0,
        $joins = [],
        $order = [],
        $manualJoins = [],
        $group = null,
        $manualWheres = [],
        $whereType = 'AND'
    ) {
        $query = 'SELECT * FROM {{' . $this->tableName . '}}';
        $countQuery = 'SELECT COUNT(*) AS {{count}} FROM {{' . $this->tableName . '}}';

        $wheres = [];
        $params = [];
        foreach ($where as $key => $value) {
            $key = $this->fieldCheck($key);

            if (!is_array($value)) {
                $params[] = $value;
                $wheres[] = $key . ' = ?';
            } else {
                if (isset($value['operator'])) {
                    if (is_array($value['value'])) {
                        if ($value['operator'] == 'between') {
                            $params[] = $value['value'][0];
                            $params[] = $value['value'][1];
                            $wheres[] = $key . ' BETWEEN ? AND ?';
                        } elseif ($value['operator'] == 'IN') {
                            $in = [];

                            foreach ($value['value'] as $item) {
                                $params[] = $item;
                                $in[] = '?';
                            }

                            $wheres[] = $key . ' IN (' . implode(', ', $in) . ') ';
                        } else {
                            $ors = [];
                            foreach ($value['value'] as $item) {
                                if ($item == 'null') {
                                    switch ($value['operator']) {
                                        case '!=':
                                            $ors[] = $key . ' IS NOT NULL';
                                            break;

                                        case '==':
                                        default:
                                            $ors[] = $key . ' IS NULL';
                                            break;
                                    }
                                } else {
                                    $params[] = $item;
                                    $ors[] = $key . ' ' . $value['operator'] . ' ?';
                                }
                            }
                            $wheres[] = '(' . implode(' OR ', $ors) . ')';
                        }
                    } else {
                        if ($value['operator'] == 'like') {
                            $params[] = '%' . $value['value'] . '%';
                            $wheres[] = $key . ' ' . $value['operator'] . ' ?';
                        } else {
                            if ($value['value'] === 'null') {
                                switch ($value['operator']) {
                                    case '!=':
                                        $wheres[] = $key . ' IS NOT NULL';
                                        break;

                                    case '==':
                                    default:
                                        $wheres[] = $key . ' IS NULL';
                                        break;
                                }
                            } else {
                                $params[] = $value['value'];
                                $wheres[] = $key . ' ' . $value['operator'] . ' ?';
                            }
                        }
                    }
                } else {
                    $wheres[] = $key . ' IN (' . implode(', ', array_map([Database::getConnection('read'), 'quote'], $value)) . ')';
                }
            }
        }

        if (count($joins)) {
            foreach ($joins as $table => $join) {
                $query .= ' LEFT JOIN {{' . $table . '}} AS ' . $join['alias'] . ' ON ' . $join['on'] . ' ';
                $countQuery .= ' LEFT JOIN {{' . $table . '}} AS ' . $join['alias'] . ' ON ' . $join['on'] . ' ';
            }
        }

        if (count($manualJoins)) {
            foreach ($manualJoins as $join) {
                $query .= ' ' . $join . ' ';
                $countQuery .= ' ' . $join . ' ';
            }
        }

        $hasWhere = false;
        if (count($wheres)) {
            $hasWhere = true;
            $query .= ' WHERE (' . implode(' ' . $whereType . ' ', $wheres) . ')';
            $countQuery .= ' WHERE (' . implode(' ' . $whereType . ' ', $wheres) . ')';
        }

        if (count($manualWheres)) {
            foreach ($manualWheres as $where) {
                if (!$hasWhere) {
                    $hasWhere = true;
                    $query .= ' WHERE ';
                    $countQuery .= ' WHERE ';
                } else {
                    $query .= ' ' . $where['type'] . ' ';
                    $countQuery .= ' ' . $where['type'] . ' ';
                }

                $query .= ' ' . $where['query'];
                $countQuery .= ' ' . $where['query'];

                if (isset($where['params'])) {
                    foreach ($where['params'] as $param) {
                        $params[] = $param;
                    }
                }
            }
        }

        if (!is_null($group)) {
            $query .= ' GROUP BY ' . $group . ' ';
        }

        if (count($order)) {
            $orders = [];
            if (is_string($order) && $order == 'rand') {
                $query .= ' ORDER BY RAND() ';
            } else {
                foreach ($order as $key => $value) {
                    $orders[] = $this->fieldCheck($key) . ' ' . $value;
                }

                $query .= ' ORDER BY ' . implode(', ', $orders);
            }
        }

        if ($limit) {
            $query .= ' LIMIT ' . $limit;
        }

        if ($offset) {
            $query .= ' OFFSET ' . $offset;
        }

        try {
            $stmt = Database::getConnection('read')->prepareCommon($countQuery);
            $stmt->execute($params);
            $res = $stmt->fetch(\PDO::FETCH_ASSOC);
            $count = (int)$res['count'];
        } catch (\PDOException $ex) {
            $count = 0;
        }

        try {
            $stmt = Database::getConnection('read')->prepareCommon($query);
            $stmt->execute($params);
            $res = $stmt->fetchAll(\PDO::FETCH_ASSOC);
            $rtn = [];

            foreach ($res as $data) {
                $rtn[] = new $this->modelName($data);
            }

            return ['items' => $rtn, 'count' => $count];
        } catch (\PDOException $ex) {
            throw $ex;
        }
    }

    public function save(Model $obj, $saveAllColumns = false)
    {
        if (!isset($this->primaryKey)) {
            throw new HttpException\BadRequestException('Save not implemented for this store.');
        }

        if (!($obj instanceof $this->modelName)) {
            throw new HttpException\BadRequestException(get_class($obj) . ' is an invalid model type for this store.');
        }

        $data = $obj->getDataArray();

        if (isset($data[$this->primaryKey])) {
            $rtn = $this->saveByUpdate($obj, $saveAllColumns);
        } else {
            $rtn = $this->saveByInsert($obj, $saveAllColumns);
        }

        return $rtn;
    }

    public function saveByUpdate(Model $obj, $saveAllColumns = false)
    {
        $rtn = null;
        $data = $obj->getDataArray();
        $modified = ($saveAllColumns) ? array_keys($data) : $obj->getModified();

        $updates       = [];
        $update_params = [];
        foreach ($modified as $key) {
            $updates[]       = $key . ' = :' . $key;
            $update_params[] = [$key, $data[$key]];
        }

        if (count($updates)) {
            $qs = 'UPDATE {{' . $this->tableName . '}} SET ' . implode(', ', $updates) . ' WHERE {{' . $this->primaryKey . '}} = :primaryKey';
            $q  = Database::getConnection('write')->prepareCommon($qs);

            foreach ($update_params as $update_param) {
                $q->bindValue(':' . $update_param[0], $update_param[1]);
            }

            $q->bindValue(':primaryKey', $data[$this->primaryKey]);
            $q->execute();

            $rtn = $this->getByPrimaryKey($data[$this->primaryKey], 'write');
        } else {
            $rtn = $obj;
        }

        return $rtn;
    }

    public function saveByInsert(Model $obj, $saveAllColumns = false)
    {
        $rtn = null;
        $data = $obj->getDataArray();
        $modified = ($saveAllColumns) ? array_keys($data) : $obj->getModified();

        $cols    = [];
        $values  = [];
        $qParams = [];
        foreach ($modified as $key) {
            $cols[] = $key;
            $values[] = ':' . $key;
            $qParams[':' . $key] = $data[$key];
        }

        if (count($cols)) {
            $qs = 'INSERT INTO {{' . $this->tableName . '}} (' . implode(', ', $cols) . ') VALUES (' . implode(', ',
                    $values) . ')';
            $q = Database::getConnection('write')->prepareCommon($qs);

            if ($q->execute($qParams)) {
                $id = !empty($data[$this->primaryKey]) ? $data[$this->primaryKey] : Database::getConnection('write')->lastInsertId();
                $rtn = $this->getByPrimaryKey($id, 'write');
            }
        }

        return $rtn;
    }

    public function delete(Model $obj)
    {
        if (!isset($this->primaryKey)) {
            throw new HttpException\BadRequestException('Delete not implemented for this store.');
        }

        if (!($obj instanceof $this->modelName)) {
            throw new HttpException\BadRequestException(get_class($obj) . ' is an invalid model type for this store.');
        }

        $data = $obj->getDataArray();

        $q = Database::getConnection('write')->prepareCommon('DELETE FROM {{' . $this->tableName . '}} WHERE {{' . $this->primaryKey . '}} = :primaryKey');
        $q->bindValue(':primaryKey', $data[$this->primaryKey]);
        $q->execute();

        return true;
    }

    protected function fieldCheck($field)
    {
        if (empty($field)) {
            throw new HttpException('You cannot have an empty field name.');
        }

        if (strpos($field, '.') === false) {
            return '{{' . $this->tableName . '}}.{{' . $field . '}}';
        }

        return $field;
    }
}
