#!/usr/bin/env python
#
# Copyright 2014 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.

"""Fix some issues in Noto fonts before releasing them."""

__author__ = "roozbeh@google.com (Roozbeh Pournader)"

import argparse
import array
import os
from os import path
import re

from fontTools import ttLib

from nototools import font_data
from nototools import notoconfig


NOTO_URL = "http://www.google.com/get/noto/"

_LICENSE_ID = 13
_LICENSE_URL_ID = 14

_SIL_LICENSE = (
    "This Font Software is licensed under the SIL Open Font License, "
    'Version 1.1. This Font Software is distributed on an "AS IS" '
    "BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express "
    "or implied. See the SIL Open Font License for the specific language, "
    "permissions and limitations governing your use of this Font Software."
)

_SIL_LICENSE_URL = "http://scripts.sil.org/OFL"


def fix_revision(font):
    """Fix the revision of the font to match its version."""
    version = font_data.font_version(font)
    match = re.match(r"Version (\d{1,5})\.(\d{1,5})", version)
    major_version = match.group(1)
    minor_version = match.group(2)

    accuracy = len(minor_version)
    font_revision = font_data.printable_font_revision(font, accuracy)
    expected_font_revision = major_version + "." + minor_version
    if font_revision != expected_font_revision:
        font["head"].fontRevision = float(expected_font_revision)
        print("Fixed fontRevision to %s" % expected_font_revision)
        return True

    return False


def fix_fstype(font):
    """Fix the fsType of the font."""
    if font["OS/2"].fsType != 0:
        font["OS/2"].fsType = 0
        print("Updated fsType to 0")
        return True
    return False


def fix_vendor_id(font):
    """Fix the vendor ID of the font."""
    if font["OS/2"].achVendID != "GOOG":
        font["OS/2"].achVendID = "GOOG"
        print("Changed font vendor ID to GOOG")
        return True
    return False


# Reversed name records in Khmer and Lao fonts
NAME_CORRECTIONS = {
    "Sans Kufi": "Kufi",
    "SansKufi": "Kufi",
    "UI Khmer": "Khmer UI",
    "UIKhmer": "KhmerUI",
    "UI Lao": "Lao UI",
    "UILao": "LaoUI",
    "SansEmoji": "Emoji",
    "Sans Emoji": "Emoji",
}

TRADEMARK_TEMPLATE = "%s is a trademark of Google Inc."


def fix_name_table(font):
    """Fix copyright and reversed values in the 'name' table."""
    modified = False
    name_records = font_data.get_name_records(font)

    copyright_data = name_records[0]
    years = re.findall("20[0-9][0-9]", copyright_data)
    year = min(years)
    copyright_data = "Copyright %s Google Inc. All Rights Reserved." % year

    if copyright_data != name_records[0]:
        print('Updated copyright message to "%s"' % copyright_data)
        font_data.set_name_record(font, 0, copyright_data)
        modified = True

    for name_id in [1, 3, 4, 6]:
        record = name_records[name_id]
        for source in NAME_CORRECTIONS:
            if source in record:
                oldrecord = record
                record = record.replace(source, NAME_CORRECTIONS[source])
                break
        if record != name_records[name_id]:
            font_data.set_name_record(font, name_id, record)
            print(
                'Updated name table record #%d from "%s" to "%s"'
                % (name_id, oldrecord, record)
            )
            modified = True

    trademark_names = ["Noto", "Arimo", "Tinos", "Cousine"]
    trademark_name = None
    font_family = name_records[1]
    for name in trademark_names:
        if font_family.find(name) != -1:
            trademark_name = name
            break
    if not trademark_name:
        print("no trademarked name in '%s'" % font_family)
    else:
        trademark_line = TRADEMARK_TEMPLATE % trademark_name
        if name_records[7] != trademark_line:
            old_line = name_records[7]
            font_data.set_name_record(font, 7, trademark_line)
            modified = True
            print(
                'Updated name table record 7 from "%s" to "%s"'
                % (old_line, trademark_line)
            )

    if name_records[11] != NOTO_URL:
        font_data.set_name_record(font, 11, NOTO_URL)
        modified = True
        print('Updated name table record 11 to "%s"' % NOTO_URL)

    if name_records[_LICENSE_ID] != _SIL_LICENSE:
        font_data.set_name_record(font, _LICENSE_ID, _SIL_LICENSE)
        modified = True
        print("Updated license id")

    if name_records[_LICENSE_URL_ID] != _SIL_LICENSE_URL:
        font_data.set_name_record(font, _LICENSE_URL_ID, _SIL_LICENSE_URL)
        modified = True
        print("Updated license url")

    # TODO: check preferred family/subfamily(16&17)

    return modified


