<?php

namespace IMATHUZH\Qfq\Core\Form\FormElement;

use IMATHUZH\Qfq\Core\Form\Form;
use IMATHUZH\Qfq\Core\Helper\KeyValueStringParser;
use IMATHUZH\Qfq\Core\Helper\Logger;
use IMATHUZH\Qfq\Core\Helper\OnArray;
use IMATHUZH\Qfq\Core\Helper\Support;
use IMATHUZH\Qfq\Core\Typo3\T3Handler;

class SubrecordFormElement extends AbstractFormElement {
    public string $dndHtmlAttributes = '';
    /**
     * Edit Link for each row (in order) to be displayed by renderer
     */
    public array $editRecordLinks = array();
    /**
     * Delete Link for each row (in order) to be displayed by renderer
     */
    public array $deleteRecordLinks = array();

    /**
     * @param $attributes
     * @param Form|null $form
     * @throws \CodeException
     * @throws \DbException
     * @throws \DownloadException
     * @throws \PhpOffice\PhpSpreadsheet\Exception
     * @throws \PhpOffice\PhpSpreadsheet\Reader\Exception
     * @throws \PhpOffice\PhpSpreadsheet\Writer\Exception
     * @throws \Twig\Error\LoaderError
     * @throws \Twig\Error\RuntimeError
     * @throws \Twig\Error\SyntaxError
     * @throws \UserFormException
     * @throws \UserReportException
     */
    public function __construct($attributes, ?Form $form = null) {
        parent::__construct($attributes, $form);

        if ($this->form->recordId == 0) {
            $this->handleNoPrimaryRecord();
            return;
        } // Don't bother building subrecord if main record does not exist yet

        $this->setDefaults();
        $this->handleSql1Missing();
        $this->handleMissingNameColumnId();
        $this->handleNewRecordLink();
        $this->handleEditDeleteRecordLinks();
        $this->handleDnd();
        $this->handleHtmlIdAndClass();
        $this->handleColumnControl();
    }

    /**
     * Adds required subrecord-specific information to $this->attributes
     *
     * @return void
     * @throws \CodeException
     * @throws \DbException
     * @throws \UserFormException
     * @throws \UserReportException
     */
    private function setDefaults(): void {
        Support::setIfNotSet($this->attributes, SUBRECORD_NAME_COLUMN_ID, 'id');
        Support::setIfNotSet($this->attributes, SUBRECORD_PRIMARY_RECORD, array());

        //Support::findInSet(SUBRECORD_NEW, $this->attributes[FE_SUBRECORD_OPTION]) && ($formElement[FE_MODE] != FE_MODE_READONLY));

        Support::setIfNotSet($this->attributes, SUBRECORD_FLAG_EDIT, false);
        Support::setIfNotSet($this->attributes, SUBRECORD_FLAG_NEW, false);
        Support::setIfNotSet($this->attributes, SUBRECORD_FLAG_DELETE, false);

        Support::setIfNotSet($this->attributes, SUBRECORD_CSS_CLASS_COLUMN_ID, '');
        Support::setIfNotSet($this->attributes, SUBRECORD_HAS_DRAG_AND_DROP, false);
        Support::setIfNotSet($this->attributes, SUBRECORD_CONTROL, array());

        Support::setIfNotSet($this->attributes, F_EXTRA_DELETE_FORM, '');
        Support::setIfNotSet($this->attributes, SUBRECORD_COLUMN_TITLE_EDIT, '');

        if (isset($this->attributes[SUBRECORD_PARAMETER_FORM])) {
            $this->attributes[F_FINAL_DELETE_FORM] = $this->attributes[F_EXTRA_DELETE_FORM] != '' ? $this->attributes[F_EXTRA_DELETE_FORM] : $this->attributes[SUBRECORD_PARAMETER_FORM];

            $this->attributes[SUBRECORD_PRIMARY_RECORD] = $this->store->getStore(STORE_RECORD);
            $this->attributes[SUBRECORD_CSS_CLASS_COLUMN_ID] = $this->store->getVar(SYSTEM_CSS_CLASS_COLUMN_ID, STORE_SYSTEM . STORE_EMPTY);
            $this->attributes[SUBRECORD_FLAG_NEW] = $this->attributes[FE_MODE] != FE_MODE_READONLY && Support::findInSet(SUBRECORD_NEW, $this->attributes[FE_SUBRECORD_OPTION]);
            $this->attributes[SUBRECORD_FLAG_EDIT] = Support::findInSet(SUBRECORD_EDIT, $this->attributes[FE_SUBRECORD_OPTION]);
            $this->attributes[SUBRECORD_FLAG_DELETE] = $this->attributes[FE_MODE] != FE_MODE_READONLY && Support::findInSet(SUBRECORD_DELETE, $this->attributes[FE_SUBRECORD_OPTION]);
        } else {
            throw new \UserFormException(json_encode(
                [ERROR_MESSAGE_TO_USER => 'Missing Parameter',
                    ERROR_MESSAGE_TO_DEVELOPER => SUBRECORD_PARAMETER_FORM]));
        }
    }

