'''
This file is a part of Arjuna
Copyright 2015-2020 Rahul Verma

Website: www.RahulVerma.net

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.
'''

import re
import os

from enum import Enum, auto
from abc import abstractmethod

from arjuna.core.enums import GuiAutomationContext
from arjuna.core.exceptions import GuiLabelNotPresentError
from arjuna.interact.gui.auto.finder.emd import GuiElementMetaData, Locator, ImplWith

class FileFormat(Enum):
    # GNS = auto()
    # MGNS = auto()
    XLS = auto()
    XLSX = auto()
    YAML = auto()

class GuiNamespaceLoaderFactory:

    # Returns GuiNamespaceLoader
    @classmethod
    def create_namespace_loader(cls, config, ns_file_path):
        from arjuna.core.enums import ArjunaOption
        multi_context_enabled = config.value(ArjunaOption.GUIAUTO_DEF_MULTICONTEXT)
        context = multi_context_enabled and None or config.guiauto_context
        _, file_extension = os.path.splitext(ns_file_path)
        ext = file_extension.upper()[1:]
        considered_path = ns_file_path
        try:
            file_format = FileFormat[ext]
        except:
            raise Exception("Unsupported format for namespace: {}".format(file_extension))
        else:
            full_file_path = ns_file_path
            if os.path.isdir(full_file_path):
                raise Exception("Namespace file path is a directory and not a file: {}".format(considered_path))
            elif not os.path.isfile(full_file_path):
                raise Exception("Namespace file path is invalid: {}".format(considered_path))
            # if file_format == FileFormat.GNS:
            #     if multi_context_enabled:
            #         return MGNSFileLoader(full_file_path)
            #     else:
            #         return GNSFileLoader(full_file_path, context)
            if file_format == FileFormat.YAML:
                return YamlGnsLoader(full_file_path, context)
            else:
                raise Exception("Unsupported format for namespace: {}".format(file_extension))


class GuiNamespace:

    def __init__(self, name):
        self.__name = name
        # dict <string, dict<GuiAutomationContext, GuiElementMetaData>>
        self.__ns = {}

    def add_element_meta_data(self, name, context, raw_locators):
        emd = GuiElementMetaData.create_lmd(*raw_locators)
        #emd = GuiElementMetaData(raw_locators, process_args=False)
        name = name.lower()
        if not self.has(name):
            self.__ns[name] = {}
        self.__ns[name][context] = emd

    def has(self, name):
        return name.lower() in self.__ns

    def has_context(self, name, context):
        if self.has(name):
            return context in self.__ns[name.lower()]
        return False

    # Needs to be thread-safe
    # Returns emd for a context for a given gui name
    def get_meta_data(self, label, context):
        if not self.has(label):
            raise GuiLabelNotPresentError(self.__name, label)
        elif not self.has_context(label, context):
            raise GuiLabelNotPresentError(self.__name, label, context)
        
        return self.__ns[label.lower()][context]


class BaseGuiNamespaceLoader:

    def __init__(self, name):
        self.__name = name
        self.__namespace = GuiNamespace(name)

    @property
    def name(self):
        return self.__name

    @property
    def namespace(self):
        return self.__namespace

    # Needs to be thread safe
    def add_element_meta_data(self, name, context, locators):
        self.__namespace.add_element_meta_data(name, context, locators)

    def _raise_notafile_exception(self, file_path):
        raise Exception("{} is not a file.".format(file_path))

    def _raise_filenotfound_exception(self, file_path):
        raise Exception("{} is not a valid file path.".format(file_path))

    def _raise_relativepath_exception(self, file_path):
        raise Exception("Gui namespace loader does not accept relative file path. {} is not a full file path.".format(file_path))


