# ----------------------------------------------------------------------------
# Copyright (c) 2016-2026, QIIME 2 development team.
#
# Distributed under the terms of the Modified BSD License.
#
# The full license is in the file LICENSE, distributed with this software.
# ----------------------------------------------------------------------------

import collections

from rachis.core.util import tuplize
from rachis.core.type.collection import List, Set, Collection
from rachis.core.type.primitive import Int, Float, Bool, Str, Jobs, Threads
from rachis.core.type.grammar import UnionExp, _ExpBase, IntersectionExp
from rachis.core.type.parse import ast_to_type


def _strip_predicates(expr):
    if isinstance(expr, UnionExp):
        return UnionExp(_strip_predicates(m) for m in expr.members).normalize()

    if hasattr(expr, 'fields'):
        new_fields = tuple(_strip_predicates(f) for f in expr.fields)

    return expr.duplicate(fields=new_fields, predicate=IntersectionExp())


def val_to_bool(value):
    if type(value) is bool:
        return value
    elif str(value).lower() == 'true':
        return True
    elif str(value).lower() == 'false':
        return False
    else:
        raise ValueError('Could not cast to bool')


def val_to_int(v):
    type_ = type(v)
    if type_ is int:
        return v
    elif type_ is str:
        return int(v)
    else:
        raise ValueError('Could not cast to int')


def val_to_float(v):
    type_ = type(v)
    if type_ is float:
        return v
    elif type_ is str:
        return float(v)
    else:
        raise ValueError('Could not cast to float')


VariadicRecord = collections.namedtuple('VariadicRecord', ['pytype', 'q2type'])
_VARIADIC = {
    'List': VariadicRecord(pytype=list, q2type=List),
    'Set': VariadicRecord(pytype=set, q2type=Set),
    'Collection': VariadicRecord(pytype=dict, q2type=Collection),
}

CoercionRecord = collections.namedtuple('CoercionRecord', ['func', 'pytype'])
# Beware visitor, order matters in this here mapper
_COERCION_MAPPER = {
    Int: CoercionRecord(pytype=int, func=val_to_int),
    Float: CoercionRecord(pytype=float, func=val_to_float),
    Bool: CoercionRecord(pytype=bool, func=val_to_bool),
    Str: CoercionRecord(pytype=str, func=str),
}
_COERCE_ERROR = ValueError(
    'Could not coerce value based on expression provided.')

CollectionStyle = collections.namedtuple(
    'CollectionStyle', ['style', 'members', 'view', 'expr', 'base'])


def _norm_input(t):
    if type(t) is dict:
        return ast_to_type(t)
    elif not isinstance(t, _ExpBase):
        raise TypeError("%r is not a QIIME 2 type" % (t,))
    return t


def is_qiime_type(t):
    try:
        _norm_input(t)
    except Exception:
        return False
    else:
        return True


def is_primitive_type(t):
    expr = _norm_input(t)
    return hasattr(expr, 'kind') and expr.kind == 'primitive'


def is_metadata_type(t):
    expr = _norm_input(t)
    return is_primitive_type(t) and expr.name.startswith('Metadata')


def is_metadata_column_type(t):
    expr = _norm_input(t)
    return is_primitive_type(t) and expr.name.endswith('MetadataColumn')


def is_semantic_type(t):
    expr = _norm_input(t)
    return hasattr(expr, 'kind') and expr.kind == 'semantic-type'


def is_visualization_type(t):
    expr = _norm_input(t)
    return hasattr(expr, 'kind') and expr.kind == 'visualization'


def is_union(t):
    expr = _norm_input(t)
    return isinstance(expr, UnionExp)


def is_collection_type(t):
    expr = _norm_input(t)

    if expr.name in _VARIADIC:
        return True

    if is_union(expr):
        for m in expr.members:
            if m.name in _VARIADIC:
                return True

    return False


def is_parallel_type(t):
    expr = _norm_input(t)
    return is_primitive_type(t) and expr in (Jobs, Threads)


