import copy

import fred.src.utils as utils
import fred.src.web_interface.wi_utils as wi_utils
import os


def parse_object(wi_object, key_yaml, read_in_whitelists, return_id=False):
    """
    This function parses a wi object back into a yaml
    :param key_yaml: the read in general structure
    :param wi_object: the filled wi object
    :return: result: a dictionary matching the metadata yaml structure
    """

    # define an empty dictionary to store the converted wi_object
    result = {}

    # TODO: needed? better solution?
    # save project id
    project_id = ""
    for elem in wi_object["project"]["input_fields"]:
        if elem["position"].split(":")[-1] == "id" and elem["value"] is not None:
            project_id = elem["value"]

    # set parameters organism, sample_name and nom -> get filled during
    # conversion
    organism = ""
    sample_name = ""
    nom = 1
    global_count = 1
    local_count = 1
    double = []
    publication_records = wi_object["publication_records"]
    # iterate over parts (from general structure to ensure order)
    for key in key_yaml:

        # make sure the key is present in the wi object
        if key in wi_object and key not in ["old_filenames", "old_sample_names"]:

            # parse every part into yaml format
            (
                result[key],
                organism,
                sample_name,
                nom,
                global_count,
                local_count,
                double,
            ) = parse_part(
                wi_object[key],
                key_yaml,
                copy.deepcopy(wi_object["all_factors"]),
                project_id,
                organism,
                sample_name,
                nom,
                global_count,
                local_count,
                double,
                read_in_whitelists,
                publication_records,
            )

    # remove keys with value None
    result = {k: v for k, v in result.items() if v is not None}

    sample_name_positions = get_sample_name_positions(result)
    for sample_pos in sample_name_positions:
        utils.fill_key(
            sample_pos,
            utils.create_sample_names(
                result,
                (
                    wi_object["old_sample_names"]
                    if "old_sample_names" in wi_object
                    else {}
                ),
                sample_pos,
                read_in_whitelists=read_in_whitelists,
            ),
            result,
        )

    filename_positions = get_filename_positions(result)
    for file_pos in filename_positions:
        utils.fill_key(
            file_pos,
            utils.create_filenames(
                result,
                double,
                file_pos,
                wi_object["old_filenames"] if "old_filenames" in wi_object else {},
                read_in_whitelists=read_in_whitelists,
            ),
            result,
        )

    if return_id:
        return (project_id, result)
    else:
        return result


def get_filename_positions(metafile):
    positions = [
        x + ["filenames"]
        for x in utils.get_key_positions(metafile, "technical_replicates", [], [])
    ]
    return positions


def get_sample_name_positions(metafile):
    positions = [
        x + ["sample_name"]
        for x in utils.get_key_positions(metafile, "technical_replicates", [], [])
    ]
    return positions


def split_name(elem, double, gene=True):
    new_name = []
    if "+" in elem:
        new_split = elem.split("+")
        for part in new_split:
            new_part, gene = split_name(part, double, gene=gene)
            if new_part != "":
                new_name.append(new_part)
        elem = "-".join(new_name)

    if "#" in elem:
        remove = elem.split("#")[0]
        elem, gene = split_name(elem[len(f"{remove}#") :], double, gene=gene)

    if "." in elem:
        if elem.lower() in [f"gn.{x.lower()}" for x in double]:
            gene = False
            elem = ""
        elif elem.lower().startswith("embl.") and gene is True:
            elem = ""
        else:
            elem = elem.split(".")[1]

    return elem, gene


