#!/usr/bin/env python3
# Copyright 2026 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Script to format Android resource XML files.

It enforces:
- Indentation of 2 spaces for children.
- Continuation indent of 4 spaces for attributes on new lines.
- Attribute ordering from tools/android/android_studio/ChromiumStyle.xml.
- Self-closing tags for empty elements.
- Collapsing of multiple empty lines between elements to a single one.
"""

import argparse
import difflib
import os
import sys
from xml.dom import minidom

INDENT_SIZE = 2
CONTINUATION_INDENT_SIZE = 4
MAX_LINE_LENGTH = 100


def sort_key(item):
  name, value = item

  if name == 'xmlns:android':
    return (0, name)
  if name.startswith('xmlns:'):
    return (1, name)
  if name == 'android:id':
    return (2, name)
  if name == 'android:name':
    return (3, name)
  if name == 'name':
    return (4, name)
  if name == 'style':
    return (5, name)
  if ':' not in name:  # No namespace
    return (6, name)
  if name.startswith('android:') and name.endswith('Style'):
    return (7, name)
  if name == 'android:width':
    return (8, name)
  if name == 'android:height':
    return (9, name)
  if name == 'android:layout_width':
    return (10, name)
  if name == 'android:layout_height':
    return (11, name)
  if name == 'android:layout_weight':
    return (12, name)
  if name == 'android:layout_margin':
    return (13, name)
  if name == 'android:layout_marginTop':
    return (14, name)
  if name == 'android:layout_marginBottom':
    return (15, name)
  if name == 'android:layout_marginStart':
    return (16, name)
  if name == 'android:layout_marginEnd':
    return (17, name)
  if name == 'android:layout_marginLeft':
    return (18, name)
  if name == 'android:layout_marginRight':
    return (19, name)
  if name.startswith('android:layout_'):
    return (20, name)
  if name == 'android:padding':
    return (21, name)
  if name == 'android:paddingTop':
    return (22, name)
  if name == 'android:paddingBottom':
    return (23, name)
  if name == 'android:paddingStart':
    return (24, name)
  if name == 'android:paddingEnd':
    return (25, name)
  if name == 'android:paddingLeft':
    return (26, name)
  if name == 'android:paddingRight':
    return (27, name)
  if name.startswith('android:'):
    return (28, name)
  if name.startswith('app:'):
    return (29, name)
  if name.startswith('tools:'):
    return (30, name)

  return (31, name)


def sort_attributes(attrs):
  """Sorts attributes according to ChromiumStyle.xml rules."""
  if not attrs:
    return []

  items = list(attrs.items())
  items.sort(key=sort_key)
  return items


def format_node(node, indent=0, indent_str=' ' * INDENT_SIZE):
  """Recursively formats an XML node.

  Args:
    node: The minidom node to format.
    indent: The current indentation level.
    indent_str: The string used for one level of indentation.
  """
  if node.nodeType == minidom.Node.DOCUMENT_NODE:
    parts = [
      format_node(child, indent, indent_str) for child in node.childNodes
    ]
    return ''.join(parts)

  ret = ''

  if node.nodeType == minidom.Node.ELEMENT_NODE:
    tag = node.tagName
    attrs = sort_attributes(node.attributes)

    real_children = [
      c
      for c in node.childNodes
      if c.nodeType != minidom.Node.TEXT_NODE or c.nodeValue.strip()
    ]

    # Try single-line formatting first
    attr_strs = [f'{name}="{value}"' for name, value in attrs]
    attr_str = ' ' + ' '.join(attr_strs) if attr_strs else ''

    if not real_children:
      single_line = f"{indent_str * indent}<{tag}{attr_str} />\n"
      if len(single_line) <= MAX_LINE_LENGTH:
        return single_line
    elif (
      len(real_children) == 1
      and real_children[0].nodeType == minidom.Node.TEXT_NODE
    ):
      text = real_children[0].nodeValue.strip()
      single_line = f"{indent_str * indent}<{tag}{attr_str}>{text}</{tag}>\n"
      if len(single_line) <= MAX_LINE_LENGTH:
        return single_line

    # Fallback to multi-line if it doesn't fit or has complex children
    ret = ''
    attr_strs = [f'{name}="{value}"' for name, value in attrs]
    attr_str = ' ' + ' '.join(attr_strs) if attr_strs else ''
    single_line_open = f"{indent_str * indent}<{tag}{attr_str}"

    if len(single_line_open) <= MAX_LINE_LENGTH or len(attrs) <= 1:
      ret += single_line_open
    else:
      continuation_indent = ' ' * (
        indent * len(indent_str) + CONTINUATION_INDENT_SIZE
      )
      attr_strs_wrapped = [
        f'\n{continuation_indent}{name}="{value}"' for name, value in attrs
      ]
      ret += f"{indent_str * indent}<{tag}{''.join(attr_strs_wrapped)}"

    if real_children:
      parts = ['>\n']
      for child in node.childNodes:
        is_text_node = child.nodeType == minidom.Node.TEXT_NODE
        if is_text_node and not child.nodeValue.strip():
          if child.nodeValue.count('\n') >= 2:
            parts.append('\n')
          continue
        parts.append(format_node(child, indent + 1, indent_str))
      parts.append(f"{indent_str * indent}</{tag}>\n")
      return ret + ''.join(parts)
    else:
      ret += ' />\n'

    return ret

  if node.nodeType == minidom.Node.TEXT_NODE:
    text = node.nodeValue.strip()
    if text:
      return f"{indent_str * indent}{text}\n"
    return ''

  if node.nodeType == minidom.Node.COMMENT_NODE:
    return f"{indent_str * indent}<!--{node.nodeValue}-->\n"

  return ''


def format_xml(content):
  """Parses and formats XML content."""
  dom = minidom.parseString(content)
  formatted = format_node(dom)

  if content.startswith('<?xml'):
    end_idx = content.find('?>')
    if end_idx != -1:
      xml_declaration = content[: end_idx + 2] + '\n'
      return xml_declaration + formatted

  return formatted


def print_diff(original, formatted, filename):
  """Prints a unified diff between original and formatted text.

  Returns:
      2 if differences were found, 0 otherwise.
  """
  diff = list(
    difflib.unified_diff(
      original.splitlines(keepends=True),
      formatted.splitlines(keepends=True),
      fromfile=f'a/{filename}',
      tofile=f'b/{filename}',
    )
  )

  if not diff:
    return 0

  sys.stdout.writelines(diff)
  return 2


def process_file(file_path, check=False, stdout=False, diff=False):
  """Processes a single XML file."""
  with open(file_path, 'r', encoding='utf-8') as f:
    content = f.read()

  formatted = format_xml(content)

  if check:
    return content == formatted

  if diff:
    return print_diff(content, formatted, file_path) == 0

  if stdout:
    sys.stdout.write(formatted)
    return True

  with open(file_path, 'w', encoding='utf-8') as f:
    f.write(formatted)
  print(f"Formatted {file_path}")
  return True


def main():
  """Main function to handle CLI arguments and run the formatter."""
  parser = argparse.ArgumentParser(
    description='Format Android resource XML files.'
  )
  parser.add_argument(
    'paths', nargs='+', help='Paths to XML files or directories to format.'
  )
  parser.add_argument(
    '--check',
    action='store_true',
    help='Check if files are formatted without modifying them.',
  )
  parser.add_argument(
    '--diff',
    action='store_true',
    help='Print diff to stdout rather than modifying files.',
  )
  parser.add_argument(
    '--stdout',
    action='store_true',
    help='Print formatted content to stdout instead of file.',
  )

  args = parser.parse_args()

  success = True
  invalid_path_found = False

  for path in args.paths:
    if os.path.isdir(path):
      for root, _, files in os.walk(path):
        for file in files:
          if file.endswith('.xml'):
            file_path = os.path.join(root, file)
            if not process_file(file_path, args.check, args.stdout, args.diff):
              success = False
              if args.check or args.diff:
                print(f"Requires formatting: {file_path}")
    elif os.path.isfile(path):
      if not process_file(path, args.check, args.stdout, args.diff):
        success = False
        if args.check or args.diff:
          print(f"Requires formatting: {path}")
    else:
      print(f"Error: {path} is not a valid file or directory", file=sys.stderr)
      invalid_path_found = True

  if invalid_path_found:
    sys.exit(1)

  if (args.check or args.diff) and not success:
    sys.exit(2)


if __name__ == '__main__':
  main()
