#    Copyright (c) 2014 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 collections
import contextlib
import functools
import gc
import inspect
import itertools
import re
import sys
import uuid
import weakref

import eventlet.greenpool
import eventlet.greenthread
from oslo_config import cfg
import semantic_version
from yaql.language import contexts
import yaql.language.exceptions
import yaql.language.expressions
from yaql.language import utils as yaqlutils

from murano.common import utils
from murano.dsl import constants
from murano.dsl import dsl_types
from murano.dsl import exceptions

_threads_sequencer = 0
# type string: ns.something.MyApp[/1.2.3-alpha][@my.package.fqn]
TYPE_RE = re.compile(r'([a-zA-Z0-9_.]+)(?:/([^@]+))?(?:@([a-zA-Z0-9_.]+))?$')
CONF = cfg.CONF


def evaluate(value, context, freeze=True):
    list_type = tuple if freeze else list
    dict_type = yaqlutils.FrozenDict if freeze else dict
    set_type = frozenset if freeze else set

    if isinstance(value, (dsl_types.YaqlExpression,
                          yaql.language.expressions.Statement)):
        return value(context)
    elif isinstance(value, yaqlutils.MappingType):
        return dict_type(
            (evaluate(d_key, context, freeze),
             evaluate(d_value, context, freeze))
            for d_key, d_value in value.items())
    elif yaqlutils.is_sequence(value):
        return list_type(evaluate(t, context, freeze) for t in value)
    elif isinstance(value, yaqlutils.SetType):
        return set_type(evaluate(t, context, freeze) for t in value)
    elif yaqlutils.is_iterable(value):
        return list_type(
            evaluate(t, context, freeze)
            for t in yaqlutils.limit_iterable(
                value, CONF.murano.dsl_iterators_limit))
    elif isinstance(value, dsl_types.MuranoObjectInterface):
        return value.object
    else:
        return value


def merge_lists(list1, list2):
    result = []
    for item in list1 + list2:
        if item not in result:
            result.append(item)
    return result


def merge_dicts(dict1, dict2, max_levels=0):
    result = {}
    for key, value1 in dict1.items():
        result[key] = value1
        if key in dict2:
            value2 = dict2[key]
            if type(value2) != type(value1):
                if ((isinstance(value1, str) or value1 is None) and
                        (isinstance(value2, str) or value2 is None)):
                    continue
                raise TypeError()
            if max_levels != 1 and isinstance(value2, dict):
                result[key] = merge_dicts(
                    value1, value2,
                    0 if max_levels == 0 else max_levels - 1)
            elif max_levels != 1 and isinstance(value2, list):
                result[key] = merge_lists(value1, value2)
            else:
                result[key] = value2
    for key, value1 in dict2.items():
        if key not in result:
            result[key] = value1
    return result


def generate_id():
    return uuid.uuid4().hex


def parallel_select(collection, func, limit=1000):
    # workaround for eventlet issue 232
    # https://github.com/eventlet/eventlet/issues/232
    context = get_context()
    object_store = get_object_store()

    def wrapper(element):
        try:
            with with_object_store(object_store), contextual(context):
                return func(element), False, None
        except Exception as e:
            return e, True, sys.exc_info()[2]

    gpool = eventlet.greenpool.GreenPool(limit)
    result = list(gpool.imap(wrapper, collection))
    try:
        exception = next(t for t in result if t[1])
    except StopIteration:
        return map(lambda t: t[0], result)
    else:
        utils.reraise(type(exception[0]), exception[0], exception[2])


def enum(**enums):
    return type('Enum', (), enums)


def get_context():
    current_thread = eventlet.greenthread.getcurrent()
    return getattr(current_thread, constants.TL_CONTEXT, None)


def get_executor():
    store = get_object_store()
    return None if store is None else store.executor


def get_type(context=None):
    context = context or get_context()
    return context[constants.CTX_TYPE]


def get_execution_session():
    executor = get_executor()
    return None if executor is None else executor.execution_session


def get_object_store():
    current_thread = eventlet.greenthread.getcurrent()
    return getattr(current_thread, constants.TL_OBJECT_STORE, None)