def fix_attachlist(font):
    """Fix duplicate attachment points in GDEF table."""
    modified = False
    try:
        attach_points = font["GDEF"].table.AttachList.AttachPoint
    except (KeyError, AttributeError):
        attach_points = []

    for attach_point in attach_points:
        points = sorted(set(attach_point.PointIndex))
        if points != attach_point.PointIndex:
            attach_point.PointIndex = points
            attach_point.PointCount = len(points)
            modified = True

    if modified:
        print("Fixed GDEF.AttachList")

    return modified


def drop_hints(font):
    """Drops a font's hint."""
    modified = False
    glyf_table = font["glyf"]
    for glyph_index in range(len(glyf_table.glyphOrder)):
        glyph_name = glyf_table.glyphOrder[glyph_index]
        glyph = glyf_table[glyph_name]
        if glyph.numberOfContours > 0:
            if glyph.program.bytecode:
                glyph.program.bytecode = array.array("B")
                modified = True
                print('Dropped hints from glyph "%s"' % glyph_name)
    return modified


def drop_tables(font, tables):
    """Drops the listed tables from a font."""
    modified = False
    for table in tables:
        if table in font:
            modified = True
            print('Dropped table "%s"' % table)
            modified = True
            del font[table]
    return modified


TABLES_TO_DROP = [
    # FontForge internal tables
    "FFTM",
    "PfEd",
    # Microsoft VOLT internatl tables
    "TSI0",
    "TSI1",
    "TSI2",
    "TSI3",
    "TSI5",
    "TSID",
    "TSIP",
    "TSIS",
    "TSIV",
]


def fix_path(file_path, is_hinted):
    file_path = re.sub(r"_(?:un)?hinted", "", file_path)
    if "hinted/" in file_path:
        # '==' is higher precedence than 'in'
        if ("unhinted/" in file_path) == is_hinted:
            if is_hinted:
                file_path = file_path.replace("unhinted/", "hinted/")
            else:
                file_path = file_path.replace("hinted/", "unhinted/")
    else:
        file_path = os.path.join("hinted" if is_hinted else "unhinted", file_path)

    # fix Naskh, assume Arabic if unspecified
    file_path = re.sub(r"NotoNaskh(-|UI-)", r"NotoNaskhArabic\1", file_path)

    # fix SansEmoji
    file_path = re.sub("NotoSansEmoji", "NotoEmoji", file_path)

    # fix Nastaliq
    file_path = re.sub("Nastaliq-", "NastaliqUrdu-", file_path)

    return file_path


def fix_os2_unicoderange(font):
    os2_bitmap = font_data.get_os2_unicoderange_bitmap(font)
    expected_bitmap = font_data.get_cmap_unicoderange_bitmap(font)
    if os2_bitmap != expected_bitmap:
        old_bitmap_string = font_data.unicoderange_bitmap_to_string(os2_bitmap)
        font_data.set_os2_unicoderange_bitmap(font, expected_bitmap)
        bitmap_string = font_data.unicoderange_bitmap_to_string(expected_bitmap)
        print(
            "Change unicoderanges from:\n  %s\nto:\n  %s"
            % (old_bitmap_string, bitmap_string)
        )
        return True
    return False


