"""
Copyright (C) 2013-2019 Calliope contributors listed in AUTHORS.
Licensed under the Apache 2.0 License (see LICENSE file).

"""

import numpy as np

from calliope.core.attrdict import AttrDict


def concat_iterable(iterable, concatenators):
    """
    Take an iterable containing iterables of strings,
    return a list of strings concatenating inner iterables with '::'.

    E.g.:
    ``
    result = concat_iterable([('x', 'y', 'z'), ('1', '2', '3')], [':', ':'])
    result == ['x:y:z', '1:2:3']

    result = concat_iterable([('x', 'y', 'z'), ('1', '2', '3')], ['::', ':'])
    result == ['x::y:z', '1::2:3']
    ``

    """
    if len(iterable) == 0:
        return []
    concats = concatenators + ['']
    string_iter_len = len(iterable[0])
    assert all(len(i) == string_iter_len for i in iterable)
    assert string_iter_len == len(concats)

    return [
        ''.join([string_iter[i] + concats[i] for i in range(string_iter_len)])
        for string_iter in iterable
    ]


def constraint_exists(model_run, loc_tech, search_entry):
    """
    Check if a cost/constraint exists for a loc_tech.

    E.g.:
    ``
    constraint_exists(model_run, 'X1::chp', 'constraints.energy_eff') will return True or False
    ``
    """
    if loc_tech in model_run.sets.loc_techs_transmission:
        loc_tech_dict = split_loc_techs_transmission(loc_tech)
        search_string = (
            'locations.{loc_from}.links.{loc_to}.techs.{tech}.{}'
            .format(search_entry, **loc_tech_dict)
        )
    else:
        search_string = (
            'locations.{0}.techs.{1}.{2}'
            .format(*loc_tech.split('::'), search_entry)
        )
    return model_run.get_key(search_string, None)


def get_all_carriers(config, direction='both'):
    if direction == 'both':
        carrier_list = ['in', 'out', 'in_2', 'out_2', 'in_3', 'out_3']
    elif direction == 'in':
        carrier_list = ['in', 'in_2', 'in_3']
    elif direction == 'out':
        carrier_list = ['out', 'out_2', 'out_3']

    carriers = flatten_list(
        [config.get_key('carrier', '')] + [
            config.get_key('carrier_{}'.format(k), '')
            for k in carrier_list
        ]
    )

    return set(carriers) - set([''])


def flatten_list(unflattened_list):
    """
    Take list of iterables/non-iterables and outputs a list of non-iterables.
    """
    flattened_list = []
    for item in unflattened_list:
        if hasattr(item, '__iter__') and not isinstance(item, str):
            flattened_list.extend(item)
        else:
            flattened_list.append(item)

    return flattened_list


def split_loc_techs_transmission(transmission_string):
    """
    from loc::tech:link get out a dictionary of {loc_from:loc, loc_to:link, tech:tech}
    """
    loc, tech_link = transmission_string.split('::')
    tech, link = tech_link.split(':')

    return {'loc_from': loc, 'loc_to': link, 'tech': tech}


def get_systemwide_constraints(tech_config):
    if 'constraints' in tech_config:
        constraints = AttrDict({
            k: tech_config.constraints[k]
            for k in tech_config.constraints.keys()
            if k.endswith('_systemwide')
        })
    else:
        constraints = AttrDict({})

    return constraints


def vincenty(coord1, coord2):
    """
    Vincenty's inverse method formula to calculate the distance in metres
    between two points on the surface of a spheroid (WGS84).
    modified from https://github.com/maurycyp/vincenty
    """

    a = 6378137  # equitorial radius in meters
    f = 1 / 298.257223563  # flattening from sphere to oblate spheroid
    b = a * (1 - f)  # polar radius in meters

    max_iter = 200
    thresh = 1e-12

    # short-circuit coincident points
    if coord1[0] == coord2[0] and coord1[1] == coord2[1]:
        return 0

    U1 = np.arctan((1 - f) * np.tan(np.radians(coord1[0])))
    U2 = np.arctan((1 - f) * np.tan(np.radians(coord2[0])))
    L = np.radians(coord2[1] - coord1[1])
    Lambda = L

    sinU1 = np.sin(U1)
    cosU1 = np.cos(U1)
    sinU2 = np.sin(U2)
    cosU2 = np.cos(U2)

    for iteration in range(max_iter):
        sinLambda = np.sin(Lambda)
        cosLambda = np.cos(Lambda)
        sinSigma = np.sqrt((cosU2 * sinLambda) ** 2 +
                             (cosU1 * sinU2 - sinU1 * cosU2 * cosLambda) ** 2)
        if sinSigma == 0:
            return 0.0  # coincident points
        cosSigma = sinU1 * sinU2 + cosU1 * cosU2 * cosLambda
        sigma = np.arctan2(sinSigma, cosSigma)
        sinAlpha = cosU1 * cosU2 * sinLambda / sinSigma
        cosSqAlpha = 1 - sinAlpha ** 2
        try:
            cos2SigmaM = cosSigma - 2 * sinU1 * sinU2 / cosSqAlpha
        except ZeroDivisionError:
            cos2SigmaM = 0
        C = f / 16 * cosSqAlpha * (4 + f * (4 - 3 * cosSqAlpha))
        LambdaPrev = Lambda
        Lambda = L + (1 - C) * f * sinAlpha * (sigma + C * sinSigma *
                                               (cos2SigmaM + C * cosSigma *
                                                (-1 + 2 * cos2SigmaM ** 2)))
        if abs(Lambda - LambdaPrev) < thresh:
            break  # successful convergence
    else:
        return None  # failure to converge

    uSq = cosSqAlpha * (a ** 2 - b ** 2) / (b ** 2)
    A = 1 + uSq / 16384 * (4096 + uSq * (-768 + uSq * (320 - 175 * uSq)))
    B = uSq / 1024 * (256 + uSq * (-128 + uSq * (74 - 47 * uSq)))
    deltaSigma = B * sinSigma * (cos2SigmaM + B / 4 * (cosSigma *
                 (-1 + 2 * cos2SigmaM ** 2) - B / 6 * cos2SigmaM *
                 (-3 + 4 * sinSigma ** 2) * (-3 + 4 * cos2SigmaM ** 2)))
    D = b * A * (sigma - deltaSigma)

    return round(D)