def parse_part(
    wi_object,
    key_yaml,
    factors,
    project_id,
    organism,
    sample_name,
    nom,
    global_count,
    local_count,
    double,
    read_in_whitelists,
    publication_records,
):
    """
    This function parses a part of the wi object to create the yaml structure
    :param key_yaml: the read in general structure
    :param sample_name: the name of a sample build from condition name and
                        index of biological replicate
    :param wi_object: a part of the filled wi object
    :param factors: the selected experimental factors
    :param organism: the selected organism
    :param project_id: the project id
    :param nom: the number of measurements
    :return: val: the parsed part in yaml structure
             organism: the shortened version of the used organism
             sample_name: the name of the current sample
             nom: the number of measurements for the current sample
    """

    # initialize the converted value with None
    val = None

    # test if the object to parse is a dictionary
    if isinstance(wi_object, dict):

        # test if the values were stored in the 'list_value' key
        # (if the key takes a list or is of type single- or multi-autofill)
        if "list_value" in wi_object and not (
            "input_type" in wi_object
            and wi_object["input_type"] in ["single_autofill", "restricted_short_text"]
        ):

            # define an empty list to store the converted list values
            val = []
            is_cond = False
            # TODO: headers for list element (if gene is a list)
            # iterate over the list elements
            for i in range(len(wi_object["list_value"])):

                # test if the element is a dictionary
                if isinstance(wi_object["list_value"][i], dict):

                    # special case: condition
                    if (
                        wi_object["list_value"][i]["position"].split(":")[-1]
                        == "condition"
                    ):

                        # define emtpy list to save parsed samples to
                        samples = []
                        local_count = 1
                        is_cond = True
                        # iterate over all samples
                        for sub_elem in wi_object["list_value"][i]["list_value"]:

                            # convert samples
                            (
                                sample,
                                organism,
                                sample_name,
                                nom,
                                global_count,
                                local_count,
                                double,
                            ) = parse_list_part(
                                sub_elem,
                                key_yaml,
                                factors,
                                project_id,
                                organism,
                                sample_name,
                                nom,
                                global_count,
                                local_count,
                                double,
                                read_in_whitelists,
                                publication_records,
                            )

                            # remove empty keys
                            sample = {k: v for k, v in sample.items() if v is not None}

                            # add sample to the samples list
                            samples.append(sample)

                        # add dictionary with condition name and biological
                        # replicates (samples) to list parsed list
                        condition = {
                            "condition_name": wi_object["list_value"][i][
                                "correct_value"
                            ],
                            "biological_replicates": {"count": len(samples)},
                        }

                        # add samples to the condition dictionary if count > 0
                        if len(samples) > 0:
                            condition["biological_replicates"]["samples"] = samples

                        # add condition to list of converted values
                        val.append(condition)

                # test if list element is a list
                elif isinstance(wi_object["list_value"][i], list):

                    # special case: experimental setting
                    if wi_object["position"].split(":")[-1] == "experimental_setting":

                        # call parse function using the experimental factors
                        # with the same index as the setting
                        (
                            c_val,
                            organism,
                            sample_name,
                            nom,
                            global_count,
                            local_count,
                            double,
                        ) = parse_list_part(
                            wi_object["list_value"][i],
                            key_yaml,
                            factors[i],
                            project_id,
                            organism,
                            sample_name,
                            nom,
                            global_count,
                            local_count,
                            double,
                            read_in_whitelists,
                            publication_records,
                        )

                        # add the converted value to the list
                        val.append(c_val)

                    # no special case
                    else:

                        # call parse function with all experimental factors
                        (
                            c_val,
                            organism,
                            sample_name,
                            nom,
                            global_count,
                            local_count,
                            double,
                        ) = parse_list_part(
                            wi_object["list_value"][i],
                            key_yaml,
                            factors,
                            project_id,
                            organism,
                            sample_name,
                            nom,
                            global_count,
                            local_count,
                            double,
                            read_in_whitelists,
                            publication_records,
                        )

                        # add the converted value to the list
                        val.append(c_val)

                # if list element is str/int/bool
                else:

                    new_val = copy.deepcopy(wi_object["list_value"][i])
                    parse_w_key = None
                    if "whitelist_keys" in wi_object:
                        for w_key in wi_object["whitelist_keys"]:
                            if new_val.endswith(f"({w_key})"):
                                new_val = new_val.rstrip(f"({w_key})").strip()
                                parse_w_key = w_key
                                break

                    if "headers" in wi_object:
                        if isinstance(wi_object["headers"], dict):
                            if parse_w_key is not None:
                                if parse_w_key in wi_object["headers"]:
                                    new_val = wi_utils.parse_headers(
                                        wi_object["headers"][parse_w_key],
                                        new_val,
                                        mode="dict",
                                    )
                        else:
                            new_val = wi_utils.parse_headers(
                                wi_object["headers"], new_val, mode="dict"
                            )
                    # add list element to list
                    val.append(new_val)

            # set the converted value to None if it is an empty list
            if len(val) == 0:
                val = None
            elif is_cond:
                val = sorted(val, key=lambda x: x["condition_name"])

        # the values are not saved as a list
        else:

            # wi object contains input fields
            if "input_fields" in wi_object:

                # special case: technical replicates
                if wi_object["position"].split(":")[-1] == "technical_replicates":

                    # file_name = get_file_name(sample_name.removesuffix(f'_{sample_name.split("_")[-1]}'), double)
                    # TODO: comment
                    # t_sample_name = []
                    # t_file_name = []
                    count = [
                        x["value"]
                        for x in wi_object["input_fields"]
                        if x["position"].split(":")[-1] == "count"
                    ][0]

                    # for c in range(count):
                    #    for m in range(nom):
                    #        t_sample_name.append(f'{project_id}_'
                    #                             f'{organism}_'
                    #                             f'{sample_name}'
                    #                             f'_t{"{:02d}".format(c + 1)}_'
                    #                             f'm{"{:02d}".format(m + 1)}')
                    #        t_file_name.append(f'{project_id}__'
                    #                           f'{global_count}__'
                    #                           f'{file_name}__'
                    #                           f'{local_count}')
                    #        local_count += 1
                    #        global_count += 1

                    val = {"count": count}

                # no special case
                else:

                    # call this function on the input fields
                    (
                        val,
                        organism,
                        sample_name,
                        nom,
                        global_count,
                        local_count,
                        double,
                    ) = parse_part(
                        wi_object["input_fields"],
                        key_yaml,
                        factors,
                        project_id,
                        organism,
                        sample_name,
                        nom,
                        global_count,
                        local_count,
                        double,
                        read_in_whitelists,
                        publication_records,
                    )

            # no input fields
            else:

                # set the value that should be converted (saved in list_value
                # if input type is 'single_autofill', else saved in value)
                convert_value = (
                    wi_object["list_value"][0]
                    if wi_object["input_type"]
                    in ["single_autofill", "restricted_short_text"]
                    and len(wi_object["list_value"]) > 0
                    else wi_object["value"]
                )

                # test if value was filled
                if convert_value is not None:

                    # wi object contains whitelist keys
                    if "whitelist_keys" in wi_object:

                        # replace value with converted one
                        convert_value = wi_utils.parse_whitelist_keys(
                            wi_object["whitelist_keys"],
                            convert_value,
                            wi_object["headers"] if "headers" in wi_object else None,
                        )

                    # wi object contains headers but no whitelist keys
                    elif "headers" in wi_object:

                        # replace the original value with the one split
                        # according to the header
                        convert_value = wi_utils.parse_headers(
                            wi_object["headers"], convert_value
                        )

                    # value is of type value_unit
                    if wi_object["input_type"] == "value_unit":

                        # save the value and unit as a dictionary
                        val = {
                            "value": wi_object["value"],
                            "unit": wi_object["value_unit"],
                        }

                    # value is of type date
                    elif wi_object["input_type"] == "date":

                        # convert the date to a string of format 'DD.MM.YYYY'
                        val = wi_utils.date_to_str(wi_object["value"])

                    # value was changed for display -> original value saved at
                    # key 'correct_value'
                    elif "correct_value" in wi_object:

                        # special_case sample name
                        if wi_object["position"].split(":")[-1] == "sample_name":

                            # split and save index of sample from sample name
                            sample_count = int(convert_value.split("_")[-1])

                            # reformat sample name with ending 'b<index>' for
                            # number of biological replicate
                            val = (
                                f'{wi_object["correct_value"]}_b'
                                f'{"{:02d}".format(sample_count)}'
                            )

                            # set the sample name to the value
                            sample_name = val

                        else:

                            # save correct value
                            val = wi_object["correct_value"]

                    else:

                        if isinstance(convert_value, str):

                            info = list(
                                utils.find_keys(
                                    key_yaml, wi_object["position"].split(":")[-1]
                                )
                            )
                            if (
                                len(info) > 0
                                and "input_type" in info[0]
                                and info[0]["input_type"] == "bool"
                            ):
                                if convert_value == "True":
                                    val = True
                                else:
                                    val = False
                            else:
                                val = (
                                    convert_value.replace("\\n", " ")
                                    .replace("\n", " ")
                                    .replace("\\", " ")
                                )
                                val = " ".join(val.split())
                        else:
                            # save value
                            val = convert_value

                        # set the number of replicates
                        if (
                            wi_object["position"].split(":")[-1]
                            == "number_of_measurements"
                        ):
                            nom = val

    # wi object is a list
    elif isinstance(wi_object, list):

        # call parse list function
        val, organism, sample_name, nom, global_count, local_count, double = (
            parse_list_part(
                wi_object,
                key_yaml,
                factors,
                project_id,
                organism,
                sample_name,
                nom,
                global_count,
                local_count,
                double,
                read_in_whitelists,
                publication_records,
            )
        )

    return val, organism, sample_name, nom, global_count, local_count, double