def fix_linegap(font):
    modified = False
    hhea_table = font["hhea"]
    if hhea_table.lineGap != 0:
        print("hhea lineGap was %s, setting to 0" % hhea_table.lineGap)
        hhea_table.lineGap = 0
        modified = True
    vhea_table = font.get("vhea")
    if vhea_table and vhea_table.lineGap != 0:
        print("vhea lineGap was %s, setting to 0" % vhea_table.lineGap)
        vhea_table.lineGap = 0
        modified = True
    os2_table = font["OS/2"]
    if os2_table.sTypoLineGap != 0:
        print("os/2 sTypoLineGap was %d, setting to 0" % os2_table.sTypoLineGap)
        os2_table.sTypoLineGap = 0
        modified = True
    return modified


def fix_font(src_root, dst_root, file_path, is_hinted, save_unmodified):
    """Fix font under src_root and write to similar path under dst_root, modulo
    fixes to the filename.  If is_hinted is false, strip hints.  If unmodified,
    don't write destination unless save_unmodified is true."""

    src_file = os.path.join(src_root, file_path)

    print("Font file: %s" % src_file)
    font = ttLib.TTFont(src_file)
    modified = False

    modified |= fix_revision(font)
    modified |= fix_fstype(font)
    modified |= fix_vendor_id(font)
    modified |= fix_name_table(font)
    modified |= fix_attachlist(font)
    modified |= fix_os2_unicoderange(font)
    # leave line gap for non-noto fonts alone, metrics are more constrained there
    if font_data.font_name(font).find("Noto") != -1:
        modified |= fix_linegap(font)

    tables_to_drop = TABLES_TO_DROP
    if not is_hinted:
        modified |= drop_hints(font)
        tables_to_drop += ["fpgm", "prep", "cvt"]

    modified |= drop_tables(font, tables_to_drop)

    fixed_path = fix_path(file_path, is_hinted)
    if fixed_path != file_path:
        print('changed file_path from "%s" to "%s"' % (file_path, fixed_path))
        modified = True

    if not modified:
        print("No modification necessary")
    if modified or save_unmodified:
        # wait until we need it before we create the dest directory
        dst_file = os.path.join(dst_root, fixed_path)
        dst_dir = path.dirname(dst_file)
        if not path.isdir(dst_dir):
            os.makedirs(dst_dir)
        font.save(dst_file)
        print("Wrote %s" % dst_file)


def fix_fonts(src_root, dst_root, name_pat, save_unmodified):
    src_root = path.abspath(src_root)
    dst_root = path.abspath(dst_root)
    name_rx = re.compile(name_pat)
    for root, dirs, files in os.walk(src_root):
        for file in files:
            if path.splitext(file)[1] not in [".ttf", ".ttc", ".otf"]:
                continue
            src_file = path.join(root, file)
            file_path = src_file[len(src_root) + 1 :]  # +1 to ensure no leading slash.
            if not name_rx.search(file_path):
                continue
            is_hinted = root.endswith("/hinted") or "_hinted" in file
            fix_font(src_root, dst_root, file_path, is_hinted, save_unmodified)


def main():
    default_src_root = notoconfig.values.get("alpha")
    default_dst_root = notoconfig.values.get("autofix")

    parser = argparse.ArgumentParser()
    parser.add_argument(
        "name_pat",
        help="regex for files to fix, " "searches relative path from src root",
    )
    parser.add_argument(
        "--src_root",
        help="root of src files (default %s)" % default_src_root,
        default=default_src_root,
    )
    parser.add_argument(
        "--dst_root",
        help="root of destination (default %s)" % default_dst_root,
        default=default_dst_root,
    )
    parser.add_argument(
        "--save_unmodified", help="save even unmodified files", action="store_true"
    )
    args = parser.parse_args()

    if not args.src_root:
        # not on command line and not in user's .notoconfig
        print("no src root specified.")
        return

    src_root = path.expanduser(args.src_root)
    if not path.isdir(src_root):
        print("%s does not exist or is not a directory" % src_root)
        return

    dst_root = path.expanduser(args.dst_root)
    if not path.isdir(dst_root):
        print("%s does not exist or is not a directory" % dst_root)
        return

    fix_fonts(src_root, dst_root, args.name_pat, args.save_unmodified)


if __name__ == "__main__":
    main()
