<?php

/**
 * Import.php
 *
 * @since     2011-05-23
 * @category  Library
 * @package   PdfFont
 * @author    Nicola Asuni <info@tecnick.com>
 * @copyright 2011-2024 Nicola Asuni - Tecnick.com LTD
 * @license   http://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE.TXT)
 * @link      https://github.com/tecnickcom/tc-lib-pdf-font
 *
 * This file is part of tc-lib-pdf-font software library.
 */

namespace Com\Tecnick\Pdf\Font;

use Com\Tecnick\File\Byte;
use Com\Tecnick\File\Dir;
use Com\Tecnick\File\File;
use Com\Tecnick\Pdf\Font\Exception as FontException;
use Com\Tecnick\Pdf\Font\Import\Core;
use Com\Tecnick\Pdf\Font\Import\TrueType;
use Com\Tecnick\Pdf\Font\Import\TypeOne;
use Com\Tecnick\Unicode\Data\Encoding;

/**
 * Com\Tecnick\Pdf\Font\Import
 *
 * @since     2011-05-23
 * @category  Library
 * @package   PdfFont
 * @author    Nicola Asuni <info@tecnick.com>
 * @copyright 2011-2024 Nicola Asuni - Tecnick.com LTD
 * @license   http://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE.TXT)
 * @link      https://github.com/tecnickcom/tc-lib-pdf-font
 *
 * @phpstan-import-type TFontData from Load
 *
 * @SuppressWarnings("PHPMD.ExcessiveClassComplexity")
 */
class Import
{
    /**
     * Content of the input font file
     */
    protected string $font = '';

    /**
     * Object used to read font bytes
     */
    protected Byte $fbyte;

    /**
     * Extracted font metrics
     *
     * @var TFontData
     */
    protected array $fdt = [
        'Ascender' => 0,
        'Ascent' => 0,
        'AvgWidth' => 0.0,
        'CapHeight' => 0,
        'CharacterSet' => '',
        'Descender' => 0,
        'Descent' => 0,
        'EncodingScheme' => '',
        'FamilyName' => '',
        'Flags' => 0,
        'FontBBox' => [],
        'FontName' => '',
        'FullName' => '',
        'IsFixedPitch' => false,
        'ItalicAngle' => 0,
        'Leading' => 0,
        'MaxWidth' => 0,
        'MissingWidth' => 0,
        'StdHW' => 0,
        'StdVW' => 0,
        'StemH' => 0,
        'StemV' => 0,
        'UnderlinePosition' => 0,
        'UnderlineThickness' => 0,
        'Version' => '',
        'Weight' => '',
        'XHeight' => 0,
        'bbox' => '',
        'cbbox' => [],
        'cidinfo' => [
            'Ordering' => '',
            'Registry' => '',
            'Supplement' => 0,
            'uni2cid' => [],
        ],
        'compress' => false,
        'ctg' => '',
        'ctgdata' => [],
        'cw' => [],
        'datafile' => '',
        'desc' => [
            'Ascent' => 0,
            'AvgWidth' => 0,
            'CapHeight' => 0,
            'Descent' => 0,
            'Flags' => 0,
            'FontBBox' => '',
            'ItalicAngle' => 0,
            'Leading' => 0,
            'MaxWidth' => 0,
            'MissingWidth' => 0,
            'StemH' => 0,
            'StemV' => 0,
            'XHeight' => 0,
        ],
        'diff' => '',
        'diff_n' => 0,
        'dir' => '',
        'dw' => 0,
        'enc' => '',
        'enc_map' => [],
        'encodingTables' => [],
        'encoding_id' => 0,
        'encrypted' => '',
        'fakestyle' => false,
        'family' => '',
        'file' => '',
        'file_n' => 0,
        'file_name' => '',
        'i' => 0,
        'ifile' => '',
        'indexToLoc' => [],
        'input_file' => '',
        'isUnicode' => false,
        'italicAngle' => 0,
        'key' => '',
        'lenIV' => 0,
        'length1' => 0,
        'length2' => 0,
        'linked' => false,
        'mode' => [
            'bold' => false,
            'italic' => false,
            'linethrough' => false,
            'overline' => false,
            'underline' => false,
        ],
        'n' => 0,
        'name' => '',
        'numGlyphs' => 0,
        'numHMetrics' => 0,
        'originalsize' => 0,
        'pdfa' => false,
        'platform_id' => 0,
        'settype' => '',
        'short_offset' => false,
        'size1' => 0,
        'size2' => 0,
        'style' => '',
        'subset' => false,
        'subsetchars' => [],
        'table' => [],
        'tot_num_glyphs' => 0,
        'type' => '',
        'underlinePosition' => 0,
        'underlineThickness' => 0,
        'unicode' => false,
        'unitsPerEm' => 0,
        'up' => 0,
        'urk' => 0.0,
        'ut' => 0,
        'weight' => '',
    ];