def parse_list_part(
    wi_object,
    key_yaml,
    factors,
    project_id,
    organism,
    sample_name,
    nom,
    global_count,
    local_count,
    double,
    read_in_whitelists,
    publication_records,
):
    """
    This function parses a part of the wi object of type list into the yaml
    structure
    :param sample_name: the name of a sample build from condition name and
                        index of biological replicate
    :param key_yaml: the read in general structure
    :param wi_object: the part of the wi object
    :param factors: the selected experimental factors
    :param organism: the selected organism
    :param project_id: the project id
    :param nom: the number of measurements
    :return: res: the parsed part in yaml structure
             organism: the shortened version of the used organism
             sample_name: the name of the current sample
             nom: the number of measurements for the current sample
    """

    # create dictionary to save parsed elements to
    res = {}

    # iterate over list
    for i in range(len(wi_object)):

        val = None

        # special case: organism
        if wi_object[i]["position"].split(":")[-1] == "organism":

            # TODO: headers
            # save the organism name
            organism = wi_object[i]["value"].split(" ")[0]

            # read in the abbrev whitelist for organisms
            short = utils.get_whitelist(
                os.path.join("abbrev", "organism_name"),
                {"organism": organism},
                whitelist_object=read_in_whitelists,
            )["whitelist"]

            # save the shortened version of the organism
            organism = short[organism]

        # special case: experimental factors
        if wi_object[i]["position"].split(":")[-1] == "experimental_factors":

            # iterate over experimental factors
            for r in range(len(factors)):

                factors[r], double = parse_factor(factors[r], key_yaml, double)

            # set val to factors
            val = factors

        elif wi_object[i]["position"].split(":")[-1] == "publication":
            publications = []
            for elem in wi_object[i]["list_value"]:
                pubmed_id = None
                for sub_elem in elem:
                    if sub_elem["position"].split(":")[-1] == "pubmed_id":
                        pubmed_id = sub_elem["value"]
                        break
                if pubmed_id is not None:
                    if str(pubmed_id) not in publication_records:
                        publications.append({"pubmed_id": pubmed_id})
                    else:
                        pub_keys = list(utils.find_keys(key_yaml, "publication"))[0]
                        publication = {}
                        for key in pub_keys["value"]:
                            if key in publication_records[str(pubmed_id)]:
                                publication[key] = publication_records[str(pubmed_id)][
                                    key
                                ]
                        publications.append(publication)
            if len(publications) > 0:
                val = publications
        # no special case
        else:

            # call parse part function
            val, organism, sample_name, nom, global_count, local_count, double = (
                parse_part(
                    wi_object[i],
                    key_yaml,
                    factors,
                    project_id,
                    organism,
                    sample_name,
                    nom,
                    global_count,
                    local_count,
                    double,
                    read_in_whitelists,
                    publication_records,
                )
            )

        # test if the value is not empty
        if val is not None and (
            (type(val) in [str, list, dict] and len(val) > 0)
            or type(val) not in [str, list, dict]
        ):

            # overwrite the old value with the converted one
            res[wi_object[i]["position"].split(":")[-1]] = val

    return (
        res if len(res) > 0 else None,
        organism,
        sample_name,
        nom,
        global_count,
        local_count,
        double,
    )


