<?php

namespace FiloBlu\Flow\Model\Parser;

use Exception;
use FiloBlu\Flow\Helper\LoggerProvider;
use FiloBlu\Flow\Helper\ProductFactory;
use FiloBlu\Flow\Model\MapFactory;
use Magento\Framework\App\ObjectManager;
use Magento\Framework\ObjectManagerInterface;
use Monolog\Logger;

use function array_key_exists;
use function count;
use function is_array;

/**
 * Class AbstractParser
 * @package FiloBlu\Flow\Model\Parser
 */
abstract class AbstractParser
{
    /**
     * @var
     */
    public $map;

    /**
     * @var
     */
    protected $header;

    /**
     * @var
     */
    protected $_channel;

    /**
     * @var Logger
     */
    protected $_logger;

    /**
     * @var ObjectManager
     */
    protected $objectManager;

    /**
     * @var ProductFactory
     */
    protected $productHelperFactory;
    /**
     * @var
     */
    protected $attribute_set_id;

    /**
     * @var
     */
    protected $configurable_attributes;

    /**
     * @var
     */
    protected $websites;

    /**
     * @var
     */
    protected $stores;

    /**
     * @var MapFactory
     */
    protected $mapFactory;

    /**
     * @var bool
     */
    protected $check_mapping_count = 1;

    /**
     * @var
     */
    protected $_config;

    /**
     * @var int
     */
    protected $linesToSkip = 0;

    /**
     * @var string
     */
    protected $separator = ';';

    /**
     * @var string
     */
    protected $delimiter = '"';

    /**
     * AbstractParser constructor.
     * @param LoggerProvider $loggerProvider
     * @param MapFactory $mapFactory
     * @param ProductFactory $productHelperFactory
     * @param ObjectManagerInterface $objectManager
     */
    public function __construct(
        LoggerProvider $loggerProvider,
        MapFactory $mapFactory,
        ProductFactory $productHelperFactory,
        ObjectManagerInterface $objectManager
    ) {
        $this->_logger = $loggerProvider->getLogger();
        $this->mapFactory = $mapFactory;
        $this->productHelperFactory = $productHelperFactory;
        $this->objectManager = $objectManager;
    }

    /**
     * @param $resource
     * @return mixed
     */
    abstract public function process($resource);

    /**
     * @param $config
     * @return $this
     */
    public function setConfig($config)
    {
        $this->_config = $config;
        $this->separator = $this->_config['separator'];
        $this->delimiter = $this->_config['delimiter'];
        $this->linesToSkip = $this->_config['lines_to_skip'];
        $this->check_mapping_count = ($this->_config['check_mapping_count'] ?? 0);

        return $this;
    }

    /**
     * @param $mapLocation
     * @return bool
     */
    public function loadMap($mapLocation)
    {
        $map = $this->mapFactory->create();
        $map->load($mapLocation);

        if ($map->initialized) {
            $this->map = $map;
            return true;
        }

        return false;
    }

    /**
     * Parse
     *
     * @param array $data
     * @param bool $key_mode
     * @throws Exception
     */
    public function parse($data, $key_mode = false)
    {
        $fields = [];
        $parsedData = [];
        $insertData = [];

        if ($this->check_mapping_count && !$key_mode && !$this->map->checkData($data)) {
            throw new Exception('Data don\'t match mapping count ' . count($data));
        }

        $channel = $this->getChannel();

        if ($channel === null && !method_exists($channel, 'insertData')) {
            throw new Exception('No method insertData for class ' . get_class($channel));
        }

        if ($channel->getChannelConfig()->usesMap()) {
            if ($this->map) {
                $fields = $this->map->getFields();
            }

            $joins = ['map' => []];

            // Default behaviour
            if ($this->check_mapping_count && count($fields)) {
                foreach ($fields as $i => $field) {
                    $fieldMap = $this->map->get($field);
                    $from = $fieldMap['from'] ?? $i;
                    $key = $key_mode ? $from : $i;

                    if (array_key_exists(trim($key), $data) || !isset($fieldMap['from'])) {
                        $fieldData = ($data[trim($key)] ?? '');
                        $result = $this->executeMappingValidatorsAndFilters($field, $data, $fieldMap, $fieldData);
                        $parsedData[$field] = $result;

                        // If there are joins

                        if (isset($fieldMap['joins']) && is_array($fieldMap['joins'])) {
                            $joins['key'] = $field;
                            if (isset($fieldMap['joins'][0][$result])) {
                                $joins['map'] = $fieldMap['joins'][0][$result];
                            }
                        }
                    }
                }

                foreach ($joins['map'] as $item) {
                    $p = $parsedData;
                    $p[$joins['key']] = $item;
                    $insertData[] = $p;
                }

                if (empty($joins['map'])) {
                    $insertData[] = $parsedData;
                }

                // Expand configurables if is set expand_configurables:1 in map
                if ($this->map->expandConfigurables()) {
                    $toExpand = $insertData;
                    $insertData = [];
                    foreach ($toExpand as $itemToExpand) {
                        $helperProduct = $this->productHelperFactory->create();
                        $children = $helperProduct->getConfigurableChildsBySku($itemToExpand['sku']);

                        foreach ($children as $child) {
                            $itemToExpand['sku'] = $child;
                            $insertData[] = $itemToExpand;
                        }
                    }
                }
                // This is no check_mapping_count (Talend parse) then CSV headers will match Magento attributes
            } elseif (!$this->check_mapping_count) {
                $fieldsCount = count($fields);
                $to = [];
                // Merging headers with current data from the csv
                // array_combine gives a warning if arrays have not equal columns number (using foreach)
                foreach ($this->header as $headerKey => $headerColumnName) {
                    $parsedData[$headerColumnName] = $data[$headerKey];
                    $fieldMap = [];

                    if ($fieldsCount) {
                        $fieldMap = $this->map->get($headerColumnName);
                        $to[] = $headerColumnName;
                    }

                    $field = $headerColumnName;
                    $fieldData = $data[$headerKey];
                    $parsedData[$field] = $this->executeMappingValidatorsAndFilters(
                        $field,
                        $data,
                        $fieldMap,
                        $fieldData
                    );
                }

                $result = $this->processVirtualColumns(
                    array_diff($this->map->getFields(), $to),
                    $data
                );

                $insertData[] = array_merge($parsedData, $result);
            }
        } else {
            $d = [];
            foreach ($this->header as $k => $v) {
                $d[$v] = $data[$k];
            }
            $insertData[] = $d;
        }

        foreach ($insertData as $toInsert) {
            $channel->insertData($toInsert);
        }
    }