    /**
     * Import the specified font and create output files.
     *
     * @param string $file        Font file to process
     * @param string $output_path Output path for generated font files (must be writeable by the web server).
     *                            Leave null for default font folder.
     * @param string $type        Font type. Leave empty for autodetect mode. Valid values are:
     *                            Core (AFM - Adobe Font Metrics) TrueTypeUnicode TrueType
     *                            Type1 CID0JP (CID-0 Japanese) CID0KR (CID-0 Korean) CID0CS
     *                            (CID-0 Chinese Simplified) CID0CT (CID-0 Chinese Traditional)
     * @param string $encoding    Name of the encoding table to use. Leave empty for default mode.
     *                            Omit this parameter for TrueType Unicode and symbolic fonts like
     *                            Symbol or ZapfDingBats.
     * @param int    $flags       Unsigned 32-bit integer containing flags specifying various characteristics
     *                            of the font as described in "PDF32000:2008 - 9.8.2 Font Descriptor Flags":
     *                            +1 for fixed width font +4 for symbol or +32 for non-symbol +64 for italic
     *                            Note: Fixed and Italic mode are generally autodetected, so you have to set
     *                            it to 32 = non-symbolic font (default) or 4 = symbolic font.
     * @param int    $platform_id Platform ID for CMAP table to extract.
     *                            For a Unicode font for Windows this
     *                            value should be 3, for Macintosh
     *                            should be 1.
     * @param int    $encoding_id Encoding ID for CMAP table to extract.
     *                            For a Unicode font for Windows this
     *                            value should be 1, for Macintosh
     *                            should be 0. When Platform ID is 3,
     *                            legal values for Encoding ID are: 0 =
     *                            Symbol, 1 = Unicode, 2 = ShiftJIS, 3 =
     *                            PRC, 4 = Big5, 5 = Wansung, 6 = Johab,
     *                            7 = Reserved, 8 = Reserved, 9 =
     *                            Reserved, 10 = UCS-4.
     * @param bool   $linked      If true, links the font file to system font instead of copying the font data
     *                            (not transportable). Note: this option do not work with Type1 fonts.
     *
     * @throws FontException in case of error
     */
    public function __construct(
        string $file,
        string $output_path = '',
        string $type = '',
        string $encoding = '',
        int $flags = 32,
        int $platform_id = 3,
        int $encoding_id = 1,
        bool $linked = false
    ) {
        if (FILE::hasDoubleDots($file) || FILE::hasForbiddenProtocol($file)) {
            throw new FontException('Invalid font file name: ' . $file);
        }

        $this->fdt['input_file'] = $file;
        $this->fdt['file_name'] = $this->makeFontName($file);
        if (empty($this->fdt['file_name'])) {
            throw new FontException('the font name is empty');
        }

        $this->fdt['dir'] = $this->findOutputPath($output_path);
        $this->fdt['datafile'] = $this->fdt['dir'] . $this->fdt['file_name'] . '.json';
        if (@file_exists($this->fdt['datafile'])) {
            throw new FontException('this font has been already imported: ' . $this->fdt['datafile']);
        }

        // get font data
        if (! is_file($file)) {
            throw new FontException('invalid font file: ' . $file);
        }

        if (($font = @file_get_contents($file)) === false) {
            throw new FontException('unable to read the input font file: ' . $file);
        }

        $this->font = $font;

        $this->fbyte = new Byte($this->font);

        $this->fdt['settype'] = $type;
        $this->fdt['type'] = $this->getFontType($type);
        $this->fdt['isUnicode'] = (($this->fdt['type'] == 'TrueTypeUnicode') || ($this->fdt['type'] == 'cidfont0'));
        $this->fdt['Flags'] = $flags;
        $this->initFlags();
        $this->fdt['enc'] = $this->getEncodingTable($encoding);
        $this->fdt['diff'] = $this->getEncodingDiff();
        $this->fdt['originalsize'] = strlen($this->font);
        $this->fdt['ctg'] = $this->fdt['file_name'] . '.ctg.z';
        $this->fdt['platform_id'] = $platform_id;
        $this->fdt['encoding_id'] = $encoding_id;
        $this->fdt['linked'] = $linked;

        $processor = match ($this->fdt['type']) {
            'Core' => new Core($this->font, $this->fdt),
            'Type1' => new TypeOne($this->font, $this->fdt),
            default => new TrueType($this->font, $this->fdt, $this->fbyte),
        };
        ;

        $this->fdt = $processor->getFontMetrics();

        $this->saveFontData();
    }