    /**
     * Check if a primary record exists. If not, set a message to be displayed by a renderer class
     *
     * @return void
     */
    private function handleNoPrimaryRecord(): void {
        if (!isset($this->attributes[SUBRECORD_NAME_COLUMN_ID]) || !isset($this->attributes[SUBRECORD_PRIMARY_RECORD][$this->attributes[SUBRECORD_NAME_COLUMN_ID]])) {
            $this->attributes[SUBRECORD_MESSAGE] = 'Please save this record first.';
        }
    }

    /**
     * Throws an exception if sql1 was not defined
     *
     * @return void
     * @throws \UserFormException
     */
    private function handleSql1Missing(): void {
        if (!is_array($this->attributes[FE_SQL1])) {
            throw new \UserFormException('Missing \'sql1\' query', ERROR_MISSING_SQL1);
        }
    }

    /**
     * Check if $nameColumnId column exists.
     * Also handle underline in front of the column name
     *
     * @return void
     * @throws \UserFormException
     */
    private function handleMissingNameColumnId(): void {
        $nameColumnId = $this->attributes[SUBRECORD_NAME_COLUMN_ID];
        if (count($this->attributes[FE_SQL1]) == 0) return;

        // Try fallback too by adding _ in front of the column name (e.g. allow 'AS id' and 'AS _id')
        if (!isset($this->attributes[FE_SQL1][0][$nameColumnId])) {
            $this->attributes[SUBRECORD_NAME_COLUMN_ID] = '_' . $this->attributes[SUBRECORD_NAME_COLUMN_ID];
            $nameColumnId = $this->attributes[SUBRECORD_NAME_COLUMN_ID];
            if (!isset($this->attributes[FE_SQL1][0][$nameColumnId])) {
                throw new \UserFormException(
                    json_encode([ERROR_MESSAGE_TO_USER => "Missing column $nameColumnId in subrecord query", ERROR_MESSAGE_TO_DEVELOPER => '']),
                    ERROR_SUBRECORD_MISSING_COLUMN_ID);
            }
        }
    }

    /**
     * Prepare the link url
     * Stores link 'new record' in $this->attributes[SUBRECORD_LINK_NEW]
     *
     * @return void
     * @throws \CodeException
     * @throws \DbException
     * @throws \UserFormException
     * @throws \UserReportException
     */
    private function handleNewRecordLink(): void {
        if (!isset($this->attributes[SUBRECORD_PARAMETER_FORM]) || !$this->attributes[SUBRECORD_FLAG_NEW]) return;
        $this->attributes[SUBRECORD_LINK_NEW] = $this->attributes[SUBRECORD_FLAG_NEW] ? $this->createFormLink($this->attributes, 0, $this->attributes[SUBRECORD_PRIMARY_RECORD], SYMBOL_NEW, 'New', $formElement[SUBRECORD_PARAMETER_NEW] ?? '') : '';
    }

    /**
     * Prepare edit link for each row
     * Stores links in array $this->editRecordLinks and $this->deleteRecordLinks
     *
     * @return void
     */
    private function handleEditDeleteRecordLinks(): void {
        // Normal Rows from SQL 1
        for ($i = 0; $i < count($this->attributes[FE_SQL1]); $i++) {
            // Edit Link
            if ($this->attributes[SUBRECORD_FLAG_EDIT]) {
                // TODO: Create Link
                array_push($this->editRecordLinks, '#');
            }
            // Delete Link
            if ($this->attributes[SUBRECORD_FLAG_DELETE]) {
                // TODO: Create Link
                array_push($this->deleteRecordLinks, '#');
            }
        }

        // Additional Rows from parameter
        //TODO: Figure this out
    }

