<?php

namespace Hura8\System\Model;

use Hura8\Interfaces\AppResponse;
use Hura8\System\Language;
use Hura8\Database\iConnectDB;
use Hura8\Interfaces\iSearch;
use Hura8\Traits\ClassCacheTrait;

abstract class aSearchBaseModel implements iSearch
{

    use ClassCacheTrait;

    /* @var iConnectDB $db */
    protected  $db;

    protected $tb_main = "";
    protected $tb_fulltext = "";

    // if false then search 'nhà tôi' = 'nha toi', if true then 'nhà tôi' != 'nha toi'
    // require rebuilding search if change this value
    protected $search_vietnamese = false;

    // require rebuilding search if change this value
    protected $star_search = true; //values: 1|0, enable star search on fulltext: abc* -> matches: abcd, abc, abcde

    // require rebuilding search if change this value
    protected $min_star_search_length = 2; //min star word length: result: abcd -> keywords: ab, abc, abcd NOT a

    // define list of fields to be the filters
    protected $config_filter_fields = [
        //format: field_name => map table_name.field
        //'price' => "tb_product.price",
        //'quantity' => "tb_product.quantity",
    ];

    protected $config_fulltext_fields = [
        //format: field_name => map [table_name.field]
        //"product_keywords" => ["tb_product.title", "tb_product.model", "tb_product.sku"],
        //"category_keywords" => ["tb_category.title"],
    ];


    public function __construct(
        $tb_main,
        array $config_fulltext_fields ,
        array $config_filter_fields = []
    ) {

        // ovewrite default
        if(defined('CONFIG_STAR_SEARCH')) $this->star_search = CONFIG_STAR_SEARCH;
        if(defined('CONFIG_STAR_SEARCH_MIN_LENGTH')) $this->min_star_search_length = CONFIG_STAR_SEARCH_MIN_LENGTH;
        if(defined('CONFIG_SEARCH_VIETNAMESE')) $this->search_vietnamese = CONFIG_SEARCH_VIETNAMESE;

        $this->db = get_db('', ENABLE_DB_DEBUG);
        $this->config_fulltext_fields = $config_fulltext_fields;
        $this->config_filter_fields = $config_filter_fields;

        $this->tb_main      = $tb_main;
        $part_name            = str_replace("tb_", "", preg_replace("/[^a-z0-9_]/i", "", $tb_main));
        $this->tb_fulltext  = "tb_search_".strtolower($part_name);
    }

    /**
     * @description get filter fields
     * @param array[string]
     */
    public function getFilterFields(): array {
        return $this->config_filter_fields;
    }


    /**
     * @description get fulltext fields
     * @param array[string]
     */
    public function getFulltextFields(): array {
        return $this->config_fulltext_fields;
    }


    public function getSampleData() : array {
        $query = $this->db->runQuery(
            "SELECT * FROM `".$this->tb_fulltext."`  ORDER BY `item_id` DESC LIMIT 10"
        );

        return $this->db->fetchAll($query);
    }


    public function getTableName() : string {
        return $this->tb_fulltext;
    }


    /**
     * @param array $field_filters ["price" => [">", 100], "brand" => ["=", 1]]
     * @return array
     */
    protected function _buildFieldFilters(array $field_filters = []) {
        $permit_operations = [">","=","<",">=","<=", "BETWEEN", "IN"];
        $where_clause = [];
        $bind_types = [];
        $bind_values = [];

        foreach ($field_filters as $field => $info) {
            list($operation, $value) = $info;

            $operation = strtoupper($operation);

            if(!in_array(strtolower($operation), $permit_operations)) continue;

            if($operation == 'BETWEEN') {
                // $value must be array(value_int_1, value_int_2)
                if(is_array($value) && sizeof($value) == 2) {
                    list($value_1, $value_2) = $value;

                    if(is_int($value_1) && is_int($value_2) && $value_1 < $value_2) {
                        $where_clause[] = " AND `".$field."` BETWEEN (?, ?) ";

                        $bind_types[] = "d";
                        $bind_types[] = "d";

                        $bind_values[] = $value_1;
                        $bind_values[] = $value_2;
                    }
                }

                continue;
            }

            if($operation == 'IN') {
                // $value must be array(value1, value2)
                if(is_array($value) && sizeof($value) > 0) {
                    $parameterized = [];
                    foreach ($value as $_v) {
                        $parameterized[] = "?";
                        $bind_types[] = "s";
                        $bind_values[] = $_v;
                    }

                    $where_clause[] = " AND `".$field."` IN (".join(",", $parameterized).") ";
                }

                continue;
            }


            // default operation, comparison requires value to be a digit
            $where_clause[] = " AND `".$field."` ".$operation." ? ";
            $bind_types[] = "d";
            $bind_values[] = $value;
        }

        return array(join(" ", $where_clause), $bind_types, $bind_values);
    }