    /**
     * Get all the extracted font metrics
     *
     * @return TFontData
     */
    public function getFontMetrics(): array
    {
        return $this->fdt;
    }

    /**
     * Get the output font name
     */
    public function getFontName(): string
    {
        return $this->fdt['file_name'];
    }

    /**
     * Initialize font flags from font name
     */
    protected function initFlags(): void
    {
        $filename = strtolower(basename($this->fdt['input_file']));

        if (
            (str_contains($filename, 'mono'))
            || (str_contains($filename, 'courier'))
            || (str_contains($filename, 'fixed'))
        ) {
            $this->fdt['Flags'] |= 1;
        }

        if (
            (str_contains($filename, 'symbol'))
            || (str_contains($filename, 'zapfdingbats'))
        ) {
            $this->fdt['Flags'] |= 4;
        }

        if (
            (str_contains($filename, 'italic'))
            || (str_contains($filename, 'oblique'))
        ) {
            $this->fdt['Flags'] |= 64;
        }
    }

    /**
     * Save the eported metadata font file
     *
     * @SuppressWarnings("PHPMD.CyclomaticComplexity")
     * @SuppressWarnings("PHPMD.NPathComplexity")
     */
    protected function saveFontData(): void
    {
        $pfile = '{"type":"' . $this->fdt['type'] . '"'
            . ',"name":"' . $this->fdt['name'] . '"'
            . ',"up":' . $this->fdt['underlinePosition']
            . ',"ut":' . $this->fdt['underlineThickness']
            . ',"dw":' . (($this->fdt['MissingWidth'] > 0) ? $this->fdt['MissingWidth'] : $this->fdt['AvgWidth'])
            . ',"diff":"' . $this->fdt['diff'] . '"'
            . ',"platform_id":' . $this->fdt['platform_id']
            . ',"encoding_id":' . $this->fdt['encoding_id'];

        if ($this->fdt['type'] == 'Core') {
            // Core
            $pfile .= ',"enc":""';
        } elseif ($this->fdt['type'] == 'Type1') {
            // Type 1
            $pfile .= ',"enc":"' . $this->fdt['enc'] . '"'
                . ',"file":"' . $this->fdt['file'] . '"'
                . ',"size1":' . $this->fdt['size1']
                . ',"size2":' . $this->fdt['size2'];
        } else {
            $pfile .= ',"originalsize":' . $this->fdt['originalsize'];
            if ($this->fdt['type'] == 'cidfont0') {
                $pfile .= ',' . UniToCid::TYPE[$this->fdt['settype']];
            } else {
                // TrueType
                $pfile .= ',"enc":"' . $this->fdt['enc'] . '"'
                    . ',"file":"' . $this->fdt['file'] . '"'
                    . ',"ctg":"' . $this->fdt['ctg'] . '"';
                // create CIDToGIDMap
                $cidtogidmap = str_pad('', 131072, "\x00"); // (256 * 256 * 2) = 131072
                foreach ($this->fdt['ctgdata'] as $cid => $gid) {
                    $cidtogidmap = $this->updateCIDtoGIDmap($cidtogidmap, (int) $cid, (int) $gid);
                }

                // store compressed CIDToGIDMap
                $file = new File();
                $fpt = $file->fopenLocal($this->fdt['dir'] . $this->fdt['ctg'], 'wb');

                $cmpr = gzcompress($cidtogidmap);
                if ($cmpr === false) {
                    throw new FontException('unable to compress CIDToGIDMap');
                }

                fwrite($fpt, $cmpr);
                fclose($fpt);
            }
        }

        if ($this->fdt['isUnicode']) {
            $pfile .= ',"isUnicode":true';
        } else {
            $pfile .= ',"isUnicode":false';
        }

        $pfile .= ',"desc":{"Flags":' . $this->fdt['Flags']
            . ',"FontBBox":"[' . $this->fdt['bbox'] . ']"'
            . ',"ItalicAngle":' . $this->fdt['italicAngle']
            . ',"Ascent":' . $this->fdt['Ascent']
            . ',"Descent":' . $this->fdt['Descent']
            . ',"Leading":' . $this->fdt['Leading']
            . ',"CapHeight":' . $this->fdt['CapHeight']
            . ',"XHeight":' . $this->fdt['XHeight']
            . ',"StemV":' . $this->fdt['StemV']
            . ',"StemH":' . $this->fdt['StemH']
            . ',"AvgWidth":' . $this->fdt['AvgWidth']
            . ',"MaxWidth":' . $this->fdt['MaxWidth']
            . ',"MissingWidth":' . $this->fdt['MissingWidth']
            . '}';
        if (! empty($this->fdt['cbbox'])) {
            $ccboxstr = '';
            foreach ($this->fdt['cbbox'] as $cid => $bbox) {
                $ccboxstr .= ',"' . $cid . '":[' . $bbox[0] . ',' . $bbox[1] . ',' . $bbox[2] . ',' . $bbox[3] . ']';
            }

            $pfile .= ',"cbbox":{' . substr($ccboxstr, 1) . '}';
        }

        if (! empty($this->fdt['cw'])) {
            $cwstr = '';
            foreach ($this->fdt['cw'] as $cid => $width) {
                $cwstr .= ',"' . $cid . '":' . $width;
            }

            $pfile .= ',"cw":{' . substr($cwstr, 1) . '}';
        }

        $pfile .= '}' . "\n";

        // store file
        $file = new File();
        $fpt = $file->fopenLocal($this->fdt['datafile'], 'wb');
        fwrite($fpt, $pfile);
        fclose($fpt);
    }

