#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright 2016 Google Inc. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Patches for Android versions of Noto fonts."""

import argparse
import codecs
import glob
import os
from os import path
import shutil
import tempfile

from nototools.py23 import unichr
from nototools import subset
from nototools import coverage
from nototools import fix_khmer_and_lao_coverage as merger
from nototools import font_data
from nototools import tool_utils
from nototools import ttc_utils
from nototools import unicode_data

from fontTools import ttLib


def patch_hyphen(srcdir, dstdir, copy_unchanged=True):
    """Add hyphen-minus glyphs to fonts that need it.

    This is to enable languages to be hyphenated properly,
    since Minikin's itemizer currently shows tofus if an
    automatically hyphenated word is displated in a font
    that has neither HYPHEN nor HYPHEN-MINUS.

    The list of font names comes from LANG_TO_SCRIPT in
    tools/font/fontchain_lint.py.

    (In practice only U+002D HYPHEN-MINUS is added, since Noto LGC fonts
    don't have U+2010 HYPHEN.)

    Bug: 21570828"""

    # Names of fonts for which Android requires a hyphen.
    # This list omits Japanese and Korean.
    script_names = [
        "Armenian",
        "Ethiopic",
        "Bengali",
        "Gujarati",
        "Devanagari",
        "Kannada",
        "Malayalam",
        "Oriya",
        "Gurmukhi",
        "Tamil",
        "Telugu",
    ]

    HYPHENS = {0x002D, 0x2010}

    for sn in script_names:
        globexp = path.join(srcdir, "Noto*%s-*.ttf" % sn)
        fonts = glob.glob(globexp)
        if not fonts:
            continue
        fonts = [path.basename(f) for f in fonts]
        for font_name in fonts:
            lgc_font_name = font_name.replace(sn, "")

            font_file = path.join(srcdir, font_name)
            lgc_font_file = path.join(srcdir, lgc_font_name)

            chars_to_add = (
                HYPHENS - coverage.character_set(font_file)
            ) & coverage.character_set(lgc_font_file)

            if chars_to_add:
                print("patch hyphens", font_name)
                merger.merge_chars_from_bank(
                    path.join(srcdir, font_name),
                    path.join(srcdir, lgc_font_name),
                    path.join(srcdir, font_name),
                    chars_to_add,
                )
            else:
                if copy_unchanged:
                    shutil.copy2(
                        path.join(srcdir, font_name), path.join(dstdir, font_name)
                    )
                    print("%s already has hyphens, copying" % font_name)
                else:
                    print("%s already has hyphens" % font_name)


