import copy
import json
from collections import defaultdict

import binary_c_python_api
from binarycpython.utils.custom_logging_functions import (
    create_and_load_logging_function,
)


def get_help_all(print_help=True, return_dict=False):
    """
    Function that reads out the output of the help_all api call to binary_c

    prints all the parameters and their descriptions.

    return_dict:  returns a dictionary
    """

    # Call function
    help_all = binary_c_python_api.return_help_all()

    # String manipulation
    split = help_all.split(
        "############################################################\n"
    )
    cleaned = [el for el in split if not el == "\n"]

    section_nums = [i for i in range(len(cleaned)) if cleaned[i].startswith("#####")]

    # Create dicts
    help_all_dict = {}

    # Select the section name and the contents of that section. Note, not all sections have content!
    for i in range(len(section_nums)):
        if not i == len(section_nums) - 1:
            params = cleaned[section_nums[i] + 1 : section_nums[i + 1]]
        else:
            params = cleaned[section_nums[i] + 1 : len(cleaned)]
        section_name = (
            cleaned[section_nums[i]]
            .lstrip("#####")
            .strip()
            .replace("Section ", "")
            .lower()
        )

        #
        params_dict = {}

        if params:

            # Clean it, replace in-text newlines with a space and then split on newlines.
            split_params = params[0].strip().replace("\n ", " ").split("\n")

            # Process params and descriptions per section
            for el in split_params:
                split_param_info = el.split(" : ")
                if not len(split_param_info) == 3:
                    # there are ocassions where the semicolon is used in the description text itself.
                    if len(split_param_info) == 4:
                        split_param_info = [
                            split_param_info[0],
                            ": ".join([split_param_info[1], split_param_info[2]]),
                            split_param_info[3],
                        ]

                    # other occassions?

                # Put the information in a dict
                param_name = split_param_info[0]
                param_description = split_param_info[1]
                rest = split_param_info[2]

                params_dict[param_name] = {
                    "param_name": param_name,
                    "description": param_description,
                    "rest": rest,
                }

            # make section_dict
            section_dict = {
                "section_name": section_name,
                "parameters": params_dict.copy(),
            }

            # Put in the total dict
            help_all_dict[section_name] = section_dict.copy()

    # Print things
    if print_help:
        for section in sorted(help_all_dict.keys()):
            print(
                "##################\n###### Section {}\n##################".format(
                    section
                )
            )
            section_dict = help_all_dict[section]
            for param_name in sorted(section_dict["parameters"].keys()):
                param = section_dict["parameters"][param_name]
                print(
                    "\n{}:\n\t{}: {}".format(
                        param["param_name"], param["description"], param["rest"]
                    )
                )

    # Loop over all the parameters an call the help() function on it. Takes a long time but this is for testing
    # for section in help_all_dict.keys():
    #     section_dict = help_all_dict[section]
    #     for param in section_dict['parameters'].keys():
    #         get_help(param)

    if return_dict:
        return help_all_dict
    else:
        return None


def create_arg_string(arg_dict):
    """
    Function that creates the arg string
    """
    arg_string = ""
    for key in arg_dict.keys():
        arg_string += "{key} {value} ".format(key=key, value=arg_dict[key])
    arg_string = arg_string.strip()
    return arg_string


def get_defaults(filter_values=False):
    """
    Function that calls the binaryc get args function and cast it into a dictionary
    All the values are strings
    
    filter_values: whether to filter out NULL and Function defaults.
    """

    default_output = binary_c_python_api.return_arglines()
    default_dict = {}

    for default in default_output.split("\n"):
        if not default in ["__ARG_BEGIN", "__ARG_END", ""]:
            key, value = default.split(" = ")

            # Filter out NULLS (not compiled anyway)
            if filter_values:
                if not value in ["NULL", "Function"]:
                    if not value == "":
                        default_dict[key] = value

            # On default, just show everything
            else:
                default_dict[key] = value

    return default_dict


