<?php

namespace Internetgalerie\IgDatapoolFe\Services;

/***************************************************************
 * Copyright notice
 *
 * (c) 2010 Markus Baumgartner <mb@internetgalerie.ch>
 * All rights reserved
 *
 * This script is part of the TYPO3 project. The TYPO3 project is
 * free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * The GNU General Public License can be found at
 * http://www.gnu.org/copyleft/gpl.html.
 *
 * This script is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * This copyright notice MUST APPEAR in all copies of the script!
 ***************************************************************/

class DataPoolV2SearchLogicService
{
    /**
     * Form Fields
     *
     * @var Array
     */
    private $fields = [];

    /**
     * List of search operations
     *
     * @var Array
     */
    public static $searchOperations = ['parse', 'substring', 'substringCase', '=', '=Case', '<', '>', '<=', '>=', 'begin', 'date<', 'date>', 'EXISTS', 'NOT EXISTS', 'in', 'inRec', 'EXISTS_REC', 'user', 'RLIKE', 'comma-separated'];
    /**
     * List of Boolean Operators
     *
     * @var Array
     */
    public static $boolOperators = ['AND', 'OR', 'NOT'];

    /**
     * escape function
     */
    private $escapeFunction = 'mysql_real_escape_string';

    /**
     * what like to use? postgres ilike.. mysql like.. etc
     *
     * @var Array
     */
    private $like = ['normal' => 'like', 'CS' => 'LIKE BINARY'];

    public function __construct(
        /**
         * Search Rules Tree
         */
        private readonly array $searchRules
    )
    {
    }

    /**
     * get Where String, accepts piVars
     *
     * @param  array $vars
     * @return String
     */
    public function getSQLWhere(array $vars)
    {
        if (!is_array($this -> searchRules)) {
            return '';
        }
        $key = key($this -> searchRules);
        $this -> fields = $vars;
        return trim($this -> parseRec($this -> searchRules, $key));
    }

    public function isBool($arr)
    {
        return isset($arr['formfield']) && isset($arr['attr']) && isset($arr['operation']);
    }

    private function timestamp($date)
    {
        $arr = preg_split('#[\/\.,\-]#m', (string) $date);
        $xxx = sprintf("%04d-%02d-%02d", (strlen((string) $arr[2]) == 4 ? $arr[2] : '20' . $arr[2]), $arr[1], $arr[0]);
        return strtotime($xxx);
    }

