<?php declare(strict_types=1);
/**
 * This file is part of escpos-php: PHP receipt printer library for use with
 * ESC/POS-compatible thermal and impact printers.
 *
 * Copyright (c) 2014-20 Michael Billington < michael.billington@gmail.com >,
 * incorporating modifications by others. See CONTRIBUTORS.md for a full list.
 *
 * This software is distributed under the terms of the MIT license. See LICENSE.md
 * for details.
 */

namespace Mike42\Escpos;

use \InvalidArgumentException;

/**
 * Class to handle data about a particular CodePage, as loaded from the receipt print
 * database.
 *
 * Also computes map between UTF-8 and this encoding if necessary, using the intl library.
 */
class CodePage
{
    /**
     * Value to use when no character is set. This is a space in ASCII.
     */
    const MISSING_CHAR_CODE = 0x20;

    /**
     * @var array|null $data
     *  Data string, null if not known (can be computed with iconv)
     */
    protected $data;

    /**
     * @var string|null $iconv
     *  Iconv encoding name, null if not known
     */
    protected $iconv;

    /**
     * @var string $id
     *  Internal ID of the CodePage
     */
    protected $id;

    /**
     * @var string $name
     *  Name of the code page. Substituted with the ID if not set.
     */
    protected $name;

    /**
     * @var string|null $notes
     *  Notes on this code page, or null if not set.
     */
    protected $notes;

    /**
     *
     * @param string $id
     *            Unique internal identifier for the CodePage.
     * @param array $codePageData
     *            Associative array of CodePage data, as
     *            specified by the upstream receipt-print-hq/escpos-printer-db database.
     *            May contain 'name', 'data', 'iconv', and 'notes' fields.
     */
    public function __construct($id, array $codePageData)
    {
        $this->id = $id;
        $this->name = isset($codePageData['name']) ? $codePageData['name'] : $id;
        $this->data = isset($codePageData['data']) ? self::encodingArrayFromData($codePageData['data']) : null;
        $this->iconv = isset($codePageData['iconv']) ? $codePageData['iconv'] : null;
        $this->notes = isset($codePageData['notes']) ? $codePageData['notes'] : null;
    }

    /**
     * Get a 128-entry array of unicode code-points from this code page.
     *
     * @throws InvalidArgumentException Where the data is now known or computable.
     * @return array Data for this encoding.
     */
    public function getDataArray() : array
    {
        // Make string
        if ($this->data !== null) {
            // Return data if known
            return $this->data;
        }
        if ($this->iconv !== null) {
            // Calculate with iconv if we know the encoding name
            $this->data = self::generateEncodingArray($this->iconv);
            return $this->data;
        }
        // Can't encode..
        throw new InvalidArgumentException("Cannot encode this code page");
    }

    /**
     *
     * @return string|null Iconv encoding name, or null if not set.
     */
    public function getIconv()
    {
        return $this->iconv;
    }

    /**
     *
     * @return string Unique identifier of the code page.
     */
    public function getId() : string
    {
        return $this->id;
    }

    /**
     * @return string Name of the code page.
     */
    public function getName() : string
    {
        return $this->name;
    }

    /**
     * The notes may explain quirks about a code-page, such as a source if it's non-standard or un-encodeable.
     *
     * @return string|null Notes on the code page, or null if not set.
     */
    public function getNotes()
    {
        return $this->notes;
    }

    /**
     *
     * @return boolean True if we can encode with this code page (ie, we know what data it holds).
     *
     * Many printers contain vendor-specific code pages, which are named but have not been identified or
     * typed out. For our purposes, this is an "un-encodeable" code page.
     */
    public function isEncodable()
    {
        return $this->iconv !== null || $this->data !== null;
    }

    /**
     * Given an ICU encoding name, generate a 128-entry array, with the unicode code points
     * for the character at positions 128-255 in this code page.
     *
     * @param string $encodingName Name of the encoding
     * @return array 128-entry array of code points
     */
    protected static function generateEncodingArray(string $encodingName) : array
    {
        // Set up converter for encoding
        $missingChar = chr(self::MISSING_CHAR_CODE);
        // Throws a lot of warnings for ambiguous code pages, but fallbacks seem fine.
        $converter = @new \UConverter("UTF-8", $encodingName);
        $converter -> setSubstChars($missingChar);
        // Loop through 128 code points
        $intArray = array_fill(0, 128, self::MISSING_CHAR_CODE);
        for ($char = 128; $char <= 255; $char++) {
            // Try to identify the UTF-8 character at this position in the code page
            $encodingChar = chr($char);
            $utf8 = $converter ->convert($encodingChar, false);
            if ($utf8 === $missingChar || $utf8 === false) {
                // Cannot be mapped to unicode
                continue;
            }
            $reverse = $converter ->convert($utf8, true);
            if ($reverse !== $encodingChar) {
                // Avoid conversions which don't reverse well (eg. multi-byte code pages)
                continue;
            }
            // Replace space with the correct character if we found it
            $intArray[$char - 128] = \IntlChar::ord($utf8);
        }
        assert(count($intArray) == 128);
        return $intArray;
    }


    private static function encodingArrayFromData(array $data) : array
    {
        $text = implode("", $data); // Join lines
        $codePointIterator = \IntlBreakIterator::createCodePointInstance();
        $codePointIterator -> setText($text);
        $ret = array_fill(0, 128, self::MISSING_CHAR_CODE);
        for ($i = 0; ($codePointIterator -> next() > 0) && ($i < 128); $i++) {
            $codePoint = $codePointIterator -> getLastCodePoint();
            $ret[$i] = $codePoint;
        }
        assert(count($ret) == 128);
        return $ret;
    }
}
