#!/bin/python3
# -*- tab-width: 4; indent-tabs-mode: nil; py-indent-offset: 4 -*-
#
# This file is part of the LibreOffice project.
#
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.

# ui-rules-enforcer enforces the .ui rules and properties used by LibreOffice
# mostly the deprecations of
# https://developer.gnome.org/gtk4/stable/gtk-migrating-3-to-4.html
# and a few other home cooked rules

# for any existing .ui this should parse it and overwrite it with the same content
# e.g. for a in `git ls-files "*.ui"`; do bin/ui-rules-enforcer.py $a; done

import lxml.etree as etree
import sys

def add_truncate_multiline(current):
  use_truncate_multiline = False
  istarget = current.get('class') == "GtkEntry" or current.get('class') == "GtkSpinButton"
  insertpos = 0
  for child in current:
    add_truncate_multiline(child)
    insertpos = insertpos + 1
    if not istarget:
        continue
    if child.tag == "property":
      attributes = child.attrib
      if attributes.get("name") == "truncate_multiline" or attributes.get("name") == "truncate-multiline":
        use_truncate_multiline = True

  if istarget and not use_truncate_multiline:
      truncate_multiline = etree.Element("property")
      attributes = truncate_multiline.attrib
      attributes["name"] = "truncate-multiline"
      truncate_multiline.text = "True"
      current.insert(insertpos - 1, truncate_multiline)

def do_replace_button_use_stock(current, use_stock, use_underline, label, insertpos):
  if not use_underline:
      underline = etree.Element("property")
      attributes = underline.attrib
      attributes["name"] = "use-underline"
      underline.text = "True"
      current.insert(insertpos - 1, underline)
  current.remove(use_stock)
  attributes = label.attrib
  attributes["translatable"] = "yes"
  attributes["context"] = "stock"
  if label.text == 'gtk-add':
    label.text = "_Add"
  elif label.text == 'gtk-apply':
    label.text = "_Apply"
  elif label.text == 'gtk-cancel':
    label.text = "_Cancel"
  elif label.text == 'gtk-close':
    label.text = "_Close"
  elif label.text == 'gtk-delete':
    label.text = "_Delete"
  elif label.text == 'gtk-edit':
    label.text = "_Edit"
  elif label.text == 'gtk-help':
    label.text = "_Help"
  elif label.text == 'gtk-new':
    label.text = "_New"
  elif label.text == 'gtk-no':
    label.text = "_No"
  elif label.text == 'gtk-ok':
    label.text = "_OK"
  elif label.text == 'gtk-remove':
    label.text = "_Remove"
  elif label.text == 'gtk-revert-to-saved':
    label.text = "_Reset"
  elif label.text == 'gtk-yes':
    label.text = "_Yes"
  else:
    raise Exception(sys.argv[1] + ': unknown label', label.text)

def replace_button_use_stock(current):
  use_underline = False
  use_stock = None
  label = None
  isbutton = current.get('class') == "GtkButton"
  insertpos = 0
  for child in current:
    replace_button_use_stock(child)
    insertpos = insertpos + 1
    if not isbutton:
        continue
    if child.tag == "property":
      attributes = child.attrib
      if attributes.get("name") == "use_underline" or attributes.get("name") == "use-underline":
        use_underline = True
      if attributes.get("name") == "use_stock" or attributes.get("name") == "use-stock":
        use_stock = child
      if attributes.get("name") == "label":
        label = child

  if isbutton and use_stock is not None:
    do_replace_button_use_stock(current, use_stock, use_underline, label, insertpos)

