#!/usr/sbin/python3
import argparse
import sys
import os
import re
import copy
import textwrap
import logging
from io import StringIO

# external modules
from lxml import etree

# Options
parser = argparse.ArgumentParser( 
    description = textwrap.dedent("""
        Given a Thunar custom action XML file, merge or delete actions present
        in other given Thunar custom actions XML files. <action>s are conidered
        as equal, if all content matches. The <unique-id> is ignored for this
        check.  The first file will be used as the base.  
        """))
parser.add_argument("-o","--output", help = "output xml file", 
    default = "-", required = False)
parser.add_argument("-f","--file", help = "base xml file", 
    required = True)
parser.add_argument("-a","--action", help = "action to take", 
    choices = ["merge","remove"], required = True)
parser.add_argument("-v","--verbose", help = "be verbose", default = False,
    action = "store_true")
parser.add_argument("files", help = "other xml files", nargs='*') 
args = parser.parse_args()

if args.verbose:
    logging.basicConfig(level = logging.DEBUG)
else:
    logging.basicConfig(level = logging.INFO)


EQUAL_TAGS = ["icon","patterns","command","description"]
ACTIONS_XPATH = "/actions"
SINGLE_ACTIONS_XPATH = "//action"
ACTION_ELEMENTS_NO_TRANSLATION_XPATH = "*[not(@xml:lang) and not(@lang)]"
UNIQUE_IDS_XPATH = "//action/unique-id"
COMMAND_XPATH = "//action/command"
DESCRIPTION_XPATH = "//action/description"
NAME_XPATH = "//action/name"
COMPARE_IGNORE_XPATH = "|".join([DESCRIPTION_XPATH,NAME_XPATH])
EMPTY_UCA_XML_STRING = "<actions></actions>"

def actions_equal(action1, action2):
    """ 
    Determine if two actions are "equal". Actions are considered "equal" if
    their string representation is equal. 

    Args:
        action1, action2 (lxml.etree.Element): the actions to compare

    Returns:
        bool : True if the actions are considered equal, False otherwise.
    """
    def tostring(action):
        tostr = etree.tostring( action, encoding = str, method = "text", 
            pretty_print = True)
        string = re.sub(string=tostr,pattern="\s*",repl="")
        return string
    string1 = tostring( action1 )
    string2 = tostring( action2 )
    logging.debug("tostring(action1):\n{}".format(string1))
    logging.debug("tostring(action2):\n{}".format(string2))
    return( string1 == string2 )
    
def corresponding_action(action,actions):
    """ 
    Find the first corresponding action in a set of given actions.
    return

    Args:
        action (lxml.etree.Element): The action of interest
        actions (lxml.etree.Element): The actions of interest

    Returns:
        lxml.etree.Element : The found corresponding element or None
    """
    action_copy= copy.deepcopy( action )
    for tag in action_copy.xpath(COMPARE_IGNORE_XPATH):
        tag.getparent().remove(tag)
    command = action_copy.xpath(COMMAND_XPATH) # get the command
    try: # remove all non-characters
        cmd = command[0]
        cmd.text = re.sub(string=cmd.text, pattern="\W+",repl="")
    except (IndexError):
        pass
    for act in actions:
        act_copy = copy.deepcopy( act )
        # remove all ignored tags
        for tag in act_copy.xpath(COMPARE_IGNORE_XPATH):
            tag.getparent().remove(tag)
        command = act_copy.xpath(COMMAND_XPATH) # get the command
        try: # remove all non-characters
            cmd = command[0]
            cmd.text = re.sub(string=cmd.text, pattern="\W+",repl="")
        except (IndexError):
            pass
            
        # if this action is equal to the action of interest, this is it!
        if actions_equal( act_copy, action_copy ):
            return act

recovering_parser = etree.XMLParser( recover = True )

def parse_xml(fh):
    string = fh.read() # read content
    logging.debug("input string before:\n{}".format(string))
    string = re.sub( string = string, pattern = "^\s*<\?xml[^>]*?\?>\s*",repl="")
    logging.debug("input string after:\n{}".format(string))
    res = etree.parse( StringIO(string) , recovering_parser )
    return res

# open base file
if args.file == "-":
    base_file = sys.stdin
    logging.debug("reading base file from STDIN...")
else:
    try:
        logging.debug("opening base file '{}'...".format(args.file))
        base_file = open(args.file,"r")
    except FileNotFoundError:
        logging.debug("file '{}' does not exist. Using empty" "default".format(
            args.file))
        base_file = StringIO( EMPTY_UCA_XML_STRING )

base_tree = parse_xml( base_file ) # read XML
base_root = base_tree.getroot() # XML root
logging.debug("There are {} actions in the base file".format(
    len(base_root.xpath(SINGLE_ACTIONS_XPATH))))

for ucafile in args.files: # loop over all files
    if ucafile == "-":
        input_file = sys.stdin
        logging.debug("reading another file from STDIN...")
    else:
        try:
            logging.debug("reading another file from '{}'...".format(ucafile))
            input_file = open(ucafile,"r")
        except FileNotFoundError:
            logging.debug("file '{}' does not exist. Using empty" 
                "default".format(ucafile))
            input_file = StringIO( EMPTY_UCA_XML_STRING )
        other_tree = parse_xml( input_file )
        other_root = other_tree.getroot() # XML root
    logging.debug("parsed the XML")

    # remove all unique ids
    logging.debug("removing all <unique-id> fields from the base file")
    for tag in base_root.xpath(UNIQUE_IDS_XPATH):
        tag.getparent().remove(tag)
    logging.debug("removing all <unique-id> fields from other file")
    for tag in other_root.xpath(UNIQUE_IDS_XPATH):
        tag.getparent().remove(tag)

    # get all actions from this file
    other_actions = other_root.xpath(SINGLE_ACTIONS_XPATH)
    logging.debug("There are {} actions in this file".format(
        len(other_actions)))

    for other_action in other_root.xpath(SINGLE_ACTIONS_XPATH):
        base_action = corresponding_action( 
            actions = base_root.xpath(SINGLE_ACTIONS_XPATH), 
            action = other_action)
        if not base_action is None:
            logging.debug("The action {} is present in the base tree".format(
                other_action))
            if args.action == "merge":
                logging.debug("No reason to merge anything, but merging anyway!")
                logging.debug("removing this action first from the base tree")
                base_action.getparent().remove(base_action)
                logging.debug("adding the new action content to the base tree")
                base_root.xpath(ACTIONS_XPATH)[0].append(other_action)
            elif args.action == "remove":
                # remove this action from the base file
                logging.debug("removing this action from the base tree")
                base_action.getparent().remove(base_action)
            else:
                raise ValueError("Unknown action '{}'".format(args.action))
        else:
            logging.debug("The action {} is not present in " 
                "the base tree".format(other_action))
            if args.action == "merge":
                # add this action to the base file
                logging.debug("adding this action to the base tree")
                base_root.xpath(ACTIONS_XPATH)[0].append(other_action)
            elif args.action == "remove":
                logging.debug("No reason to remove anything.")
            else:
                raise ValueError("Unknown action '{}'".format(args.action))
                    

if args.output == "-":
    output_file = sys.stdout
else:
    directory = os.path.dirname( args.output ) 
    if not os.path.exists( directory ):
        os.makedirs( directory )
    output_file = open(args.output,"w")


# write XML
xml_string = etree.tostring( base_root, pretty_print = True, 
    encoding = "utf-8", xml_declaration = True)
output_file.write( xml_string.decode("utf-8") )

