# -*- coding: utf-8 -*-
from bw2data import Database, databases
from ..units import normalize_units as normalize_units_function
from ..errors import StrategyError
from ..utils import activity_hash, DEFAULT_FIELDS
from copy import deepcopy
import numbers
import numpy as np
import pprint


def format_nonunique_key_error(obj, fields, others):
    template = """Object in source database can't be uniquely linked to target database.\nProblematic dataset is:\n{ds}\nPossible targets include (at least one not shown):\n{targets}"""
    fields_to_print = list(fields or DEFAULT_FIELDS) + ["filename"]
    _ = lambda x: {field: x.get(field, "(missing)") for field in fields_to_print}
    return template.format(
        ds=pprint.pformat(_(obj)), targets=pprint.pformat([_(x) for x in others])
    )


def link_iterable_by_fields(
    unlinked, other=None, fields=None, kind=None, internal=False, relink=False
):
    """Generic function to link objects in ``unlinked`` to objects in ``other`` using fields ``fields``.

    The database to be linked must have uniqueness for each object for the given ``fields``.

    If ``kind``, limit objects in ``unlinked`` of type ``kind``.

    If ``relink``, link to objects which already have an ``input``. Otherwise, skip already linked objects.

    If ``internal``, linked ``unlinked`` to other objects in ``unlinked``. Each object must have the attributes ``database`` and ``code``."""
    if kind:
        kind = {kind} if isinstance(kind, str) else kind
        if relink:
            filter_func = lambda x: x.get("type") in kind
        else:
            filter_func = lambda x: x.get("type") in kind and not x.get("input")
    else:
        if relink:
            filter_func = lambda x: True
        else:
            filter_func = lambda x: not x.get("input")

    if internal:
        other = unlinked

    duplicates, candidates = {}, {}
    try:
        # Other can be a generator, so a bit convoluted
        for ds in other:
            key = activity_hash(ds, fields)
            if key in candidates:
                duplicates.setdefault(key, []).append(ds)
            else:
                candidates[key] = (ds["database"], ds["code"])
    except KeyError:
        raise StrategyError(
            "Not all datasets in database to be linked have "
            "``database`` or ``code`` attributes"
        )

    for container in unlinked:
        for obj in filter(filter_func, container.get("exchanges", [])):
            key = activity_hash(obj, fields)
            if key in duplicates:
                raise StrategyError(
                    format_nonunique_key_error(obj, fields, duplicates[key])
                )
            elif key in candidates:
                obj["input"] = candidates[key]
    return unlinked


def assign_only_product_as_production(db):
    """Assign only product as reference product.

    Skips datasets that already have a reference product or no production exchanges. Production exchanges must have a ``name`` and an amount.

    Will replace the following activity fields, if not already specified:

    * 'name' - name of reference product
    * 'unit' - unit of reference product
    * 'production amount' - amount of reference product

    """
    for ds in db:
        if ds.get("reference product"):
            continue
        products = [x for x in ds.get("exchanges", []) if x.get("type") == "production"]
        if len(products) == 1:
            product = products[0]
            assert product['name']
            ds['reference product'] = product.get('reference product',[]) or product['name']
            ds['production amount'] = product['amount']
            ds['name'] = ds.get('name') or product['name']
            ds['unit'] = ds.get('unit') or product.get('unit') or 'Unknown'
    return db


def link_technosphere_by_activity_hash(db, external_db_name=None, fields=None):
    """Link technosphere exchanges using ``activity_hash`` function.

    If ``external_db_name``, link against a different database; otherwise link internally.

    If ``fields``, link using only certain fields."""
    TECHNOSPHERE_TYPES = {"technosphere", "substitution", "production"}
    if external_db_name is not None:
        if external_db_name not in databases:
            raise StrategyError(
                "Can't find external database {}".format(external_db_name)
            )
        other = (
            obj
            for obj in Database(external_db_name)
            if obj.get("type", "process") == "process"
        )
        internal = False
    else:
        other = None
        internal = True
    return link_iterable_by_fields(
        db, other, internal=internal, kind=TECHNOSPHERE_TYPES, fields=fields
    )


def set_code_by_activity_hash(db, overwrite=False):
    """Use ``activity_hash`` to set dataset code.

    By default, won't overwrite existing codes, but will if ``overwrite`` is ``True``."""
    for ds in db:
        if "code" not in ds or overwrite:
            ds["code"] = activity_hash(ds)
    return db


def tupleize_categories(db):
    for ds in db:
        if ds.get("categories"):
            ds["categories"] = tuple(ds["categories"])
        for exc in ds.get("exchanges", []):
            if exc.get("categories"):
                exc["categories"] = tuple(exc["categories"])
    return db


def drop_unlinked(db):
    """This is the nuclear option - use at your own risk!"""
    for ds in db:
        ds["exchanges"] = [obj for obj in ds["exchanges"] if obj.get("input")]
    return db


def normalize_units(db):
    """Normalize units in datasets and their exchanges"""
    for ds in db:
        if "unit" in ds:
            ds["unit"] = normalize_units_function(ds["unit"])
        for exc in ds.get("exchanges", []):
            if "unit" in exc:
                exc["unit"] = normalize_units_function(exc["unit"])
        for param in ds.get("parameters", {}).values():
            if "unit" in param:
                param["unit"] = normalize_units_function(param["unit"])
    return db


def add_database_name(db, name):
    """Add database name to datasets"""
    for ds in db:
        ds["database"] = name
    return db


def convert_uncertainty_types_to_integers(db):
    """Generic number conversion function convert to floats. Return to integers."""
    for ds in db:
        for exc in ds["exchanges"]:
            try:
                exc["uncertainty type"] = int(exc["uncertainty type"])
            except:
                pass
    return db


def drop_falsey_uncertainty_fields_but_keep_zeros(db):
    """Drop fields like '' but keep zero and NaN.

    Note that this doesn't strip `False`, which behaves *exactly* like 0.

    """
    uncertainty_fields = [
        "minimum",
        "maximum",
        "scale",
        "shape",
        "loc",
    ]

    def drop_if_appropriate(exc):
        for field in uncertainty_fields:
            if field not in exc or exc[field] == 0:
                continue
            elif isinstance(exc[field], numbers.Number) and np.isnan(exc[field]):
                continue
            elif not exc[field]:
                del exc[field]

    for ds in db:
        for exc in ds["exchanges"]:
            drop_if_appropriate(exc)
    return db


def convert_activity_parameters_to_list(data):
    """Convert activity parameters from dictionary to list of dictionaries"""

    def _(key, value):
        dct = deepcopy(value)
        dct["name"] = key
        return dct

    for ds in data:
        if "parameters" in ds:
            ds["parameters"] = [_(x, y) for x, y in ds["parameters"].items()]

    return data
