#!/usr/bin/env python3
"""Main file of yaml2nxdl tool.
Users create NeXus instances by writing a YAML file
which details a hierarchy of data/metadata elements

"""
# -*- coding: utf-8 -*-
#
# Copyright The NOMAD Authors.
#
# This file is part of NOMAD. See https://nomad-lab.eu for further info.
#
# 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.
#

# pylint: disable=E1101


import os
import sys
from typing import List

import xml.etree.ElementTree as ET
from xml.dom import minidom
import click

from nexusparser.tools.dataconverter import helpers
from nexusparser.tools.yaml2nxdl import yaml2nxdl_forward_tools
from nexusparser.tools.yaml2nxdl import yaml2nxdl_backward_tools


def pretty_print_xml(xml_root, output_xml):
    """Print better human-readable idented and formatted xml file
using built-in libraries and add preceding XML processing instruction

    """
    dom = minidom.parseString(ET.tostring(
        xml_root, encoding='utf-8', method='xml'))
    sibling = dom.createProcessingInstruction(
        'xml-stylesheet', 'type="text/xsl" href="nxdlformat.xsl"')
    root = dom.firstChild
    dom.insertBefore(sibling, root)
    xml_string = dom.toprettyxml()
    with open(output_xml, "w") as file_out:
        file_out.write(xml_string)


def yaml2nxdl(input_file: str, verbose: bool):
    """Main of the yaml2nxdl converter, creates XML tree,
namespace and schema, then evaluates a dictionary
nest of groups recursively and fields or (their) attributes as childs of the groups

    """
    yml_appdef = yaml2nxdl_forward_tools.yml_reader(input_file)

    if verbose:
        sys.stdout.write(f'input-file: {input_file}\n')
        sys.stdout.write('application/base contains the following root-level entries:\n')
        sys.stdout.write(str(yml_appdef.keys()))
    xml_root = ET.Element(
        'definition', {
            'xmlns': 'http://definition.nexusformat.org/nxdl/3.1',
            'xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance',
            'xsi:schemaLocation': 'http://www.w3.org/2001/XMLSchema-instance'
        }
    )
    assert 'category' in yml_appdef.keys(
    ), 'Required root-level keyword category is missing!'
    assert yml_appdef['category'] in ['application', 'base'], 'Only \
application and base are valid categories!'
    assert 'doc' in yml_appdef.keys(), 'Required root-level keyword doc is missing!'

    xml_root.set('extends', 'NXobject')
    xml_root.set('type', 'group')
    if yml_appdef['category'] == 'application':
        xml_root.set('category', 'application')
        del yml_appdef['category']
    else:
        xml_root.set('category', 'base')
        del yml_appdef['category']

    assert isinstance(yml_appdef['doc'], str) and yml_appdef['doc'] != '', 'Doc \
has to be a non-empty string!'
    doctag = ET.SubElement(xml_root, 'doc')
    doctag.text = yml_appdef['doc']
    del yml_appdef['doc']

    if 'symbols' in yml_appdef.keys():
        yaml2nxdl_forward_tools.xml_handle_symbols(xml_root, yml_appdef['symbols'])
        del yml_appdef['symbols']

    assert len(yml_appdef.keys()) == 1, 'Accepting at most keywords: category, \
doc, symbols, and NX... at root-level!'
    keyword = list(yml_appdef.keys())[0]  # which is the only one
    assert (keyword[0:3] == '(NX' and keyword[-1:] == ')' and len(keyword) > 4), 'NX \
keyword has an invalid pattern, or is too short!'
    xml_root.set('name', keyword[1:-1])
    yaml2nxdl_forward_tools.recursive_build(xml_root, yml_appdef[keyword], verbose)

    pretty_print_xml(xml_root, input_file.split(".", 1)[0] + '.nxdl.xml')
    if verbose:
        sys.stdout.write('Parsed YAML to NXDL successfully\n')