def get_arg_keys():
    """
    Function that return the list of possible keys to give in the arg string
    """

    return get_defaults().keys()


def get_help(param_name, return_dict=False):
    """
    Function that returns the help info for a given parameter. 

    Binary_c will output things in the following order;
    - Did you mean?
    - binary_c help for variable
    - default 
    - available macros

    This function reads out that structure and catches the different components of this output
    
    Will print a dict

    return_dict: wether to return the help info dictionary

    """

    available_arg_keys = get_arg_keys()

    if param_name in available_arg_keys:
        help_info = binary_c_python_api.return_help(param_name)
        cleaned = [el for el in help_info.split("\n") if not el == ""]

        # Get line numbers
        did_you_mean_nr = [
            i for i, el in enumerate(cleaned) if el.startswith("Did you mean")
        ]
        parameter_line_nr = [
            i for i, el in enumerate(cleaned) if el.startswith("binary_c help")
        ]
        default_line_nr = [
            i for i, el in enumerate(cleaned) if el.startswith("Default")
        ]
        macros_line_nr = [
            i for i, el in enumerate(cleaned) if el.startswith("Available")
        ]

        help_info_dict = {}

        # Get alternatives
        if did_you_mean_nr:
            alternatives = cleaned[did_you_mean_nr[0] + 1 : parameter_line_nr[0]]
            alternatives = [el.strip() for el in alternatives]
            help_info_dict["alternatives"] = alternatives

        # Information about the parameter
        parameter_line = cleaned[parameter_line_nr[0]]
        parameter_name = parameter_line.split(":")[1].strip().split(" ")[0]
        parameter_value_input_type = (
            " ".join(parameter_line.split(":")[1].strip().split(" ")[1:])
            .replace("<", "")
            .replace(">", "")
        )

        help_info_dict["parameter_name"] = parameter_name
        help_info_dict["parameter_value_input_type"] = parameter_value_input_type

        description_line = " ".join(
            cleaned[parameter_line_nr[0] + 1 : default_line_nr[0]]
        )
        help_info_dict["description"] = description_line

        # Default:
        default_line = cleaned[default_line_nr[0]]
        default_value = default_line.split(":")[-1].strip()

        help_info_dict["default"] = default_value

        # Get Macros:
        if macros_line_nr:
            macros = cleaned[macros_line_nr[0] + 1 :]
            help_info_dict["macros"] = macros

        # for key in help_info_dict.keys():
        #     print("{}:\n\t{}".format(key, help_info_dict[key]))

        if return_dict:
            return help_info_dict

    else:
        print(
            "{} is not a valid parameter name. Please choose from the following parameters:\n\t{}".format(
                param_name, list(available_arg_keys)
            )
        )
        return None


# get_help("RLOF_method")


def run_system(**kwargs):
    """
    Wrapper to run a system with settings 
    
    This function determines which underlying python-c api function will be called based upon the arguments that are passed via kwargs.

    - if custom_logging_code or custom_logging_dict is included in the kwargs then it will     
    - if 

    """

    # Load default args
    args = get_defaults()
    if "custom_logging_code" in kwargs:
        # Use kwarg value to override defaults and add new args
        for key in kwargs.keys():
            if not key == "custom_logging_code":
                args[key] = kwargs[key]

        # Generate library and get memaddr
        func_memaddr = create_and_load_logging_function(kwargs["custom_logging_code"])

        # Construct arguments string and final execution string
        arg_string = create_arg_string(args)
        arg_string = "binary_c {}".format(arg_string)

        # Run it and get output
        output = binary_c_python_api.run_binary_custom_logging(arg_string, func_memaddr)
        return output

    elif "log_filename" in kwargs:
        # Use kwarg value to override defaults and add new args
        for key in kwargs.keys():
            args[key] = kwargs[key]

        # Construct arguments string and final execution string
        arg_string = create_arg_string(args)
        arg_string = "binary_c {}".format(arg_string)

        # Run it and get output
        output = binary_c_python_api.run_binary_with_logfile(arg_string)
        return output

    else:  # run the plain basic type

        # Use kwarg value to override defaults and add new args
        for key in kwargs.keys():
            args[key] = kwargs[key]

        # Construct arguments string and final execution string
        arg_string = create_arg_string(args)
        arg_string = "binary_c {}".format(arg_string)

        # Run it and get output
        output = binary_c_python_api.run_binary(arg_string)

        return output


