#!/usr/bin/env python3
# Copyright 2018 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.
#
"""Utility to setup a font for addition to Piper.


Generate METADATA.pb files for font families.

METADATA.pb files are used to serve the families on http://fonts.google.com.
Font families are stored in this repo by license type. The following
directories contain font families:

../fonts/ofl
../fonts/apache
../fonts/ufl

Generating a METADATA.pb file for a new family:

1. Determine the family's license type, ofl, ufl or apache
2. Create a new folder under the license type directory
3. Name the folder so it's the family name, all lowercase and no spaces.
4. Run the following: gftools add-font /path/to/new/family
5. Update the category field in the generated METADATA.pb file.

Generating a METADATA.pb file for an existing family:

1. run the following: gftools add-font /path/to/existing/family
"""

from __future__ import print_function
from functools import cmp_to_key
import argparse
import contextlib
import errno
import glob
import re
import os
import sys
import time
from fontTools import ttLib


from gflanguages import LoadLanguages
from gfmetadata import FamilyProto
from gftools.util import google_fonts as fonts
from gftools.utils import cmp
from axisregistry import AxisRegistry
from gfsubsets import SubsetsInFont
from gfmetadata import text_format
from gftools.utils import remove_url_prefix, primary_script

RELAXED_SUBSETS = ["math", "symbols"]

axis_registry = AxisRegistry()


parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
    "--min_pct",
    type=int,
    default=50,
    help="What percentage of subset codepoints have to be supported"
    " for a non-ext subset.",
)
parser.add_argument(
    "--min_pct_ext",
    type=float,
    default=2,
    help="What percentage of subset codepoints have to be supported"
    " for a -ext subset.",
)
parser.add_argument(
    "--min_relaxed_pct",
    type=int,
    default=50,
    help="What percentage of subset codepoints have to be supported"
    f' for a relaxed subset ({", ".join(RELAXED_SUBSETS)}).',
)
parser.add_argument(
    "--lang", type=str, help="Path to lang metadata package", default=None
)
parser.add_argument("directory", type=str, help="A directory containing a font family")
parser.add_argument(
    "--github_url",
    type=str,
    default=None,
    help="The font family's github url which gets written to new description files",
)


def _FileFamilyStyleWeights(fontdir):
    """Extracts file, family, style, weight 4-tuples for each font in dir.

    Args:
      fontdir: Directory that supposedly contains font files for a family.
    Returns:
      List of fonts.FileFamilyStyleWeightTuple ordered by weight, style
      (normal first).
    Raises:
      OSError: If the font directory doesn't exist (errno.ENOTDIR) or has no font
      files (errno.ENOENT) in it.
      RuntimeError: If the font directory appears to contain files from multiple
      families.
    """
    if not os.path.isdir(fontdir):
        raise OSError(errno.ENOTDIR, "No such directory", fontdir)

    files = glob.glob(os.path.join(fontdir, "*.[ot]tf"))
    if not files:
        raise OSError(errno.ENOENT, "no font files found")

    result = [fonts.FamilyStyleWeight(f) for f in files]

    def _Cmp(r1, r2):
        return cmp(r1.weight, r2.weight) or -cmp(r1.style, r2.style)

    result = sorted(result, key=cmp_to_key(_Cmp))

    family_names = {i.family for i in result}
    if len(family_names) > 1:
        raise RuntimeError("Ambiguous family name; possibilities: %s" % family_names)

    return result


def _MakeMetadata(args, is_new):
    """Builds a dictionary matching a METADATA.pb file.

    Args:
      fontdir: Directory containing font files for which we want metadata.
      is_new: Whether this is an existing or new family.
    Returns:
      A FamilyProto message, the METADATA.pb structure.
    Raises:
      RuntimeError: If the variable font axes info differs between font files of
      same family.
    """
    file_family_style_weights = _FileFamilyStyleWeights(args.directory)

    first_file = file_family_style_weights[0].file
    old_metadata_file = os.path.join(args.directory, "METADATA.pb")

    metadata = FamilyProto()
    metadata.name = file_family_style_weights[0].family

    subsets_in_font = [
        s[0] for s in SubsetsInFont(first_file, args.min_pct, args.min_pct_ext)
    ]

    relaxed_subsets = set(RELAXED_SUBSETS) & set(
        [
            s[0]
            for s in SubsetsInFont(
                first_file, args.min_relaxed_pct, args.min_relaxed_pct
            )
        ]
    )

    subsets_in_font = list(set(subsets_in_font) | relaxed_subsets)

    if not is_new:
        old_metadata = fonts.ReadProto(FamilyProto(), old_metadata_file)
        metadata.designer = old_metadata.designer
        metadata.category[:] = old_metadata.category
        metadata.date_added = old_metadata.date_added
        subsets = set(old_metadata.subsets) | set(subsets_in_font)
        metadata.languages[:] = old_metadata.languages
        metadata.fallbacks.extend(old_metadata.fallbacks)
        if old_metadata.classifications:
            metadata.classifications[:] = old_metadata.classifications
        if old_metadata.stroke:
            metadata.stroke = old_metadata.stroke
        if old_metadata.is_noto:
            metadata.is_noto = True
        if old_metadata.display_name:
            metadata.display_name = old_metadata.display_name
        if old_metadata.primary_script:
            metadata.primary_script = old_metadata.primary_script
        if old_metadata.sample_text and old_metadata.sample_text.ByteSize():
            metadata.sample_text.CopyFrom(old_metadata.sample_text)
        if old_metadata.minisite_url:
            metadata.minisite_url = old_metadata.minisite_url
        if old_metadata.registry_default_overrides:
            metadata.registry_default_overrides.update(
                old_metadata.registry_default_overrides
            )
        if old_metadata.source:
            metadata.source.CopyFrom(old_metadata.source)
    else:
        metadata.designer = "UNKNOWN"
        metadata.category.append("SANS_SERIF")
        metadata.date_added = time.strftime("%Y-%m-%d")
        subsets = ["menu"] + subsets_in_font
        with ttLib.TTFont(file_family_style_weights[0][0]) as ttfont:
            script = primary_script(ttfont)
            if script is not None and script not in (
                "Latn",
                "Cyrl",
                "Grek",
            ):
                metadata.primary_script = script

    try:
        font_license = fonts.LicenseFromPath(args.directory)
        metadata.license = font_license
    except ValueError:
        print("WARNING: Could not determine license for %s" % args.directory)
        print("Continuing without setting license.")
    subsets = sorted(subsets)
    for subset in subsets:
        metadata.subsets.append(subset)

    for fontfile, family, style, weight in file_family_style_weights:
        filename = os.path.basename(fontfile)
        font_psname = fonts.ExtractName(
            fontfile, fonts.NAME_PSNAME, os.path.splitext(filename)[0]
        )
        font_copyright = fonts.ExtractName(
            fontfile, fonts.NAME_COPYRIGHT, "???."
        ).strip()

        font_metadata = metadata.fonts.add()
        font_metadata.name = family
        font_metadata.style = style
        font_metadata.weight = weight
        font_metadata.filename = filename
        font_metadata.post_script_name = font_psname
        default_fullname = os.path.splitext(filename)[0].replace("-", " ")
        font_metadata.full_name = fonts.ExtractName(
            fontfile, fonts.NAME_FULLNAME, default_fullname
        )
        font_metadata.copyright = font_copyright

    axes_info_from_font_files = {_AxisInfo(f.file) for f in file_family_style_weights}
    if len(axes_info_from_font_files) != 1:
        raise RuntimeError("Variable axes info not matching between font files")

    for axes_info in axes_info_from_font_files:
        if axes_info:
            for axes in axes_info:
                var_axes = metadata.axes.add()
                var_axes.tag = axes[0]
                var_axes.min_value = axes[1]
                var_axes.max_value = axes[2]

    registry_overrides = _RegistryOverrides(axes_info_from_font_files)
    if registry_overrides:
        for k, v in registry_overrides.items():
            metadata.registry_default_overrides[k] = v
    return metadata