    /**
     * @param $keyword
     * @param array $field_filters ["price" => [">", 100], "brand" => ["=", 1]]
     * @param array $fulltext_fields ['title_keywords', "category_keywords"]
     * @param int $limit_result
     * @return int[]
     */
    public function find($keyword, array $field_filters = [], array $fulltext_fields = [], $limit_result = 2000) : array
    {

        if(!sizeof($fulltext_fields)) $fulltext_fields = array_keys($this->config_fulltext_fields);

        $cache_key = md5("find-".$keyword);

        return static::getCache($cache_key, function () use ($keyword, $field_filters, $fulltext_fields, $limit_result){

            $keyword_clean = ($this->search_vietnamese) ? $this->make_text_search_vn($keyword) : $this->make_text_clean($keyword) ; //make_text_search_vn

            $keyword_clean_ele = array_filter(explode(" ", $keyword_clean));
            $build_boolean_search = "+".join(" +", $keyword_clean_ele);

            if(!$limit_result || $limit_result > 5000) $limit_result = 5000;
            $limit_condition = ($limit_result > 0) ? " LIMIT ".$limit_result : "";

            list($where_clause, $bind_types, $bind_values) = $this->_buildFieldFilters($field_filters);

            //validate search fields
            $validated_fulltext_fields = array_filter($fulltext_fields, function ($item) { return array_key_exists($item, $this->config_fulltext_fields); });

            if(!sizeof($validated_fulltext_fields)) {
                return [];
            }

            $query = $this->db->runQuery(
                "SELECT `item_id` FROM `".$this->tb_fulltext."` 
                          WHERE MATCH(".join(',', $validated_fulltext_fields).") AGAINST('".$build_boolean_search."' IN BOOLEAN MODE) 
                          ".$where_clause."
                          ORDER BY `item_id` DESC
                          ".$limit_condition."
                       ",
                $bind_types, $bind_values
            );

            return array_map(function ($item){ return $item['item_id']; }, $this->db->fetchAll($query)  );

        });

    }


    protected function getItemInfo($item_id)
    {
        $query = $this->db->runQuery(
            "SELECT * FROM `".$this->tb_fulltext."`  WHERE `item_id` = ? LIMIT 1",
            ['d'], [$item_id]
        );

        return $this->db->fetchAssoc($query);
    }


    protected function checkExist($item_id) : bool
    {
        $query = $this->db->runQuery(
            "SELECT `item_id` FROM `".$this->tb_fulltext."`  WHERE `item_id` = ? LIMIT 1",
            ['d'], [$item_id]
        );

        return (bool) $this->db->fetchAssoc($query);
    }


    // get all table_fields to watch which will affect the search
    protected function getWatchTableFields() {
        $result = [];
        foreach ($this->config_filter_fields as $field => $table_field ) {
            $result[] = $table_field;
        }

        foreach ($this->config_fulltext_fields as $field => $table_fields ) {
            $result = array_merge($result, $table_fields);
        }

        return $result;
    }


