# this file contains all tools necessary to build the docstrings in _doc.py

import copy
from inspect import getfullargspec as args
from re import sub

from plotext_plus._utility import colorize
from plotext_plus._utility import uncolorize

method_name_color = "blue+"
method_name_style = "bold"
alias_style = "italic"

parameters_title_color = "none"
parameters_title_style = "none"

parameter_name_color = "red+"
parameter_name_style = "bold"

parameter_specs_color = "orange+"
parameter_specs_style = "dim"

parameter_doc_style = "italic"

return_color = "orange+"
return_style = "bold"

warning = colorize("Warning", "orange", "bold")

nl = "\n"
sp = " "
cm = ", "
sc = "; "


def correct_doc(doc):
    doc = doc.strip()
    doc = doc[:1].upper() + doc[1:]
    doc = doc if len(doc) == 0 or doc[-1] == "." else doc + "."
    doc = sub("  ", " ", doc)
    return doc


class ParameterClass:  # parameter doc
    def __init__(self, name, doc="", type="", default=""):
        self.name = name.lower()
        self.doc = correct_doc(doc)
        self.type = None if type is None else str(type)
        self.set_default(default)

    def set_default(self, default=""):
        if default == "":
            self.default = ""
        elif isinstance(default, str):
            self.default = "'" + default + "'"
        elif isinstance(default, float):
            self.default = str(round(default, 3))
        else:
            self.default = str(default)

    def get_doc(self):
        name = colorize(self.name, parameter_name_color, parameter_name_style)
        type = "" if self.type == "" else "type: " + str(self.type)
        default = "" if self.default == "" else "default: " + self.default
        specs = sc.join([spec for spec in [type, default] if spec != ""])
        specs = (
            nl + colorize(specs, parameter_specs_color, parameter_specs_style)
            if specs != ""
            else ""
        )
        doc = colorize(self.doc, style=parameter_doc_style)
        return nl + name + sp + doc + specs

    def copy(self, default=""):
        par = copy.copy(self)
        par.set_default(default)
        return par


class ParametersClass:
    def __init__(self):
        self.list = []

    def append(self, parameter):
        self.list.append(parameter)

    def add(self, name, doc="", type="", default=""):
        self.append(ParameterClass(name, doc, type, default))

    def get_title(self):
        lp = len(self.list)
        title = "This is its parameter:" if lp == 1 else "These are its parameters:"
        return colorize(title, parameters_title_color, parameters_title_style)

    def get_doc(self):
        docs = [el.get_doc() for el in self.list]
        return nl + self.get_title() + nl + nl.join(docs) if len(self.list) != 0 else ""

    def get_parameter(self, name):
        names = [el.name for el in self.list]
        if name not in names:
            print(warning, "no parameter", name, "found")
        index = names.index(name) if name in names else None
        return self.list[index] if name in names else None


class OutputClass:
    def __init__(self, doc="", type=None):
        self.type = type
        self.doc = correct_doc(doc)

    def get_doc(self):
        title = colorize("Returns", return_color, return_style)
        type = "" if self.type is None else "type: " + str(self.type)
        type = (
            colorize(type, parameter_specs_color, parameter_specs_style)
            if type != ""
            else ""
        )
        doc = colorize(self.doc, style=parameter_doc_style)
        return nl + title + sp + doc + nl + type if self.doc != "" else ""


class MethodClass:
    def __init__(self, name, alias=None):
        self.name = name.lower()
        self.set_doc()
        self.alias = alias

        self.parameters = ParametersClass()
        self.set_output()
        self.status = False

    def set_doc(self, doc=""):
        self.doc = correct_doc(doc)

    def set_output(self, doc="", type=""):
        self.output = OutputClass(doc, type)

    def append_parameter(self, parameter_object):
        self.parameters.append(parameter_object)

    def add_parameter(self, name, doc="", type="", default=""):
        self.parameters.add(name, doc, type, default)

    def get_title(self):
        return colorize(self.name, method_name_color, method_name_style)

    def get_doc(self):
        alias = (
            (
                nl
                + "The methods "
                + colorize(self.name + "()", style=alias_style)
                + " and "
                + colorize(self.alias + "()", style=alias_style)
                + " are equivalent."
            )
            if self.alias != ""
            else ""
        )
        pars = self.parameters.get_doc()
        out = self.output.get_doc()
        return nl.join([el for el in [self.doc, alias, pars, out] if el != ""])

    def get_parameters(self):
        return [el.name for el in self.parameters.list]

    def get_parameter(self, name):
        return self.parameters.get_parameter(name)

    def show(self):
        print(self.get_doc())


def get_parameters(method):
    spec = args(method)
    parameters = (
        ([spec.varargs] if spec.varargs is not None else [])
        + spec.args
        + spec.kwonlyargs
    )
    parameters = [el for el in parameters if el != "self"]
    # defaults = spec.defaults if spec.defaults is not None else spec.kwonlydefaults.values() if spec.kwonlydefaults is not None else []
    # lp, ld = len(parameters), len(defaults)
    # defaults = [None] * (lp - ld) + list(defaults)
    # return [(parameters[i], defaults[i]) for i in range(lp)]
    return parameters  # , defaults