    /**
     * Make the output font name
     *
     * @param string $font_file Input font file
     */
    protected function makeFontName(string $font_file): string
    {
        $font_path_parts = pathinfo($font_file);
        if (empty($font_path_parts['filename'])) {
            throw new FontException('Invalid font file name: ' . $font_file);
        }

        $fname = preg_replace('/[^a-z0-9_]/', '', strtolower($font_path_parts['filename']));
        if ($fname === null) {
            throw new FontException('Invalid font file name: ' . $font_file);
        }

        return str_replace(
            ['bold', 'oblique', 'italic', 'regular'],
            ['b', 'i', 'i', ''],
            $fname
        );
    }

    /**
     * Find the path where to store the processed font.
     *
     * @param string $output_path Output path for generated font files (must be writeable by the web server).
     *                            Leave null for default font folder (K_PATH_FONTS).
     */
    protected function findOutputPath(string $output_path = ''): string
    {
        if (
            $output_path !== ''
            && (strpos($output_path, '://') === false)
            && !FILE::hasDoubleDots($output_path)
            && is_writable($output_path)
        ) {
            return $output_path;
        }

        if (defined('K_PATH_FONTS') && is_writable(K_PATH_FONTS)) {
            return K_PATH_FONTS;
        }

        $dirobj = new Dir();
        $dir = $dirobj->findParentDir('fonts', __DIR__);
        if ($dir == '/') {
            $dir = sys_get_temp_dir();
        }

        if (! str_ends_with($dir, '/')) {
            $dir .= '/';
        }

        return $dir;
    }