    /**
     * @description update or create item if not exist
     * @param $item_id
     * @param array $table_field_values
        $table_field_values = [
            "tb_product.price" => 2000000,
            "tb_product.title" => "Máy tính ABC",
            "tb_category.price" => "Máy tính",

            "tb_product.field_not_be_update" => "Mmodel",
        ];
     * @return AppResponse
     */
    public function updateItem($item_id, array $table_field_values = []) : AppResponse
    {

        $fulltext_fields = [];
        foreach ($this->config_fulltext_fields as $fulltext_field => $fulltext_map_fields ) {
            foreach ($fulltext_map_fields as $_table_field) {
                if(array_key_exists($_table_field, $table_field_values)) {
                    $fulltext_fields[$fulltext_field][$_table_field] = $table_field_values[$_table_field];
                }
            }
        }

        //echo "fulltext_fields";
        //debug_var($fulltext_fields);
        //echo "<br />";

        $field_filters = [];
        foreach ($this->config_filter_fields as $filter_field => $filter_map_field ) {
            if(array_key_exists($filter_map_field, $table_field_values)) {
                $field_filters[$filter_field] = $table_field_values[$filter_map_field];
            }
        }

        //echo "field_filters";
        //debug_var($field_filters);

        // nothing to update
        if(!sizeof($fulltext_fields) && !sizeof($field_filters)) {
            return new AppResponse('error', "nothing to update" );
        }


        // create entry if not exist
        if(!$this->checkExist($item_id)) {
            $this->db->insert($this->tb_fulltext, ['item_id' => $item_id, 'create_time' => CURRENT_TIME]);
        }

        $current_info = $this->getItemInfo($item_id);
        if(!$current_info) {
            return new AppResponse('error', "Cannot find record for ".$item_id );
        }

        // update
        $updated_fulltext = [];

        foreach ($this->config_fulltext_fields as $_filter_field => $_n ) {
            if(!isset($fulltext_fields[$_filter_field])) continue;

            $current_filter_field_base = ($current_info[$_filter_field."_base"]) ? \json_decode($current_info[$_filter_field."_base"], true) : [];

            $new_filter_field_base = $current_filter_field_base;

            foreach ($fulltext_fields[$_filter_field] as $_table_field => $_new_value) {
                $new_filter_field_base[$_table_field] = $_new_value;
            }

            if(json_encode($new_filter_field_base) != json_encode($current_filter_field_base)) {
                $updated_fulltext[$_filter_field."_base"] = $new_filter_field_base;
                $updated_fulltext[$_filter_field] = ($this->search_vietnamese) ? $this->make_text_search_vn(join(" ", array_values($new_filter_field_base))) : $this->make_text_clean(join(" ", array_values($new_filter_field_base)));

            }
        }


        $updated_filters = [];
        foreach ($this->config_filter_fields as $_filter_field => $_table_field ) {
            if(!isset($field_filters[$_filter_field])) continue;

            if($field_filters[$_filter_field] != $current_info[$_filter_field] ) {
                $updated_filters[$_filter_field] = $field_filters[$_filter_field];
            }
        }


        // nothing to update
        if(!sizeof($updated_filters) && !sizeof($updated_fulltext)) {
            return new AppResponse('error', "nothing to update" );
        }

        $updated_info = array_merge($updated_filters, $updated_fulltext);
        $updated_info['last_update'] = CURRENT_TIME;

        //echo "updated_info = <br /> ";
        //debug_var($updated_info);

        $this->db->update(
            $this->tb_fulltext,
            $updated_info,
            ['item_id' => $item_id]
        );

        return new AppResponse('ok');
    }

    public function deleteItem($item_id) : AppResponse
    {
        $this->db->runQuery(
            "DELETE FROM `".$this->tb_fulltext."`  WHERE `item_id` = ? LIMIT 1",
            ['d'], [$item_id]
        );

        return new AppResponse('ok');
    }

    public function deleteItems(array $item_list_ids) : AppResponse
    {
        list($parameterized_ids, $bind_types) = create_bind_sql_parameter_from_value_list($item_list_ids, "int");

        $this->db->runQuery(
            "DELETE FROM `".$this->tb_fulltext."`  WHERE `item_id` IN (".$parameterized_ids.") ",
            $bind_types,
            $item_list_ids
        );

        return new AppResponse('ok');
    }

    public function recreateTableSearch() {
        if($this->db->checkTableExist($this->tb_fulltext)) {
            $this->db->runQuery(
                "DROP TABLE `".$this->tb_fulltext."` "
            );
        }

        $this->createTableSearch();
    }

