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

from .enums import WithType, GenericLocateWith
from arjuna.core.utils.repr_utils import repr_dict

class _WithXMetaClass(type):

    def __getattr__(cls, mname):
        from arjuna import Arjuna 
        withx_ref = Arjuna.get_withx_ref()
        if not withx_ref.has_locator(mname):
            raise Exception("There is no built-in or extended locator available as With.{}".format(mname))

        def withx(*vargs, **kwargs): 
            wtype, wvalue = withx_ref.format_args(mname, vargs, kwargs)
            return getattr(With, wtype.lower())(wvalue)
        return withx


class With(metaclass=_WithXMetaClass):

    def __init__(self, with_type, with_value):
        '''

        :param with_type: an enum constant of type WithType
        :param with_value: a string or int or a With object.
        '''
        self.__with_type = with_type
        self.__with_value = None
        self.__has_content_locator = False
        self.__content_locator = None

        if with_type == WithType.ELEMENT:
            if not isinstance(with_value, With):
                raise Exception("For identification with element locator, the argument must be a With object.")
            else:
                self.__has_content_locator = True
                self.__content_locator = with_value
        else:
            self.__with_value = with_value

    @property
    def wtype(self):
        return self.__with_type

    @property
    def wvalue(self):
        return self.__with_value

    @wvalue.setter
    def wvalue(self, value):
        self.__with_value = value

    def as_impl_locator(self):

        impl_with = ImplWith(
            wtype = self.__with_type.name,
            wvalue = self.__has_content_locator and self.__content_locator.as_impl_locator() or self.__with_value,
            has_content_locator = self.__has_content_locator
        )

        return impl_with

    def as_map(self):
        map = dict()
        map["withType"] = self.__with_type.name
        if not self.__has_content_locator:
            map["withValue"] = self.__with_value
        else:
            map["withValue"] = self.__content_locator.as_map()
        
        return map

    @classmethod
    def id(cls, id):
        return With(WithType.ID, id)

    @classmethod
    def name(cls, name):
        return With(WithType.NAME, name)

    @classmethod
    def classes(cls, *names):
        return With(WithType.CLASSES, names)

    @classmethod
    def link(cls, text):
        return With(WithType.LINK, text)

    @classmethod
    def flink(cls, text):
        return With(WithType.FLINK, text)

    @classmethod
    def selector(cls, selector):
        return With(WithType.SELECTOR, selector)

    @classmethod
    def tag(cls, selector):
        return With(WithType.TAG, selector)

    @classmethod
    def xpath(cls, xpath):
        return With(WithType.XPATH, xpath)

    @classmethod
    def text(cls, text, tag=None):
        return With(WithType.TEXT, text)

    @classmethod
    def ftext(cls, text, tag=None):
        return With(WithType.FTEXT, text)

    @classmethod
    def btext(cls, text, tag=None):
        return With(WithType.BTEXT, text)

    @classmethod
    def etext(cls, text, tag=None):
        return With(WithType.ETEXT, text)

    @classmethod
    def title(cls, title, tag=None):
        return With(WithType.TITLE, title)

    @classmethod
    def __validate_attr_map(self, map):
        try:
            rmap = {k.lower():v for k,v in map.items()}
            rmap["name"]
            rmap["value"]
            return rmap
        except:
            raise Exception("Name and value must be supplied for {}. Got: {}".format(rlvalue, rltype))

    @classmethod
    def _attr(cls, map):
        map = cls.__validate_attr_map(map)
        return With(WithType.ATTR, map)

    @classmethod
    def _fattr(cls, map):
        map = cls.__validate_attr_map(map)
        return With(WithType.FATTR, map)

    @classmethod
    def _battr(cls, map):
        map = cls.__validate_attr_map(map)
        return With(WithType.BATTR, map)

    @classmethod
    def _eattr(cls, map):
        map = cls.__validate_attr_map(map)
        return With(WithType.EATTR, map)   

    @classmethod
    def attr(cls, map):
        return cls._attr(map)

    @classmethod
    def fattr(cls, map):
        return cls._fattr(map)

    @classmethod
    def battr(cls, map):
        return cls._battr(map)

    @classmethod
    def eattr(cls, map):
        return cls._eattr(map)

    @classmethod
    def value(cls, value, tag=None):
        return With(WithType.VALUE, value)

    @classmethod
    def type(cls, type):
        return With(WithType.TYPE, type)

    @classmethod
    def point(cls, point):
        return With(WithType.POINT, point)

    @classmethod
    def js(cls, js):
        return With(WithType.JS, js)

    @classmethod
    def index(cls, index):
        return With(WithType.INDEX, index)

    @classmethod
    def window_title(cls, title):
        return With(WithType.WINDOW_TITLE, title)

    @classmethod
    def window_ptitle(cls, ptitle):
        return With(WithType.WINDOW_PTITLE, ptitle)

    @classmethod
    def element(cls, with_obj):
        return With(WithType.ELEMENT, with_obj)

    @classmethod
    def label(cls, name):
        return With(WithType.LABEL, name)