    /**
     * Get the font type
     *
     * @param string $font_type Font type. Leave empty for autodetect mode.
     */
    protected function getFontType(string $font_type): string
    {
        // autodetect font type
        if ($font_type === '') {
            if (str_starts_with($this->font, 'StartFontMetrics')) {
                // AFM type - we use this type only for the 14 Core fonts
                return 'Core';
            }

            if (str_starts_with($this->font, 'OTTO')) {
                throw new FontException('Unsupported font format: OpenType with CFF data');
            }

            if ($this->fbyte->getULong(0) == 0x10000) {
                return 'TrueTypeUnicode';
            }

            return 'Type1';
        }

        if (str_starts_with($font_type, 'CID0')) {
            return 'cidfont0';
        }

        if (in_array($font_type, ['Core', 'Type1', 'TrueType', 'TrueTypeUnicode'])) {
            return $font_type;
        }

        throw new FontException('unknown or unsupported font type: ' . $font_type);
    }

    /**
     * Get the encoding table
     *
     * @param string $encoding Name of the encoding table to use. Leave empty for default mode.
     *                         Omit this parameter for TrueType Unicode and symbolic fonts like
     *                         Symbol or ZapfDingBats.
     */
    protected function getEncodingTable(string $encoding = ''): string
    {
        if ($encoding === '') {
            if (($this->fdt['type'] == 'Type1') && (($this->fdt['Flags'] & 4) == 0)) {
                return 'cp1252';
            }

            return '';
        }

        $enc = preg_replace('/[^A-Za-z0-9_\-]/', '', $encoding);
        if ($enc === null) {
            throw new FontException('Invalid encoding name: ' . $encoding);
        }

        return $enc;
    }

    /**
     * If required, get differences between the reference encoding (cp1252) and the current encoding
     *
     * @SuppressWarnings("PHPMD.CyclomaticComplexity")
     */
    protected function getEncodingDiff(): string
    {
        $diff = '';
        if (
            (($this->fdt['type'] == 'TrueType') || ($this->fdt['type'] == 'Type1'))
            && (! empty($this->fdt['enc'])
            && (is_string($this->fdt['enc']))
            && ($this->fdt['enc'] != 'cp1252')
            && isset(Encoding::MAP[$this->fdt['enc']]))
        ) {
            // build differences from reference encoding
            $enc_ref = Encoding::MAP['cp1252'];
            $enc_target = Encoding::MAP[$this->fdt['enc']];
            $last = 0;
            for ($idx = 32; $idx <= 255; ++$idx) {
                if ($enc_target[$idx] != $enc_ref[$idx]) {
                    if ($idx != $last + 1) {
                        $diff .= $idx . ' ';
                    }

                    $last = $idx;
                    $diff .= '/' . $enc_target[$idx] . ' ';
                }
            }
        }

        return $diff;
    }

    /**
     * Update the CIDToGIDMap string with a new value
     *
     * @param string $map CIDToGIDMap.
     * @param int    $cid CID value.
     * @param int    $gid GID value.
     */
    protected function updateCIDtoGIDmap(string $map, int $cid, int $gid): string
    {
        if (($cid >= 0) && ($cid <= 0xFFFF) && ($gid >= 0)) {
            if ($gid > 0xFFFF) {
                $gid -= 0x10000;
            }

            $map[($cid * 2)] = chr($gid >> 8);
            $map[(($cid * 2) + 1)] = chr($gid & 0xFF);
        }

        return $map;
    }
}