class YamlGnsLoader(BaseGuiNamespaceLoader):

    def __init__(self, ns_file_path, context):
        super().__init__(os.path.basename(ns_file_path))
        self.__context = context
        self.__ns_file = None
        self.__ns_path = None

        #Map<String, Map<GuiAutomationContext,List<Locator>>>
        self.__ns = {}

        if not os.path.isabs(ns_file_path):
            super()._raise_relativepath_exception(ns_file_path)
        elif not os.path.exists(ns_file_path):
            super()._raise_filenotfound_exception(ns_file_path)
        elif not os.path.isfile(ns_file_path):
            super()._raise_notafile_exception(ns_file_path)

        self.__ns_path = ns_file_path
        self.__ns_file = open(self.__ns_path, "r")
        self.__contexts = [context]

        self.__withx = None

        # self.__process()

    def load(self):

        from arjuna.configure.impl.validator import ConfigValidator
        import yaml

        ydict = yaml.load(self.__ns_file, Loader=yaml.SafeLoader)

        if not ydict: return

        sections = list(ydict.keys())
        lsections = [k.lower() for k in sections]

        if "labels" not in lsections:
            # print("No labels configured. Skipping...")
            return

        if "withx" in lsections:
            self.__withx = {k.lower():v for k,v in ydict[sections[lsections.index("withx")]].items()}
        else: 
            self.__withx = dict()

        for label, label_map in ydict[sections[lsections.index("labels")]].items():
            ConfigValidator.arjuna_name(label)
            self.__ns[label.lower()] = dict()
            self.__ns[label.lower()][self.__context] = []
            for loc, loc_obj in label_map.items():
                if loc.lower() in self.__withx:
                    if type(loc_obj) is str:
                        loc_obj = [loc_obj]
                    fmt = self.__withx[loc.lower()]
                    fmt_wtype =  fmt["with_type"].strip().upper()
                    fmt_value = fmt["with_value"].strip()
                    with_value = fmt_value.format(*loc_obj)
                    iloc = ImplWith(wtype=fmt_wtype.upper(), wvalue=with_value, named_args=dict(), has_content_locator=False)
                    self.__ns[label.lower()][self.__context].append(iloc)
                else:
                    iloc = ImplWith(wtype=loc.upper(), wvalue=loc_obj, named_args=dict(), has_content_locator=False)
                    self.__ns[label.lower()][self.__context].append(iloc)
            if not self.__ns[label.lower()][self.__context]:
                raise Exception("No locators defined for label: {}".format(label))

        if "load" in lsections:
            self.__load_targets = {k.lower():v for k,v in ydict[sections[lsections.index("load")]].items()}        

            if "root" in self.__load_targets:
                self.__ns["__root__"] = dict()
                self.__ns["__root__"][self.__context] = list()
                self.__ns["__root__"][self.__context].extend(self.__ns[self.__load_targets["root"]][self.__context])

            if "anchor" in self.__load_targets:
                self.__ns["__anchor__"] = dict()
                self.__ns["__anchor__"][self.__context] = list()
                self.__ns["__anchor__"][self.__context].extend(self.__ns[self.__load_targets["anchor"]][self.__context])

        for ename, context_data in self.__ns.items():
            for context, locators in context_data.items():
                self.add_element_meta_data(ename, context, locators)