def interrogate_collection_type(t):
    expr = _norm_input(t)
    style = None    # simple, monomorphic, composite, complex
    members = None  # T     , [T1, T2]   , [T1, T2],  [[T1], [T2, T3]]
    view = None  # set, list
    base = None

    if expr.name in _VARIADIC:
        view, base = _VARIADIC[expr.name]
        field, = expr.fields
        if isinstance(field, UnionExp):
            style = 'composite'
            members = list(field.members)
        else:
            style = 'simple'
            members = field
    elif isinstance(expr, UnionExp):
        if expr.members[0].name in _VARIADIC:
            members = []
            for member in expr.members:
                field, = member.fields
                if isinstance(field, UnionExp):
                    style = 'complex'
                    members.append(list(field.members))
                else:
                    members.append([field])
                    if style != 'complex':
                        style = 'monomorphic'

            # use last iteration
            view, base = _VARIADIC[member.name]
            if style == 'monomorphic':
                members = [m[0] for m in members]

    return CollectionStyle(style=style, members=members, view=view,
                           expr=expr, base=base)


def _ordered_coercion(types):
    types = tuple(types)
    return tuple(k for k in _COERCION_MAPPER.keys() if k in types)


def _interrogate_types(allowed, value):
    ordered_allowed = _ordered_coercion(allowed)
    for coerce_type in (_COERCION_MAPPER[x].func for x in ordered_allowed):
        try:
            return coerce_type(value)
        except ValueError:
            pass
    raise _COERCE_ERROR


def parse_primitive(t, value):
    expr = _norm_input(t)
    result = []
    allowed = None
    homogeneous = True

    keys = None
    if isinstance(value, dict):
        keys = list(value.keys())
        value = list(value.values())

    if is_metadata_type(expr):
        raise ValueError('%r may not be parsed with this util.' % (expr,))

    expr = _strip_predicates(expr)
    collection_style = interrogate_collection_type(expr)

    if collection_style.style in ('simple', 'monomorphic', 'composite'):
        allowed = list(collection_style.members)

    if collection_style.style == 'composite':
        homogeneous = False
    elif collection_style.style == 'complex':
        # Sort here so that we can start with any simple lists in the memberset
        for subexpr in sorted(collection_style.members, key=len):
            expr = collection_style.base[UnionExp(subexpr)]
            try:
                return parse_primitive(expr, value)
            except ValueError:
                pass
        raise _COERCE_ERROR
    elif collection_style.style is None:
        value = tuplize(value)
        if expr in (Int, Float, Bool, Str):
            # No sense in walking over all options when we know
            # what it should be
            allowed = [expr]
        else:
            allowed = list(_COERCION_MAPPER.keys())
    else:
        pass

    assert allowed is not None

    # Int <= Float, make sure its added in
    if Float in allowed and Int not in allowed:
        allowed.append(Int)

    for v in value:
        result.append(_interrogate_types(allowed, v))

    # Some exprs require homogeneous values, make it so
    if homogeneous:
        all_matching = False
        for member in allowed:
            if all(type(x) is _COERCION_MAPPER[member].pytype
                   for x in result):
                all_matching = True
                break
        if not all_matching and collection_style.style == 'monomorphic':
            for subexpr in allowed:
                expr = collection_style.base[subexpr]
                try:
                    return parse_primitive(expr, value)
                except ValueError:
                    pass
            raise _COERCE_ERROR

    if collection_style.view is None:
        return result[0]
    else:
        # If we are supposed to have a dict we need to give it key, value
        # pairs. We end up here when we invoke a command that takes a
        # Collection on the command line
        if collection_style.view is dict:
            # If we have keys, value was originally a dict, and we want to
            # reattach the keys to the values
            if keys is not None:
                return {k: v for k, v in zip(keys, result)}
            else:
                return {str(k): v for k, v in enumerate(result)}
        return collection_style.view(result)