def get_node_parent_info(tree, node):
    """Return tuple of (parent, index) where:
        parent = node of parent within tree
        index = index of node under parent"""

    parent_map = {c: p for p in tree.iter() for c in p}
    parent = parent_map[node]
    return parent, list(parent).index(node)


class Nxdl2yaml():
    """Parse XML file and print a YML file

"""

    def __init__(
            self,
            symbol_list: List[str],
            root_level_definition: List[str],
            root_level_doc='',
            root_level_symbols=''):
        self.append_flag = True
        self.found_definition = False
        self.jump_symbol_child = False
        self.root_level_doc = root_level_doc
        self.root_level_symbols = root_level_symbols
        self.root_level_definition = root_level_definition
        self.symbol_list = symbol_list

    def handle_symbols(self, depth, node):
        """Handle symbols field and its childs

        """
        self.root_level_symbols = '{indent}{tag}: {text}'.format(
            indent=0 * '  ',
            tag=helpers.remove_namespace_from_tag(node.tag),
            text=node.text.strip() if node.text else '')
        depth += 1
        for child in list(node):
            tag = helpers.remove_namespace_from_tag(child.tag)
            if tag == ('doc'):
                self.symbol_list.append(
                    '{indent}{tag}: "{text}"'.format(
                        indent=1 * '  ',
                        tag=helpers.remove_namespace_from_tag(child.tag),
                        text=child.text.strip().replace('\"', '\'') if child.text else ''))
            elif tag == ('symbol'):
                if 'doc' in child.attrib:
                    self.symbol_list.append(
                        '{indent}{key}: "{value}"'.format(
                            indent=0 * '  ',
                            key=child.attrib['name'],
                            value=child.attrib['doc'] or ''))
                else:
                    for symbol_doc in list(child):
                        tag = symbol_doc.tag.split("}", 1)[1]
                        if tag == ('doc'):
                            self.symbol_list.append(
                                '{indent}{key}: "{text}"'.format(
                                    indent=1 * '  ',
                                    key=child.attrib['name'],
                                    text=symbol_doc.text.strip().replace('\"', '\'')))

    def handle_definition(self, node):
        """Handle definition group and its attributes

        """
        for item in node.attrib:
            if 'schemaLocation' not in item \
                    and 'name' not in item \
                    and 'extends' not in item \
                    and 'type' not in item:
                self.root_level_definition.append(
                    '{key}: {value}'.format(
                        key=item,
                        value=node.attrib[item] or ''))
        if 'name' in node.attrib.keys():
            self.root_level_definition.append(
                '({value}):'.format(
                    value=node.attrib['name'] or ''))

    def handle_root_level_doc(self, node):
        """Handle the documentation field found at root level

"""
        for child in list(node):
            tag = helpers.remove_namespace_from_tag(child.tag)
            if tag == ('doc'):
                self.root_level_doc = '{indent}{tag}: "{text}"'.format(
                    indent=0 * '  ',
                    tag=helpers.remove_namespace_from_tag(child.tag),
                    text=child.text.strip().replace('\"', '\'') if child.text else '')
                node.remove(child)

    def print_root_level_doc(self, file_out):
        """Print at the root level of YML file \
the general documentation field found in XML file

 """
        file_out.write(
            '{indent}{root_level_doc}\n'.format(
                indent=0 * '  ',
                root_level_doc=self.root_level_doc))
        self.root_level_doc = ''

    def print_root_level_info(self, depth, file_out):
        """Print at the root level of YML file \
the information stored as definition attributes in the XML file

"""
        if depth > 0 \
                and [s for s in self.root_level_definition if "category: application" in s]\
                or depth == 1 \
                and [s for s in self.root_level_definition if "category: base" in s]:
            if self.root_level_symbols:
                file_out.write(
                    '{indent}{root_level_symbols}\n'.format(
                        indent=0 * '  ',
                        root_level_symbols=self.root_level_symbols))
                for symbol in self.symbol_list:
                    file_out.write(
                        '{indent}{symbol}\n'.format(
                            indent=0 * '  ',
                            symbol=symbol))
            if self.root_level_definition:
                for defs in self.root_level_definition:
                    file_out.write(
                        '{indent}{defs}\n'.format(
                            indent=0 * '  ',
                            defs=defs))
            self.found_definition = False

    def recursion_in_xml_tree(self, depth, xml_tree, output_yml, verbose):
        """Descend lower level in xml tree. If we are in the symbols branch, \
the recursive behaviour is not triggered as we already handled the symbols' childs
"""
        tree = xml_tree['tree']
        node = xml_tree['node']
        if self.jump_symbol_child is True:
            self.jump_symbol_child = False
        else:
            for child in list(node):
                xml_tree_children = {'tree': tree, 'node': child}
                Nxdl2yaml.xmlparse(self, output_yml, xml_tree_children, depth, verbose)

    def xmlparse(self, output_yml, xml_tree, depth, verbose):
        """Main of the nxdl2yaml converter.
It parses XML tree,
then prints recursively each level of the tree

    """
        tree = xml_tree['tree']
        node = xml_tree['node']
        if verbose:
            sys.stdout.write(f'Node tag: {helpers.remove_namespace_from_tag(node.tag)}\n')
            sys.stdout.write(f'Attributes: {node.attrib}\n')
        with open(output_yml, "a") as file_out:
            tag = helpers.remove_namespace_from_tag(node.tag)
            if tag == ('definition'):
                self.found_definition = True
                Nxdl2yaml.handle_definition(self, node)
            if depth == 0 and not self.root_level_doc:
                Nxdl2yaml.handle_root_level_doc(self, node)
            if tag == ('doc') and depth != 1:
                parent = get_node_parent_info(tree, node)[0]
                doc_parent = helpers.remove_namespace_from_tag(parent.tag)
                if doc_parent != 'item':
                    yaml2nxdl_backward_tools.handle_not_root_level_doc(depth, node, file_out)
            if tag == ('symbols'):
                Nxdl2yaml.handle_symbols(self, depth, node)
                self.jump_symbol_child = True
            # End of root level definition parsing. Print root-level definitions in file
            if self.root_level_doc \
                    and self.append_flag is True \
                    and (depth in (0, 1)):
                Nxdl2yaml.print_root_level_doc(self, file_out)
            if self.found_definition is True and self.append_flag is True:
                Nxdl2yaml.print_root_level_info(self, depth, file_out)
            # End of print root-level definitions in file
            if tag in ('field', 'group') and depth != 0:
                yaml2nxdl_backward_tools.handle_group_or_field(depth, node, file_out)
            if tag == ('enumeration'):
                yaml2nxdl_backward_tools.handle_enumeration(depth, node, file_out)
            if tag == ('attribute'):
                yaml2nxdl_backward_tools.handle_attributes(depth, node, file_out)
            if tag == ('dimensions'):
                yaml2nxdl_backward_tools.handle_dimension(depth, node, file_out)
        depth += 1
        # Write nested nodes
        Nxdl2yaml.recursion_in_xml_tree(self, depth, xml_tree, output_yml, verbose)


