"""This module contains functions for patching properties' and entities' objects generated by PyOData library.

The objects created by PyOData library are altered in order to set up a proper interface for a next generation and
mutation. When the fuzzer starts generating, or mutating, requests, it goes through this common routine frequently:
1. A selection of a property that is destined to be used in the filter query option.
2. A value generation for the corresponding property, based on its type and other characteristics.
The second stage is crucial for a computational time and a code complexity. ODfuzz omits all unnecessary checks for
such characteristics of a property by binding new methods for each property.

In this module, the methods 'operators()', 'generate()' and 'mutate()' are dynamically patched to the existing
properties' objects. Therefore, it is possible to call `property.generate()` or `property.mutate(old_value)`, while
the implementation details of the methods are hidden and created values for the property are *always* valid according
to the specification given by metadata protocol (e.g. string size, precision of decimal numbers, etc.).

Same applies for entity sets. But in this case we are constantly searching for associated entities. A method for
iterating over associated ends is bounded to a particular entity set. This saves a lot of processing time because
the associated ends are tied to the corresponding entity sets.
"""

import random
import logging
import types

from pyodata.v2.model import VariableDeclaration, ComplexType

from odfuzz.generators import (
    EdmString,
    EdmDateTime,
    EdmBoolean,
    EdmByte,
    EdmSByte,
    EdmSingle,
    EdmGuid,
    EdmDecimal,
    EdmDateTimeOffset,
    EdmTime,
    EdmBinary,
    EdmDouble,
    EdmInt16,
    EdmInt32,
    EdmInt64,
)
from odfuzz.mutators import (
    BooleanMutator,
    DateTimeMutator,
    DecimalMutator,
    GuidMutator,
    NumberMutator,
    StringMutator,
)
from odfuzz.constants import (
    BOOLEAN_OPERATORS,
    EXPRESSION_OPERATORS,
    INTERVAL_OPERATORS,
    VALUE,
)

MAX_STRING_LENGTH = 100
MAX_PRECISION = 20
MAX_SCALE = 10


def patch_entity_set(entity_set, association_sets):
    """
    Add a reference to the corresponding association set ends. By default, entity sets provided by PyOData
    do not dispose this reference. The ends are stored in the list.

    :param entity_set: An entity set which is going to be patched.
    :param association_sets: An iterable with all association sets.
    :return: None
    """

    def association_set_ends_generator(self):
        for association_end in association_set_ends:
            yield association_end

    association_set_ends = []
    for association_set in association_sets:
        for end in association_set.end_roles:
            if end.entity_set.name == entity_set.name:
                association_set_ends.append(end)

    entity_set.association_set_ends = types.MethodType(association_set_ends_generator, entity_set)


def patch_proprties(entity_set_name, proprties, restrictions):
    for proprty in proprties:
        patch_complex_types(entity_set_name, proprty, restrictions)
        patch_proprty_max_length(proprty)
        patch_proprty_precision_scale(proprty)
        patch_proprty_generator(entity_set_name, proprty, restrictions)
        patch_proprty_mutator(entity_set_name, proprty, restrictions)
        patch_proprty_operator(proprty)


def patch_complex_types(entity_set_name, proprty, restrictions):
    if isinstance(proprty.typ, ComplexType):
        patch_proprties(entity_set_name, proprty.typ.proprties(), restrictions)


def patch_proprty_max_length(proprty):
    proprty_type = proprty.typ.name
    if proprty_type == 'Edm.String':
        if not proprty.max_length or proprty.max_length == VariableDeclaration.MAXIMUM_LENGTH:
            proprty._max_length = MAX_STRING_LENGTH


def patch_proprty_precision_scale(proprty):
    proprty_type = proprty.typ.name
    if proprty_type == 'Edm.Decimal':
        if proprty.precision == 0:
            proprty._precision = MAX_PRECISION
            if proprty.scale == 0:
                proprty._scale = MAX_SCALE