def do_replace_image_stock(current, stock):
  attributes = stock.attrib
  attributes["name"] = "icon-name"
  if stock.text == 'gtk-add':
    stock.text = "list-add"
  elif stock.text == 'gtk-remove':
    stock.text = "list-remove"
  elif stock.text == 'gtk-paste':
    stock.text = "edit-paste"
  elif stock.text == 'gtk-index':
    stock.text = "vcl/res/index.png"
  elif stock.text == 'gtk-refresh':
    stock.text = "view-refresh"
  elif stock.text == 'gtk-dialog-error':
    stock.text = "dialog-error"
  elif stock.text == 'gtk-apply':
    stock.text = "sw/res/sc20558.png"
  elif stock.text == 'gtk-missing-image':
    stock.text = "missing-image"
  elif stock.text == 'gtk-copy':
    stock.text = "edit-copy"
  elif stock.text == 'gtk-go-back':
    stock.text = "go-previous"
  elif stock.text == 'gtk-go-forward':
    stock.text = "go-next"
  elif stock.text == 'gtk-go-down':
    stock.text = "go-down"
  elif stock.text == 'gtk-go-up':
    stock.text = "go-up"
  elif stock.text == 'gtk-goto-first':
    stock.text = "go-first"
  elif stock.text == 'gtk-goto-last':
    stock.text = "go-last"
  elif stock.text == 'gtk-new':
    stock.text = "document-new"
  elif stock.text == 'gtk-open':
    stock.text = "document-open"
  elif stock.text == 'gtk-media-stop':
    stock.text = "media-playback-stop"
  elif stock.text == 'gtk-media-play':
    stock.text = "media-playback-start"
  elif stock.text == 'gtk-media-next':
    stock.text = "media-skip-forward"
  elif stock.text == 'gtk-media-previous':
    stock.text = "media-skip-backward"
  elif stock.text == 'gtk-close':
    stock.text = "window-close"
  elif stock.text == 'gtk-help':
    stock.text = "help-browser"
  else:
    raise Exception(sys.argv[1] + ': unknown stock name', stock.text)

def replace_image_stock(current):
  stock = None
  isimage = current.get('class') == "GtkImage"
  for child in current:
    replace_image_stock(child)
    if not isimage:
        continue
    if child.tag == "property":
      attributes = child.attrib
      if attributes.get("name") == "stock":
        stock = child

  if isimage and stock is not None:
    do_replace_image_stock(current, stock)

def remove_check_button_align(current):
  xalign = None
  yalign = None
  ischeckorradiobutton = current.get('class') == "GtkCheckButton" or current.get('class') == "GtkRadioButton"
  for child in current:
    remove_check_button_align(child)
    if not ischeckorradiobutton:
        continue
    if child.tag == "property":
      attributes = child.attrib
      if attributes.get("name") == "xalign":
        xalign = child
      if attributes.get("name") == "yalign":
        yalign = child

  if ischeckorradiobutton:
    if xalign is not None:
      if xalign.text != "0":
        raise Exception(sys.argv[1] + ': non-default xalign', xalign.text)
      current.remove(xalign)
    if yalign is not None:
      if yalign.text != "0.5":
        raise Exception(sys.argv[1] + ': non-default yalign', yalign.text)
      current.remove(yalign)

def remove_check_button_relief(current):
  relief = None
  ischeckorradiobutton = current.get('class') == "GtkCheckButton" or current.get('class') == "GtkRadioButton"
  for child in current:
    remove_check_button_relief(child)
    if not ischeckorradiobutton:
        continue
    if child.tag == "property":
      attributes = child.attrib
      if attributes.get("name") == "relief":
        relief = child

  if ischeckorradiobutton:
    if relief is not None:
      current.remove(relief)

def remove_check_button_image_position(current):
  image_position = None
  ischeckorradiobutton = current.get('class') == "GtkCheckButton" or current.get('class') == "GtkRadioButton"
  for child in current:
    remove_check_button_image_position(child)
    if not ischeckorradiobutton:
        continue
    if child.tag == "property":
      attributes = child.attrib
      if attributes.get("name") == "image_position" or attributes.get("name") == "image-position":
        image_position = child

  if ischeckorradiobutton:
    if image_position is not None:
      current.remove(image_position)

def remove_spin_button_input_purpose(current):
  input_purpose = None
  isspinbutton = current.get('class') == "GtkSpinButton"
  for child in current:
    remove_spin_button_input_purpose(child)
    if not isspinbutton:
        continue
    if child.tag == "property":
      attributes = child.attrib
      if attributes.get("name") == "input_purpose" or attributes.get("name") == "input-purpose":
        input_purpose = child

  if isspinbutton:
    if input_purpose is not None:
      current.remove(input_purpose)

def remove_caps_lock_warning(current):
  caps_lock_warning = None
  iscandidate = current.get('class') == "GtkSpinButton" or current.get('class') == "GtkEntry"
  for child in current:
    remove_caps_lock_warning(child)
    if not iscandidate:
        continue
    if child.tag == "property":
      attributes = child.attrib
      if attributes.get("name") == "caps_lock_warning" or attributes.get("name") == "caps-lock-warning":
        caps_lock_warning = child

  if iscandidate:
    if caps_lock_warning is not None:
      current.remove(caps_lock_warning)

def remove_spin_button_max_length(current):
  max_length = None
  isspinbutton = current.get('class') == "GtkSpinButton"
  for child in current:
    remove_spin_button_max_length(child)
    if not isspinbutton:
        continue
    if child.tag == "property":
      attributes = child.attrib
      if attributes.get("name") == "max_length" or attributes.get("name") == "max-length":
        max_length = child

  if isspinbutton:
    if max_length is not None:
      current.remove(max_length)

