# -*- coding: utf-8 -*-
"""Samila functions."""

import requests
import io
import os
import re
import json
import random
import matplotlib
from matplotlib import cm
from matplotlib.colors import ListedColormap
from .params import DEFAULT_MARKER, DEFAULT_START, DEFAULT_STOP, DEFAULT_STEP, DEFAULT_COLOR, DEFAULT_IMAGE_SIZE, DEFAULT_DEPTH
from .params import DEFAULT_CMAP, DEFAULT_CMAP_RANGE
from .params import DEFAULT_BACKGROUND_COLOR, DEFAULT_SPOT_SIZE, DEFAULT_PROJECTION, DEFAULT_ALPHA, DEFAULT_LINEWIDTH
from .params import Projection, Marker, VALID_COLORS, HEX_COLOR_PATTERN, NFT_STORAGE_API, NFT_STORAGE_LINK, OVERVIEW
from .params import DATA_TYPE_ERROR, DATA_FORMAT_ERROR, CONFIG_TYPE_ERROR, CONFIG_FORMAT_ERROR, PLOT_DATA_ERROR, CONFIG_NO_STR_FUNCTION_ERROR
from .params import NO_FIG_ERROR_MESSAGE, FIG_SAVE_SUCCESS_MESSAGE, NFT_STORAGE_SUCCESS_MESSAGE, SAVE_NO_DATA_ERROR
from .params import INVALID_COLOR_TYPE_ERROR, COLOR_SIZE_ERROR
from .params import BOTH_COLOR_COMPLEMENT_WARNING, COLOR_NOT_FOUND_WARNING
from .params import DATA_SAVE_SUCCESS_MESSAGE, SEED_LOWER_BOUND, SEED_UPPER_BOUND
from .params import ELEMENTS_LIST, ARGUMENTS_LIST, OPERATORS_LIST, RANDOM_COEF_LIST, RANDOM_EQUATION_GEN_COMPLEXITY
from .errors import samilaDataError, samilaPlotError, samilaConfigError
from warnings import warn


def random_equation_gen():
    """
    Generate random equation.

    :return: equation as str
    """
    num_elements = random.randint(1, RANDOM_EQUATION_GEN_COMPLEXITY)
    result = ""
    index = 1
    random_coef = random.choice(RANDOM_COEF_LIST)
    while(index <= num_elements):
        argument = random.choice(ARGUMENTS_LIST)
        if random.randint(0, 1) == 1:
            argument = random.choice(ELEMENTS_LIST).format(
                random_coef, argument)
        result = result + \
            random.choice(ELEMENTS_LIST).format(random_coef, argument)
        if index < num_elements:
            result = result + random.choice(OPERATORS_LIST)
        index = index + 1
    if random.randint(0, 1) == 1:
        result = random.choice(ELEMENTS_LIST).format(random_coef, result)
    return result


def random_hex_color_gen():
    """
    Generate random hex color code.

    :return: color code as str
    """
    random_color = "#%06x" % random.randint(0, 0xFFFFFF)
    return random_color


def float_range(start, stop, step):
    """
    Generate float range.

    :param start: start point
    :type start: float
    :param stop: stop point
    :type step: float
    :param step: step
    :type step: float
    :return: yield result
    """
    while start < stop:
        yield float(start)
        start += step


def distance_calc(s1, s2):
    """
    Calculate Levenshtein distance between two words.

    :param s1: first string
    :type s1 : str
    :param s2: second string
    :type s2 : str
    :return: distance between two string

    References :
    1- https://stackoverflow.com/questions/2460177/edit-distance-in-python
    2- https://en.wikipedia.org/wiki/Levenshtein_distance
    """
    if len(s1) > len(s2):
        s1, s2 = s2, s1

    distances = range(len(s1) + 1)
    for i2, c2 in enumerate(s2):
        distances_ = [i2 + 1]
        for i1, c1 in enumerate(s1):
            if c1 == c2:
                distances_.append(distances[i1])
            else:
                distances_.append(
                    1 + min((distances[i1], distances[i1 + 1], distances_[-1])))
        distances = distances_
    return distances[-1]