def parse_factor(factors, key_yaml, double):

    # nested factor
    if len(factors["values"]) == 1 and isinstance(factors["values"][0], dict):

        if "nested_infos" in factors:
            for key_ in factors["nested_infos"]:
                if "double" in factors["nested_infos"][key_]:
                    double = list(set(double + factors["nested_infos"][key_]["double"]))

        # remove key 'multi'
        if "multi" in factors["values"][0]:
            factors["values"][0].pop("multi")

        # remove keys with None as value
        factors["values"][0] = {
            k: v for k, v in factors["values"][0].items() if v is not None
        }

        # test if the key in the 'values' dictionary matches the
        # factor and overwrite the dictionary with its list values
        # -> this is the case if a factor can be a list and can
        #    therefor occur multiple times in a condition
        # -> key multi has to be added -> change 'values' from list
        #    to dict
        # -> e.g. factor tissue -> values {tissue: [...], multi: }
        if list(factors["values"][0].keys()) == [factors["factor"]]:
            factors["values"] = factors["values"][0][factors["factor"]]

        for i in range(len(factors["values"])):
            if isinstance(factors["values"][i], dict):
                remove_keys = []
                for key in factors["values"][i]:

                    whitelist_keys = None
                    headers = None

                    if "nested_infos" in factors and key in factors["nested_infos"]:
                        if "whitelist_keys" in factors["nested_infos"][key]:
                            whitelist_keys = factors["nested_infos"][key][
                                "whitelist_keys"
                            ]
                        if "headers" in factors["nested_infos"][key]:
                            headers = factors["nested_infos"][key]["headers"]

                    if factors["values"][i][key] is None or (
                        isinstance(factors["values"][i][key], list)
                        and len(factors["values"][i][key]) == 0
                    ):
                        remove_keys.append(key)

                    if whitelist_keys is not None:

                        for j in range(len(factors["values"][i][key])):
                            factors["values"][i][key][j] = (
                                wi_utils.parse_whitelist_keys(
                                    whitelist_keys,
                                    factors["values"][i][key][j],
                                    headers,
                                    mode="dict",
                                )
                            )
                    elif headers is not None:
                        for j in range(len(factors["values"][i][key])):
                            factors["values"][i][key][j] = wi_utils.parse_headers(
                                headers, factors["values"][i][key][j], mode="dict"
                            )

                    # if isinstance(factors['values'][i][key], dict):
                    #    if not 'factor' in factors['values'][i][key]:
                    #        factors['values'][i][key]['factor'] = key
                    #    factors['values'][i][key] = parse_factor(factors['values'][i][key], key_yaml)
                    #    factors['values'][i][key].pop('factor')
                for r_key in remove_keys:
                    factors["values"][i].pop(r_key)

    else:

        # fetch the properties of the experimental factor from the
        # general structure
        infos = list(utils.find_keys(key_yaml, factors["factor"]))

        if "double" in factors:
            double = set(double + factors["double"])

        # iterate over values of experimental factor
        for j in range(len(factors["values"])):

            # factor contains whitelist keys
            if "whitelist_keys" in factors:

                # replace value with converted one
                factors["values"][j] = wi_utils.parse_whitelist_keys(
                    factors["whitelist_keys"],
                    factors["values"][j],
                    factors["headers"] if "headers" in factors else None,
                )

            # factor contains headers but no whitelist keys
            elif "headers" in factors:

                # replace the original value with the one split
                # according to the header
                factors["values"][j] = wi_utils.parse_headers(
                    factors["headers"], factors["values"][j]
                )

            # factor of type value_unit
            elif (
                len(infos) > 0
                and "special_case" in infos[0]
                and "value_unit" in infos[0]["special_case"]
            ):

                factors["values"][j] = utils.split_value_unit(factors["values"][j])

    # remove the keys 'header' and 'whitelist_keys'
    if "whitelist_keys" in factors:
        factors.pop("whitelist_keys")
    if "headers" in factors:
        factors.pop("headers")
    if "nested_infos" in factors:
        factors.pop("nested_infos")
    if "double" in factors:
        factors.pop("double")
    return factors, double