def remove_entry_shadow_type(current):
  shadow_type = None
  isentry = current.get('class') == "GtkEntry"
  for child in current:
    remove_entry_shadow_type(child)
    if not isentry:
        continue
    if child.tag == "property":
      attributes = child.attrib
      if attributes.get("name") == "shadow_type" or attributes.get("name") == "shadow-type":
        shadow_type = child

  if isentry:
    if shadow_type is not None:
      current.remove(shadow_type)

def remove_label_pad(current):
  xpad = None
  ypad = None
  islabel = current.get('class') == "GtkLabel"
  for child in current:
    remove_label_pad(child)
    if not islabel:
        continue
    if child.tag == "property":
      attributes = child.attrib
      if attributes.get("name") == "xpad":
        xpad = child
      elif attributes.get("name") == "ypad":
        ypad = child

  if xpad is not None:
    current.remove(xpad)
  if ypad is not None:
    current.remove(ypad)

def remove_label_angle(current):
  angle = None
  islabel = current.get('class') == "GtkLabel"
  for child in current:
    remove_label_angle(child)
    if not islabel:
        continue
    if child.tag == "property":
      attributes = child.attrib
      if attributes.get("name") == "angle":
        angle = child

  if angle is not None:
    current.remove(angle)

def remove_track_visited_links(current):
  track_visited_links = None
  islabel = current.get('class') == "GtkLabel"
  for child in current:
    remove_track_visited_links(child)
    if not islabel:
        continue
    if child.tag == "property":
      attributes = child.attrib
      if attributes.get("name") == "track_visited_links" or attributes.get("name") == "track-visited-links":
        track_visited_links = child

  if track_visited_links is not None:
    current.remove(track_visited_links)

def remove_toolbutton_focus(current):
  can_focus = None
  classname = current.get('class')
  istoolbutton = classname and classname.endswith("ToolButton")
  for child in current:
    remove_toolbutton_focus(child)
    if not istoolbutton:
        continue
    if child.tag == "property":
      attributes = child.attrib
      if attributes.get("name") == "can_focus" or attributes.get("name") == "can-focus":
        can_focus = child

  if can_focus is not None:
    current.remove(can_focus)

def remove_double_buffered(current):
  double_buffered = None
  for child in current:
    remove_double_buffered(child)
    if child.tag == "property":
      attributes = child.attrib
      if attributes.get("name") == "double_buffered" or attributes.get("name") == "double-buffered":
        double_buffered = child

  if double_buffered is not None:
    current.remove(double_buffered)

def remove_label_yalign(current):
  label_yalign = None
  for child in current:
    remove_label_yalign(child)
    if child.tag == "property":
      attributes = child.attrib
      if attributes.get("name") == "label_yalign" or attributes.get("name") == "label-yalign":
        label_yalign = child

  if label_yalign is not None:
    current.remove(label_yalign)

def remove_skip_pager_hint(current):
  skip_pager_hint = None
  for child in current:
    remove_skip_pager_hint(child)
    if child.tag == "property":
      attributes = child.attrib
      if attributes.get("name") == "skip_pager_hint" or attributes.get("name") == "skip-pager-hint":
        skip_pager_hint = child

  if skip_pager_hint is not None:
    current.remove(skip_pager_hint)

def remove_gravity(current):
  gravity = None
  for child in current:
    remove_gravity(child)
    if child.tag == "property":
      attributes = child.attrib
      if attributes.get("name") == "gravity":
        gravity = child

  if gravity is not None:
    current.remove(gravity)

def remove_expander_label_fill(current):
  label_fill = None
  isexpander = current.get('class') == "GtkExpander"
  for child in current:
    remove_expander_label_fill(child)
    if not isexpander:
        continue
    if child.tag == "property":
      attributes = child.attrib
      if attributes.get("name") == "label_fill" or attributes.get("name") == "label-fill":
        label_fill = child

  if label_fill is not None:
    current.remove(label_fill)

def remove_expander_spacing(current):
  spacing = None
  isexpander = current.get('class') == "GtkExpander"
  for child in current:
    remove_expander_spacing(child)
    if not isexpander:
        continue
    if child.tag == "property":
      attributes = child.attrib
      if attributes.get("name") == "spacing":
        spacing = child

  if spacing is not None:
    current.remove(spacing)