class DocumentationClass:  # a list of MethodClass objects
    "It contains the doc-strings of all the main plotext functions."

    def __init__(self):
        self._methods = []

    def _add_method(self, name, alias=""):
        method = MethodClass(name, alias)
        self._methods.append(method)
        setattr(self, name, method.show)

    def _last(self):
        return self._methods[-1]

    def _set_doc(self, doc):
        self._last().set_doc(doc)

    def _add_parameter(self, name, doc="", type="", default=""):
        self._last().add_parameter(name, doc, type, default)

    def _set_output(self, doc="", type=""):
        self._last().set_output(doc, type)

    def _get_method(self, name, silent=True):
        names = [el.name for el in self._methods]
        if name not in names:
            if not silent:
                print(warning, "no method", name + "() found")
        return self._methods[names.index(name)] if name in names else None

    def _get_parameters(self, parameter, method):
        method = self.get_method(method)
        return method.get_parameters(parameter) if method is not None else None

    def _add_past_parameter(self, name, method, default=None):
        method = self._get_method(method)
        parameter = method.get_parameter(name) if method is not None else None
        parameter = parameter if default is None else parameter.copy(default)
        self._last().append_parameter(parameter) if parameter is not None else None

    def _set_past_output(self, method):
        method = self.get_method(method)
        output = method.output
        self._set_output(output.type, output.doc)

    def all(self):
        docs = (nl * 3).join(
            [
                el.get_title() + nl + el.get_doc()
                for el in self._methods
                if el.status in [0, 1]
            ]
        )
        print(docs)

    def _add_function(self, function, silent=True):
        name = function.__name__
        method = self._get_method(name, silent=silent)
        name += "()"
        if method is None:
            if not silent:
                print(warning, name, "doc not present")
            return
        doc = method.get_doc()
        function.__doc__ = uncolorize(doc)
        function.doc = lambda: print(doc)
        parameters_actual = get_parameters(function)
        parameters_found = method.get_parameters()
        if parameters_actual != parameters_found:
            if not silent:
                actual = colorize(cm.join(parameters_actual), style="italic")
                found = colorize(cm.join(parameters_found), style="italic")
                print(warning, "the parameters of", name, "are", actual, "not", found + ".")


class ParameterTypes:
    def __init__(self):
        self.int = "an integer"
        self.float = "a float"
        self.num = "a number"
        self.str = "a string"
        self.bool = "a Boolean"

        self.ints = "integers"
        self.floats = "floats"
        self.nums = "numbers"
        self.strs = "strings"
        self.bools = "Booleans"

        self.list_int = lambda n="many": self.plural(self.ints, n)
        self.list_float = lambda n="many": self.plural(self.floats, n)
        self.list_num = lambda n="many": self.plural(self.nums, n)
        self.list_str = lambda n="many": self.plural(self.strs, n)
        self.list_bool = lambda n="many": self.plural(self.bools, n)

        self.fig = "a plotext figure"
        self.xy = "one or two lists of numbers or string dates"
        self.multiple_xy = "an optional list of numbers or date strings and a mandatory matrix of numbers"
        self.x = "a list of numbers or string dates"
        self.marker = "a string or a list of strings"
        self.color = "a string or an integer (from 0 to 255) or a tuple of 3 integers (from 0 to 255)"
        self.colors = "strings or integers (from 0 to 255) or tuples of 3 integers (from 0 to 255)"
        self.list_color = lambda n="many": self.plural(self.colors, n)
        self.color_list = self.color + " or a list of those"
        self.str_list = self.mix(self.str, self.list_str())

        self.str_int = self.mix(self.str, self.int)
        self.str_num = self.mix(self.str, self.num)
        self.list_str_num = lambda n="many": self.plural(
            self.mix(self.strs, self.nums), n
        )
        self.list_num_bool = lambda n="many": self.plural(
            self.mix(self.nums, self.bools), n
        )
        self.bool_num_str = self.mix(self.bool, self.num, self.str)
        self.dic = "a dictionary with mandatory keys: 'Open', 'Close', 'High', 'Low'; each value should be a list of numbers."
        self.matrix = "a list of numbers or a list of tuples 3 integers (from 0 to 255)"
        self.datetime = "a datetime object"
        self.list_datetime = self.plural(self.datetime)
        self.data = "a 2 dimensional matrix of numbers or strings"

    def plural(self, type, n="many"):
        return "a list of " + (str(n) + sp if not isinstance(n, str) else "") + type

    def mix(self, *types):
        return " or ".join(types)


documentation = DocumentationClass()
method = documentation._add_method
doc = documentation._set_doc
par = documentation._add_parameter
past = documentation._add_past_parameter
out = documentation._set_output
past_out = documentation._set_past_output
add = documentation._add_function

t = ParameterTypes()