def is_valid_color(color):
    """
    Check that input color format is valid or not.

    :param color: given color
    :type color: any format
    :return: result as bool
    """
    if color is None:
        return True
    try:
        _ = matplotlib.colors.to_hex(color)
        return True
    except ValueError:
        return False


def color_complement(color):
    """
    Calculate complement color.

    :param color: given color (hex format)
    :type color: str
    :return: complement color (hex format) as str
    """
    color = color[1:]
    color = int(color, 16)
    comp_color = 0xFFFFFF ^ color
    comp_color = "#%06x" % comp_color
    return comp_color


def filter_color(color, bgcolor):
    """
    Filter given color and bgcolor.

    :param color: given color
    :type color: any format
    :param bgcolor: given background color
    :type bgcolor: any format
    :return: filtered version of color and bgcolor
    """
    color = select_color(color)
    bgcolor = select_color(bgcolor)
    if color == "COMPLEMENT" and bgcolor == "COMPLEMENT":
        warn(BOTH_COLOR_COMPLEMENT_WARNING, RuntimeWarning)
        return None, None
    if color == "COMPLEMENT":
        bgcolor = matplotlib.colors.to_hex(bgcolor)
        color = color_complement(bgcolor)
    if bgcolor == "COMPLEMENT":
        color = matplotlib.colors.to_hex(color)
        bgcolor = color_complement(color)
    return color, bgcolor


def filter_cmap(cmap):
    """
    Filter given cmap.

    :param cmap: color map
    :type cmap: matplotlib.colors.Colormap or list of colors
    :return: filtered version of cmap
    """
    if isinstance(cmap, str):
        cmap = cm.get_cmap(cmap, 256)
    if type(cmap) == matplotlib.colors.Colormap:
        cmap = cm.get_cmap(cmap.__getattribute__("name"))
    if isinstance(cmap, matplotlib.colors.ListedColormap):
        return cmap
    if isinstance(cmap, (matplotlib.colors.LinearSegmentedColormap)):
        cmap = cmap(range(DEFAULT_CMAP_RANGE))
        return ListedColormap(cmap)
    if isinstance(cmap, list):
        cmap = list(map(select_color, cmap))
        return ListedColormap(cmap)
    return None


def select_color(color):
    """
    Select color and return it.

    :param color: given color
    :type color: any format
    :return: color
    """
    if isinstance(color, str):
        if color.upper() == "TRANSPARENT":
            return "TRANSPARENT"
        if color.upper() == "COMPLEMENT":
            return "COMPLEMENT"
        if color.upper() == "RANDOM":
            return random_hex_color_gen()
        if re.match(HEX_COLOR_PATTERN, color):
            return color.lower()
        distance_list = list(map(lambda x: distance_calc(color, x),
                                 VALID_COLORS))
        min_distance = min(distance_list)
        most_similar_color = VALID_COLORS[distance_list.index(min_distance)]
        if min_distance != 0:
            warn(
                COLOR_NOT_FOUND_WARNING.format(
                    color,
                    most_similar_color),
                RuntimeWarning)
        return most_similar_color
    if is_valid_color(color):
        return color
    raise samilaPlotError(INVALID_COLOR_TYPE_ERROR)


def set_background(bgcolor, fig, ax):
    """
    Set background for figure and axis.

    :param bgcolor: given background color
    :type bgcolor: any format
    :param fig: figure
    :type fig: matplotlib.figure.Figure
    :param ax: axis
    :type ax: matplotlib.axes._subplots.AxesSubplot
    :return: None
    """
    if bgcolor == "TRANSPARENT":
        ax.patch.set_visible(False)
        fig.patch.set_visible(False)
        return
    fig.set_facecolor(bgcolor)
    ax.set_facecolor(bgcolor)
    return


