#!/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.

"""Compare two cmap data files produced by lint_cmap_reqs or
   noto_font_cmaps."""

import argparse
import collections

from nototools import cmap_data
from nototools import lint_config
from nototools import tool_utils
from nototools import unicode_data


def title_from_metadata(metadata):
    items = [metadata.date, metadata.program]
    if metadata.args:
        items.extend(["%s=%s" % t for t in metadata.args])
    return " ".join(items)


CompareOptions = collections.namedtuple(
    "CompareOptions", "only_cps, except_cps, no_additions, no_removals"
)


_DEFAULT_COMPARE_OPTIONS = CompareOptions(None, None, False, False)


def compare_cmaps(base_cps, target_cps, opts=None):
    """Returns a tuple of added, removed."""
    if opts is None:
        opts = _DEFAULT_COMPARE_OPTIONS
    only_cps, except_cps, no_additions, no_removals = opts
    if only_cps:
        base_cps &= only_cps
        target_cps &= only_cps
    if except_cps:
        base_cps -= except_cps
        target_cps -= except_cps
    if base_cps != target_cps:
        added = None if no_additions else target_cps - base_cps
        removed = None if no_removals else base_cps - target_cps
    else:
        added, removed = None, None
    return added, removed


def compare_data(base_data, target_data, opts=None):
    base_cps = lint_config.parse_int_ranges(base_data.ranges)
    target_cps = lint_config.parse_int_ranges(target_data.ranges)
    added, removed = compare_cmaps(base_cps, target_cps, opts)
    base_xcps, target_xcps = set(), set()
    if int(getattr(base_data, "xcount", -1)) != -1:
        base_xcps = lint_config.parse_int_ranges(base_data.xranges)
    if int(getattr(target_data, "xcount", -1)) != -1:
        target_xcps = lint_config.parse_int_ranges(target_data.xranges)
    xadded, xremoved = compare_cmaps(base_xcps, target_xcps, opts)
    return added, removed, xadded, xremoved


def compare_cmap_data(
    base_cmap_data, target_cmap_data, scripts, except_scripts, opts=None
):
    result = {}
    base_map = cmap_data.create_map_from_table(base_cmap_data.table)
    target_map = cmap_data.create_map_from_table(target_cmap_data.table)

    for script in sorted(base_map):
        if except_scripts and script in except_scripts:
            continue
        if scripts and script not in scripts:
            continue
        if script in target_map:
            base_data = base_map[script]
            target_data = target_map[script]
            result[script] = compare_data(base_data, target_data, opts)
    return result


def compare_cmap_data_files(base_file, target_file, scripts, except_scripts, opts=None):
    base_cmap_data = cmap_data.read_cmap_data_file(base_file)
    target_cmap_data = cmap_data.read_cmap_data_file(target_file)
    compare = compare_cmap_data(
        base_cmap_data, target_cmap_data, scripts, except_scripts, opts
    )
    return compare, base_cmap_data, target_cmap_data


def compare_interscript(base_map, base_script, target_script, opts=None):
    missing_scripts = []
    if base_script not in base_map:
        missing_scripts.append(base_script)
    if target_script not in base_map:
        missing_scripts.append(target_script)
    if missing_scripts:
        print(
            "Cannot compare %s and %s, %s not in cmap data."
            % (base_script, target_script, ", ".join(missing_scripts))
        )
        return
    base_data = base_map[base_script]
    target_data = base_map[target_script]
    return compare_data(base_data, target_data, opts)


def compare_interscript_data(base_cmap_data, scripts, opts=None):
    result = {}
    base_map = cmap_data.create_map_from_table(base_cmap_data.table)
    for i in range(len(scripts) - 1):
        base_script = scripts[i]
        for j in range(i + 1, len(scripts)):
            target_script = scripts[j]
            result[base_script, target_script] = compare_interscript(
                base_map, base_script, target_script, opts
            )
    return result


def compare_interscript_data_file(base_file, scripts, opts=None):
    base_cmap_data = cmap_data.read_cmap_data_file(base_file)
    compare = compare_interscript_data(base_cmap_data, scripts, opts)
    return compare, base_cmap_data


def _print_detailed(cps, inverted_target=None):
    last_block = None
    undefined_start = -1
    undefined_end = -1

    def show_undefined(start, end):
        if start >= 0:
            if end > start:
                print(
                    "      %04x-%04x Zzzz <%d undefined>"
                    % (start, end, end - start - 1)
                )
            else:
                print("      %04x Zzzz <1 undefined>" % start)

    for cp in sorted(cps):
        block = unicode_data.block(cp)
        if block != last_block or (undefined_end > -1 and cp > undefined_end + 1):
            show_undefined(undefined_start, undefined_end)
            undefined_start, undefined_end = -1, -1
            if block != last_block:
                print("    %s" % block)
                last_block = block
        script = unicode_data.script(cp)
        if script == "Zzzz":
            if undefined_start >= 0:
                undefined_end = cp
            else:
                undefined_start, undefined_end = cp, cp
            continue

        show_undefined(undefined_start, undefined_end)
        undefined_start, undefined_end = -1, -1
        extensions = unicode_data.script_extensions(cp) - {script}
        if extensions:
            extensions = " (script %s)" % ", ".join(sorted(extensions))
        else:
            extensions = ""
        if not inverted_target:
            extra = ""
        elif cp not in inverted_target:
            extra = " !missing"
        else:
            scripts = sorted(inverted_target[cp])
            if len(scripts) > 3:
                script_text = ", ".join(scripts[:3]) + "... " + scripts[-1]
            else:
                script_text = ", ".join(scripts)
            extra = " (font %s)" % script_text
        print(
            "    %6s %4s %2s %3s %s%s%s"
            % (
                "%04x" % cp,
                script,
                unicode_data.category(cp),
                unicode_data.age(cp),
                unicode_data.name(cp, ""),
                extensions,
                extra,
            )
        )
    show_undefined(undefined_start, undefined_end)