def _RegistryOverrides(axes_info):
    """Get registry default value overrides for family axes.

    Args:
      axes_info: set of Variable axes info

    Returns:
      A dict structured {axis_tag: font_axis_default_value}
    """
    res = {}
    for font in axes_info:
        for axis_tag, min_val, max_val, dflt_val in font:
            if axis_tag not in axis_registry:
                continue
            default_val = axis_registry[axis_tag].default_value
            if default_val >= min_val and default_val <= max_val:
                continue
            if axis_tag not in res:
                res[axis_tag] = dflt_val
            else:
                res[axis_tag] = min(res[axis_tag], dflt_val)
    return res


def _AxisInfo(fontfile):
    """Gets variable axes info.

    Args:
      fontfile: Font file to look at for variation info

    Returns:
      Variable axes info
    """
    with contextlib.closing(ttLib.TTFont(fontfile)) as font:
        if "fvar" not in font:
            return frozenset()
        else:
            fvar = font["fvar"]
            axis_info = [
                (a.axisTag, a.minValue, a.maxValue, a.defaultValue) for a in fvar.axes
            ]
            return tuple(sorted(axis_info))


def _GetAvgSize(file_family_style_weights):
    """Gets average file size of all font weights.

    Returns:
         average file size.

    Args:
      file_family_style_weights: List of fonts.FileFamilyStyleWeightTuple.
    """
    total_size = 0
    for list_tuple in file_family_style_weights:
        total_size += os.stat(list_tuple.file).st_size
    return total_size / len(file_family_style_weights)


def _WriteTextFile(filename, text):
    """Write text to file.

    Nop if file exists with that exact content. This allows running against files
    that are in Piper and not marked for editing; you will get an error only if
    something changed.

    Args:
      filename: The file to write.
      text: The content to write to the file.
    """
    if os.path.isfile(filename):
        with open(filename, "r") as f:
            current = f.read()
        if current == text:
            print("No change to %s" % filename)
            return

    with open(filename, "w") as f:
        f.write(text)
    print("Wrote %s" % filename)


def _AddHumanReadableDateComment(text_proto):
    return re.sub(
        r"(date_added: \d+)", r"\1  # " + time.strftime("%Y-%m-%d"), text_proto
    )


def main(args=None):
    args = parser.parse_args(args)
    is_new = True
    fontdir = args.directory
    old_metadata_file = os.path.join(fontdir, "METADATA.pb")
    if os.path.isfile(old_metadata_file):
        is_new = False

    language_comments = fonts.LanguageComments(LoadLanguages(base_dir=args.lang))
    metadata = _MakeMetadata(args, is_new)
    fonts.WriteMetadata(
        metadata, os.path.join(fontdir, "METADATA.pb"), comments=language_comments
    )

    desc = os.path.join(fontdir, "DESCRIPTION.en_us.html")
    articledir = os.path.join(fontdir, "article")
    article = os.path.join(articledir, "ARTICLE.en_us.html")
    if os.path.isfile(desc):
        print("DESCRIPTION.en_us.html exists")
    elif os.path.isfile(article):
        print("ARTICLE.en_us.html exists")
    else:
        os.makedirs(os.path.join(fontdir, "article"))
        desc_text = "N/A"
        if args.github_url:
            human_url = remove_url_prefix(args.github_url)
            desc_text += f'\n<p>To contribute, please see <a href="{args.github_url}">{human_url}</a>.</p>'
        _WriteTextFile(article, desc_text)


if __name__ == "__main__":
    main()