'''

class AbstractGNFileLoader(BaseGuiNamespaceLoader):
    NAME_PATTERN = re.compile(r"\[\s*(.*?)\s*\]$")
    LOCATOR_PATTERN = re.compile(r"\s*(.*?)\s*=\s*(.*?)\s*$")

    def __init__(self, ns_file_path):
        super().__init__(os.path.basename(ns_file_path))
        self.__ns_file = None
        self.__ns_path = None
        self.__header_found = False
        self.__last_header = None
        self.__last_auto_contexts = None # list of auto contexts
        self.__withx = dict()

        #Map<String, Map<GuiAutomationContext,List<Locator>>>
        self.__ns = {}

        if not os.path.isabs(ns_file_path):
            super()._raise_relativepath_exception(ns_file_path)
        elif not os.path.exists(ns_file_path):
            super()._raise_filenotfound_exception(ns_file_path)
        elif not os.path.isfile(ns_file_path):
            super()._raise_notafile_exception(ns_file_path)

        self.__ns_path = ns_file_path
        self.__ns_file = open(self.__ns_path)

    @property
    def header_found(self):
        return self.__header_found

    @header_found.setter
    def header_found(self, flag):
        self.__header_found = flag

    @property
    def last_header(self):
        return self.__last_header

    @last_header.setter
    def last_header(self, name):
        self.__last_header = name

    @property
    def last_auto_contexts(self):
        return self.__last_auto_contexts

    @last_auto_contexts.setter
    def last_auto_contexts(self, contexts):
        self.__last_auto_contexts = contexts

    @property
    def ns_path(self):
        return self.__ns_path

    @property
    def ns_file(self):
        return self.__ns_file

    def load(self):
        for line in self.__ns_file.readlines():
            line = line.strip()
            if not line: 
                continue
            if self._match_header(line):
                self.header_found = True
                self._init_section()
                continue
            else:
                if not self.header_found:
                    raise Exception("Namespace contents must be contained inside a [name] header.")
                else:
                    self._load_section_line(line)
        
        self.__ns_file.close()

        for ename, context_data in self.__ns.items():
            for context, locators in context_data.items():
                self.add_element_meta_data(ename, context, locators)

    def __is_defined(self, name):
        return name.lower() in self.__ns

    def __is_not_first_header(self):
        return self.last_header is not None

    def __validate_duplicate_entry(self, last_name, new_name):
        if (last_name.lower() == new_name.lower()) or self.__is_defined(new_name):
            raise Exception("Found duplicate namespace definition for {} element.".format(new_name))

    def __validate_empty_last_section(self, name):
        if len(self.__ns[name]) == 0:
            raise Exception("Found empty namespace definition for {} element.".format(name))
        else:
            for context, data in self.__ns[name].items():
                if len(data) == 0:
                    raise Exception("Found empty namespace definition for {} context for {} element.".format(context.name, name))


    def _match_header(self, input):
        match = self.NAME_PATTERN.match(input)
        if match:
            current_header = match.group(1)

            if self.__is_not_first_header():
                self.__validate_duplicate_entry(self.last_header, current_header)
                self.__validate_empty_last_section(self.last_header)
            
            # Initialise for new section found
            self.last_header = current_header
            self.last_auto_contexts = None
            if self.last_header.lower() not in {"__root__", "__state__"}:
                from arjuna.configure.impl.validator import ConfigValidator
                ConfigValidator.arjuna_name(self.last_header)
            self.__ns[self.last_header] = {}
            return True
        else:
            return False

    def _init_contexts_dict(self, name, contexts):
        for context in contexts:
            if context in self.__ns[name]:
                raise Exception("Found duplicate automation context {} in {} namespace definition.".format(context.name, self.last_header))
            else:
                self.__ns[name][context] = []

    @abstractmethod
    def _match_contexts(self, input):
        pass

    @abstractmethod
    def _load_section_line(self, input):
        pass

    @abstractmethod
    def _init_section(self):
        pass

    def _match_locator(self, input):
        match = self.LOCATOR_PATTERN.match(input)
        if match:       
            # locator = Locator(match.group(1), match.group(2), named_args=dict())
            locator = ImplWith(wtype=match.group(1).upper(), wvalue=match.group(2), named_args=dict(), has_content_locator=False)
            # locator = getattr(With, match.group(1).lower())(match.group(2)) # e.g. getattr(With, "ID".lower())("abc")
            if (self.last_auto_contexts is None):
                raise Exception("Locators must be preceded with context information as #context1, context2 construct. Current line: " + input)   

            for context in self.last_auto_contexts:
                self.__ns[self.last_header][context].append(locator)
            return True
        else:
            return False

class GNSFileLoader(AbstractGNFileLoader):

    def __init__(self, ns_file_path, context):
        super().__init__(ns_file_path)
        self.__contexts = [context]

    @property
    def contexts(self):
        return self.__contexts

    def _init_section(self):
        self.last_auto_contexts = self.__contexts
        self._init_contexts_dict(self.last_header, self.contexts)

    def _match_contexts(self, input):
        pass

    def _load_section_line(self, line):
        if self._match_locator(line):
            return
        else:
            raise Exception("Unexpected namespace file entry. Namspace content can either be plaforms or identification definition: " + line)


class MGNSFileLoader(AbstractGNFileLoader):
    PLATFORM_PATTERN = re.compile(r"\s*\#\s*(.*?)\s*$")

    def __init__(self, ns_file_path):
        super().__init__(ns_file_path)

    def _init_section(self):
        pass

    def _match_contexts(self, input):
        match = self.PLATFORM_PATTERN.match(input)
        if match:        
            try:
                raw_contexts = [m.strip().upper() for m in match.group(1).split(",")]
                contexts = [GuiAutomationContext[n] for n in raw_contexts]
            except Exception as e:
                raise Exception("Invalid context name found in header: {}".format(e))
            else:
                self._init_contexts_dict(self.last_header, contexts)
            
            self.last_auto_contexts = contexts
            return True
        else:
            return False

    def _load_section_line(self, line):
        if self._match_contexts(line):
            return
        elif self._match_locator(line):
            return
        else:
            raise Exception("Unexpected namespace file entry. Namspace content can either be plaforms or identification definition: " + line)


# Temp hook. We'll try YAML format with this. Once it works, then we can change the rest.
'''