def filter_projection(projection):
    """
    Filter given projection.

    :param projection: given projection
    :type projection: Projection enum
    :return: filtered version of projection
    """
    if isinstance(projection, Projection):
        projection_value = projection.value
        if projection_value == "random":
            projection_list = list(Projection)
            projection_list.remove(Projection.RANDOM)
            projection_value = random.choice(projection_list).value
        return projection_value
    return None


def filter_marker(marker):
    """
    Filter given marker.

    :param marker: given marker
    :type marker: Marker enum
    :return: filtered version of marker
    """
    if isinstance(marker, Marker):
        marker_value = marker.value
        if marker_value == "random":
            marker_list = list(Marker)
            marker_list.remove(Marker.RANDOM)
            marker_value = random.choice(marker_list).value
        return marker_value
    return None


def filter_float(value):
    """
    Filter given float value.

    :param value: given value
    :type value: float
    :return: filtered version of value
    """
    if isinstance(value, (float, int)):
        return value
    return None


def filter_size(size):
    """
    Filter given image size.

    :param value: given size
    :type value: tuple of float
    :return: filtered version of size
    """
    if isinstance(size, tuple):
        if not any(map(lambda x: x != filter_float(x), size)):
            return size
    return None


def plot_params_filter(
        g,
        color=None,
        bgcolor=None,
        cmap=None,
        spot_size=None,
        size=None,
        projection=None,
        marker=None,
        alpha=None,
        linewidth=None):
    """
    Filter plot method parameters.

    :param g: generative image instance
    :type g: GenerativeImage
    :param color: point colors
    :type color: str
    :param bgcolor: background color
    :type bgcolor: str
    :param cmap: color map
    :type cmap: matplotlib.colors.Colormap or list of colors
    :param spot_size: point spot size
    :type spot_size: float
    :param size: figure size
    :type size: tuple
    :param projection: projection type
    :type projection: str
    :param marker: marker type
    :type marker: str
    :param alpha: point transparency
    :type alpha: float
    :param linewidth: width of line
    :type linewidth: float
    :return: None
    """
    if g.data1 is None:
        raise samilaPlotError(PLOT_DATA_ERROR.format(1))
    if g.data2 is None:
        raise samilaPlotError(PLOT_DATA_ERROR.format(2))
    if isinstance(color, list):
        if len(color) != len(g.data1):
            raise samilaPlotError(COLOR_SIZE_ERROR)
        bgcolor = select_color(bgcolor)
    else:
        color, bgcolor = filter_color(color, bgcolor)
    cmap = filter_cmap(cmap)
    projection = filter_projection(projection)
    marker = filter_marker(marker)
    alpha = filter_float(alpha)
    linewidth = filter_float(linewidth)
    spot_size = filter_float(spot_size)
    size = filter_size(size)
    if color is None:
        color = g.color
    if bgcolor is None:
        bgcolor = g.bgcolor
    if cmap is None:
        cmap = g.cmap
    if spot_size is None:
        spot_size = g.spot_size
    if size is None:
        size = g.size
    if projection is None:
        projection = g.projection
    if marker is None:
        marker = g.marker
    if alpha is None:
        alpha = g.alpha
    if linewidth is None:
        linewidth = g.linewidth
    g.color, g.bgcolor, g.cmap, g.spot_size, g.size, g.projection, g.marker, g.alpha, g.linewidth = \
        color, bgcolor, cmap, spot_size, size, projection, marker, alpha, linewidth