def get_package_loader():
    executor = get_executor()
    return None if executor is None else executor.package_loader


def get_this(context=None):
    context = context or get_context()
    return context[constants.CTX_THIS]


def get_caller_context(context=None):
    context = context or get_context()
    return context[constants.CTX_CALLER_CONTEXT]


def get_attribute_store():
    executor = get_executor()
    return None if executor is None else executor.attribute_store


def get_current_instruction(context=None):
    context = context or get_context()
    return context[constants.CTX_CURRENT_INSTRUCTION]


def get_current_method(context=None):
    context = context or get_context()
    return context[constants.CTX_CURRENT_METHOD]


def get_yaql_engine(context=None):
    context = context or get_context()
    return None if context is None else context[constants.CTX_YAQL_ENGINE]


def get_current_exception(context=None):
    context = context or get_context()
    return context[constants.CTX_CURRENT_EXCEPTION]


def are_property_modifications_allowed(context=None):
    context = context or get_context()
    return context[constants.CTX_ALLOW_PROPERTY_WRITES] or False


def get_names_scope(context=None):
    context = context or get_context()
    return context[constants.CTX_NAMES_SCOPE]


def get_class(name, context=None):
    context = context or get_context()
    murano_type = get_names_scope(context)
    name = murano_type.namespace_resolver.resolve_name(name)
    return murano_type.package.find_class(name)


def get_contract_passkey():
    current_thread = eventlet.greenthread.getcurrent()
    return getattr(current_thread, constants.TL_CONTRACT_PASSKEY, None)


def is_objects_dry_run_mode():
    current_thread = eventlet.greenthread.getcurrent()
    return bool(getattr(current_thread, constants.TL_OBJECTS_DRY_RUN, False))


def get_current_thread_id():
    global _threads_sequencer

    current_thread = eventlet.greenthread.getcurrent()
    thread_id = getattr(current_thread, constants.TL_ID, None)
    if thread_id is None:
        thread_id = 'T' + str(_threads_sequencer)
        _threads_sequencer += 1
        setattr(current_thread, constants.TL_ID, thread_id)
    return thread_id


@contextlib.contextmanager
def thread_local_attribute(name, value):
    current_thread = eventlet.greenthread.getcurrent()
    old_value = getattr(current_thread, name, None)
    if value is not None:
        setattr(current_thread, name, value)
    elif hasattr(current_thread, name):
        delattr(current_thread, name)
    try:
        yield
    finally:
        if old_value is not None:
            setattr(current_thread, name, old_value)
        elif hasattr(current_thread, name):
            delattr(current_thread, name)


def contextual(ctx):
    return thread_local_attribute(constants.TL_CONTEXT, ctx)


def with_object_store(object_store):
    return thread_local_attribute(constants.TL_OBJECT_STORE, object_store)


def parse_version_spec(version_spec):
    if isinstance(version_spec, semantic_version.Spec):
        return normalize_version_spec(version_spec)
    if isinstance(version_spec, semantic_version.Version):
        return normalize_version_spec(
            semantic_version.Spec('==' + str(version_spec)))
    if not version_spec:
        version_spec = '0'
    version_spec = re.sub('\s+', '', str(version_spec))

    # NOTE(kzaitsev): semantic_version 2.3.X thinks that '=0' is not
    # a valid version spec and only accepts '==0', this regexp adds
    # an extra '=' before such specs
    version_spec = re.sub(r'^=(\d)', r'==\1', version_spec)

    if version_spec[0].isdigit():
        version_spec = '==' + str(version_spec)
    version_spec = semantic_version.Spec(version_spec)
    return normalize_version_spec(version_spec)


def parse_version(version):
    if isinstance(version, semantic_version.Version):
        return version
    if not version:
        version = '0'
    return semantic_version.Version.coerce(str(version))


def traverse(seed, producer=None, track_visited=True):
    if not yaqlutils.is_iterable(seed):
        seed = [seed]
    visited = None if not track_visited else set()
    queue = collections.deque(seed)
    while queue:
        item = queue.popleft()
        if track_visited:
            if item in visited:
                continue
            visited.add(item)
        produced = (yield item)
        if produced is None and producer:
            produced = producer(item)
        if produced:
            queue.extend(produced)