def enforce_menubutton_indicator_consistency(current):
  draw_indicator = None
  image = None
  ismenubutton = current.get('class') == "GtkMenuButton"
  insertpos = 0
  for child in current:
    enforce_menubutton_indicator_consistency(child)
    if not ismenubutton:
      continue
    if child.tag == "property":
      insertpos = insertpos + 1
      attributes = child.attrib
      if attributes.get("name") == "draw_indicator" or attributes.get("name") == "draw-indicator":
        draw_indicator = child
      elif attributes.get("name") == "image":
        image = child

  if ismenubutton:
    if draw_indicator is None:
      if image is None:
        # if there is no draw indicator and no image there should be a draw indicator
        draw_indicator = etree.Element("property")
        attributes = draw_indicator.attrib
        attributes["name"] = "draw-indicator"
        draw_indicator.text = "True"
        current.insert(insertpos, draw_indicator)
      else:
        # if there is no draw indicator but there is an image that image should be open-menu-symbolic or x-office-calendar
        for status_elem in tree.xpath("/interface/object[@id='" + image.text + "']/property[@name='icon_name' or @name='icon-name']"):
          if status_elem.text != 'x-office-calendar':
            status_elem.text = "open-menu-symbolic"

def enforce_active_in_group_consistency(current):
  group = None
  active = None
  isradiobutton = current.get('class') == "GtkRadioButton"
  insertpos = 0
  for child in current:
    enforce_active_in_group_consistency(child)
    if not isradiobutton:
        continue
    if child.tag == "property":
      insertpos = insertpos + 1
      attributes = child.attrib
      if attributes.get("name") == "group":
        group = child
      if attributes.get("name") == "active":
        active = child

  if isradiobutton:
    if active is not None and active.text != "True":
      raise Exception(sys.argv[1] + ': non-standard active value', active.text)
    if group is not None and active is not None:
      # if there is a group then we are not the leader and should not be active
      current.remove(active)
    elif group is None and active is None:
      # if there is no group then we are the leader and should be active
      active = etree.Element("property")
      attributes = active.attrib
      attributes["name"] = "active"
      active.text = "True"
      current.insert(insertpos, active)

def enforce_toolbar_can_focus(current):
  can_focus = None
  istoolbar = current.get('class') == "GtkToolbar"
  insertpos = 0
  for child in current:
    enforce_toolbar_can_focus(child)
    if not istoolbar:
        continue
    if child.tag == "property":
      insertpos = insertpos + 1
      attributes = child.attrib
      if attributes.get("name") == "can-focus" or attributes.get("name") == "can_focus":
        can_focus = child

  if istoolbar:
    if can_focus is None:
      can_focus = etree.Element("property")
      attributes = can_focus.attrib
      attributes["name"] = "can-focus"
      can_focus.text = "True"
      current.insert(insertpos, can_focus)
    else:
      can_focus.text = "True"

def enforce_entry_text_column_id_column_for_gtkcombobox(current):
  entrytextcolumn = None
  idcolumn = None
  isgtkcombobox = current.get('class') == "GtkComboBox"
  insertpos = 0
  for child in current:
    enforce_entry_text_column_id_column_for_gtkcombobox(child)
    if not isgtkcombobox:
        continue
    if child.tag == "property":
      insertpos = insertpos + 1
      attributes = child.attrib
      if attributes.get("name") == "entry_text_column" or attributes.get("name") == "entry-text-column":
        entrytextcolumn = child
      if attributes.get("name") == "id_column" or attributes.get("name") == "id-column":
        idcolumn = child

  if isgtkcombobox:
    if entrytextcolumn is not None and entrytextcolumn.text != "0":
      raise Exception(sys.argv[1] + ': non-standard entry_text_column value', entrytextcolumn.text)
    if idcolumn is not None and idcolumn.text != "1":
      raise Exception(sys.argv[1] + ': non-standard id_column value', idcolumn.text)
    if entrytextcolumn is None:
      # if there is no entry_text_column, create one
      entrytextcolumn = etree.Element("property")
      attributes = entrytextcolumn.attrib
      attributes["name"] = "entry-text-column"
      entrytextcolumn.text = "0"
      current.insert(insertpos, entrytextcolumn)
      insertpos = insertpos + 1
    if idcolumn is None:
      # if there is no id_column, create one
      idcolumn = etree.Element("property")
      attributes = idcolumn.attrib
      attributes["name"] = "id-column"
      idcolumn.text = "1"
      current.insert(insertpos, idcolumn)