def patch_proprty_generator(entity_set_name, proprty, restrictions):
    filter_restriction = restrictions.get(VALUE)
    if filter_restriction.include:
        entity_set_restr = filter_restriction.include.get(entity_set_name)
        if entity_set_restr:
            proprty_restr = entity_set_restr.get(proprty.name)
            if proprty_restr:
                proprty.generate = lambda: random.choice(proprty_restr)
                return

    proprty_type = proprty.typ.name
    if proprty_type == 'Edm.String':
        proprty.generate = types.MethodType(EdmString.generate, proprty)
    elif proprty_type == 'Edm.DateTime':
        proprty.generate = EdmDateTime.generate
    elif proprty_type == 'Edm.Boolean':
        proprty.generate = EdmBoolean.generate
    elif proprty_type == 'Edm.Byte':
        proprty.generate = EdmByte.generate
    elif proprty_type == 'Edm.SByte':
        proprty.generate = EdmSByte.generate
    elif proprty_type == 'Edm.Single':
        proprty.generate = EdmSingle.generate
    elif proprty_type == 'Edm.Guid':
        proprty.generate = EdmGuid.generate
    elif proprty_type == 'Edm.Decimal':
        proprty.generate = types.MethodType(EdmDecimal.generate, proprty)
    elif proprty_type == 'Edm.DateTimeOffset':
        proprty.generate = EdmDateTimeOffset.generate
    elif proprty_type == 'Edm.Time':
        proprty.generate = EdmTime.generate
    elif proprty_type == 'Edm.Binary':
        proprty.generate = EdmBinary.generate
    elif proprty_type == 'Edm.Double':
        proprty.generate = EdmDouble.generate
    elif proprty_type.startswith('Edm.Int'):
        if proprty_type.endswith('16'):
            proprty.generate = EdmInt16.generate
        elif proprty_type.endswith('32'):
            proprty.generate = EdmInt32.generate
        elif proprty_type.endswith('64'):
            proprty.generate = EdmInt64.generate
        else:
            logging.info('Property type {} is not supported by generator yet'.format(proprty_type))
    else:
        logging.info('Property type {} is not supported by generator yet'.format(proprty_type))


def patch_proprty_mutator(entity_set_name, proprty, restrictions):
    filter_restriction = restrictions.get(VALUE)
    if filter_restriction.include:
        entity_set_restr = filter_restriction.include.get(entity_set_name)
        if entity_set_restr:
            proprty_restr = entity_set_restr.get(proprty.name)
            if proprty_restr:
                proprty.mutate = lambda _: random.choice(proprty_restr)
                return

    proprty_type = proprty.typ.name
    if proprty_type == 'Edm.String':
        proprty.mutate = types.MethodType(StringMutator._mutate, proprty)
    elif proprty_type.startswith('Edm.Int'):
        proprty.mutate = types.MethodType(NumberMutator._mutate, proprty)
    elif proprty_type == 'Edm.Decimal':
        proprty.mutate = types.MethodType(DecimalMutator._mutate, proprty)
    elif proprty_type == 'Edm.Guid':
        proprty.mutate = GuidMutator.replace_char
    elif proprty_type == 'Edm.Boolean':
        proprty.mutate = BooleanMutator.flip_value
    elif proprty_type == 'Edm.DateTime':
        proprty.mutate = types.MethodType(DateTimeMutator._mutate, proprty)
    else:
        proprty.mutate = lambda value: value
        logging.info('Property type {} is not supported by mutator yet'.format(proprty_type))


def patch_proprty_operator(proprty):
    proprty_type = proprty.typ.name
    if proprty.filter_restriction in ('single-value', 'multi-value'):
        proprty.operators = Operators({'eq': 1.0})
    elif proprty.filter_restriction == 'interval':
        proprty.operators = IntervalOperators((INTERVAL_OPERATORS, {'eq': 1.0}))
    elif proprty_type == 'Edm.Boolean':
        proprty.operators = Operators(BOOLEAN_OPERATORS)
    else:
        proprty.operators = Operators(EXPRESSION_OPERATORS)


class Operators:
    def __init__(self, operators):
        self._operators = operators

    def get_all(self):
        return self._operators.items()


class IntervalOperators:
    def __init__(self, operators_groups):
        self._operators_groups = operators_groups

    def get_all(self):
        operators = random.choice(self._operators_groups)
        return operators.items()
