#!/opt/local/Library/Frameworks/Python.framework/Versions/3.12/bin/python3.12
# -*- coding: utf-8 -*-
#
# pyfontaine
#
# Copyright (c) 2013,
# Виталий Волков <hash.3g@gmail.com>
# Dave Crossland <dave@understandinglimited.com>
#
# Released under the GNU General Public License version 3 or later.
# See accompanying LICENSE.txt file for details.
import argparse
import codecs
import io
import os
import re
import sys

path = os.path.join(os.path.dirname(__file__), '..')
sys.path.append(os.path.realpath(path))

from fontaine._version import version as __version__
from fontaine.cmap import library
from fontaine.builder import Builder, Director, CharMapGen, GlyphMapGen
from fontaine.font import TTFont
from importlib import import_module
from operator import attrgetter

import tabulate


class FontaineArgParser(argparse.ArgumentParser):

    def error(self, message):
        if '--collections' not in message and '--set' not in message:
            sys.stderr.write('error: %s\n' % message)
        elif '--collections' in message:
            result = get_collections()
            max_length = 0
            for t in result:
                max_length = max(len(t[0]), max_length)
            err = u'{0:<%s}{1}' % (max_length + 2)
            sys.stderr.write('%s\n' % message)
            string = '\n'.join(map(lambda x: err.format(x[0], x[1]), result))
            print(string)
        elif '--set' in message:
            sys.stderr.write('%s\n' % message)
            result = get_availablesets()
            rows = []
            for t in result:
                rows.append([t.short_name, t.native_name, t.common_name])
            print(tabulate.tabulate(rows, tablefmt='plain'))
        sys.exit(2)


def get_collections():
    from fontaine.ext.base import PackageRequiredException
    import fontaine.ext

    result = [('all', 'All collections'),
              ('pyfontaine', 'PyFontaine collections')]
    for ext in fontaine.ext.__all__:
        try:
            module = import_module('fontaine.ext.%s' % ext)
            extension_name = module.Extension.extension_name
            description = getattr(module.Extension, 'description', '')
            result.append((extension_name, description))
        except (ImportError, AttributeError):
            continue
        except PackageRequiredException as ex:
            import sys
            sys.stderr.write(u'WARNING: %s\n' % ex.message)
            continue
    return result


def get_availablesets():
    return sorted(library.charsets, key=attrgetter('common_name'))


def availablesets(s):
    if not s:
        return
    result = get_availablesets()

    shorts = [x.short_name for x in result if x.short_name]
    naive_names = [x.native_name for x in result if x.native_name]
    commons = [x.common_name for x in result if x.common_name]

    names = shorts + naive_names + commons
    invalid = []
    for r in s.split(','):
        if r in names:
            return s
        invalid.append(r)

    error = 'invalid set "%s"' % ' '.join(invalid)
    raise argparse.ArgumentTypeError(error)


def collections(s):
    if not s:
        return
    result = get_collections()

    shorts = map(lambda x: x[0], result)
    invalid = []
    for r in s.split(','):
        if r in shorts:
            return s
        invalid.append(r)

    error = 'invalid collections "%s"' % ' '.join(invalid)
    raise argparse.ArgumentTypeError(error)


def usage():
    description = """
    pyfontaine compares fonts, characters or glyphnames to defined glyph lists or character sets.
    """

    parser = FontaineArgParser(description=description, add_help=True)
    # parser = argparse.ArgumentParser(description=description)
    parser.add_argument('font', metavar='font', type=str, nargs='+')
    parser.add_argument('--update-data', action='store_true',
                        help='Clean application cache')
    parser.add_argument('--disable-unames', action='store_true',
                        help='This will prevent using Unicode names data')
    parser.add_argument('--missing', action='store_true',
                        help='Print additionally a list of each unicode value'
                             ' that is missing from a character set')
    parser.add_argument('--text', action='store_true',
                        help='Output information in plain text')
    parser.add_argument('--xml', action='store_true',
                        help='Output information into XML')
    parser.add_argument('--json', action='store_true',
                        help='Output information in JSON')
    parser.add_argument('--wiki', action='store_true',
                        help='Print information using Mediawiki syntax')
    parser.add_argument('--csv', action='store_true',
                        help=('Output font coverage information in CSV.'
                              ' If glyphs have been passed to script then'
                              ' it shows rows with character set and values'
                              ' True if glyph exists in collections'))
    parser.add_argument('--show-hilbert', action='store_true',
                        help='Create PNG files representing coverage of font'
                             ' charset in coverage_pngs directory')
    parser.add_argument('--set', type=availablesets, default='',
                        help='Specify the sets based on either common'
                             ' or native name')
    parser.add_argument('--text-compact', action='store_true')
    parser.add_argument('--collections', type=collections, default='',
                        help='Specify collections of charsets to print')
    parser.add_argument('--new-glyphmap-txt', action='store_true',
                        help='Output a glyphlist from '
                             'the glyphs present in a font to a text file.')
    parser.add_argument('--new-charset-txt', action='store_true',
                        help='Output a list of characters from '
                             'the characters present in a font to a text file.')
    parser.add_argument('--new-charset-py', action='store_true',
                        help='Output a list of characters from the characters '
                             'present in a font to a python file.')
    parser.add_argument('--new-glyphmap-py', action='store_true',
                        help='Output a list of glyphs from the glyphs '
                             'present in a font to a python file.')
    parser.add_argument('--compare-chars', action='store_true',
                        help='Compare chars in fonts.')
    parser.add_argument('--compare-glyphs', action='store_true',
                        help='Compare glyphs in fonts.')

    url = 'https://github.com/googlefonts/pyfontaine'
    param = '%(version)s (%(url)s)' % {'url': url, 'version': __version__}
    parser.add_argument('-V', '--version', action='version',
                        version="%(prog)s " + param)
    if len(sys.argv) == 1:
        print(f'pyFontaine {__version__} (https://github.com/googlefonts/pyfontaine)')
        parser.print_help()
        sys.exit(1)
    return parser.parse_args()