def enforce_button_always_show_image(current):
  image = None
  always_show_image = None
  isbutton = current.get('class') == "GtkButton"
  insertpos = 0
  for child in current:
    enforce_button_always_show_image(child)
    if not isbutton:
        continue
    if child.tag == "property":
      insertpos = insertpos + 1
      attributes = child.attrib
      if attributes.get("name") == "always_show_image" or attributes.get("name") == "always-show-image":
        always_show_image = child
      elif attributes.get("name") == "image":
        image = child

  if isbutton and image is not None:
    if always_show_image is None:
      always_show_image = etree.Element("property")
      attributes = always_show_image.attrib
      attributes["name"] = "always-show-image"
      always_show_image.text = "True"
      current.insert(insertpos, always_show_image)
    else:
      always_show_image.text = "True"

def enforce_noshared_adjustments(current, adjustments):
  for child in current:
    enforce_noshared_adjustments(child, adjustments)
    if child.tag == "property":
      attributes = child.attrib
      if attributes.get("name") == "adjustment":
        if child.text in adjustments:
          raise Exception(sys.argv[1] + ': adjustment used more than once', child.text)
        adjustments.add(child.text)

def enforce_no_productname_in_accessible_description(current, adjustments):
  for child in current:
    enforce_no_productname_in_accessible_description(child, adjustments)
    if child.tag == "property":
      attributes = child.attrib
      if attributes.get("name") == "AtkObject::accessible-description":
        if "%PRODUCTNAME" in child.text:
          raise Exception(sys.argv[1] + ': %PRODUCTNAME used in accessible-description:' , child.text)

def enforce_label_child_is_label(current):
  # Ensure that only GtkLabel is used for a <child type="label">,
  # which is an assumption that e.g. weld::Frame::set_label implementations make
  label_children = current.findall(".//child[@type='label']")
  for label_child in label_children:
    for child in label_child:
      classname = child.get('class')
      if classname and classname != 'GtkLabel':
        raise Exception(sys.argv[1] + ': <child type="label"> is not a GtkLabel', child.attrib.get("id"))

def enforce_menuitem_id(current):
  # gtk4 VCL plugin requires "id" attribute for menu items
  for child in current:
    enforce_menuitem_id(child)
    if child.get('class') in ("GtkMenuItem", "GtkRadioMenuItem"):
      if not child.attrib.get("id"):
        raise Exception(sys.argv[1] + ': menu item does not have an id set' , child.text)

with open(sys.argv[1], encoding="utf-8") as f:
  header = f.readline()
  f.seek(0)
  # remove_blank_text so pretty-printed input doesn't disrupt pretty-printed
  # output if nodes are added or removed
  parser = etree.XMLParser(remove_blank_text=True)
  tree = etree.parse(f, parser)
  # make sure <property name="label" translatable="no"></property> stays like that
  # and doesn't change to <property name="label" translatable="no"/>
  for status_elem in tree.xpath("//property[@name='label' and string() = '']"):
    status_elem.text = ""
  root = tree.getroot()

# do some targeted conversion here
# tdf#138848 Copy-and-Paste in input box should not append an ENTER character
if not sys.argv[1].endswith('/multiline.ui'): # let this one alone not truncate multiline pastes
  add_truncate_multiline(root)
replace_button_use_stock(root)
replace_image_stock(root)
remove_check_button_align(root)
remove_check_button_relief(root)
remove_check_button_image_position(root)
remove_spin_button_input_purpose(root)
remove_caps_lock_warning(root)
remove_spin_button_max_length(root)
remove_track_visited_links(root)
remove_label_pad(root)
remove_label_angle(root)
remove_expander_label_fill(root)
remove_expander_spacing(root)
enforce_menubutton_indicator_consistency(root)
enforce_label_child_is_label(root)
enforce_menuitem_id(root)
enforce_active_in_group_consistency(root)
enforce_entry_text_column_id_column_for_gtkcombobox(root)
remove_entry_shadow_type(root)
remove_double_buffered(root)
remove_label_yalign(root)
remove_skip_pager_hint(root)
remove_gravity(root)
remove_toolbutton_focus(root)
enforce_toolbar_can_focus(root)
enforce_button_always_show_image(root)
enforce_noshared_adjustments(root, set())
enforce_no_productname_in_accessible_description(root, set())

with open(sys.argv[1], 'wb') as o:
  # without encoding='unicode' (and the matching encode("utf8")) we get &#XXXX replacements for non-ascii characters
  # which we don't want to see changed in the output
  o.write(etree.tostring(tree, pretty_print=True, method='xml', encoding='unicode', doctype=header[0:-1]).encode("utf8"))

# vim: set shiftwidth=4 softtabstop=4 expandtab:
