<?php

namespace Lycoreco\Includes\Model;

abstract class BaseModel
{
    protected $id = null;
    static protected $table_name = 'none';

    /**
     * Field and format. Format for every field. Can be string, bool, int, DateTime
     * ['id' => 'int', 'name' => 'string']
     * @var array
     */
    static protected $table_fields = ['id' => 'int'];
    /**
     * Join some fields from other tables to variables in model
     * Example:
     * [
     *  [
     *      "field" => ["COUNT(*) AS keys_count"], // where keys_count is variable in the class
     *      "join_table"     => "table_name tb1 ON tb1.prod_id = obj.id"
     *  ]
     * ]
     * @var array
     */
    static protected $additional_fields = [];
    /**
     * Search fields with prefix (example obj.)
     * ['obj.name', 'obj.email']
     * @var array
     */
    static protected $search_fields = array();

    static public $last_query = '';
    static public $last_args_query = array();

    private $func_total_count = 0; // Only for count() function

    public function __construct($args = array(), $has_prefix = false)
    {
        foreach ($args as $key => $value) {
            // Default SELECT which used in filter() or get() with field_{key}
            if ($has_prefix)
                if (str_starts_with($key, 'field_'))
                    $key = substr($key, strlen('field_'));

            // If not exists in standart table field names, this field is not for this model (it's needed for LEFT JOIN with other models)
            if (!key_exists(
                $key,
                static::$table_fields
            )) {
                $this->{$key} = $value;
                continue;
            }


            // Exception only for id field
            if ($key == 'id') {
                $this->id = $value;
                continue;
            }
            if (gettype($value) == 'string')
                $value = trim($value);

            // Set type for variable
            if ($value === null) {
                // If $value has null, skip
                continue;
            }

            switch (static::$table_fields[$key]) {
                case 'int':
                    $this->{'field_' . $key} = (int)$value;
                    break;
                case 'float':
                    $this->{'field_' . $key} = (float)$value;
                    break;
                case 'DateTime':
                    $this->{'field_' . $key} = new CustomDateTime($value);
                    break;
                case 'bool';
                    $this->{'field_' . $key} = (bool)$value;
                    break;
                case 'string':
                default:
                    $this->{'field_' . $key} = (string)$value;
                    break;
            }
        }
    }
    public function get_id()
    {
        return $this->id;
    }
    public function is_saved()
    {
        if (isset($this->id))
            return true;
        else
            return false;
    }

    public static function init_table()
    {
        $result = db_query('');
        return $result;
    }
    public static function get_table_name()
    {
        return static::$table_name;
    }
    protected static function get_additional_fields()
    {
        return static::$additional_fields;
    }

    private static function get_fields_for_select($additional_fields = array())
    {
        $fields = array_keys(static::$table_fields);
        $result = '';

        // Standart fields
        for ($i = 0; $i < count($fields); $i++) {
            $result .= 'obj.' . $fields[$i] . ' AS ' . 'field_' . $fields[$i];

            // Place ', ' for every field excerpt last
            if ($i + 1 != count($fields))
                $result .= ', ';
        }

        // Additionl fields
        $additional_fields += static::get_additional_fields();
        if (!empty($additional_fields)) {
            $fields = array();
            foreach ($additional_fields as $ad_field) {
                $fields = array_merge($ad_field['field'], $fields);
            }

            $add_fields_str = implode(', ', $fields);
            $result .= ', ' . $add_fields_str;
        }

        return $result;
    }
    /**
     * Collect fields from model as in the database
     * @return array
     */
    protected function get_db_fields()
    {
        $result_fields = [];
        // Getting all the public properties of the object
        $class_fields = array_keys(get_object_vars($this));

        foreach ($class_fields as $field) {
            // Checking for the 'field_' prefix
            if (str_starts_with($field, 'field_')) {
                // Check if $field has value
                if (!isset($this->{$field}))
                    continue;

                // Removing the prefix and adding as a result
                $result_fields[] = substr($field, strlen('field_'));
            }
        }
        return $result_fields;
    }
    /**
     * Collect values from model as in the database
     * @return array
     */
    protected function get_db_values()
    {
        $fields = $this->get_db_fields();
        $values = [];
        foreach ($fields as $field) {
            $val = $this->{"field_$field"};

            switch (gettype($val)) {
                case 'boolean':
                    $val = (int)$val;
                    break;
            }

            $values[] = $val;
        }
        return $values;
    }

