import re
import os

from enum import Enum, auto
from abc import abstractmethod

from arjuna.core.enums import GuiAutomationContext
from arjuna.interact.gui.auto.impl.locator.emd import GuiElementMetaData, Locator, ImplWith
from arjuna.tpi.guiauto.helpers import With
from arjuna.tpi.enums import ArjunaOption

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

class GuiNamespaceLoaderFactory:

    # Returns GuiNamespaceLoader
    @classmethod
    def create_namespace_loader(cls, config, ns_file_path):
        multi_context_enabled = config.get_arjuna_option_value(ArjunaOption.GUIAUTO_DEF_MULTICONTEXT).as_bool()
        context = multi_context_enabled and None or config.get_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)
            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, name, context):
        if not self.has(name):
            raise Exception("Gui namespace >{}< does not contain element with name: {}".format(self.__name, name))
        elif not self.has_context(name, context):
            raise Exception("Gui namespace >{}< does not contain element with name: {} for context {}".format(self.__name, name, context))
        
        return self.__ns[name.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 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

        #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
            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)