    /**
     * Determine if the subrecord uses drag and drop.
     * If yes, then set $this->attributes[SUBRECORD_HAS_DRAG_AND_DROP] = true
     *
     * @return void
     * @throws \CodeException
     * @throws \DbException
     * @throws \UserFormException
     * @throws \UserReportException
     */
    private function handleDnd(): void {
        // Determine if DragAndDrop is active
        $orderColumn = $this->attributes[FE_ORDER_COLUMN] ?? DND_COLUMN_ORD;
        $dndTable = '';

        if (isset($this->attributes[FE_DND_TABLE])) {
            // Table is specified in parameter field
            $dndTable = $this->attributes[FE_DND_TABLE];
        } elseif (!empty($this->attributes[SUBRECORD_PARAMETER_FORM])) {
            // Read table from form specified in subrecord (not the current form!)
            $formName = $this->attributes[SUBRECORD_PARAMETER_FORM];

            $form = $this->databaseManager->getDbByIndex($this->databaseManager->indexQfq)->sql("SELECT * FROM `Form` AS f WHERE `f`.`" . F_NAME . "` LIKE ? AND `f`.`deleted`='no'", ROW_REGULAR, [$formName]);
            $dndTable = $form[0][F_TABLE_NAME] ?? '';
        } else {
            throw new \UserFormException(json_encode(
                [ERROR_MESSAGE_TO_USER => 'Missing Parameter',
                    ERROR_MESSAGE_TO_DEVELOPER => FE_DND_TABLE . ' or ' . SUBRECORD_PARAMETER_FORM]),
                ERROR_MISSING_TABLE_NAME);
        }

        if ($dndTable) {
            $columns = $this->databaseManager->getDbByIndex($this->form->specFinal[F_DB_INDEX])->sql("SHOW COLUMNS FROM $dndTable");
            foreach ($columns as $column) {
                if ($column[COLUMN_FIELD] === $orderColumn) {
                    // DragAndDrop is active if the dndTable has the orderColumn
                    $this->attributes[SUBRECORD_HAS_DRAG_AND_DROP] = true;
                    break;
                }
            }
        }

        // DragAndDrop HTML Attribute
        $dndAttributes = '';
        if ($this->attributes[SUBRECORD_HAS_DRAG_AND_DROP]) {

            $orderInterval = $this->attributes[FE_ORDER_INTERVAL] ?? FE_ORDER_INTERVAL_DEFAULT;
            if ($orderInterval == '' || !is_numeric($orderInterval)) {
                $orderInterval = FE_ORDER_INTERVAL_DEFAULT;
            }

            $numColumns = 0;
            if (count($this->attributes[FE_SQL1]) > 0) {
                $numColumns = count($this->attributes[FE_SQL1][0]) + (int)$this->attributes[SUBRECORD_FLAG_DELETE]
                    + (int)($this->attributes[SUBRECORD_FLAG_NEW] || $this->attributes[SUBRECORD_FLAG_EDIT]);
            }

            $dataDndApi = DND_SUBRECORD_ID . '=' . $this->attributes[FE_ID];
            $dataDndApi .= '&' . DND_SUBRECORD_FORM_ID . '=' . $this->store->getVar('id', STORE_RECORD);
            $dataDndApi .= '&' . DND_SUBRECORD_FORM_TABLE . '=' . $this->form->specFinal[F_TABLE_NAME];
            $dataDndApi .= '&' . FE_ORDER_INTERVAL . '=' . $orderInterval;
            $dataDndApi .= '&' . FE_ORDER_COLUMN . '=' . $orderColumn;
            $dataDndApi .= '&' . FE_DND_TABLE . '=' . $dndTable;
            $dataDndApi .= '&' . DND_DB_INDEX . '=' . $this->form->specFinal[F_DB_INDEX];

            $this->dndHtmlAttributes = Support::doAttribute('class', 'qfq-dnd-sort');
            $this->dndHtmlAttributes .= $this->evaluate->parse("{{ '$dataDndApi' AS _data-dnd-api }}") . ' ';
            $this->dndHtmlAttributes .= Support::doAttribute('data-columns', $numColumns);
        }
    }