def print_yml(input_file, verbose):
    """Parse an XML file provided as input and print a YML file

"""
    output_yml = input_file[:-9] + '_parsed.yml'
    if os.path.isfile(output_yml):
        os.remove(output_yml)
    my_file = Nxdl2yaml([], [])
    depth = 0
    tree = ET.parse(input_file)
    xml_tree = {'tree': tree.getroot(), 'node': tree.getroot()}
    my_file.xmlparse(output_yml, xml_tree, depth, verbose)


def append_yml(input_file, append, verbose):
    """Append to an existing Nexus base class new elements provided in YML input file \
and print both an XML and YML file of the extended base class.

"""
    nexus_def_path = os.path.join(os.path.abspath(os.path.dirname(__file__)), '../../definitions')
    assert [s for s in os.listdir(os.path.join(nexus_def_path, 'base_classes')
                                  ) if append.strip() == s.split('.nxdl.xml')[0]], \
        'Your base class extension does not match any existing Nexus base classes'
    tree = ET.parse(os.path.join(nexus_def_path + '/base_classes', append + '.nxdl.xml'))
    root = tree.getroot()
    # warning: tmp files are printed on disk and removed at the ends!!
    pretty_print_xml(root, 'tmp.nxdl.xml')
    print_yml('tmp.nxdl.xml', verbose)
    yaml2nxdl('tmp_parsed.yml', verbose)
    tree = ET.parse('tmp_parsed.nxdl.xml')
    tree2 = ET.parse(input_file)
    root_no_duplicates = ET.Element(
        'definition', {'xmlns': 'http://definition.nexusformat.org/nxdl/3.1',
                       'xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance',
                       'xsi:schemaLocation': 'http://www.w3.org/2001/XMLSchema-instance'
                       }
    )
    for attribute_keys in root.attrib.keys():
        if attribute_keys != '{http://www.w3.org/2001/XMLSchema-instance}schemaLocation':
            attribute_value = root.attrib[attribute_keys]
            root_no_duplicates.set(attribute_keys, attribute_value)
    for elems in root.iter():
        if 'doc' in elems.tag:
            root_doc = ET.SubElement(root_no_duplicates, 'doc')
            root_doc.text = elems.text
            break
    group = '{http://definition.nexusformat.org/nxdl/3.1}group'
    root_no_duplicates = yaml2nxdl_backward_tools.compare_niac_and_my(tree, tree2, verbose,
                                                                      group,
                                                                      root_no_duplicates)
    field = '{http://definition.nexusformat.org/nxdl/3.1}field'
    root_no_duplicates = yaml2nxdl_backward_tools.compare_niac_and_my(tree, tree2, verbose,
                                                                      field,
                                                                      root_no_duplicates)
    attribute = '{http://definition.nexusformat.org/nxdl/3.1}attribute'
    root_no_duplicates = yaml2nxdl_backward_tools.compare_niac_and_my(tree, tree2, verbose,
                                                                      attribute,
                                                                      root_no_duplicates)
    pretty_print_xml(root_no_duplicates, f'{input_file.split(".", 1)[0]}'
                     f'_appended.nxdl.xml')
    print_yml(input_file.split(".", 1)[0] + '_appended.nxdl.xml', verbose)
    yaml2nxdl(input_file.split(".", 1)[0] + '_appended_parsed.yml', verbose)
    os.rename(f'{input_file.split(".", 1)[0]}_appended_parsed.yml',
              f'{input_file.split(".", 1)[0]}_appended.yml')
    os.rename(f'{input_file.split(".", 1)[0]}_appended_parsed.nxdl.xml',
              f'{input_file.split(".", 1)[0]}_appended.nxdl.xml')
    os.remove('tmp.nxdl.xml')
    os.remove('tmp_parsed.yml')
    os.remove('tmp_parsed.nxdl.xml')


@click.command()
@click.option(
    '--input-file',
    help='The path to the XML or YAML input data file to read and create \
a YAML or XML file from, respectively.'
)
@click.option(
    '--append',
    help='Parse xml file and append to base class, given that the xml file has same name \
of an existing base class'
)
@click.option(
    '--verbose',
    is_flag=True,
    default=False,
    help='Print in standard output keywords and value types to help \
possible issues in yaml files'
)
def launch_tool(input_file, verbose, append):
    """Main function that distiguishes the input file format and launches the tools.

"""
    if input_file.split(".", 1)[1] in ('yml', 'yaml'):
        yaml2nxdl(input_file, verbose)
        if append:
            append_yml(input_file.split(".", 1)[0] + '.nxdl.xml', append, verbose)
        else:
            pass
    elif input_file.split(".", 1)[1] == 'nxdl.xml':
        if not append:
            print_yml(input_file, verbose)
        else:
            append_yml(input_file, append, verbose)


if __name__ == '__main__':
    launch_tool().parse()  # pylint: disable=no-value-for-parameter
