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

"""Compare summaries of ttf files in two noto file trees"""

__author__ = "dougfelt@google.com (Doug Felt)"

import argparse
import filecmp
import os
import os.path

from nototools import noto_lint
from nototools import summary
from nototools import tool_utils


def cmp(x, y):
    """
    Replacement for built-in function cmp that was removed in Python 3

    Compare the two objects x and y and return an integer according to
    the outcome. The return value is negative if x < y, zero if x == y
    and strictly positive if x > y.
    """

    return (x > y) - (x < y)


def summary_to_map(summary_list):
    result = {}
    for tuple in summary_list:
        key = tuple[0]
        result[key] = tuple
    return result


def get_key_lists(base_map, target_map, base_root, target_root):
    added = []
    removed = []
    shared = []
    identical = []
    for k in sorted(base_map):
        target = target_map.get(k)
        if not target:
            removed.append(k)
        elif filecmp.cmp(os.path.join(base_root, k), os.path.join(target_root, k)):
            identical.append(k)
        else:
            shared.append(k)
    for k in sorted(target_map):
        if not base_map.get(k):
            added.append(k)
    return added, removed, shared, identical


def print_keys(key_list):
    for k in key_list:
        print("  " + k)


def compare_table_info(base_info, target_info):
    biggest_deltas = []
    others = []  # checksum changes
    added = []
    removed = []

    for k in target_info:
        b_tup = base_info.get(k)
        t_tup = target_info.get(k)
        if not b_tup:
            added.append((k, t_tup[0]))
        else:
            b_len = b_tup[0]
            t_len = t_tup[0]
            delta = t_len - b_len
            if delta == 0:
                if b_tup[1] != t_tup[1]:
                    others.append(k)
                continue
            biggest_deltas.append((k, delta))

    for k in base_info:
        if not target_info.get(k):
            removed.append(k)

    biggest_deltas.sort(
        lambda lhs, rhs: -cmp(abs(lhs[1]), abs(rhs[1])) or cmp(lhs[0], rhs[0])
    )
    del biggest_deltas[5:]

    result = []
    if biggest_deltas:

        def print_delta(t):
            if t[1] == 0:
                return t[0]
            return "%s(%+d)" % t

        biggest_delta_strings = [print_delta(t) for t in biggest_deltas]
        # if a table changed size, the head table will change the checksum, don't
        # report this.
        others = [k for k in others if k != "head"]
        if len(others) > 0 and len(biggest_deltas) < 5:
            other_count = len(others)
            biggest_delta_strings.append(
                "%s other%s" % (other_count, "s" if other_count != 1 else "")
            )
        result.append("changed " + ", ".join(biggest_delta_strings))
    if added:
        result.append("added " + ", ".join("%s(%s)" % t for t in sorted(added)))
    if removed:
        result.append("removed " + ", ".join(sorted(removed)))
    return "; ".join(result)


def print_difference(k, base_tuple, target_tuple, other_difference):
    (
        b_path,
        b_version,
        b_name,
        b_size,
        b_numglyphs,
        b_numchars,
        b_cmap,
        b_tableinfo,
    ) = base_tuple
    (
        t_path,
        t_version,
        t_name,
        t_size,
        t_numglyphs,
        t_numchars,
        t_cmap,
        t_tableinfo,
    ) = target_tuple
    print("  " + k)
    versions_differ = b_version != t_version
    diff_list = []
    if versions_differ:
        if float(b_version) > float(t_version):
            msg = "(base is newer!)"
        else:
            msg = ""
        print("    version: %s vs %s %s" % (b_version, t_version, msg))
    if b_name != t_name:
        diff_list.append("name")
        print("    name: '%s' vs '%s'" % (b_name, t_name))
    if b_size != t_size:
        diff_list.append("size")
        delta = int(t_size) - int(b_size)
        if delta < 0:
            msg = "%d byte%s smaller" % (-delta, "" if delta == -1 else "s")
        else:
            msg = "%d byte%s bigger" % (delta, "" if delta == 1 else "s")
        print("    size: %s vs %s (%s)" % (b_size, t_size, msg))
    table_diffs = compare_table_info(b_tableinfo, t_tableinfo)
    if table_diffs:
        diff_list.append("table")
        print("    tables: %s" % table_diffs)
    if b_numglyphs != t_numglyphs:
        diff_list.append("glyph count")
        delta = int(t_numglyphs) - int(b_numglyphs)
        if delta < 0:
            msg = "%d fewer glyph%s" % (-delta, "" if delta == -1 else "s")
        else:
            msg = "%d more glyph%s" % (delta, "" if delta == 1 else "s")
        print("    glyphs: %s vs %s (%s)" % (b_numglyphs, t_numglyphs, msg))
    if b_numchars != t_numchars:
        diff_list.append("char count")
        delta = int(t_numchars) - int(b_numchars)
        if delta < 0:
            msg = "%d fewer char%s" % (-delta, "" if delta == -1 else "s")
        else:
            msg = "%d more char%s" % (delta, "" if delta == 1 else "s")
        print("    chars: %s vs %s (%s)" % (b_numchars, t_numchars, msg))
    if b_cmap != t_cmap:
        removed_from_base = b_cmap - t_cmap
        if removed_from_base:
            print(
                "    cmap removed: "
                + noto_lint.printable_unicode_range(removed_from_base)
            )
        added_in_target = t_cmap - b_cmap
        if added_in_target:
            print(
                "    cmap added: " + noto_lint.printable_unicode_range(added_in_target)
            )
    if diff_list and not versions_differ:
        print("    %s differs but revision number is the same" % ", ".join(diff_list))
    if not diff_list and other_difference:
        print("    other difference")