def cast(obj, murano_class, pov_or_version_spec=None):
    if isinstance(obj, dsl_types.MuranoObjectInterface):
        obj = obj.object
    if isinstance(pov_or_version_spec, dsl_types.MuranoType):
        pov_or_version_spec = pov_or_version_spec.package
    elif isinstance(pov_or_version_spec, str):
        pov_or_version_spec = parse_version_spec(pov_or_version_spec)

    if isinstance(murano_class, dsl_types.MuranoTypeReference):
        murano_class = murano_class.type
    if isinstance(murano_class, dsl_types.MuranoType):
        if pov_or_version_spec is None:
            pov_or_version_spec = parse_version_spec(murano_class.version)
        murano_class = murano_class.name

    candidates = []
    for cls in itertools.chain((obj.type,), obj.type.ancestors()):
        if cls.name != murano_class:
            continue
        elif isinstance(pov_or_version_spec, semantic_version.Version):
            if cls.version != pov_or_version_spec:
                continue
        elif isinstance(pov_or_version_spec, semantic_version.Spec):
            if cls.version not in pov_or_version_spec:
                continue
        elif isinstance(pov_or_version_spec, dsl_types.MuranoPackage):
            requirement = pov_or_version_spec.requirements.get(
                cls.package.name)
            if requirement is None:
                raise exceptions.NoClassFound(murano_class)
            if cls.version not in requirement:
                continue
        elif pov_or_version_spec is not None:
            raise ValueError('pov_or_version_spec of unsupported '
                             'type {0}'.format(type(pov_or_version_spec)))
        candidates.append(cls)
    if not candidates:
        raise exceptions.NoClassFound(murano_class)
    elif len(candidates) > 1:
        raise exceptions.AmbiguousClassName(murano_class)
    return obj.cast(candidates[0])


def is_instance_of(obj, class_name, pov_or_version_spec=None):
    if not isinstance(obj, (dsl_types.MuranoObject,
                            dsl_types.MuranoObjectInterface)):
        return False
    try:
        cast(obj, class_name, pov_or_version_spec)
        return True
    except (exceptions.NoClassFound, exceptions.AmbiguousClassName):
        return False


def memoize(func):
    cache = {}
    return get_memoize_func(func, cache)


def get_memoize_func(func, cache):
    @functools.wraps(func)
    def wrap(*args):
        if args not in cache:
            result = func(*args)
            cache[args] = result
            return result
        else:
            return cache[args]
    return wrap


def normalize_version_spec(version_spec):
    def coerce(v):
        return semantic_version.Version('{0}.{1}.{2}'.format(
            v.major, v.minor or 0, v.patch or 0
        ))

    def increment(v):
        # NOTE(ativelkov): replace these implementations with next_minor() and
        # next_major() calls when the semantic_version is updated in global
        # requirements.
        if v.minor is None:
            return semantic_version.Version(
                '.'.join(str(x) for x in [v.major + 1, 0, 0]))
        else:
            return semantic_version.Version(
                '.'.join(str(x) for x in [v.major, v.minor + 1, 0]))

    def extend(v):
        return semantic_version.Version(str(v) + '-0')

    transformations = {
        '>': [('>=', (increment, extend))],
        '>=': [('>=', (coerce,))],
        '<': [('<', (coerce, extend))],
        '<=': [('<', (increment, extend))],
        '!=': [('>=', (increment, extend))],
        '==': [('>=', (coerce,)), ('<', (increment, coerce, extend))]
    }

    new_parts = []
    for item in version_spec.specs:
        if item.kind == '*':
            continue
        elif item.spec.patch is not None:
            new_parts.append(str(item))
        else:
            for op, funcs in transformations[item.kind]:
                new_parts.append('{0}{1}'.format(
                    op,
                    functools.reduce(lambda v, f: f(v), funcs, item.spec)
                ))
    if not new_parts:
        return semantic_version.Spec('*')
    return semantic_version.Spec(*new_parts)