class ImplWith:
    
    def __init__(self, *, wtype, wvalue, has_content_locator):
        self.wtype = wtype
        self.wvalue = wvalue
        self.has_content_locator = has_content_locator

    def __str__(self):
        return "ImplWith: {}".format(vars(self))


class Locator:

    def __init__(self, ltype, lvalue=None):
        self.ltype = ltype
        self.lvalue = lvalue

    def set_value(self, value):
        self.lvalue = value

    def create_formatted_locator(self, **fargs):

        def get_global_value(in_str):
            from arjuna import C, L, R
            gtype, query = in_str.split(".", 1)
            gtype = gtype.upper()
            return locals()[gtype](query)


        pattern = r"\$(\s*[\w\.]*?\s*)\$"
        repl_dict = {k.lower():v for k,v in fargs.items()}

        new_locator = Locator(self.ltype)

        if self.ltype == GenericLocateWith.ELEMENT:
            new_locator.set_value(locator.lvalue.create_formatted_locator(**fargs))
            return new_locator

        fmt_locator_value = self.lvalue.replace("\{", "__LB__").replace("}", "__RB__")

        # Find params
        matches = re.findall(pattern, fmt_locator_value)
        
        for match in matches:
            names_set = None
            target = "${}$".format(match)
            processed_name = match.lower().strip()
            repl_value = None
            if processed_name.startswith("c.") or processed_name.startswith("l.") or processed_name.startswith("r."):
                repl_value = get_global_value(processed_name)
            else:
                if processed_name not in repl_dict:
                    raise Exception("You must provide a named argument for a custom format string.")
                repl_value = repl_dict[processed_name]

            fmt_locator_value = fmt_locator_value.replace(target, repl_value)
            

        fmt_locator_value = fmt_locator_value.replace("__LB__", "{").replace("__RB__", "}")
        new_locator.set_value(fmt_locator_value)
        return new_locator

    def __str__(self):
        return repr_dict(vars(self), replace_value_enum=True)

    def is_layered_locator(self):
        return False

class GuiGenericLocator(Locator):

    def __init__(self, ltype, lvalue=None):
        super().__init__(ltype, lvalue)

    def as_map(self):
        map = dict()
        map["withType"] = self.ltype.name
        if self.ltype.name.lower() != "content_locator":
            map["withValue"] = self.lvalue
        else:
            map["withValue"] = [l.as_map() for l in self.lvalue.locators]

        # if self.__named_args:
        #     map["named_args"] = self.__named_args
        
        return map

# class GuiGenericChildLocator(Locator):
#     def __init__(self, ltype, lvalue):
#         super().__init__(ltype, lvalue)
    
#     def set_value(self, value):
#         self.lvalue = value    

#     def is_layered_locator(self):
#         return True
