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

import argparse
import collections
import datetime
from os import path

from nototools import cmap_data
from nototools import coverage
from nototools import tool_utils

from xml.etree import ElementTree as ET

"""Generate a coverage.xml file listing the codepoints covered by
a font family plus a name."""

MetaData = collections.namedtuple("MetaData", "date,program,args")
CmapData = collections.namedtuple("CmapData", "name,ranges")
CoverageData = collections.namedtuple("CoverageData", "metadata,cmapdata")


def get_cps_from_files(paths):
    """Return a tuple of the cps and the paths that we actually read."""
    cps = set()
    new_paths = set()
    for f in paths:
        ext = path.splitext(f)[1]
        if ext not in [".otf", ".ttf"]:
            continue
        cps |= coverage.character_set(f)
        new_paths.add(f)
    return cps, sorted(new_paths)


def get_cps_from_cmap_data_file(data_file):
    cps = set()
    data = cmap_data.read_cmap_data_file(data_file)
    for row in data.table.rows:
        cps |= tool_utils.parse_int_ranges(row.ranges)
    return cps


def _create_metadata(**kwargs):
    """Create a MetaData object from the args.  'date' defaults to today's
    date."""
    date = str(kwargs.pop("date", datetime.date.today()))
    program = str(kwargs.pop("program", "generate_coverage_data"))
    arglist = [(k, v) for k, v in sorted(kwargs.items()) if v is not None]
    return MetaData(date, program, arglist)


def _create_cmapdata(name, cmap):
    """Create a CmapData object from the name and cmap."""
    return CmapData(name, tool_utils.write_int_ranges(cmap))


def create(name, cps, paths=None, cmap_data=None):
    """Generate the coverage data object."""
    metadata = _create_metadata(paths=paths, cmap_data=cmap_data)
    cmapdata = _create_cmapdata(name, cps)
    return CoverageData(metadata, cmapdata)


def _common_path_prefix(items):
    """Assuming items is an array of paths using path.sep as a path separator,
    return a common path prefix of the items."""
    prefix = None
    if len(items) <= 1:
        return ""
    for item in items:
        if prefix is None:
            last = item.rfind(path.sep)
            if last < 0:
                return ""
            prefix = item[: last + 1]
            continue
        end = len(item)
        while True:
            last = item.rfind(path.sep, 0, end)
            if last < 0:
                return ""
            item_prefix = item[: last + 1]
            if prefix == item_prefix:
                break
            if prefix.startswith(item_prefix):
                prefix = item_prefix
                break
            end = last
    return prefix


def _build_meta_elem(metadata):
    """Convert metadata to ET node."""
    meta = ET.Element("meta", date=metadata.date, program=metadata.program)
    if metadata.args:
        args = ET.Element("args")
        for k, v in metadata.args:
            if isinstance(v, list):
                arg = ET.Element(k, isList="true")
                # handle items that are path names
                prefix = _common_path_prefix(v)
                plen = 0
                if prefix:
                    arg.set("prefix", prefix)
                    plen = len(prefix)
                for item in v:
                    item_elem = ET.Element("item")
                    item_elem.text = str(item)[plen:]
                    arg.append(item_elem)
            else:
                arg = ET.Element(str(k), val=str(v))
            args.append(arg)
        meta.append(args)
    return meta


def _build_cmap_elem(cmapdata):
    """Convert data to ET node."""
    data = ET.Element("data")
    name = ET.Element("name")
    name.text = cmapdata.name
    data.append(name)
    ranges = ET.Element("ranges")
    ranges.text = cmapdata.ranges
    data.append(ranges)
    return data


def _prettify(root, indent=""):
    """Pretty-print the root element if it has no text and children
    by adding to the root text and each child's tail."""
    if not root.text and len(root):
        indent += "  "
        sfx = "\n" + indent
        root.text = sfx
        for elem in root:
            elem.tail = sfx
            _prettify(elem, indent)
        elem.tail = sfx[:-2]


def _build_tree(coveragedata):
    root = ET.Element("coveragedata")
    root.append(_build_meta_elem(coveragedata.metadata))
    root.append(_build_cmap_elem(coveragedata.cmapdata))
    _prettify(root)
    return ET.ElementTree(element=root)


def write(coveragedata, out_file=None):
    """Write coverage data to xml."""
    tree = _build_tree(coveragedata)
    if out_file:
        tree.write(out_file, encoding="utf-8", xml_declaration=True)
    else:
        print(ET.tostring(tree.getroot(), encoding="utf-8"))


def _read_meta(meta_elem):
    date = meta_elem.get("date")
    program = meta_elem.get("program")
    args = []
    args_node = meta_elem.find("args")
    if args_node is not None:
        for arg_node in args_node:
            key = arg_node.tag
            if arg_node.attrib.get("isList", False):
                prefix = arg_node.attrib.get("prefix", "")
                val = []
                for item in arg_node.findall("item"):
                    val.append(prefix + item.text)
            else:
                val = arg_node.get("val").strip()
            args.append((key, val))
    return MetaData(date, program, args)


def _read_data(data_elem):
    name = data_elem.find("name").text.strip()
    ranges = data_elem.find("ranges").text.strip()
    return CmapData(name, ranges)


def _read_tree(root_elem):
    meta = _read_meta(root_elem.find("meta"))
    data = _read_data(root_elem.find("data"))
    return CoverageData(meta, data)


def read(coveragefile):
    return _read_tree(ET.parse(coveragefile).getroot())


def main():
    default_coverage_file = "[tools]/nototools/data/noto_cmap_phase3.xml"

    parser = argparse.ArgumentParser()
    parser.add_argument(
        "-o", "--output_file", help="name of xml file to output", metavar="file"
    )
    parser.add_argument(
        "-d",
        "--dirs",
        help="directories containing font files",
        metavar="dir",
        nargs="+",
    )
    parser.add_argument("-f", "--files", help="font files", metavar="file", nargs="+")
    parser.add_argument(
        "-n",
        "--name",
        help="short name of this collection, used in reports",
        metavar="name",
        required=True,
    )
    parser.add_argument(
        "-c",
        "--cmap_data",
        help="cmap data file (default %s)" % default_coverage_file,
        const=default_coverage_file,
        nargs="?",
        metavar="file",
    )
    args = parser.parse_args()

    cmap_path = None
    if args.dirs or args.files:
        paths = tool_utils.collect_paths(args.dirs, args.files)
        cps, paths = get_cps_from_files(paths)
    elif args.cmap_data:
        cmap_path = tool_utils.resolve_path(args.cmap_data)
        cps = get_cps_from_cmap_data_file(cmap_path)
        paths = None
    else:
        print("Please specify font files, directories, or a cmap data file.")
        return
    coverage = create(args.name, cps, paths=paths, cmap_data=cmap_path)
    write(coverage, args.output_file)


if __name__ == "__main__":
    main()