def _remove_cjk_emoji(cjk_font_names, srcdir, dstdir):
    """
    Remove default emoji characters from CJK fonts.

    Twenty-six characters that Unicode Technical Report #51 "Unicode
    Emoji" defines as defaulting to emoji styles used to be displayed as
    black and white ("text" style) before this. This patch removes those
    characters from Noto CJK fonts, so they get displayed as color.

    (1c4749e20391a4)
    """

    # Since subsetting changes tables in a way that would prevent a compact
    # .ttc file, this simply removes entries from the cmap table.  This
    # does not affect other tables in the font.  There are no emoji presentation
    # variation sequences in the fonts.

    def _remove_from_cmap(infile, outfile, exclude=[]):
        font = ttLib.TTFont(infile)
        font_data.delete_from_cmap(font, exclude)
        font.save(outfile)

    # Characters supported in Noto CJK fonts that UTR #51 recommends default to
    # emoji-style.
    EMOJI_IN_CJK = {
        0x26BD,  # ⚽ SOCCER BALL
        0x26BE,  # ⚾ BASEBALL
        0x1F18E,  # 🆎 NEGATIVE SQUARED AB
        0x1F191,  # 🆑 SQUARED CL
        0x1F192,  # 🆒 SQUARED COOL
        0x1F193,  # 🆓 SQUARED FREE
        0x1F194,  # 🆔 SQUARED ID
        0x1F195,  # 🆕 SQUARED NEW
        0x1F196,  # 🆖 SQUARED NG
        0x1F197,  # 🆗 SQUARED OK
        0x1F198,  # 🆘 SQUARED SOS
        0x1F199,  # 🆙 SQUARED UP WITH EXCLAMATION MARK
        0x1F19A,  # 🆚 SQUARED VS
        0x1F201,  # 🈁 SQUARED KATAKANA KOKO
        0x1F21A,  # 🈚 SQUARED CJK UNIFIED IDEOGRAPH-7121
        0x1F22F,  # 🈯 SQUARED CJK UNIFIED IDEOGRAPH-6307
        0x1F232,  # 🈲 SQUARED CJK UNIFIED IDEOGRAPH-7981
        0x1F233,  # 🈳 SQUARED CJK UNIFIED IDEOGRAPH-7A7A
        0x1F234,  # 🈴 SQUARED CJK UNIFIED IDEOGRAPH-5408
        0x1F235,  # 🈵 SQUARED CJK UNIFIED IDEOGRAPH-6E80
        0x1F236,  # 🈶 SQUARED CJK UNIFIED IDEOGRAPH-6709
        0x1F238,  # 🈸 SQUARED CJK UNIFIED IDEOGRAPH-7533
        0x1F239,  # 🈹 SQUARED CJK UNIFIED IDEOGRAPH-5272
        0x1F23A,  # 🈺 SQUARED CJK UNIFIED IDEOGRAPH-55B6
        0x1F250,  # 🉐 CIRCLED IDEOGRAPH ADVANTAGE
        0x1F251,  # 🉑 CIRCLED IDEOGRAPH ACCEPT
    }

    # Characters we have decided we are doing as emoji-style in Android,
    # despite UTR #51's recommendation
    ANDROID_EMOJI = {
        0x2600,  # ☀ BLACK SUN WITH RAYS
        0x2601,  # ☁ CLOUD
        0x260E,  # ☎ BLACK TELEPHONE
        0x261D,  # ☝ WHITE UP POINTING INDEX
        0x263A,  # ☺ WHITE SMILING FACE
        0x2660,  # ♠ BLACK SPADE SUIT
        0x2663,  # ♣ BLACK CLUB SUIT
        0x2665,  # ♥ BLACK HEART SUIT
        0x2666,  # ♦ BLACK DIAMOND SUIT
        0x270C,  # ✌ VICTORY HAND
        0x2744,  # ❄ SNOWFLAKE
        0x2764,  # ❤ HEAVY BLACK HEART
    }

    # We don't want support for ASCII control chars.
    CONTROL_CHARS = tool_utils.parse_int_ranges("0000-001F")

    EXCLUDED_CODEPOINTS = sorted(EMOJI_IN_CJK | ANDROID_EMOJI | CONTROL_CHARS)

    for font_name in cjk_font_names:
        print("remove cjk emoji", font_name)
        _remove_from_cmap(
            path.join(srcdir, font_name),
            path.join(dstdir, font_name),
            exclude=EXCLUDED_CODEPOINTS,
        )


def patch_cjk_ttc(ttc_srcfile, ttc_dstfile):
    """Take the source ttc, break it apart, remove the cjk emoji
    from each file, then repackage them into a new ttc."""

    tmp_dir = tempfile.mkdtemp()
    font_names = ttc_utils.ttcfile_extract(ttc_srcfile, tmp_dir)
    tmp_patched_dir = path.join(tmp_dir, "patched")
    os.mkdir(tmp_patched_dir)
    _remove_cjk_emoji(font_names, tmp_dir, tmp_patched_dir)
    # have ttcfile_build resolve names relative to patched dir
    with tool_utils.temp_chdir(tmp_patched_dir):
        ttc_utils.ttcfile_build(ttc_dstfile, font_names)
    shutil.rmtree(tmp_dir)


def patch_cjk_ttcs(srcdir, dstdir):
    """Call patch_cjk_ttc for each ttc file in srcdir, writing the
    result to dstdir using the same name."""

    if not path.isdir(srcdir):
        print("%s is not a directory" % srcdir)
        return

    ttc_files = [f for f in os.listdir(srcdir) if f.endswith(".ttc")]
    if not ttc_files:
        print("no .ttc file to patch in %s" % srcdir)
        return

    tool_utils.ensure_dir_exists(dstdir)
    for f in ttc_files:
        patch_cjk_ttc(path.join(srcdir, f), path.join(dstdir, f))