def generate_params_filter(
        g,
        seed=None,
        start=None,
        step=None,
        stop=None):
    """
    Filter generate method parameters.

    :param g: generative image instance
    :type g: GenerativeImage
    :param seed: random seed
    :type seed: int
    :param start: range start point
    :type start: float
    :param step: range step size
    :type step: float
    :param stop: range stop point
    :type stop: float
    :return: None
    """
    start, step, stop = map(filter_float, [start, step, stop])
    if start is None:
        start = g.start
    if step is None:
        step = g.step
    if stop is None:
        stop = g.stop
    if seed is None:
        seed = g.seed
        if g.seed is None:
            seed = random.randint(SEED_LOWER_BOUND, SEED_UPPER_BOUND)
    g.seed, g.start, g.step, g.stop = seed, start, step, stop


def save_params_filter(g, depth=None):
    """
    Filter save_image method parameters.

    :param depth: depth of image
    :type depth: float
    :return: None
    """
    if depth is None:
        depth = g.depth
    g.depth = depth


def _GI_initializer(g, function1, function2):
    """
    Initialize the generative image.

    :param g: generative image instance
    :type g: GenerativeImage
    :param function1: function 1
    :type function1: python or lambda function
    :param function2: function 2
    :type function2: python or lambda function
    :return: None
    """
    g.matplotlib_version = matplotlib.__version__
    g.function1 = function1
    g.function1_str = None
    g.function2 = function2
    g.function2_str = None
    g.fig = None
    g.seed = None
    g.start = DEFAULT_START
    g.step = DEFAULT_STEP
    g.stop = DEFAULT_STOP
    g.data1 = None
    g.data2 = None
    g.color = DEFAULT_COLOR
    g.bgcolor = DEFAULT_BACKGROUND_COLOR
    g.cmap = DEFAULT_CMAP
    g.spot_size = DEFAULT_SPOT_SIZE
    g.size = DEFAULT_IMAGE_SIZE
    g.projection = DEFAULT_PROJECTION
    g.marker = DEFAULT_MARKER
    g.alpha = DEFAULT_ALPHA
    g.linewidth = DEFAULT_LINEWIDTH
    g.depth = DEFAULT_DEPTH
    g.missed_points_number = 0


def nft_storage_upload(api_key, data):
    """
    Upload file to nft.storage.

    :param api_key: API key
    :type api_key: str
    :param data: image data
    :type data: binary
    :return: result as dict
    """
    result = {"status": True, "message": NFT_STORAGE_SUCCESS_MESSAGE}
    try:
        headers = {'Authorization': 'Bearer {0}'.format(api_key)}
        response = requests.post(
            url=NFT_STORAGE_API,
            data=data,
            headers=headers)
        response_json = response.json()
        if response_json["ok"]:
            result["message"] = NFT_STORAGE_LINK.format(
                response_json['value']['cid'])
            return result
        result["status"] = False
        result["message"] = response_json["error"]["message"]
        return result
    except Exception as e:
        result["status"] = False
        result["message"] = str(e)
        return result


def save_data_file(g, file_adr):
    """
    Save data as file.

    :param g: generative image instance
    :type g: GenerativeImage
    :param file_adr: file address
    :type file_adr: str
    :return: result as dict
    """
    data = get_data(g)
    result = {"status": True, "message": DATA_SAVE_SUCCESS_MESSAGE}
    try:
        with open(file_adr, 'w') as fp:
            json.dump(data, fp)
        result["message"] = os.path.abspath(file_adr)
    except Exception as e:
        result["status"] = False
        result["message"] = str(e)
    return result


def get_data(g):
    """
    Return data.

    :param g: generative image instance
    :type g: GenerativeImage
    :return: data as a dict
    """
    matplotlib_version = matplotlib.__version__
    data = {}
    if g.data1 is None or g.data2 is None:
        raise samilaDataError(SAVE_NO_DATA_ERROR)
    data['data1'] = g.data1
    data['data2'] = g.data2
    data['plot'] = {
        "color": g.color,
        "bgcolor": g.bgcolor,
        "cmap": _serialize_cmap(g.cmap),
        "spot_size": g.spot_size,
        "projection": g.projection,
        "marker": g.marker,
        "alpha": g.alpha,
        "linewidth": g.linewidth,
        "depth": g.depth
    }
    data['matplotlib_version'] = matplotlib_version
    return data


