#!/usr/bin/env python

#
# Copyright (C) 2024 The Android Open Source Project
#
# 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.
#

"""Build XML."""

import dataclasses
import functools
from xml.dom import minidom
from xml.etree import ElementTree
from alias_builder import Alias
from commandline import CommandlineArgs
from fallback_builder import FallbackEntry
from family_builder import Family
from font_builder import Font


@dataclasses.dataclass
class XmlFont:
  """Class used for writing XML. All elements are str or None."""

  file: str
  weight: str | None
  style: str | None
  index: str | None
  supported_axes: str | None
  post_script_name: str | None
  fallback_for: str | None
  axes: dict[str | str]


def font_to_xml_font(font: Font, fallback_for=None) -> XmlFont:
  axes = None
  if font.axes:
    axes = {key: str(value) for key, value in font.axes.items()}
  return XmlFont(
      file=font.file,
      weight=str(font.weight) if font.weight is not None else None,
      style=font.style,
      index=str(font.index) if font.index is not None else None,
      supported_axes=font.supported_axes,
      post_script_name=font.post_script_name,
      fallback_for=fallback_for,
      axes=axes,
  )


@dataclasses.dataclass
class XmlFamily:
  """Class used for writing XML. All elements are str or None."""

  name: str | None
  lang: str | None
  variant: str | None
  fonts: [XmlFont]


def family_to_xml_family(family: Family) -> XmlFamily:
  return XmlFamily(
      name=family.name,
      lang=family.lang,
      variant=family.variant,
      fonts=[font_to_xml_font(f) for f in family.fonts],
  )


@dataclasses.dataclass
class XmlAlias:
  """Class used for writing XML. All elements are str or None."""

  name: str
  to: str
  weight: str | None


def alias_to_xml_alias(alias: Alias) -> XmlAlias:
  return XmlAlias(
      name=alias.name,
      to=alias.to,
      weight=str(alias.weight) if alias.weight is not None else None,
  )


@dataclasses.dataclass
class FallbackXml:
  families: [XmlFamily]
  aliases: [XmlAlias]


class FallbackOrder:
  """Provides a ordering of the family."""

  def __init__(self, fallback: [FallbackEntry]):
    # Preprocess fallbacks from flatten key to priority value.
    # The priority is a index appeared the fallback entry.
    # The key will be lang or file prefixed string, e.g. "lang:und-Arab" -> 0,
    # "file:Roboto-Regular.ttf" -> 10, etc.
    fallback_priority = {}
    for priority, fallback in enumerate(fallback):
      if fallback.lang:
        fallback_priority['lang:%s' % fallback.lang] = priority
      else:  # fallback.file is not None
        fallback_priority['id:%s' % fallback.id] = priority

    self.priority = fallback_priority

  def __call__(self, family: Family):
    """Returns priority of the family. Lower value means higher priority."""
    priority = None
    if family.id:
      priority = self.priority.get('id:%s' % family.id)
    if priority is None and family.lang:
      priority = self.priority.get('lang:%s' % family.lang)

    assert priority is not None, 'Unknown priority for %s' % family

    # Priority adjustments.
    # First, give extra score to compact for compatibility.
    priority = priority * 10
    if family.variant == 'compact':
      priority = priority + 5

    # Next, give extra priority score. The priority is -100 to 100,
    # Not to mixed in other scores, shift this range to 0 to 200 and give it
    # to current priority.
    priority = priority * 1000
    custom_priority = family.priority if family.priority else 0
    priority = priority + custom_priority + 100

    return priority