semver_to_api_map = {
    '>': 'gt',
    '>=': 'ge',
    '<': 'lt',
    '<=': 'le',
    '!=': 'ne',
    '==': 'eq'
}


def breakdown_spec_to_query(normalized_spec):
    res = []
    for item in normalized_spec.specs:
        if item.kind == '*':
            continue
        else:
            res.append("%s:%s" % (semver_to_api_map[item.kind],
                                  item.spec))
    return res


def link_contexts(parent_context, context):
    if not context:
        return parent_context
    return contexts.LinkedContext(parent_context, context)


def inspect_is_static(cls, name):
    m = cls.__dict__.get(name)
    if m is None:
        return False
    return isinstance(m, staticmethod)


def inspect_is_classmethod(cls, name):
    m = cls.__dict__.get(name)
    if m is None:
        return False
    return isinstance(m, classmethod)


def inspect_is_method(cls, name):
    m = getattr(cls, name, None)
    if m is None:
        return False
    return ((inspect.isfunction(m) or inspect.ismethod(m)) and not
            inspect_is_static(cls, name) and not
            inspect_is_classmethod(cls, name))


def inspect_is_property(cls, name):
    m = getattr(cls, name, None)
    if m is None:
        return False
    return inspect.isdatadescriptor(m)


def updated_dict(d, val):
    if d is None:
        d = {}
    else:
        d = d.copy()
    if val is not None:
        d.update(val)
    return d


def resolve_type(value, scope_type, return_reference=False):
    if value is None:
        return None
    if isinstance(scope_type, dsl_types.MuranoTypeReference):
        scope_type = scope_type.type
    if not isinstance(value, (dsl_types.MuranoType,
                              dsl_types.MuranoTypeReference)):
        name = scope_type.namespace_resolver.resolve_name(value)
        result = scope_type.package.find_class(name)
    else:
        result = value

    if isinstance(result, dsl_types.MuranoTypeReference):
        if return_reference:
            return result
        return result.type
    elif return_reference:
        return result.get_reference()
    return result


def parse_object_definition(spec, scope_type, context):
    if not isinstance(spec, yaqlutils.MappingType):
        return None

    if context:
        spec = evaluate(spec, context, freeze=False)
    else:
        spec = spec.copy()
    system_data = None
    type_obj = None
    props = {}
    ns_resolver = scope_type.namespace_resolver if scope_type else None
    for key in spec:
        if (ns_resolver and ns_resolver.is_typename(key, False) or
                isinstance(key, (dsl_types.MuranoTypeReference,
                                 dsl_types.MuranoType))):
            type_obj = resolve_type(key, scope_type)
            props = spec.pop(key) or {}
            system_data = spec
            break
    if system_data is None:
        props = spec
        if '?' in spec:
            system_data = spec.pop('?')
            obj_type = system_data.get('type')
            if isinstance(obj_type, dsl_types.MuranoTypeReference):
                type_obj = obj_type.type
            elif isinstance(obj_type, dsl_types.MuranoType):
                type_obj = obj_type
            elif obj_type:
                type_str, version_str, package_str = parse_type_string(
                    obj_type,
                    system_data.get('classVersion'),
                    system_data.get('package')
                )
                version_spec = parse_version_spec(version_str)
                package_loader = get_package_loader()
                if package_str:
                    package = package_loader.load_package(
                        package_str, version_spec)
                else:
                    package = package_loader.load_class_package(
                        type_str, version_spec)
                type_obj = package.find_class(type_str, False)
        else:
            system_data = {}

    return {
        'type': type_obj,
        'properties': yaqlutils.filter_parameters_dict(props),
        'id': system_data.get('id'),
        'name': system_data.get('name'),
        'metadata': system_data.get('metadata'),
        'destroyed': system_data.get('destroyed', False),
        'dependencies': system_data.get('dependencies', {}),
        'extra': {
            key: value for key, value in system_data.items()
            if key.startswith('_')
        }
    }


