'''
This file is a part of Test Mile Arjuna
Copyright 2018 Test Mile Software Testing Pvt Ltd

Website: www.TestMile.com
Email: support [at] testmile.com
Creator: Rahul Verma

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 abc
import inspect
from numbers import Number as PyNumber
from weakref import WeakKeyDictionary
from enum import Enum

from . import constants


def _get_cstr(context=None):
    return context and "Encountered Type error for {}.".format(context) or ""


class TypeValidationError(TypeError):
    pass


class Descriptor:
    pass

class String(Descriptor):
    @staticmethod
    def validate(input, context=None):
        if String.check(input):
            raise TypeValidationError("{} {} [type: {}] is not a string.".format(_get_cstr(context)))
        return input

    @staticmethod
    def check(input):
        return isinstance(input, str)

class Integer(Descriptor):
    @staticmethod
    def validate(input, context=None):
        if Integer.check(input):
            raise TypeValidationError("{} {} [type: {}] is not a integer.".format(_get_cstr(context)))
        return input

    @staticmethod
    def check(input):
        return isinstance(input, str)

class Float(Descriptor):
    @staticmethod
    def validate(input, context=None):
        if Float.check(input):
            raise TypeValidationError("{} {} [type: {}] is not a flot.".format(_get_cstr(context)))
        return input

    @staticmethod
    def check(input):
        return isinstance(input, str)

class Number(Descriptor):
    @staticmethod
    def validate(input, context=None):
        if Number.check(input):
            raise TypeValidationError("{} {} [type: {}] is not a number.".format(_get_cstr(context)))
        return input

    @staticmethod
    def check(input):
        return isinstance(input, PyNumber)

class Bool(Descriptor):
    @staticmethod
    def validate(input, context=None):
        if Bool.check(input):
            raise TypeValidationError("{} {} [type: {}] is not a bool.".format(_get_cstr(context)))
        return input

    @staticmethod
    def check(input):
        return isinstance(input, bool)

    @staticmethod
    def force_convert(input):
        if Bool.check(input):
            return input
        else:
            if input in constants.TRUES:
                return True
            elif input in constants.FALSES:
                return False
            else:
                return bool(input)

class Enumeration(Descriptor):
    @staticmethod
    def validate(input, context=None):
        if Enumeration.check(input):
            raise TypeValidationError("{} Provided enum_type: {} [type: {}] is not a class.".format(_get_cstr(context), input, type(input)))
        return input

    @staticmethod
    def check(input):
        if not inspect.isclass(input) or not issubclass(input, Enum):
            return False

class EnumConstant(Descriptor):
    @staticmethod
    def validate(input, context=None):
        if EnumConstant.check(input):
            raise TypeValidationError("{} {} [type: {}] is not a enum.".format(_get_cstr(context)))
        return input

    @staticmethod
    def check(input):
        return isinstance(input, Enum)

    @staticmethod
    def convert(enum_type, estr, context=None, enum_type_validated=False):
        if not enum_type_validated: Enumeration.validate(enum_type)

        if String.check(estr):
            uestr = estr.upper()
            if uestr in enum_type.__members__:
                return enum_type[uestr]
            else:
                raise TypeError("{} {} is not a valid enum constant for enum {}.".format(_get_cstr(context), estr, enum_type))

class EnumConstantList(Descriptor):
    @staticmethod
    def validate(input, context=None):
        if EnumConstantList.check(input):
            raise TypeValidationError("{} {} [type: {}] is not an enum constant list.".format(_get_cstr(context)))
        return input

    @staticmethod
    def check(input):
        if not List.check(input): return False
        for i in input:
            if not EnumConstant.check(i):
                return False
        return True

    @staticmethod
    def convert(enum_type, input, context=None):
        Enumeration.validate(enum_type)
        il = List.check(input) and input or [input]
        return [EnumConstant.convert(enum_type, estr, enum_type_validated=True) for estr in il]

class List(Descriptor):
    @staticmethod
    def validate(input, context=None):
        if not List.check(input):
            raise TypeValidationError("{} {} [type: {}] is not a list.".format(_get_cstr(context)))
        return input

    @staticmethod
    def check(input):
        return isinstance(input, list)

class Tuple(Descriptor):
    @staticmethod
    def validate(input, context=None):
        if not Tuple.check(input):
            raise TypeValidationError("{} {} [type: {}] is not a tuple.".format(_get_cstr(context)))
        return input

    @staticmethod
    def check(input):
        return isinstance(input, tuple)

class Set(Descriptor):
    @staticmethod
    def validate(input, context=None):
        if not Set.check(input):
            raise TypeValidationError("{} {} [type: {}] is not a set.".format(_get_cstr(context)))
        return input

    @staticmethod
    def check(input):
        return isinstance(input, set)

class ListTupleOrSet(Descriptor):

    @staticmethod
    def validate(input, context=None):
        if not ListTupleOrSet.check(input):
            raise TypeValidationError("{} {} [type: {}] is not a list, tuple or set.".format(_get_cstr(context)))
        return input

    @staticmethod
    def check(input):
        return List.check(input) or Tuple.check(input) or Set.check(input)

class StringList(Descriptor):
    @staticmethod
    def validate(input, context=None):
        if not isinstance(input, list):
            raise TypeValidationError("{} {} [type: {}] is not a list.".format(_get_cstr(context)))
        else:
            for i in input:
                String.validate(i, "Validating String List. Provided List: {}.".format(str(input)))
        return input

    @staticmethod
    def force_convert(input):
        if input is None:
            return None
        else:
            if String.check(input):
                return [input]
            elif Number.check(input):
                return [str(input)]
            elif ListTupleOrSet.check(input):
                return [str(i) for i in input]
            else:
                raise TypeError("Can not force convert >>{}<< to a string list.".format(str(input)))

class NaturalNumber(Descriptor):
    @staticmethod
    def validate(input, context=None):
        Integer.validate(input, context)
        if input <= 0:
            raise TypeValidationError("{} {} [type: {}] is not a natural number.".format(_get_cstr(context)))
        return input


class RangeBoundInt(Descriptor):
    @staticmethod
    def validate(input, left, right, context=None):
        Integer.validate(input, context)
        if input <= 0:
            raise TypeValidationError("{} {} [type: {}] is not in the range [{}, {}].".format(_get_cstr(context)))
        return input
