#!/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()