def generate_xml(
    fallback: [FallbackEntry], aliases: [Alias], families: [Family]
) -> FallbackXml:
  """Generats FallbackXML objects."""

  # Step 1. Categorize families into following three.

  # The named family is converted to XmlFamily in this step.
  named_families: [str | XmlFamily] = {}
  # The list of Families used for locale fallback.
  fallback_families: [Family] = []
  # The list of Families that has fallbackFor attribute.
  font_fallback_families: [Family] = []

  for family in families:
    if family.name:  # process named family
      assert family.name not in named_families, (
          'Duplicated named family entry: %s' % family.name
      )
      named_families[family.name] = family_to_xml_family(family)
    elif family.fallback_for:
      font_fallback_families.append(family)
    else:
      fallback_families.append(family)

  # Step 2. Convert Alias to XmlAlias with validation.
  xml_aliases = []
  available_names = set(named_families.keys())
  for alias in aliases:
    assert alias.name not in available_names, (
        'duplicated name alias: %s' % alias
    )
    available_names.add(alias.name)

  for alias in aliases:
    assert alias.to in available_names, 'unknown alias to: %s' % alias
    xml_aliases.append(alias_to_xml_alias(alias))

  # Step 3. Reorder the fallback families with fallback priority.
  order = FallbackOrder(fallback)
  fallback_families.sort(
      key=functools.cmp_to_key(lambda l, r: order(l) - order(r))
  )
  for i, j in zip(fallback_families, fallback_families[1:]):
    assert order(i) != order(j), 'Same priority: %s vs %s' % (i, j)

  # Step 4. Place named families first.
  # Place sans-serif at the top of family list.
  assert 'sans-serif' in named_families, 'sans-serif family must exists'
  xml_families = [family_to_xml_family(named_families.pop('sans-serif'))]
  xml_families = xml_families + list(named_families.values())

  # Step 5. Convert fallback_families from Family to XmlFamily.
  # Also create ID to XmlFamily map which is used for resolving fallbackFor
  # attributes.
  id_to_family: [str | XmlFamily] = {}
  for family in fallback_families:
    xml_family = family_to_xml_family(family)
    xml_families.append(xml_family)
    if family.id:
      id_to_family[family.id] = xml_family

  # Step 6. Add font fallback to the target XmlFamily
  for family in font_fallback_families:
    assert family.fallback_for in named_families, (
        'Unknown fallback for: %s' % family
    )
    assert family.target in id_to_family, 'Unknown target for %s' % family

    xml_family = id_to_family[family.target]
    xml_family.fonts = xml_family.fonts + [
        font_to_xml_font(f, family.fallback_for) for f in family.fonts
    ]

  # Step 7. Build output
  return FallbackXml(aliases=xml_aliases, families=xml_families)


def write_xml(outfile: str, xml: FallbackXml):
  """Writes given xml object into into outfile as XML."""
  familyset = ElementTree.Element('familyset')

  for family in xml.families:
    family_node = ElementTree.SubElement(familyset, 'family')
    if family.lang:
      family_node.set('lang', family.lang)
    if family.name:
      family_node.set('name', family.name)
    if family.variant:
      family_node.set('variant', family.variant)

    for font in family.fonts:
      font_node = ElementTree.SubElement(family_node, 'font')
      if font.weight:
        font_node.set('weight', font.weight)
      if font.style:
        font_node.set('style', font.style)
      if font.index:
        font_node.set('index', font.index)
      if font.supported_axes:
        font_node.set('supportedAxes', font.supported_axes)
      if font.fallback_for:
        font_node.set('fallbackFor', font.fallback_for)
      if font.post_script_name:
        font_node.set('postScriptName', font.post_script_name)

      font_node.text = font.file

      if font.axes:
        for tag, value in font.axes.items():
          axis_node = ElementTree.SubElement(font_node, 'axis')
          axis_node.set('tag', tag)
          axis_node.set('stylevalue', value)

  for alias in xml.aliases:
    alias_node = ElementTree.SubElement(familyset, 'alias')
    alias_node.set('name', alias.name)
    alias_node.set('to', alias.to)
    if alias.weight:
      alias_node.set('weight', alias.weight)

  doc = minidom.parseString(ElementTree.tostring(familyset, 'utf-8'))
  with open(outfile, 'w') as f:
    doc.writexml(f, encoding='utf-8', newl='\n', indent='', addindent='  ')


def main(args: CommandlineArgs):
  xml = generate_xml(args.fallback, args.aliases, args.families)
  write_xml(args.outfile, xml)