def assemble_object_definition(parsed, model_format=dsl_types.DumpTypes.Mixed):
    if model_format == dsl_types.DumpTypes.Inline:
        result = {
            parsed['type']: parsed['properties'],
            'id': parsed['id'],
            'name': parsed['name'],
            'metadata': parsed['metadata'],
            'dependencies': parsed['dependencies'],
            'destroyed': parsed['destroyed']
        }
        result.update(parsed['extra'])
        return result
    result = parsed['properties']
    header = {
        'id': parsed['id'],
        'name': parsed['name'],
        'metadata': parsed['metadata']
    }
    if parsed['destroyed']:
        header['destroyed'] = True
    header.update(parsed['extra'])
    result['?'] = header
    if model_format == dsl_types.DumpTypes.Mixed:
        header['type'] = parsed['type']
        return result
    elif model_format == dsl_types.DumpTypes.Serializable:
        cls = parsed['type']
        if cls:
            header['type'] = format_type_string(cls)
        return result
    else:
        raise ValueError('Invalid Serialization Type')


def function(c):
    if hasattr(c, '__func__'):
        return c.__func__
    return c


def list_value(v):
    if v is None:
        return []
    if not yaqlutils.is_sequence(v):
        v = [v]
    return v


def weak_proxy(obj):
    if obj is None or isinstance(obj, weakref.ProxyType):
        return obj
    if isinstance(obj, weakref.ReferenceType):
        obj = obj()
    return weakref.proxy(obj)


def weak_ref(obj):
    class MuranoObjectWeakRef(weakref.ReferenceType):
        def __init__(self, murano_object):
            self.ref = weakref.ref(murano_object)
            self.object_id = murano_object.object_id

        def __call__(self):
            res = self.ref()
            if not res:
                object_store = get_object_store()
                if object_store:
                    res = object_store.get(self.object_id)
                    if res:
                        self.ref = weakref.ref(res)
            return res

    if obj is None or isinstance(obj, weakref.ReferenceType):
        return obj

    if isinstance(obj, dsl_types.MuranoObject):
        return MuranoObjectWeakRef(obj)
    return weakref.ref(obj)


def parse_type_string(type_str, default_version, default_package):
    res = TYPE_RE.match(type_str)
    if res is None:
        return None
    parsed_type = res.group(1)
    parsed_version = res.group(2)
    parsed_package = res.group(3)
    return (
        parsed_type,
        default_version if parsed_version is None else parsed_version,
        default_package if parsed_package is None else parsed_package
    )


def format_type_string(type_obj):
    if isinstance(type_obj, dsl_types.MuranoTypeReference):
        type_obj = type_obj.type
    if isinstance(type_obj, dsl_types.MuranoType):
        return '{0}/{1}@{2}'.format(
            type_obj.name, type_obj.version, type_obj.package.name)
    else:
        raise ValueError('Invalid argument')


def patch_dict(dct, path, value):
    parts = path.split('.')
    for i in range(len(parts) - 1):
        if not isinstance(dct, dict):
            dct = None
            break
        dct = dct.get(parts[i])
    if isinstance(dct, dict):
        if value is yaqlutils.NO_VALUE:
            dct.pop(parts[-1])
        else:
            dct[parts[-1]] = value


def format_scalar(value):
    if isinstance(value, str):
        return "'{0}'".format(value)
    return str(value)


def is_passkey(value):
    passkey = get_contract_passkey()
    return passkey is not None and value is passkey


def find_object_owner(obj, predicate):
    p = obj.owner
    while p:
        if predicate(p):
            return p
        p = p.owner
    return None


# This function is not intended to be used in the code but is very useful
# for debugging object reference leaks
def walk_gc(obj, towards, handler):
    visited = set()
    queue = collections.deque([(obj, [])])
    while queue:
        item, trace = queue.popleft()
        if id(item) in visited:
            continue
        if handler(item):
            if towards:
                yield trace + [item]
            else:
                yield [item] + trace

        visited.add(id(item))
        if towards:
            try:
                queue.extend(
                    [(t, trace + [item]) for t in gc.get_referrers(item)]
                )
            except StopIteration:
                return
        else:
            try:
                queue.extend(
                    [(t, [item] + trace) for t in gc.get_referents(item)]
                )
            except StopIteration:
                return