    /**
     * Sets html id (e.g. for tablesorter) and a default css class (no visual effect, just data)
     *
     * @return void
     * @throws \CodeException
     * @throws \DbException
     * @throws \UserFormException
     * @throws \UserReportException
     */
    private function handleHtmlIdAndClass(): void {
        $x = T3Handler::useSlugsInsteadOfPageAlias() ? $this->attributes[F_ID] : $this->store::getVar(TYPO3_PAGE_ALIAS, STORE_TYPO3);
        $tableSorterHtmlId = $x . '-' . $this->attributes[FE_ID];
        $this->htmlAttributes['id'] = $tableSorterHtmlId;
        array_push($this->cssClasses, $this->attributes[FE_SUBRECORD_TABLE_CLASS]);
        if ($this->attributes[FE_MODE] == FE_MODE_HIDDEN) array_push($this->cssClasses, FE_MODE_HIDDEN);
    }

    /**
     * Renders an Link with a symbol (edit/new) and register a new SIP to grant permission to the link.
     *
     * Returns <a href="<Link>">[icon]</a>
     *
     * Link: <page>?s=<SIP>&<standard typo3 params>
     * SIP: form = $formElement['form'] (provided via formElement[FE_PARAMETER])
     *      r = $targetRecordId
     *      Parse  $formElement['detail'] with possible key/value pairs. E.g.: detail=id:gr_id,#{{a}}:p_id,#12:x_id
     *        gr_id = <<primarytable.id>>
     *        p_id = <<variable defined in SIP or Client>>
     *        x_id = 12 (constant)
     *
     * $strLink - Optional. Only used if custom parameter (e.g. in subrecord '_rowEdit', or fe.parameter.new) should be set.
     *                      Will overwrite all other settings (exception: url default/explicit params are merged).
     *
     * @param array $formElement
     * @param $targetRecordId
     * @param array $primaryRecord
     * @param $symbol
     * @param $toolTip
     * @param $strLink - Optional. Only used if custom parameter should be set. Will overwrite all other settings. Exception: url params are merged
     * @param $currentRow
     * @return string
     * @throws \CodeException
     * @throws \DbException
     * @throws \UserFormException
     * @throws \UserReportException
     *
     */
    private function createFormLink(array $formElement, $targetRecordId, array $primaryRecord, $symbol, $toolTip, $strLink, $currentRow = array()): string {

        $queryStringArray = [
            SIP_FORM => $formElement[SUBRECORD_PARAMETER_FORM],
            SIP_RECORD_ID => $targetRecordId,
            PARAM_DB_INDEX_DATA => $this->form->specFinal[F_DB_INDEX],
        ];

        // Inherit current F_MODE:
        if ($this->form->specFinal[F_MODE_GLOBAL] != '') {
            $queryStringArray[F_MODE_GLOBAL] = $this->form->specFinal[F_MODE_GLOBAL];
        }

        // In case the subrecord FE is set to 'readonly': subforms will be called with formModeGlobal=readonly
        if ($formElement[FE_MODE] == FE_MODE_READONLY) {
            $queryStringArray[F_MODE_GLOBAL] = FE_MODE_READONLY;
        }

        // Add custom query parameter
        if (isset($formElement[SUBRECORD_PARAMETER_DETAIL])) {

            $detailParam = KeyValueStringParser::parse($formElement[SUBRECORD_PARAMETER_DETAIL]);

            foreach ($detailParam as $src => $dest) {

                if (($src[0] ?? '') == '') {
                    throw new \UserFormException("FormElement.parameter.detail: src definition is a) empty or b) not defined as a constant in 'detail = ".$formElement[SUBRECORD_PARAMETER_DETAIL]."'");
                }

                // Constants
                if ($src[0] == '&') {
                    $queryStringArray[$dest] = substr($src, 1);
                    continue;
                }
                // Form record values or parameter
                if (isset($primaryRecord[$src])) {
                    $queryStringArray[$dest] = $primaryRecord[$src];
                    continue;
                }

                // Current row - check '$src' and  '_$src' )
                foreach (['', '_'] as $pre) {
                    if (isset($currentRow[$pre . $src])) {
                        $queryStringArray[$dest] = $currentRow[$pre . $src];
                        continue 2;
                    }
                }

                // None of the above matched: poor solution of error reporting
                $queryStringArray[$dest] = ERROR_SUBRECORD_DETAIL_COLUMN_NOT_FOUND;
            }
        }

        // Check if detail contains Typo3 reserved keywords. Especially 'id' is forbidden at this state
        if (isset($queryStringArray[CLIENT_PAGE_ID]) || isset($queryStringArray[CLIENT_PAGE_TYPE]) || isset($queryStringArray[CLIENT_PAGE_LANGUAGE])) {
            throw new \UserFormException("Reserved Typo3 keyword (id, type, L) in formElement.parameter.detail - please use something else.");
        }

        Support::appendTypo3ParameterToArray($queryStringArray);
        // If there is a specific targetpage defined, take it.
        if (isset($formElement[SUBRECORD_PARAMETER_PAGE]) && $formElement[SUBRECORD_PARAMETER_PAGE] !== '') {
            $queryStringArray[CLIENT_PAGE_ID] = $formElement[SUBRECORD_PARAMETER_PAGE];
        }

        $queryString = Support::arrayToQueryString($queryStringArray);

        // Define defaults
        $strDefault = 'p:' . $queryString . '|s|r:7';

        // In case $strLink is given: copy computed urlParam to $strLink, than the App-Developer do not have to implent
        // subrecord.detail manually in subrecord.sql1
        $strLink = $this->copyDefaultUrlParamToExplicit($queryStringArray, $strLink);

        return $this->link->renderLink($strLink, $strDefault);
    }