def report_cmap_compare(
    label,
    added,
    removed,
    xadded,
    xremoved,
    inverted_target=None,
    detailed=True,
    report_same=False,
):
    def report_cps(label, cps, inverted=None):
        if not cps:
            return
        print("  %s (%d): %s" % (label, len(cps), lint_config.write_int_ranges(cps)))
        if detailed:
            _print_detailed(cps, inverted)

    if report_same:
        print(label)
    if added or removed or xadded or xremoved:
        if not report_same:
            print(label)
        removed_to_fallback = removed & xadded if removed and xadded else None
        if removed_to_fallback:
            removed -= removed_to_fallback
            xadded -= removed_to_fallback
        added_from_fallback = added & xremoved if added and xremoved else None
        if added_from_fallback:
            added -= added_from_fallback
            xremoved -= added_from_fallback
        report_cps("added", added)
        report_cps("added, no longer fallback", added_from_fallback)
        report_cps("removed", removed, inverted_target)
        report_cps("removed, now in fallback", removed_to_fallback, inverted_target)
        report_cps("added to fallback", xadded, inverted_target)
        report_cps("removed from fallback", xremoved)


def report_compare(compare_result, detailed=True):
    compare, base_cmap_data, target_cmap_data = compare_result
    base_map = cmap_data.create_map_from_table(base_cmap_data.table)
    target_map = cmap_data.create_map_from_table(target_cmap_data.table)

    inverted_target = collections.defaultdict(set)
    for script, row in target_map.items():
        cps = tool_utils.parse_int_ranges(row.ranges)
        for cp in cps:
            inverted_target[cp].add(script)

    base_title = title_from_metadata(base_cmap_data.meta)
    target_title = title_from_metadata(target_cmap_data.meta)

    print("base: %s" % base_title)
    print("target: %s" % target_title)
    for script in sorted(compare):
        added, removed, xadded, xremoved = compare[script]
        label = "%s # %s" % (script, base_map[script].name)
        report_cmap_compare(
            label, added, removed, xadded, xremoved, inverted_target, detailed
        )


def report_interscript_compare(compare_result, detailed=True):
    compare, base_cmap_data = compare_result
    base_map = cmap_data.create_map_from_table(base_cmap_data.table)
    title = title_from_metadata(base_cmap_data.meta)
    print("data: %s" % title)
    for t in sorted(compare):
        added, removed, xadded, xremoved = compare[t]
        base_script, target_script = t
        label = "base: %s (%s), target: %s (%s)" % (
            base_script,
            base_map[base_script].name,
            target_script,
            base_map[target_script].name,
        )
        report_cmap_compare(label, added, removed, xadded, xremoved, None, detailed)


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument(
        "-b", "--base", help="base cmap data file", metavar="file", required=True
    )
    parser.add_argument("-t", "--target", help="target cmap data file", metavar="file")
    parser.add_argument(
        "-s",
        "--scripts",
        help="only these scripts (except ignored)",
        metavar="code",
        nargs="+",
    )
    parser.add_argument(
        "-r",
        "--ranges",
        help="only these ranges (except ignored)",
        metavar="ranges",
        nargs="+",
    )
    parser.add_argument(
        "-xs",
        "--except_scripts",
        help="ignore these scripts",
        metavar="code",
        nargs="+",
    )
    parser.add_argument(
        "-xr",
        "--except_ranges",
        help="ignore these codepoints",
        metavar="ranges",
        nargs="+",
    )
    parser.add_argument(
        "-na",
        "--no_additions",
        help="do not report additions (in target " "but not base)",
        action="store_true",
    )
    parser.add_argument(
        "-nr",
        "--no_removals",
        help="do not report removals (in base " "but not target)",
        action="store_true",
    )
    parser.add_argument("-d", "--detailed", help="report details", action="store_true")

    args = parser.parse_args()
    if args.scripts:
        scripts = set(args.scripts)
    else:
        scripts = None

    if args.ranges:
        cps = lint_config.parse_int_ranges(" ".join(args.ranges))
    else:
        cps = None

    if args.except_ranges:
        except_cps = lint_config.parse_int_ranges(" ".join(args.except_ranges))
    else:
        except_cps = None

    if args.except_scripts:
        except_scripts = set(args.except_scripts)
    else:
        except_scripts = None

    opts = CompareOptions(cps, except_cps, args.no_additions, args.no_removals)

    if not args.target:
        if not scripts or len(scripts) < 2:
            print("Interscript comparison requires two or more scripts.")
            return
        result = compare_interscript_data_file(args.base, args.scripts, opts)
        report_interscript_compare(result, detailed=args.detailed)
        return

    result = compare_cmap_data_files(
        args.base, args.target, scripts, except_scripts, opts
    )
    report_compare(result, detailed=args.detailed)


if __name__ == "__main__":
    main()
