#    Copyright (c) 2016 Mirantis, Inc.
#
#    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 six

from yaql.language import expressions
from yaql.language import runner
from yaql.language import specs
from yaql.language import utils
from yaql.language import yaqltypes
from yaql import yaqlization


REGEX_TYPE = type(re.compile('.'))


class Yaqlized(yaqltypes.GenericType):
    def __init__(self, can_access_attributes=False, can_call_methods=False,
                 can_index=False):
        def check_value(value, context, *args, **kwargs):
            settings = yaqlization.get_yaqlization_settings(value)
            if settings is None:
                return False
            if can_access_attributes and not settings['yaqlizeAttributes']:
                return False
            if can_call_methods and not settings['yaqlizeMethods']:
                return False
            if can_index and not settings['yaqlizeIndexer']:
                return False
            return True

        super(Yaqlized, self).__init__(checker=check_value, nullable=False)


def _match_name_to_entry(name, entry):
    if name == entry:
        return True
    elif isinstance(entry, REGEX_TYPE):
        return entry.search(name) is not None
    elif six.callable(entry):
        return entry(name)
    return False


def _validate_name(name, settings, exception_cls=AttributeError):
    if name.startswith('_'):
        raise exception_cls('Cannot access ' + name)
    whitelist = settings['whitelist']
    if whitelist:
        for entry in whitelist:
            if _match_name_to_entry(name, entry):
                return
        raise exception_cls('Cannot access ' + name)
    blacklist = settings['blacklist']
    if blacklist:
        for entry in blacklist:
            if _match_name_to_entry(name, entry):
                raise exception_cls('Cannot access ' + name)


def _remap_name(name, settings):
    return settings['attributeRemapping'].get(name, name)


def _auto_yaqlize(value, settings):
    if not settings['autoYaqlizeResult']:
        return
    if isinstance(value, type):
        cls = value
    else:
        cls = type(value)
    if cls.__module__ == int.__module__:
        return
    try:
        yaqlization.yaqlize(value, auto_yaqlize_result=True)
    except Exception:
        pass


@specs.parameter('receiver', Yaqlized(can_call_methods=True))
@specs.parameter('expr', yaqltypes.YaqlExpression(expressions.Function))
@specs.name('#operator_.')
def op_dot(receiver, expr, context, engine):
    settings = yaqlization.get_yaqlization_settings(receiver)
    mappings = _remap_name(expr.name, settings)

    _validate_name(expr.name, settings)
    if not isinstance(mappings, six.string_types):
        name = mappings[0]
        if len(mappings) > 0:
            arg_mappings = mappings[1]
        else:
            arg_mappings = {}
    else:
        name = mappings
        arg_mappings = {}

    func = getattr(receiver, name)
    args, kwargs = runner.translate_args(False, expr.args, {})
    args = tuple(arg(utils.NO_VALUE, context, engine) for arg in args)
    for key, value in six.iteritems(kwargs):
        kwargs[arg_mappings.get(key, key)] = value(
            utils.NO_VALUE, context, engine)
    res = func(*args, **kwargs)
    _auto_yaqlize(res, settings)
    return res


@specs.parameter('obj', Yaqlized(can_access_attributes=True))
@specs.parameter('attr', yaqltypes.Keyword())
@specs.name('#operator_.')
def attribution(obj, attr):
    settings = yaqlization.get_yaqlization_settings(obj)
    _validate_name(attr, settings)
    attr = _remap_name(attr, settings)
    res = getattr(obj, attr)
    _auto_yaqlize(res, settings)
    return res


@specs.parameter('obj', Yaqlized(can_index=True))
@specs.name('#indexer')
def indexation(obj, key):
    settings = yaqlization.get_yaqlization_settings(obj)
    _validate_name(key, settings, KeyError)
    res = obj[key]
    _auto_yaqlize(res, settings)
    return res


def register(context):
    context = context.create_child_context()
    context.register_function(op_dot)
    context.register_function(attribution)
    context.register_function(indexation)
    return context