def get_config(g):
    """
    Return config.

    :param g: generative image instance
    :type g: GenerativeImage
    :return: config as a dict
    """
    matplotlib_version = matplotlib.__version__
    config = {}
    if g.function1_str is None or g.function2_str is None:
        raise samilaConfigError(CONFIG_NO_STR_FUNCTION_ERROR)
    config['f1'] = g.function1_str
    config['f2'] = g.function2_str
    config['generate'] = {
        "seed": g.seed,
        "start": g.start,
        "step": g.step,
        "stop": g.stop
    }
    config['plot'] = {
        "color": g.color,
        "bgcolor": g.bgcolor,
        "cmap": _serialize_cmap(g.cmap),
        "spot_size": g.spot_size,
        "projection": g.projection,
        "marker": g.marker,
        "alpha": g.alpha,
        "linewidth": g.linewidth,
        "depth": g.depth
    }
    config['matplotlib_version'] = matplotlib_version
    return config


def save_config_file(g, file_adr):
    """
    Save config as file.

    :param g: generative image instance
    :type g: GenerativeImage
    :param file_adr: file address
    :type file_adr: str
    :return: result as dict
    """
    config = get_config(g)
    result = {"status": True, "message": DATA_SAVE_SUCCESS_MESSAGE}
    try:
        with open(file_adr, 'w') as fp:
            json.dump(config, fp, indent=4)
        result["message"] = os.path.abspath(file_adr)
    except Exception as e:
        result["status"] = False
        result["message"] = str(e)
    return result


def save_fig_file(figure, file_adr, depth):
    """
    Save figure as file.

    :param figure: matplotlib figure
    :type figure: matplotlib.figure.Figure
    :param file_adr: file address
    :type file_adr: str
    :param depth: image depth
    :type depth: float
    :return: result as dict
    """
    if figure is None:
        return {"status": False, "message": NO_FIG_ERROR_MESSAGE}
    result = {"status": True, "message": FIG_SAVE_SUCCESS_MESSAGE}
    try:
        figure.savefig(
            file_adr,
            dpi=depth * figure.dpi,
            facecolor=figure.get_facecolor(),
            edgecolor='none')
        result["message"] = os.path.abspath(file_adr)
        return result
    except Exception as e:
        result["status"] = False
        result["message"] = str(e)
        return result


def save_fig_buf(figure, depth):
    """
    Save figure as buffer.

    :param figure: matplotlib figure
    :type figure: matplotlib.figure.Figure
    :param depth: image depth
    :type depth: float
    :return: result as dict
    """
    if figure is None:
        return {"status": False, "message": NO_FIG_ERROR_MESSAGE}
    result = {
        "status": True,
        "message": FIG_SAVE_SUCCESS_MESSAGE,
        "buffer": None}
    try:
        buf = io.BytesIO()
        figure.savefig(
            buf,
            dpi=depth * figure.dpi,
            format='png',
            facecolor=figure.get_facecolor(),
            edgecolor='none')
        result["buffer"] = buf
        return result
    except Exception as e:
        result["status"] = False
        result["message"] = str(e)
        return result


def samila_help():
    """
    Print samila details.

    :return: None
    """
    print(OVERVIEW)
    print("Website : https://www.samila.site")
    print("Repo : https://github.com/sepandhaghighi/samila")


def is_same_data(data1, data2, precision=10**-5):
    """
    Compare two data to be the same.

    :param data1: given data1
    :type data1: list
    :param data2: given data2
    :type data2: list
    :param precision: comparing precision
    :type precision: float
    :return: True if they are the same
    """
    if len(data1) != len(data2):
        return False
    is_same = map(lambda x, y: abs(x - y) < precision, data1, data2)
    return all(is_same)