# below are used by _subset_symbols

# Unicode blocks that we want to include in the font
BLOCKS_TO_INCLUDE = """
20D0..20FF; Combining Diacritical Marks for Symbols
2100..214F; Letterlike Symbols
2190..21FF; Arrows
2200..22FF; Mathematical Operators
2300..23FF; Miscellaneous Technical
2400..243F; Control Pictures
2440..245F; Optical Character Recognition
2460..24FF; Enclosed Alphanumerics
2500..257F; Box Drawing
2580..259F; Block Elements
25A0..25FF; Geometric Shapes
2600..26FF; Miscellaneous Symbols
2700..27BF; Dingbats
27C0..27EF; Miscellaneous Mathematical Symbols-A
27F0..27FF; Supplemental Arrows-A
2800..28FF; Braille Patterns
2A00..2AFF; Supplemental Mathematical Operators
"""

# One-off characters to be included, needed for backward compatibility and
# supporting various character sets, including ARIB sets and black and white
# emoji
ONE_OFF_ADDITIONS = {
    0x27D0,  # ⟐ WHITE DIAMOND WITH CENTRED DOT
    0x2934,  # ⤴ ARROW POINTING RIGHTWARDS THEN CURVING UPWARDS
    0x2935,  # ⤵ ARROW POINTING RIGHTWARDS THEN CURVING DOWNWARDS
    0x2985,  # ⦅ LEFT WHITE PARENTHESIS
    0x2986,  # ⦆ RIGHT WHITE PARENTHESIS
    0x2B05,  # ⬅ LEFTWARDS BLACK ARROW
    0x2B06,  # ⬆ UPWARDS BLACK ARROW
    0x2B07,  # ⬇ DOWNWARDS BLACK ARROW
    0x2B24,  # ⬤ BLACK LARGE CIRCLE
    0x2B2E,  # ⬮ BLACK VERTICAL ELLIPSE
    0x2B2F,  # ⬯ WHITE VERTICAL ELLIPSE
    0x2B56,  # ⭖ HEAVY OVAL WITH OVAL INSIDE
    0x2B57,  # ⭗ HEAVY CIRCLE WITH CIRCLE INSIDE
    0x2B58,  # ⭘ HEAVY CIRCLE
    0x2B59,  # ⭙ HEAVY CIRCLED SALTIRE
    0x1F19B,  # 🆛 SQUARED THREE D
    0x1F19C,  # 🆜 SQUARED SECOND SCREEN
    0x1F19D,  # 🆝 SQUARED TWO K;So;0;L;;;;;N;;;;;
    0x1F19E,  # 🆞 SQUARED FOUR K;So;0;L;;;;;N;;;;;
    0x1F19F,  # 🆟 SQUARED EIGHT K;So;0;L;;;;;N;;;;;
    0x1F1A0,  # 🆠 SQUARED FIVE POINT ONE;So;0;L;;;;;N;;;;;
    0x1F1A1,  # 🆡 SQUARED SEVEN POINT ONE;So;0;L;;;;;N;;;;;
    0x1F1A2,  # 🆢 SQUARED TWENTY-TWO POINT TWO;So;0;L;;;;;N;;;;;
    0x1F1A3,  # 🆣 SQUARED SIXTY P;So;0;L;;;;;N;;;;;
    0x1F1A4,  # 🆤 SQUARED ONE HUNDRED TWENTY P;So;0;L;;;;;N;;;;;
    0x1F1A5,  # 🆥 SQUARED LATIN SMALL LETTER D;So;0;L;;;;;N;;;;;
    0x1F1A6,  # 🆦 SQUARED HC;So;0;L;;;;;N;;;;;
    0x1F1A7,  # 🆧 SQUARED HDR;So;0;L;;;;;N;;;;;
    0x1F1A8,  # 🆨 SQUARED HI-RES;So;0;L;;;;;N;;;;;
    0x1F1A9,  # 🆩 SQUARED LOSSLESS;So;0;L;;;;;N;;;;;
    0x1F1AA,  # 🆪 SQUARED SHV;So;0;L;;;;;N;;;;;
    0x1F1AB,  # 🆫 SQUARED UHD;So;0;L;;;;;N;;;;;
    0x1F1AC,  # 🆬 SQUARED VOD;So;0;L;;;;;N;;;;;
    0x1F23B,  # 🈻 SQUARED CJK UNIFIED IDEOGRAPH-914D
}