    /**
     * Validate single field applying Validators
     * @return mixed
     */
    public function getChannel()
    {
        return $this->_channel;
    }

    /**
     *
     * @param $channel
     */
    public function setChannel($channel)
    {
        $this->_channel = $channel;
    }

    /**
     * @param $field
     * @param $data
     * @param $fieldMap
     * @param $fieldData
     * @return null
     * @throws Exception
     */
    private function executeMappingValidatorsAndFilters($field, $data, $fieldMap, $fieldData)
    {
        if (!$fieldMap || (isset($fieldMap['skip']) && $fieldMap['skip'])) {
            return null;
        }

        $filters = null;
        $validators = null;
        $value = null;

        if (isset($fieldMap['validators'])) {
            $validators = $fieldMap['validators'];
        }

        if (isset($fieldMap['filters'])) {
            $filters = $fieldMap['filters'];
        }

        if ($this->_validate($fieldData, $validators, $field)) {
            return $this->_filter($fieldData, $filters, $data);
        }

        throw new Exception("{$value} is not Valid for {$field}");
    }

    /**
     * Parse Data
     *
     * @param $value
     * @param $validators
     * @param $field
     * @return bool
     */
    protected function _validate($value, $validators, $field)
    {
        if (!$validators || count($validators) === 0) {
            return true;
        }

        foreach ($validators as $validator) {
            $validatorClass = 'FiloBlu\Bouncer\Model\Parser\Validators\\' . ucfirst($validator);
            $validator = $this->objectManager->create($validatorClass);

            if (!$validator->isValid($value)) {
                return false;
            }
        }

        return true;
    }

    /**
     * @param $value
     * @param $filters
     * @param $data
     * @return mixed
     */
    protected function _filter($value, $filters, $data)
    {
        if (!$filters || count($filters) === 0) {
            return $value;
        }

        $columnMap = array_flip($this->getHeaders());
        foreach ($filters as $filterData) {
            $filterClass = 'FiloBlu\Flow\Model\Filters\\' . ucfirst($filterData['class']);
            $filter = $this->objectManager->create($filterClass);

            if (method_exists($filter, 'setRow')) {
                $filter->setRow($data);
            }

            if (method_exists($filter, 'setColumnsMap')) {
                $filter->setColumnsMap($columnMap);
            }

            $value = $filter->filter($value, $filterData['args']);
        }

        return $value;
    }

    /**
     * @return array
     */
    public function getHeaders()
    {
        return $this->header;
    }

    /**
     * @throws \Exception
     */
    public function processVirtualColumns($virtualColumns, $originalData)
    {
        $parsedData = [];
        $data = array_combine($this->header, $originalData);
        foreach ($virtualColumns as $column) {
            $map = $this->map->get($column);
            $value = null;
            if (isset($map['from'], $data[$map['from']]) && $map['from']) {
                $value = $data[$map['from']];
            }

            $parsedData[$column] = $this->executeMappingValidatorsAndFilters($column, $originalData, $map, $value);
        }

        return $parsedData;
    }

    /**
     * @return bool
     */
    public function checkMappingCount()
    {
        return $this->check_mapping_count;
    }
}