    /**
     * Return some list by filter and sort
     * @param array $fields
     * @param array $sort_by
     * @param int $count
     * @param string $field_relation
     * @param int $offset
     * @param string $search
     * @param array $additional_fields like static::$additional_fields
     * @return array(self)
     */
    static function filter(
        $fields = array(),
        $sort_by = array(),
        $count = 10,
        $field_relation = 'AND',
        $offset = 0,
        $search = '',
        $additional_fields = array()
    ) {
        /**
         * $fields = array(
         *      [
         *          'name' => 'obj.price'
         *          'type' => '>=' // default =
         *          'value' => 10
         *      ]
         * )
         * $sort_by = ['-price', 'comments']
         */

        // Filter part
        $where = [];
        $params = [];
        $whereSql = '';

        $having = [];
        $havingSql = '';
        if (!empty($fields)) {
            foreach ($fields as $field) {
                // Having or Where
                $is_having = $field['is_having'] ?? false;


                // For null values
                if ($field['value'] === null) {
                    $where[] = "{$field['name']} {$field['type']} NULL";
                    continue;
                }
                // For array and IN
                if ($field['type'] == 'IN') {
                    if (!is_array($field['value']))
                        continue;

                    $placeholders = implode(
                        ', ',
                        array_fill(0, count($field['value']), '?')
                    );
                    $where[] = "{$field['name']} IN ({$placeholders})";

                    $params = array_merge($params, $field['value']);
                    continue;
                }

                $condition = isset($field['type']) ? $field['type'] : '='; // use '=' as default

                if ($is_having)
                    $having[] = "{$field['name']} {$condition} ?";
                else
                    $where[] = "{$field['name']} {$condition} ?";

                if ($field['type'] != 'IN')
                    $params[] = $field['value'];
            }
            $whereSql = implode(' ' . $field_relation . ' ', $where);

            if (!empty($having))
                $havingSql = 'HAVING ' . implode(' ' . $field_relation . ' ', $having);
        }

        // Addition JOIN fields from other tables
        $join_table = '';
        $additional_fields = array_merge($additional_fields, static::get_additional_fields());
        if (!empty($additional_fields)) {
            $tables = array();
            foreach ($additional_fields as $val) {
                if (!isset($val['join_table']))
                    continue;

                $tables[] = 'LEFT JOIN ' . $val['join_table'];
            }
            $join_table = implode(' ', $tables);
        }

        // Search part
        $whereSearchSql = [];
        if (!empty($search)) {
            foreach (static::$search_fields as $field_name) {
                $whereSearchSql[] = "{$field_name} LIKE ?";
                $params[] = "%" . $search . "%";
            }
            $whereSearchSql = implode(' OR ', $whereSearchSql);
        };

        // Search and filter in WHERE
        if (!empty($whereSql) && !empty($whereSearchSql))
            $whereSql = 'WHERE (' . $whereSql . ') AND (' . $whereSearchSql . ')';
        elseif (!empty($whereSql))
            $whereSql = 'WHERE (' . $whereSql . ')';
        elseif (!empty($whereSearchSql))
            $whereSql = 'WHERE (' . $whereSearchSql . ')';

        // Sort part
        $sortSql = '';
        $sort_by[] = 'obj.id';  // Sort by id on the end
        if (!empty($sort_by)) {
            $sortSql = 'ORDER BY ' . implode(', ', array_map(function ($sort) {
                return ltrim($sort, '-') . ' ' . (strpos($sort, '-') === 0 ? 'DESC' : 'ASC');
            }, $sort_by));
        }

        // Limit part
        $limit = "LIMIT " . (int) $count;
        $offsetSql = "OFFSET " . (int)$offset;

        // Fields to select from db
        $select_fields = static::get_fields_for_select($additional_fields);

        // Create full sql query
        $sql = "SELECT " . $select_fields . " FROM " . static::$table_name . " obj " . $join_table . " " . $whereSql . " GROUP BY obj.id " . $havingSql . " " . $sortSql . " " . $limit . " " . $offsetSql;

        static::$last_query = $sql;
        static::$last_args_query = $params;
        $result = db_prepare($sql, $params);

        // Return result as objects or mysqli_result
        return static::createObjectsFromQuery($result);
    }
    /**
     * Return first row from db
     * @param array $fields 
     * @return static|false
     */
    static function get($fields, $sort_by = array(), $field_relation = 'AND')
    {
        $result = static::filter(
            $fields,
            $sort_by,
            1,
            $field_relation
        );
        if (count($result) > 0)
            return $result[0];
        else
            return false;
    }
    static function count($fields, $search = '', $additional_fields = array())
    {
        $filter_result = static::filter(
            $fields,
            array(),
            1,
            'AND',
            0,
            $search,
            array_merge($additional_fields, array(
                [
                    'field' => ['COUNT(*) OVER () AS func_total_count']
                ]
            ))
        );
        if (empty($filter_result))
            return 0;
        else
            return $filter_result[0]->func_total_count;
    }

    public function delete()
    {
        // Object is not in the table        
        if (!isset($this->id))
            return false;

        $sql = "DELETE FROM " . static::$table_name . " WHERE id = ?";
        db_prepare($sql, [$this->id]);
    }
    protected function after_save() {}
    public function save()
    {
        global $pdo;

        // Check is valid fields or not
        if ($this->valid() !== true)
            throw new ValidationError($this->valid());

        // Collect field and valus from fields in the class
        $fields = $this->get_db_fields();
        $values = $this->get_db_values();

        // Create SQL Prepare
        if (isset($this->id)) {
            // If we have id, then model is already in database

            // SQL for UPDATE .. SET ...
            $set_clause = implode(
                ', ',
                array_map(
                    fn($field) => "$field = ?",
                    $fields
                )
            );

            // Query and values
            $query = 'UPDATE ' . static::$table_name . ' SET ' . $set_clause . ' WHERE id = ?';
            $query_values = $values;
            $query_values[] = $this->id;
        } else {
            // if we don't have id (not in db), we create a new row

            // placeholder for SQL: ?, ?, ?, ?,...
            $placeholders = implode(', ', array_fill(0, count($fields), '?'));
            // list for SQL Insert
            $field_list = implode(', ', $fields);

            // Query and values
            $query = 'INSERT INTO ' . static::$table_name . ' (' . $field_list . ') VALUES (' . $placeholders . ')';
            $query_values = $values;
        }
        db_prepare($query, $query_values);

        if ($this->id === null) {
            $this->id = $pdo->lastInsertId();
        }

        $this->after_save();
    }
    /**
     * Check fields. Return true or array with string errors
     * @return array|bool
     */
    public function valid()
    {
        return true;
    }

    /**
     * Return model from Mysql result
     * @param array $pdo_result pdo resut FETCH_MODE = FETCH_ASSOC
     * @return array
     */
    protected static function createObjectsFromQuery(array $pdo_result)
    {
        $models = [];

        foreach ($pdo_result as $row) {
            $models[] = new static($row, true);
        }

        return $models;
    }
}