def check_for_unicodechar(char):
    if len(char) == 1:
        return [ord(char)]

    if re.match('0x[0-9A-Fa-f]+', char):
        return [eval(char)]

    unimatch = re.match(r'U\+([0-9A-Fa-f]+)', char)
    if unimatch:
        return [eval('0x%s' % unimatch.group(1))]

    return [ord(x) for x in char]


def main(*argv):
    args = usage()

    if args.update_data:
        from fontaine.ext.update import bulkupdate
        bulkupdate()

    glyphs_groups = []
    fonts = []

    for arg in args.font:
        if os.path.exists(arg):
            fonts.append(arg)
            continue

        unicodecode = check_for_unicodechar(arg)

        if unicodecode:
            glyphs_groups.append(unicodecode)
            continue

        print("'%s' is not found" % (arg))

    if not fonts and not glyphs_groups:
        sys.exit(0)

    if args.collections:
        library.collections = args.collections.split(',')

    if len(fonts) == 1 and (args.compare_chars or args.compare_glyphs):
        print('Need two or more fonts for comparison.', file=sys.stderr)
        sys.exit(1)

    if fonts and not glyphs_groups:
        if args.disable_unames:
            os.environ['DISABLE_UNAMES'] = 'disable'

        director = Director(show_hilbert=args.show_hilbert,
                            charsets=(args.set or '').split(','),
                            missing=args.missing)
        if args.text_compact:
            Builder.compact_(fonts)
        elif args.xml:
            tree = director.construct_tree(fonts)
            Builder.xml_(tree).display()
        elif args.csv:
            sys.stdout.write(Builder.csv_(fonts))
        elif args.json:
            tree = director.construct_tree(fonts)
            Builder.json_(tree)
        elif args.wiki:
            Builder.wiki(fonts)
        elif args.new_glyphmap_txt:
            for glyph in TTFont(fonts[0]).getGlyphNames():
                print(glyph)
        elif args.new_charset_txt:
            for char in TTFont(fonts[0])._fontFace.getCharmap():
                print(char)
        elif args.new_charset_py:
            font = fonts[0]
            charset = TTFont(font)._fontFace.getCharmapFull()
            charset_gen = CharMapGen(
                charset,
                common_name=os.path.splitext(os.path.basename(font))[0]
            )
            charset_gen.generate()
            print(charset_gen.py_gen.get_code())
        elif args.new_glyphmap_py:
            font = fonts[0]
            glyphmap = TTFont(font)._fontFace.ttf.getReverseGlyphMap().items()
            glyphmap_gen = GlyphMapGen(
                glyphmap,
                common_name=os.path.splitext(os.path.basename(font))[0]
            )
            glyphmap_gen.generate()
            print(glyphmap_gen.py_gen.get_code())
        elif args.compare_chars:
            standard_font = fonts.pop(0)
            standard_charset = TTFont(standard_font)._fontFace.getCharmapFull()
            print('All fonts are compared to "{0}".'.format(standard_font))
            for font in fonts:
                charset = TTFont(font)._fontFace.getCharmapFull()
                missing_right = set(standard_charset) - set(charset)
                missing_left = set(charset) - set(standard_charset)
                if missing_right:
                    col_width = [max(len(str(c)) for c in item) \
                                 for item in zip(*missing_right)]
                    print('\nFont "{0}" is missing {1} '
                          'chars:'.format(font, len(missing_right)))
                    for codepoint, symbol, uni_symbol in missing_right:
                        print(str(codepoint).ljust(col_width[0] + 2),
                              symbol.ljust(col_width[1] + 2),
                              uni_symbol.ljust(col_width[2] + 2))
                if missing_left:
                    col_width = [max(len(str(c)) for c in item) \
                                 for item in zip(*missing_left)]
                    print('\nFont "{0}" has {1} chars that are not present in '
                          'target font:'.format(font, len(missing_left)))
                    for codepoint, symbol, uni_symbol in missing_left:
                        print(str(codepoint).ljust(col_width[0] + 2),
                              symbol.ljust(col_width[1] + 2),
                              uni_symbol.ljust(col_width[2] + 2))
                elif not(missing_left or missing_right):
                    print('\nFonts "{0}" and "{1}" have equal '
                          'characters sets.'.format(standard_font, font))
        elif args.compare_glyphs:
            standard_font = fonts.pop(0)
            standard_ttf = TTFont(standard_font)._fontFace.ttf
            standard_glyphnames = standard_ttf.getGlyphNames()
            print('All fonts are compared to "{0}".'.format(standard_font))
            for font in fonts:
                ttf = TTFont(font)._fontFace.ttf
                glyphnames = ttf.getGlyphNames()
                missing_right = set(standard_glyphnames) - set(glyphnames)
                missing_left = set(glyphnames) - set(standard_glyphnames)
                if missing_right:
                    missing = [(g_name, standard_ttf.getGlyphID(g_name)) \
                               for g_name in missing_right]
                    col_width = [max(len(str(c)) for c in item) \
                                 for item in zip(*missing)]
                    print('\nFont "{0}" is missing {1} '
                          'glyphs:'.format(font, len(missing_right)))
                    for g_name, g_id in missing:
                        print(g_name.ljust(col_width[0] + 2),
                              str(g_id).ljust(col_width[1] + 2))
                if missing_left:
                    missing = [(g_name, ttf.getGlyphID(g_name)) \
                               for g_name in missing_left]

                    col_width = [max(len(str(c)) for c in item) \
                                 for item in zip(*missing)]
                    print('\nFont "{0}" has {1} glyphs that are not present in '
                          'target font:'.format(font, len(missing_left)))
                    for g_name, g_id in missing:
                        print(g_name.ljust(col_width[0] + 2),
                              str(g_id).ljust(col_width[1] + 2))
                elif not(missing_left or missing_right):
                    print('\nFonts "{0}" and "{1}" have equal '
                          'glyphs sets.'.format(standard_font, font))
        else:
            tree = director.construct_tree(fonts)
            Builder.text_(tree).display()

    if glyphs_groups and not fonts:
        header = ['Character Set']

        rows = []
        for charset in library.charsets:
            if hasattr(charset, 'glyphs'):
                charset_glyphs = charset.glyphs
            else:
                charset_glyphs = charset.glyphnames
            if callable(charset_glyphs):
                charset_glyphs = charset_glyphs()

            row = [charset.common_name]
            for glyphs in glyphs_groups:
                has_true = False
                true_values = []
                for glyph in glyphs:
                    value = glyph in charset_glyphs

                    if value:
                        has_true = True
                        true_values.append(value)

                    value = bool(len(true_values) == len(glyphs))

                    glyph_unicode = str.upper('U+%04x' % glyph)
                    if glyph_unicode not in header:
                        header.append(glyph_unicode)
                row.append(str(value or ''))

            if has_true:
                rows.append(row)

        if args.csv:
            data = io.StringIO()
            doc = UnicodeWriter(data, delimiter=',')
            doc.writerows([header] + rows)
            data.seek(0)
            print(data.read().decode('utf-8'))
        else:
            print(tabulate.tabulate(rows, header, tablefmt='plain'))

    if glyphs_groups and fonts:
        header = ['Font']
        rows = []
        for font in fonts:

            unicodevalues = TTFont(font)._unicodeValues

            row = [os.path.basename(font)]
            for glyphs_group in glyphs_groups:
                value = False
                for glyph in glyphs_group:
                    value = glyph in unicodevalues
                    glyph_unicode = str.upper('U+%04x' % glyph)
                    if glyph_unicode not in header:
                        header.append(glyph_unicode)
                    row.append(str(value))

            rows.append(row)

        if args.csv:
            data = io.StringIO()
            doc = UnicodeWriter(data, delimiter=',')
            doc.writerows([header] + rows)
            data.seek(0)
            print(data.read().decode('utf-8'))
        else:
            print(tabulate.tabulate(rows, header, tablefmt='plain'))

import csv


class UnicodeWriter:

    def __init__(self, f, dialect=csv.excel, encoding="utf-8", **kwds):
        # Redirect output to a queue
        self.queue = io.StringIO()
        self.writer = csv.writer(self.queue, dialect=dialect, **kwds)
        self.stream = f
        self.encoder = codecs.getincrementalencoder(encoding)()

    def writerow(self, row):
        self.writer.writerow([s.encode("utf-8") for s in row])
        # Fetch UTF-8 output from the queue ...
        data = self.queue.getvalue()
        data = data.decode("utf-8")
        # ... and reencode it into the target encoding
        data = self.encoder.encode(data)
        # write to the target stream
        self.stream.write(data)
        # empty queue
        self.queue.truncate(0)

    def writerows(self, rows):
        for row in rows:
            self.writerow(row)

if __name__ == '__main__':
    main()