# letter-based characters, provided by Roboto
# TODO see if we need to change this subset based on Noto Serif coverage
# (so the serif fallback chain would support them)
LETTERLIKE_CHARS_IN_ROBOTO = {
    0x2100,  # ℀ ACCOUNT OF
    0x2101,  # ℁ ADDRESSED TO THE SUBJECT
    0x2103,  # ℃ DEGREE CELSIUS
    0x2105,  # ℅ CARE OF
    0x2106,  # ℆ CADA UNA
    0x2109,  # ℉ DEGREE FAHRENHEIT
    0x2113,  # ℓ SCRIPT SMALL L
    0x2116,  # № NUMERO SIGN
    0x2117,  # ℗ SOUND RECORDING COPYRIGHT
    0x211E,  # ℞ PRESCRIPTION TAKE
    0x211F,  # ℟ RESPONSE
    0x2120,  # ℠ SERVICE MARK
    0x2121,  # ℡ TELEPHONE SIGN
    0x2122,  # ™ TRADE MARK SIGN
    0x2123,  # ℣ VERSICLE
    0x2125,  # ℥ OUNCE SIGN
    0x2126,  # Ω OHM SIGN
    0x212A,  # K KELVIN SIGN
    0x212B,  # Å ANGSTROM SIGN
    0x212E,  # ℮ ESTIMATED SYMBOL
    0x2132,  # Ⅎ TURNED CAPITAL F
    0x213B,  # ℻ FACSIMILE SIGN
    0x214D,  # ⅍ AKTIESELSKAB
    0x214F,  # ⅏ SYMBOL FOR SAMARITAN SOURCE
}

ANDROID_EMOJI = {
    0x2600,  # ☀ BLACK SUN WITH RAYS
    0x2601,  # ☁ CLOUD
    0x260E,  # ☎ BLACK TELEPHONE
    0x261D,  # ☝ WHITE UP POINTING INDEX
    0x263A,  # ☺ WHITE SMILING FACE
    0x2660,  # ♠ BLACK SPADE SUIT
    0x2663,  # ♣ BLACK CLUB SUIT
    0x2665,  # ♥ BLACK HEART SUIT
    0x2666,  # ♦ BLACK DIAMOND SUIT
    0x270C,  # ✌ VICTORY HAND
    0x2744,  # ❄ SNOWFLAKE
    0x2764,  # ❤ HEAVY BLACK HEART
}

# TV symbols, see https://github.com/googlefonts/noto-fonts/issues/557
TV_SYMBOLS_FOR_SUBSETTED = tool_utils.parse_int_ranges("1f19b-1f1ac 1f23b")

EMOJI = unicode_data.get_presentation_default_emoji() | ANDROID_EMOJI


def _format_set(char_set, name, filename):
    lines = ["%s = {" % name]
    for cp in sorted(char_set):
        name = unicode_data.name(cp)
        lines.append("    0x%04X,  # %s %s" % (cp, unichr(cp), name))
    lines.append("}\n")
    with codecs.open(filename, "w", "UTF-8") as f:
        f.write("\n".join(lines))
    print("wrote", filename)


