<?php

declare(strict_types=1);

namespace Seka\Portal\Modul\Statistiky\Services\Analyzators;

use PhpOffice\PhpSpreadsheet\IOFactory;
use Seka\Portal\Modul\Statistiky\Services\Analyzators\Validators\ValidatorContext;
use Seka\Portal\Modul\Statistiky\Services\Import\File;
use Seka\Portal\Modul\Statistiky\Services\Transformators\XLSXDataTransformator;

/**
 * Class XLSXDataAnalyzator
 *
 * @package Seka\Portal\Modul\Statistiky\Services\Analyzators
 */
final class XLSXDataAnalyzator implements DataAnalyzatorContract
{
    /**
     * @var array
     */
    private array $validators;

    /**
     * @var array
     */
    private array $columnIndexes;

    /**
     * @var array
     */
    private array $requiredFields;

    /**
     * @var array
     */
    private array $errors;

    /**
     * @var array
     */
    private array $results = [];

    /**
     * XLSXDataAnalyzator constructor.
     *
     * @param array $required
     */
    public function __construct(array $required)
    {
        $this->validators = array();
        $this->columnIndexes = array();
        $this->errors = array();
        $this->results = array();
        $this->requiredFields = $required;
    }

    /**
     * @param string $type
     * @param        $args
     *
     * @return bool
     */
    public function analyze(string $type, $args): bool
    {
        $passed = false;

        if (method_exists($this, $type)) {
            $start = microtime(true);
            $passed = $this->$type($args);
            $time_taken = microtime(true) - $start;

            if ($passed) {
                $this->results[] = sprintf('The %s analysis has successfully passed all validations.', $type);
            }

            $this->results[] = sprintf('Time taken: %s seconds', $time_taken);
        }

        return $passed;
    }

    /**
     * @param array $data
     *
     * @return bool
     */
    protected function parseFirstRow(array $data): bool
    {
        $valid = true;
        //Clean the data
        $data = array_filter(array_map('trim', array_map('strtolower', $data)));

        //Ensure that there aren't duplicated columns
        $dupes = array_diff_key($data, array_unique($data));

        if (!empty($dupes)) {
            $this->errors[] = sprintf('The following columns are duplicated on the XLSX: "%s".', implode('", "', $dupes));
            $valid = false;
        }

        //Ensure all required columns are present
        if (
            $valid
            //The number of columns is lower than the required fields, we don't need to keep checking, some columns are missing.
            && (
                count($data) < count($this->requiredFields) ||
                //The number of optional fields must match with the number of fields that are not required, otherwise something is missing.
                count(array_diff($data, $this->requiredFields)) !== (count($data) - count($this->requiredFields))
            )
        ) {
            $required = implode(',', array_diff($this->requiredFields, $data));

            if ($required) {
                $required .= '" and "categories" or "category_ids';
            } else {
                $required = 'categories" or "category_ids';
            }

            $this->errors[] = sprintf('The following columns are missing on the XLSX: "%s".', $required);
            $valid = false;
        }

        if ($valid) {
            //Instantiate all the lexical validators
            foreach ($data as $key => $value) {
                $this->validators[$key] = new ValidatorContext(self::LEXICAL, $value);
                $this->columnIndexes[$key] = $value;
            }
        }

        return $valid;
    }

    /**
     * Perform the lexical analysis over the XLSX
     * Iterate over the file and exit if an error is found
     *
     * @param \Seka\Portal\Modul\Statistiky\Services\Import\File $file
     *
     * @return bool whether the file is valid (from a lexical point of view) or not
     * @throws \PhpOffice\PhpSpreadsheet\Reader\Exception|\PhpOffice\PhpSpreadsheet\Exception
     */
    protected function lexical(File $file): bool
    {
        $file->check();

        $reader = IOFactory::createReader(IOFactory::READER_XLSX);
        $reader->setReadDataOnly(true);
        $spreadsheet = $reader->load($file->fileName());
        $spreadsheet->garbageCollect();
        $worksheet = $spreadsheet->getActiveSheet();
        $worksheet->garbageCollect();
        $rowIterator = $worksheet->getRowIterator();

        $valid = false;
        $errors = array();

        foreach ($rowIterator as $row) {
            if ($row->getRowIndex() == 1) {
                $firstRow = [];
                foreach ($row->getCellIterator() as $cell) {
                    $firstRow[] = $cell->getValue();
                }

                //Parse the first row, instantiate all the validators
                $valid = $this->parseFirstRow($firstRow);
                continue;
            }

            $cellIdx = 0;
            foreach ($row->getCellIterator() as $cell) {
                $value = $cell->getValue();

                // prekonvertuje XLSX ciselny format datumu na datum vo formate d.m.Y
                if ($this->columnIndexes[$cellIdx] == XLSXHeader::$header[0]) {
                    $value = XLSXDataTransformator::convertIntToDate('DD.MM.YYYY', $value);
                }

                //For each column
                $value = trim($value ?? '');

                //Validate
                $errors[$this->columnIndexes[$cellIdx]] = $this->validators[$cellIdx]->validate($value);

                $valid = $valid && $errors[$this->columnIndexes[$cellIdx]];

                ++$cellIdx;
            }

            //If any error was found, exit
            if (!$valid) {
                $filtered_errors = array_keys($errors, false);

                if (count($filtered_errors) > 0) {
                    //Store the errors founds on the current line
                    $this->errors[$row->getRowIndex()] = $filtered_errors;
                }

                break;
            }
        }

        $spreadsheet->disconnectWorksheets();
        unset($rowIterator, $worksheet, $spreadsheet, $reader, $errors);

        return $valid;
    }

    /**
     * @return array
     */
    public function getErrors(): array
    {
        return $this->errors;
    }

    /**
     * @return array
     */
    public function getResults(): array
    {
        return $this->results;
    }
}