    /**
     * If exist" get TOKEN_PAGE from $strLink, explode and merge with $queryStringArray. Return result as string.
     *
     * @param $queryStringArray
     * @param $strLink
     * @return string
     * @throws \UserFormException
     */
    private function copyDefaultUrlParamToExplicit($queryStringArray, $strLink): string {

        // If it is empty, then the default will be used: nothing to do here.
        if (empty($strLink)) {
            return '';
        }

        $arrStrLink = KeyValueStringParser::parse($strLink, PARAM_KEY_VALUE_DELIMITER, PARAM_DELIMITER);
        // If it is empty, then the default will be used: nothing to do here.
        if (empty($arrStrLink[TOKEN_PAGE])) {
            return $strLink;
        }

        // Explode explicit given url params: id=123&pId=45&r=56&form=address
        $arr = KeyValueStringParser::parse($arrStrLink[TOKEN_PAGE], '=', '&');

        // Merge default and explicit given. Explicit should overwrite default
        $arrStrLink[TOKEN_PAGE] = Support::arrayToQueryString(array_merge($queryStringArray, $arr));

        // Implode $strLink again.
        return KeyValueStringParser::unparse($arrStrLink, PARAM_TOKEN_DELIMITER, PARAM_DELIMITER);
    }

    /**
     * @return void
     * @throws \UserFormException
     */
    private function handleColumnControl(): void {
        if (count($this->attributes[FE_SQL1]) == 0) return;

        if (!isset($this->attributes[FE_SQL1]) || count($this->attributes[FE_SQL1]) == 0) $this->attributes[SUBRECORD_CONTROL] = array();

        $allTitles = array_keys($this->attributes[FE_SQL1][0]);

        $control = array();

        foreach ($allTitles as $columnName) {

            switch ($columnName) {
                case SUBRECORD_COLUMN_ROW_CLASS:
                case SUBRECORD_COLUMN_ROW_TOOLTIP:
                case SUBRECORD_COLUMN_ROW_TITLE: // Backward compatibility
                    continue 2;
                default:
                    break;
            }

            $flagWidthLimit = true;
            $control[SUBRECORD_COLUMN_MAX_LENGTH][$columnName] = SUBRECORD_COLUMN_DEFAULT_MAX_LENGTH;

            // a) 'City|maxLength=40', b) 'Status|icon', c) 'Mailto@maxLength=80|nostrip'
            $arr = KeyValueStringParser::parse($columnName, '=', '|', KVP_IF_VALUE_EMPTY_COPY_KEY);
            foreach ($arr as $attribute => $value) {
                switch ($attribute) {
                    case SUBRECORD_COLUMN_MAX_LENGTH:
                    case SUBRECORD_COLUMN_NO_STRIP:
                    case SUBRECORD_COLUMN_TITLE:
                    case SUBRECORD_COLUMN_LINK:
                        break;
                    case SUBRECORD_COLUMN_ICON:
                    case SUBRECORD_COLUMN_URL:
                    case SUBRECORD_COLUMN_MAILTO:
                        $flagWidthLimit = false;
                        break;
                    default:
                        $attribute = is_numeric($value) ? SUBRECORD_COLUMN_MAX_LENGTH : SUBRECORD_COLUMN_TITLE;
                        break;
                }
                $control[$attribute][$columnName] = $value;
            }

            if (!isset($control[SUBRECORD_COLUMN_TITLE][$columnName])) {
                $control[SUBRECORD_COLUMN_TITLE][$columnName] = ''; // Fallback:  Might be wrong, but better than nothing.
            }

            // Don't render Columns starting with '_...'.
            if (substr($control[SUBRECORD_COLUMN_TITLE][$columnName], 0, 1) === '_') {
                unset($control[SUBRECORD_COLUMN_TITLE][$columnName]); // Do not render column later.
                continue;
            }

            // Limit title length
            $maxLength = $control[SUBRECORD_COLUMN_MAX_LENGTH][$columnName];
            if ($maxLength != 0) {
                $control[SUBRECORD_COLUMN_TITLE][$columnName] = mb_substr($control[SUBRECORD_COLUMN_TITLE][$columnName], 0, $maxLength);
            }
            if (!$flagWidthLimit) {
                $control[SUBRECORD_COLUMN_MAX_LENGTH][$columnName] = false;
            }
        }

        $this->attributes[SUBRECORD_CONTROL] = $control;
    }