def run_system_with_log(**kwargs):
    """
    Wrapper to run a system with settings AND logs the files to a designated place defined by the log_filename parameter.
    """

    # Load default args
    args = get_defaults()
    # args = {}

    # For example
    # physics_args['M_1'] = 20
    # physics_args['separation'] = 0 # 0 = ignored, use period
    # physics_args['orbital_period'] = 100000000000 # To make it single

    # Use kwarg value to override defaults and add new args
    for key in kwargs.keys():
        args[key] = kwargs[key]

    # Construct arguments string and final execution string
    arg_string = create_arg_string(args)
    arg_string = "binary_c {}".format(arg_string)

    # print(arg_string)

    # Run it and get output
    output = binary_c_python_api.run_binary_with_logfile(arg_string)

    return output


# run_system_with_log()


def parse_output(output, selected_header):
    """
    Function that parses output of binary_c:

    This function works in two cases:
    if the caught line contains output like 'example_header time=12.32 mass=0.94 ..'
    or if the line contains output like 'example_header 12.32 0.94'

    You can give a 'selected_header' to catch any line that starts with that. 
    Then the values will be put into a dictionary.
    
    TODO: Think about exporting to numpy array or pandas instead of a defaultdict
    """

    value_dicts = []
    val_lists = []

    # split output on newlines
    for i, line in enumerate(output.split("\n")):
        # Skip any blank lines
        if not line == "":
            split_line = line.split()

            # Select parts
            header = split_line[0]
            values_list = split_line[1:]

            # print(values_list)
            # Catch line starting with selected header
            if header == selected_header:
                # Check if the line contains '=' symbols:
                value_dict = {}
                if all("=" in el for el in values_list):
                    for el in values_list:
                        key, val = el.split("=")
                        value_dict[key.strip()] = val.strip()
                    value_dicts.append(value_dict)
                else:
                    if any("=" in el for el in values_list):
                        raise ValueError(
                            "Caught line contains some = symbols but not all of them do. aborting run"
                        )
                    else:
                        for i, val in enumerate(values_list):
                            value_dict[i] = val
                        value_dicts.append(value_dict)

    if len(value_dicts) == 0:
        print(
            "Sorry, didnt find any line matching your header {}".format(selected_header)
        )
        return None

    keys = value_dicts[0].keys()

    # Construct final dict.
    final_values_dict = defaultdict(list)
    for value_dict in value_dicts:
        for key in keys:
            final_values_dict[key].append(value_dict[key])

    return final_values_dict


def load_logfile(logfile):
    with open(logfile, "r") as f:
        logfile_data = f.readlines()

    time_list = []
    m1_list = []
    m2_list = []
    k1_list = []
    k2_list = []
    sep_list = []
    ecc_list = []
    rel_r1_list = []
    rel_r2_list = []
    event_list = []

    random_seed = logfile_data[0].split()[-2]
    random_count = logfile_data[0].split()[-1]
    probability = logfile_data[-1].split()

    for line in logfile_data[1:-1]:
        split_line = line.split()

        time_list.append(split_line[0])
        m1_list.append(split_line[1])
        m2_list.append(split_line[2])
        k1_list.append(split_line[3])
        k2_list.append(split_line[4])
        sep_list.append(split_line[5])
        ecc_list.append(split_line[6])
        rel_r1_list.append(split_line[7])
        rel_r2_list.append(split_line[8])
        event_list.append(" ".join(split_line[9:]))

    print(event_list)


# load_logfile()