def _serialize_color(color):
    """
    Serialize the given color to a json serializable object.

    :param color: given color
    :type color: str or nd.array
    :return: the serializable version of the color
    """
    if isinstance(color, str):
        return color
    return list(color)


def _serialize_cmap(cmap):
    """
    Serialize the cmap for saving.

    :param cmap: color map
    :type cmap: matplotlib.colors.Colormap
    :return: list of colors
    """
    return list(map(_serialize_color, cmap.colors))


def _load_cmap(config):
    """
    Load the cmap from config.

    :param config: plot part configuration
    :type config: dict or json
    :return: ListedColormap from cmap
    """
    if "cmap" not in config:
        return DEFAULT_CMAP
    cmap = config["cmap"]
    return ListedColormap(cmap)


def load_data(g, data):
    """
    Load data file.

    :param g: generative image instance
    :type g: GenerativeImage
    :param data: prior generated data
    :type data: (io.IOBase & file)
    :return: None
    """
    if isinstance(data, io.IOBase):
        data = json.load(data)
        g.data1 = data.get('data1')
        g.data2 = data.get('data2')
        if g.data1 is None or g.data2 is None:
            raise samilaDataError(DATA_FORMAT_ERROR)
        if 'matplotlib_version' in data:
            g.matplotlib_version = data['matplotlib_version']
        plot_config = data.get("plot")
        if plot_config is not None:
            g.color = plot_config.get("color", DEFAULT_COLOR)
            g.bgcolor = plot_config.get("bgcolor", DEFAULT_BACKGROUND_COLOR)
            g.cmap = _load_cmap(plot_config)
            g.spot_size = plot_config.get("spot_size", DEFAULT_SPOT_SIZE)
            g.projection = plot_config.get("projection", DEFAULT_PROJECTION)
            g.marker = plot_config.get("marker", DEFAULT_MARKER)
            g.alpha = plot_config.get("alpha", DEFAULT_ALPHA)
            g.linewidth = plot_config.get("linewidth", DEFAULT_LINEWIDTH)
            g.depth = plot_config.get("depth", DEFAULT_DEPTH)
        return
    raise samilaDataError(DATA_TYPE_ERROR)


def load_config(g, config):
    """
    Load config file.

    :param g: generative image instance
    :type g: GenerativeImage
    :param config: config JSON file
    :type config: (io.IOBase & file)
    :return: None
    """
    if isinstance(config, io.IOBase):
        data = json.load(config)
        g.function1_str = data.get("f1")
        g.function2_str = data.get("f2")
        if g.function1_str is None or g.function2_str is None:
            raise samilaConfigError(CONFIG_FORMAT_ERROR)
        if 'matplotlib_version' in data:
            g.matplotlib_version = data['matplotlib_version']
        generate_config = data.get("generate")
        if generate_config is not None:
            g.seed = generate_config.get("seed")
            g.start = generate_config.get("start", DEFAULT_START)
            g.step = generate_config.get("step", DEFAULT_STEP)
            g.stop = generate_config.get("stop", DEFAULT_STOP)
        plot_config = data.get("plot")
        if plot_config is not None:
            g.color = plot_config.get("color", DEFAULT_COLOR)
            g.bgcolor = plot_config.get("bgcolor", DEFAULT_BACKGROUND_COLOR)
            g.cmap = _load_cmap(plot_config)
            g.spot_size = plot_config.get("spot_size", DEFAULT_SPOT_SIZE)
            g.projection = plot_config.get("projection", DEFAULT_PROJECTION)
            g.marker = plot_config.get("marker", DEFAULT_MARKER)
            g.alpha = plot_config.get("alpha", DEFAULT_ALPHA)
            g.linewidth = plot_config.get("linewidth", DEFAULT_LINEWIDTH)
            g.depth = plot_config.get("depth", DEFAULT_DEPTH)
        return
    raise samilaConfigError(CONFIG_TYPE_ERROR)