    private function parserSearch($field, $parseString)
    {
        $quote = false;
        $psA = str_split((string) $parseString);
        $parseString = ' ';
        if (!in_array($psA[0], ['+', '-'])) {
            $parseString .= '+';
        }
        $c = 0;
        foreach ($psA as $val) {
            $quote = ($val == '"' ? !$quote : $quote);
            if ($quote) {
                $parseString .= $val;
            } else {
                if ($val == ' ' && isset($psA[$c + 1]) && !in_array($psA[$c + 1], ['-', '+'])) {
                    $parseString .= ' +';
                } elseif ($val == '|') {
                    $parseString .= '#|#';
                } else {
                    $parseString .= $val;
                }
            }
            $c++;
        }
        $matches = [];
        $pattern = '
/
	\s(\+|\-) #modifier nach whitespache
	(
		("[^"]+") #klammer begriff
		|
		([^\-\+][^\"\s]*) #1. buchstabe kein +-, dann beliebig viele inkl +-
	)
/x';
        preg_match_all($pattern, $parseString, $matches);
        $all = [];
        foreach ($matches [0] as $expr) {
            $expr = ltrim($expr, ' ');
            $op = $expr[0];
            $expr = ltrim($expr, '+ -');
            $all[] = '(' . $this -> parseSearchExpr($field, $op, $expr) . ')';
        }
        return implode(' AND ', $all);
    }

    private function parseSearchExpr($field, $op, $expr)
    {
        $elems = explode('#|#', (string) $expr);
        foreach ($elems as $expr) {
            $parsed[] = $this -> parseSearchElem($field, $op, $expr);
        }
        return (implode(' OR ', $parsed));
    }

    private function parseSearchElem($field, $op, $expr)
    {
        $expr = $this -> escape(trim((string) $expr, '"'));
        return $field . ' ' . (($op == '-') ? 'NOT ' : '') . $this -> like['normal'] . ' \'%' . $expr . '%\'';
    }

    private function buildBoolExp($bool)
    {
        //fixed for compatibility
        if ($bool['attrib']) {
            $bool['attr'] = $bool['attrib'];
        }
        if (!$this -> isBool($bool)) {
            return 0;
        }
        if (($value = $this -> fields[$bool['formfield']]) == '') {
            if (($value = $bool['default']) == '') {
                return 0;
            }
        }
        if ($bool['operation'] == 'parse') {
            return $this -> parserSearch($bool['attr'], $value);
        }

        $value = $this -> escape($value);

        switch ($bool ['operation']) {
        case 'substring':
            $exp = '(' . $bool['attr'] . ' ' . $this -> like['normal'] . ' ' . '\'%' . $value . '%\')';
            break;
        case 'substringCase':
            $exp = '(' . $bool['attr'] . ' ' . $this -> like['CS'] . ' ' . '\'%' . $value . '%\')';
            break;
        case '=':
            $exp = $bool['attr'] . '=\'' . $value . '\'';
            break;
        case '=Case':
            $exp = 'STRCMP(' . $bool['attr'] . ', BINARY ' . $value . ')=0';
            break;
        case '<':
            $exp = $bool['attr'] . '<' . $value . '';
            break;
        case '>':
            $exp = $bool['attr'] . '>' . $value . '';
            // no break
        case '<=':
            $exp = $bool['attr'] . '<=' . $value . '';
            break;
        case '>=':
            $exp = $bool['attr'] . '>=' . $value . '';
            break;
        case 'begin':
            $exp = '(' . $bool['attr'] . ' ' . $this -> like['normal'] . ' ' . '\'' . $value . '%\')';
            break;
        case 'date=':
            $exp = $bool['attr'] . ' = ' . $this -> timestamp($value);
            break;
        case 'date<':
            $exp = $bool['attr'] . ' < ' . $this -> timestamp($value);
            break;
        case 'date>':
            $exp = $bool['attr'] . ' > ' . $this -> timestamp($value);
            break;
        case 'exists':
        case 'EXISTS':
            if (is_array($value)) {
                $value = implode(',', $value);
            }
            $attr2 = str_replace('###ID###', '\'' . $value . '\'', $bool['attr']);
            // if more than one ID is necesary (as example IN Funtion SQL / Array Parameter)
            $attr2 = str_replace('###IDS###', $value, $attr2);

            //    $attr2 = str_replace ( '###IDU###', $value, $attr2 );
            $exp = 'EXISTS(' . $attr2 . ')';
            break;
        case 'not exists':
        case 'NOT EXISTS':
            $attr2 = str_replace('###ID###', '\'' . $value . '\'', $bool['attr']);
            //$attr2 = str_replace ( '###IDU###', $value, $attr2 );
            $exp = 'NOT EXISTS(' . $attr2 . ')';
            break;
        case 'EXISTS_REC':
            $temp = t3lib_div::trimExplode('|', $bool['attr']);
            $bool['attr'] = $temp[0];
            $ids = tx_datapool_util_hierarchicTools::getChildrenUid($temp[1], rtrim((string) $value, ','), true);
            $exp = 'EXISTS(' . str_replace('###IDS###', '(' . implode(',', $ids) . ')', $bool['attr']) . ')';
            break;
        case 'inRec':
            $temp = t3lib_div::trimExplode('|', $bool['attr']);
            $bool['attr'] = $temp[0];
            $ids = '(' . implode(',', tx_datapool_util_hierarchicTools::getChildrenUid($temp[1], rtrim((string) $value, ','), true)) . ')';
            $exp = $bool['attr'] . ' IN ' . $ids;
            break;
        case 'user':
            $exp = str_replace('###VALUE###', $value, $bool['attr']);
            break;
        case 'RLIKE':
            $exp = '(' . $bool['attr'] . ' RLIKE ' . '\'' . $value . '\')';
            break;
        case 'in':
            $ids = [];
            if (!is_array($value)) {
                $value = t3lib_div::trimExplode(',', $value, true);
            }
            foreach ($value as $el) {
                $el = t3lib_div::trimExplode('_', $el);
                $ids[] = intval(end($el));
            }
            $exp = $bool['attr'] . ' IN (' . implode(',', $ids) . ')';
            break;
        case 'comma-separated':
            $exp = $bool['attr'] . ' REGEXP \'[[:<:]]' . $value . '[[:>:]]\'';
            break;
        default:
            return 0;
        }
        return $exp;
    }

    /**
     * Start the parser
     *
     * @param  array $boolop
     * @param  array $arr
     */
    private function parse($boolop, $arr)
    {
        $exp = [];
        foreach ($arr as $elem) {
            if (is_array($elem)) {
                $elem = $this -> buildBoolExp($elem);
            }
            if ($elem) {
                $exp[] = $elem;
            }
        }
        if ($boolop == 'NOT') {
            if (count($exp) == 0) {
                return '';
            }
            return ($boolop . ' (' . array_pop($exp) . ')');
        }

        $str = '';
        for ($i = 0; $i < count($exp); $i++) {
            $str .= $exp[$i];
            if ($i < count($exp) - 1) {
                $str .= ' ' . $boolop . ' ';
            }
        }
        if (count($exp) > 1) {
            $str = '(' . $str . ')';
        }
        return $str;
    }

    /**
     * Enter description here...
     *
     * @param  unknown_type $arr
     * @param  unknown_type $op
     * @return unknown
     */
    private function parseRec($arr, $op)
    {
        foreach ($arr as $keyorig => $val) {
            $key = trim((string) $keyorig, '0123456789');
            if ($key != 'field') {
                unset($arr[$keyorig]);
                $arr[] = $this -> parseRec($val, $key);
            }
        }
        return $this -> parse($op, $arr);
    }

    private function getFormFields()
    {
        if (!is_array($this -> searchRules)) {
            return [];
        }

        //only 1 field
        if ($this -> isBool($this -> searchRules)) {
            return [$this -> searchRules];
        }
        $formarr = [];
        $this -> getFormFieldsRec($formarr, $this -> searchRules);
        return $formarr;
    }

    private function getFormFieldsRec(&$arr, $tree): void
    {
        foreach ($tree as $node) {
            if ($this -> isBool($node)) {
                $arr[] = $node;
            } else {
                ($this -> getFormFieldsRec($arr, $node));
            }
        }
    }

    private function escape($str)
    {
        $esc = $this -> escapeFunction;
        if (is_array($str)) {
            foreach ($str as &$val) {
                $val = $esc($val);
            }
            return $str;
        } else {
            return $esc($str);
        }
    }
}