def subset_symbols(srcdir, dstdir):
    """Subset Noto Sans Symbols in a curated way.

    Noto Sans Symbols is now subsetted in a curated way. Changes include:

    * Currency symbols now included in Roboto are removed.

    * All combining marks for symbols (except for combining keycap) are
      added, to combine with other symbols if needed.

    * Characters in symbol blocks that are also covered by Noto CJK fonts
      are added, for better harmony with the rest of the fonts in non-CJK
      settings. The dentistry characters at U+23BE..23CC are not added,
      since they appear to be Japan-only and full-width.

    * Characters that UTR #51 defines as default text are added, although
      they may also exist in the color emoji font, to make sure they get
      a default text style.

    * Characters that UTR #51 defines as default emoji are removed, to
      make sure they don't block the fallback to the color emoji font.

    * A few math symbols that are currently included in Roboto are added,
      to prepare for potentially removing them from Roboto when they are
      lower-quality in Roboto.

    Based on subset_noto_sans_symbols.py from AOSP external/noto-fonts."""

    # TODO see if we need to change this subset based on Noto Serif coverage
    # (so the serif fallback chain would support them)

    target_coverage = set()
    # Add all characters in BLOCKS_TO_INCLUDE
    for first, last, _ in unicode_data._parse_code_ranges(BLOCKS_TO_INCLUDE):
        target_coverage.update(range(first, last + 1))

    # Add one-off characters
    target_coverage |= ONE_OFF_ADDITIONS
    # Remove characters preferably coming from Roboto
    target_coverage -= LETTERLIKE_CHARS_IN_ROBOTO
    # Remove default emoji presentation (including ones Android prefers default)
    target_coverage -= EMOJI

    # Remove COMBINING ENCLOSING KEYCAP. It's needed for Android's color emoji
    # mechanism to work properly
    target_coverage.remove(0x20E3)

    # Remove dentistry symbols, as their main use appears to be for CJK:
    # http://www.unicode.org/L2/L2000/00098-n2195.pdf
    target_coverage -= set(range(0x23BE, 0x23CC + 1))

    for font_file in glob.glob(path.join(srcdir, "NotoSansSymbols-*.ttf")):
        print("main subset", font_file)
        out_file = path.join(dstdir, path.basename(font_file)[:-4] + "-Subsetted.ttf")
        subset.subset_font(font_file, out_file, include=target_coverage)

    # The second subset will be a fallback after the color emoji, for
    # explicit text presentation sequences.
    target_coverage = EMOJI | unicode_data.get_unicode_emoji_variants()

    for font_file in glob.glob(path.join(srcdir, "NotoSansSymbols-*.ttf")):
        print("secondary subset", font_file)
        out_file = path.join(dstdir, path.basename(font_file)[:-4] + "-Subsetted2.ttf")
        subset.subset_font(font_file, out_file, include=target_coverage)


def patch_post_table(srcdir, dstdir):
    """Replace post table version 2.0 with version 3.0"""

    # Leave alone OTF
    for font_file in glob.glob(path.join(srcdir, "*.ttf")):
        print("change post table to 3.0", font_file)
        out_file = path.join(dstdir, path.basename(font_file))
        if path.isfile(out_file):
            print("  repatching", out_file)
            font_file = out_file
        font = ttLib.TTFont(font_file)
        font["post"].formatType = 3.0
        font.save(out_file)


def patch_fonts(srcdir, dstdir):
    """Remove dstdir and repopulate with patched contents of srcdir (and
    its 'cjk' subdirectory if it exists)."""

    srcdir = tool_utils.resolve_path(srcdir)
    dstdir = tool_utils.resolve_path(dstdir)

    tool_utils.ensure_dir_exists(dstdir, clean=True)

    patch_hyphen(srcdir, dstdir)
    patch_cjk_ttcs(path.join(srcdir, "cjk"), path.join(dstdir, "cjk"))
    subset_symbols(srcdir, dstdir)
    patch_post_table(srcdir, dstdir)


def main():
    SRC_DIR = "[tools]/packages/android"
    DST_DIR = "[tools]/packages/android-patched"

    parser = argparse.ArgumentParser()
    parser.add_argument(
        "-s",
        "--srcdir",
        help="directory containing fonts to patch " "(default %s)" % SRC_DIR,
        default=SRC_DIR,
        metavar="dir",
    )
    parser.add_argument(
        "-d",
        "--dstdir",
        help="directory into which to write patched fonts " "(default %s)" % DST_DIR,
        default=DST_DIR,
        metavar="dir",
    )
    args = parser.parse_args()
    patch_fonts(args.srcdir, args.dstdir)


if __name__ == "__main__":
    main()
