<?php

namespace FiloBlu\Flow\Model\Parser;

use Exception;

/**
 * Class Csv
 * @package FiloBlu\Flow\Model\Parser
 */
class Csv extends AbstractParser
{
    /** @var string UTF-32, big-endian */
    const BOM_UTF_32_BE = b"\x00\x00\xFE\xFF";
    /** @var string UTF-32, little-endian */
    const BOM_UTF_32_LE = b"\xFF\xFE\x00\x00";
    /** @var string UTF-16, big-endian */
    const BOM_UTF_16_BE = b"\xFE\xFF";
    /** @var string UTF-16, little-endian */
    const BOM_UTF_16_LE = b"\xFF\xFE";
    /** @var string UTF-8 */
    const BOM_UTF8 = b"\xEF\xBB\xBF";
    /** @var null */
    const PLAIN_TEXT = null;

    /**
     * @param string $resource absolute path of the csv file
     * @throws Exception
     */
    public function process($resource)
    {
        $channel = $this->getChannel();
        $count = 0;

        if (($handle = @fopen($resource, 'rb')) === false) {
            throw new Exception("Unable to open {$resource}");
        }

        // Check if CSV has BOM at the beginning of file
        // Only UTF-8 and PLAIN_TEXT are supported
        // @see https://it.wikipedia.org/wiki/Byte_Order_Mark
        $this->handleBom($handle, $resource);

        while (($data = fgetcsv($handle, 0, $this->separator, $this->delimiter)) !== false) {
            $count++;

            if ($count === 1) {
                if (!empty($this->header)) {
                    $this->header = [];
                }

                foreach ($data as $header) {
                    // Remove BOM Marker
                    $header = trim($header, self::BOM_UTF8);
                    $this->header[] = strtolower(trim(trim($header)));
                }
            }

            if ($count <= $this->linesToSkip) {
                continue;
            }

            try {
                array_walk($data, static function (&$value) {
                    $value = trim($value ?? '');
                    if (ctype_space($value)) {
                        $value = null;
                    }
                });

                if (!$this->isEmptyRow($data)) {
                    $this->parse($data);
                }
            } catch (Exception $e) {
                throw new Exception($e->getMessage() . ' at row' . $count);
            }
        }

        fclose($handle);

        if ($channel !== null && method_exists($channel, 'commitData')) {
            $channel->commitData();
        }
    }

    /**
     * @param array $row
     * @return bool
     */
    public function isEmptyRow(array $row): bool
    {
        foreach ($row as $column) {
            if ($column) {
                return false;
            }
        }

        return true;
    }

    /**
     * @param $file
     * @param $filename
     * @throws Exception
     */
    public function handleBom($file, $filename)
    {
        switch ($this->getBom($file)) {
            case self::BOM_UTF_32_BE:
                throw new Exception('Unsupported CSV file format. BOM UTF-32, big-endian detected');
            case self::BOM_UTF_32_LE:
                throw new Exception('Unsupported CSV file format. BOM UTF-32, little-endian detected');
            case self::BOM_UTF_16_BE:
                throw new Exception('Unsupported CSV file format. BOM UTF-16, big-endian detected');
            case self::BOM_UTF_16_LE:
                throw new Exception('Unsupported CSV file format. BOM UTF-16, little-endian detected');
            case self::BOM_UTF8:
                $this->_logger->warning("$filename has BOM marker");
                break;
            case self::PLAIN_TEXT:
                break;
        }
    }

    /**
     * @param $file
     * @return string
     */
    public function getBom($file)
    {
        fseek($file, 0);
        $data = fread($file, 10);
        fseek($file, 0);

        foreach ([
                     self::BOM_UTF_32_BE => 4,
                     self::BOM_UTF_32_LE => 4,
                     self::BOM_UTF_16_BE => 2,
                     self::BOM_UTF_16_LE => 2,
                     self::BOM_UTF8      => 3
                 ] as $bom => $bytes) {
            if (strncmp($bom, $data, $bytes) === 0) {
                return $bom;
            }
        }

        return self::PLAIN_TEXT;
    }
}