    public function createTableSearch() {
        // check if table exist
        if($this->db->checkTableExist($this->tb_fulltext)) {
            return true;
        }

        if(!$this->db->checkTableExist($this->tb_main)) {
            return false;
        }

        $sql = "CREATE TABLE `".$this->tb_fulltext."` ( `item_id` INT(11) UNSIGNED NOT NULL DEFAULT '0', ";

        // add fields

        foreach ($this->config_filter_fields as $_filter_field => $_map_table_field ) {
            list($_table, $_f) = explode(".", $_map_table_field);

            $_table_info = $this->db->getTableInfo($_table);

            $column_default = $_table_info[$_f]['COLUMN_DEFAULT'];

            $default_value = (in_array($_table_info[$_f]['DATA_TYPE'], ['mediumint', 'int', 'tinyint', 'double', 'float'])) ? 0 : $column_default;

            $sql .= " `".$_filter_field."` ".$_table_info[$_f]['COLUMN_TYPE']." NOT NULL DEFAULT '".$default_value."',  ";
        }

        foreach ($this->config_fulltext_fields as $_filter_field => $_t ) {
            $sql .= " `".$_filter_field."` TEXT NULL ,  ";

            // create field-data to compare new values before re-indexing
            $sql .= " `".$_filter_field."_base` TEXT NULL ,  ";
        }

        $sql .= " `create_time` int(11) NOT NULL DEFAULT '0',  ";
        $sql .= " `last_update` int(11) NOT NULL DEFAULT '0',  ";

        // index

        foreach ($this->config_filter_fields as $_filter_field => $_data_type ) {
            $sql .= " INDEX (`".$_filter_field."`),  ";
        }

        // fulltext on separate columns
        foreach ($this->config_fulltext_fields as $_filter_field => $_t ) {
            $sql .= " FULLTEXT INDEX (`".$_filter_field."` ) ,  ";
        }

        // create full text for all columns combined, so we can do query match(col_1, col_2) against keyword
        $_filter_fields = array_keys($this->config_fulltext_fields);
        if(sizeof($_filter_fields)) $sql .= " FULLTEXT INDEX (".join(", ", $_filter_fields).") ,  ";

        $sql .= " PRIMARY KEY ( `item_id` ) ";
        $sql .= " ) ENGINE=InnoDB DEFAULT CHARSET=utf8";

        //return $sql;
        $this->db->runQuery($sql);

        // add foreign key constraint
        $this->db->runQuery("
            ALTER TABLE `".$this->tb_fulltext."` ADD FOREIGN KEY (`item_id`) REFERENCES `".$this->tb_main."`(`id`) ON DELETE CASCADE;
        ");

        return true;
    }

    //11-11-2012 chuyen text thanh tieng viet voi dau chuyen doi: vd. nhà = nhas
    //because mysql allow fulltext search with minimum 4 characters, we have a trick: add hura to everyone what has length < 4
    protected function make_text_search_vn($text, $enable_star = false){
        $text = Language::convertText($text);
        $text = preg_replace("@[^a-z0-9\s]@si", " ", strtolower($text));
        //$text = str_replace("  "," ",$text);
        $text_ele = array_filter(explode(" ", $text));
        $new_text =  "";
        foreach($text_ele as $ele) {
            if($ele == '/' ) continue; //skip this character if it stands alone

            if(strlen($ele) < 4 || $ele == 'plus') $new_text .= " hura".$ele;
            else $new_text .= " ".$ele;
        }
        return trim($new_text);
    }

    //11-11-2012 chuyen text thanh tieng viet khong dau: vd. nhà = nha
    /*
    @variable:
    - $text: text to be index

    //16-11-2012
    - $enable_star:
        true: allow search abcd by word: ab or abc
        false: not enabled
    - $min_len: if $enable_star = true, abcd -> tao thanh: ab, abd, abcd (if $min_len = 2)
    */
    protected function make_text_clean($_text, $enable_star = false){
        //$text = Language::chuyenKhongdau(Language::convert_lower($text));
        $text = Language::chuyenKhongdau($_text);

        $text = preg_replace("@[^a-z0-9\s]@si", " ", strtolower($text));

        $text_ele = array_filter(explode(" ", $text));
        $new_text =  "";
        foreach($text_ele as $ele) {

            if($this->star_search && $enable_star) {

                $word_list = static::create_star_search($ele, $this->min_star_search_length);
                foreach($word_list as $new_ele) {
                    if(strlen($new_ele) < 4 || $ele == 'plus') $new_text .= " hura".$new_ele;
                    else $new_text .= " ".$new_ele;
                }

            }else{
                if(strlen($ele) < 4 || $ele == 'plus') $new_text .= " hura".$ele;
                else $new_text .= " ".$ele;
            }
        }
        return trim($new_text);
    }

    //16-11-2012
    //create all possible combination for a word for star search: abcd -> tao thanh: ab, abd, abcd, bc, bcd, cd (if $min_len = 2)
    protected function create_star_search($word, $min_len = 2){
        if($min_len < 2) $min_len = 2; //in case missing value for CONFIG_STAR_SEARCH_MIN_LENGTH
        $word_len = strlen($word);
        $result = array();
        if($word_len <= $min_len) return array($word);
        for($i = $min_len; $i < $word_len; $i ++ ){
            $result[] = substr($word, 0, $i);
        }
        $result[] = $word;

        //09-01-2012
        //reduce $word 1 character to create new word and create combination from there
        $new_word = substr($word, 1);
        $new_result = self::create_star_search($new_word, $min_len = 2);
        foreach($new_result as $el){
            $result[] = $el;
        }

        return array_unique($result);
    }

}