    /**
     * Create own formElement for subrecord additional rows to handle it separately.
     * Possible that additional rows have own form or delete form.
     *
     * @param array $formElement
     * @return array
     */
    private function createFeSubrecordAdditionalRow(array $formElement): array {
        $formElementAppendRows = array();
        $formElementAppendRows[FE_HTML_ID] = $formElement[FE_HTML_ID];
        $formElementAppendRows[FE_MODE] = $formElement[FE_MODE];
        $formElementAppendRows[FE_SUBRECORD_APPEND_SQL] = $formElement[FE_SUBRECORD_APPEND_SQL];

        // Using own class possible. If isset but empty, default class will be deactivated.
        if (isset($formElement[FE_SUBRECORD_APPEND_CLASS])) {
            $formElementAppendRows[FE_SUBRECORD_APPEND_CLASS] = $formElement[FE_SUBRECORD_APPEND_CLASS];
        }

        if (isset($formElement[SUBRECORD_PARAMETER_PAGE])) {
            $formElementAppendRows[SUBRECORD_PARAMETER_PAGE] = $formElement[SUBRECORD_PARAMETER_PAGE];
        }

        if (isset($formElement[SUBRECORD_PARAMETER_DETAIL])) {
            $formElementAppendRows[SUBRECORD_PARAMETER_DETAIL] = $formElement[SUBRECORD_PARAMETER_DETAIL];
        }

        // Using own form possible.
        if (isset($formElement[FE_SUBRECORD_APPEND_FORM])) {
            $formElementAppendRows[SUBRECORD_PARAMETER_FORM] = $formElement[FE_SUBRECORD_APPEND_FORM];
        } else {
            $formElementAppendRows[SUBRECORD_PARAMETER_FORM] = $formElement[SUBRECORD_PARAMETER_FORM];
        }

        // Using own delete form possible.
        if (isset($formElement[FE_SUBRECORD_APPEND_EXTRA_DELETE_FORM])) {
            $formElementAppendRows[F_FINAL_DELETE_FORM] = $formElement[FE_SUBRECORD_APPEND_EXTRA_DELETE_FORM];
        } else {
            $formElementAppendRows[F_FINAL_DELETE_FORM] = $formElementAppendRows[SUBRECORD_PARAMETER_FORM];
        }

        return $formElementAppendRows;
    }
}