def print_changed(key_list, base_map, target_map, comparefn):
    for k in key_list:
        base_tuple = base_map.get(k)
        target_tuple = target_map.get(k)
        other_difference = comparefn(base_tuple, target_tuple)
        print_difference(k, base_tuple, target_tuple, other_difference)


def tuple_compare(base_t, target_t):
    return base_t == target_t


def tuple_compare_no_size(base_t, target_t):
    for i in range(len(base_t)):
        if i == 3:
            continue
        if base_t[i] != target_t[i]:
            return False
    return True


def compare_summary(
    base_root,
    target_root,
    name=None,
    comparefn=tuple_compare,
    show_added=True,
    show_removed=True,
    show_identical=True,
    show_paths=True,
):
    base_map = summary_to_map(summary.summarize(base_root, name))
    target_map = summary_to_map(summary.summarize(target_root, name))
    added, removed, changed, identical = get_key_lists(
        base_map, target_map, base_root, target_root
    )

    # no nonlocal in 2.7
    have_output_hack = [False]

    def header_line(msg):
        if have_output_hack[0]:
            print()
        else:
            have_output_hack[0] = True
        if msg:
            print(msg)

    if show_paths:
        header_line(None)
        print("base root: " + base_root)
        print("target root: " + target_root)
    if show_added and added:
        header_line("added")
        print_keys(added)
    if show_removed and removed:
        header_line("removed")
        print_keys(removed)
    if changed:
        header_line("changed")
        print_changed(changed, base_map, target_map, comparefn)
    if show_identical and identical:
        header_line("identical")
        print_keys(identical)


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument(
        "-b",
        "--base_root",
        help="root of directory tree, base for comparison " "(default [fonts])",
        metavar="dir",
        default="[fonts]",
    )
    parser.add_argument(
        "-t",
        "--target_root",
        help="root of directory tree, target for comparison",
        metavar="dir",
        required=True,
    )
    parser.add_argument(
        "--name", help="only examine files whose subpath+names contain this regex"
    )
    parser.add_argument(
        "--compare_size", help="include size in comparisons", action="store_true"
    )
    parser.add_argument(
        "--removed", help="list files not in target", action="store_true"
    )
    parser.add_argument("--added", help="list files not in base", action="store_true")
    parser.add_argument(
        "--identical",
        help="list files that are identical in base and target",
        action="store_true",
    )
    parser.add_argument(
        "--nopaths",
        help="do not print root paths",
        action="store_false",
        default=True,
        dest="show_paths",
    )
    args = parser.parse_args()

    args.base_root = tool_utils.resolve_path(args.base_root)
    args.target_root = tool_utils.resolve_path(args.target_root)

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

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

    comparefn = tuple_compare if args.compare_size else tuple_compare_no_size

    compare_summary(
        args.base_root,
        args.target_root,
        args.name,
        comparefn,
        args.added,
        args.removed,
        args.identical,
        args.show_paths,
    )


if __name__ == "__main__":
    main()
