"""
    Binary_c-python's grid logging functions.
"""

import logging
import os
import strip_ansi
import sys
import time

import binarycpython.utils.functions
from binarycpython.utils.functions import (
    format_number,
    trem,
    verbose_print
    )
from binarycpython.utils.grid_options_defaults import (
    secs_per_day
    )

class grid_logging():

    def __init__(self, **kwargs):
        # don't do anything: we just inherit from this class
        return

    def _set_custom_logging(self):
        """
        Function/routine to set all the custom logging so that the function memory pointer
        is known to the grid.

        When the memory adress is loaded and the library file is set we'll skip rebuilding the library
        """

        # Only if the values are the 'default' unset values
        if (
            self.grid_options["custom_logging_func_memaddr"] == -1
            and self.grid_options["_custom_logging_shared_library_file"] is None
        ):
            self.verbose_print(
                "Creating and loading custom logging functionality",
                self.grid_options["verbosity"],
                1,
            )
            # C_logging_code gets priority of C_autogen_code
            if self.grid_options["C_logging_code"]:
                # Generate entire shared lib code around logging lines
                custom_logging_code = binary_c_log_code(
                    self.grid_options["C_logging_code"],
                    verbosity=self.grid_options["verbosity"]
                    - (self._CUSTOM_LOGGING_VERBOSITY_LEVEL - 1),
                )

                # Load memory address
                (
                    self.grid_options["custom_logging_func_memaddr"],
                    self.grid_options["_custom_logging_shared_library_file"],
                ) = create_and_load_logging_function(
                    custom_logging_code,
                    verbosity=self.grid_options["verbosity"]
                    - (self._CUSTOM_LOGGING_VERBOSITY_LEVEL - 1),
                    custom_tmp_dir=self.grid_options["tmp_dir"],
                )

            elif self.grid_options["C_auto_logging"]:
                # Generate real logging code
                logging_line = autogen_C_logging_code(
                    self.grid_options["C_auto_logging"],
                    verbosity=self.grid_options["verbosity"]
                    - (self._CUSTOM_LOGGING_VERBOSITY_LEVEL - 1),
                )

                # Generate entire shared lib code around logging lines
                custom_logging_code = binary_c_log_code(
                    logging_line,
                    verbosity=self.grid_options["verbosity"]
                    - (self._CUSTOM_LOGGING_VERBOSITY_LEVEL - 1),
                )

                # Load memory address
                (
                    self.grid_options["custom_logging_func_memaddr"],
                    self.grid_options["_custom_logging_shared_library_file"],
                ) = create_and_load_logging_function(
                    custom_logging_code,
                    verbosity=self.grid_options["verbosity"]
                    - (self._CUSTOM_LOGGING_VERBOSITY_LEVEL - 1),
                    custom_tmp_dir=self.grid_options["tmp_dir"],
                )
        else:
            self.verbose_print(
                "Custom logging library already loaded. Not setting them again.",
                self.grid_options["verbosity"],
                1,
            )

    def _print_info(self, run_number, total_systems, full_system_dict):
        """
        Function to print info about the current system and the progress of the grid.

        # color info tricks from https://ozzmaker.com/add-colour-to-text-in-python/
        https://stackoverflow.com/questions/287871/how-to-print-colored-text-in-terminal-in-python
        """

        # Define frequency
        if self.grid_options["verbosity"] == 1:
            print_freq = 1
        else:
            print_freq = 10

        if run_number % print_freq == 0:
            binary_cmdline_string = self._return_argline(full_system_dict)
            info_string = "{color_part_1} \
            {text_part_1}{end_part_1}{color_part_2} \
            {text_part_2}{end_part_2}".format(
                color_part_1="\033[1;32;41m",
                text_part_1="{}/{}".format(run_number, total_systems),
                end_part_1="\033[0m",
                color_part_2="\033[1;32;42m",
                text_part_2="{}".format(binary_cmdline_string),
                end_part_2="\033[0m",
            )
            print(info_string)

    def _set_loggers(self):
        """
        Function to set the loggers for the execution of the grid
        """

        # Set log file
        binary_c_logfile = self.grid_options["log_file"]

        # Create directory
        os.makedirs(os.path.dirname(binary_c_logfile), exist_ok=True)

        # Set up logger
        self.logger = logging.getLogger("binary_c_python_logger")
        self.logger.setLevel(self.grid_options["verbosity"])

        # Reset handlers
        self.logger.handlers = []

        # Set formatting of output
        log_formatter = logging.Formatter(
            "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
        )

        # Make and add file handlers
        # make handler for output to file
        handler_file = logging.FileHandler(filename=os.path.join(binary_c_logfile))
        handler_file.setFormatter(log_formatter)
        handler_file.setLevel(logging.INFO)

        # Make handler for output to stdout
        handler_stdout = logging.StreamHandler(sys.stdout)
        handler_stdout.setFormatter(log_formatter)
        handler_stdout.setLevel(logging.INFO)

        # Add the loggers
        self.logger.addHandler(handler_file)
        self.logger.addHandler(handler_stdout)

    ######################
    # Status logging

    def vb1print(self, ID, now, system_number, system_dict):
        """
        Verbosity-level 1 printing, to keep an eye on a grid.
        Arguments:
                 ID: thread ID for debugging (int)
                 now: the time now as a UNIX-style epoch in seconds (float)
                 system_number: the system number

        TODO: add information about the number of cores. the TPR shows the dt/dn but i want to see the number per core too
        """

        # calculate estimated time of arrive (eta and eta_secs), time per run (tpr)
        localtime = time.localtime(now)

        # calculate stats
        n = self.shared_memory["n_saved_log_stats"].value
        if n < 2:
            # simple 1-system calculation: inaccurate
            # but best for small n
            dt = now - self.shared_memory["prev_log_time"][0]
            dn = system_number - self.shared_memory["prev_log_system_number"][0]
        else:
            # average over n_saved_log_stats
            dt = (
                self.shared_memory["prev_log_time"][0]
                - self.shared_memory["prev_log_time"][n - 1]
            )
            dn = (
                self.shared_memory["prev_log_system_number"][0]
                - self.shared_memory["prev_log_system_number"][n - 1]
            )

        eta, units, tpr, eta_secs = trem(
            dt, system_number, dn, self.grid_options["_total_starcount"]
        )

        # compensate for multithreading and modulo
        tpr *= self.grid_options["num_processes"] * self.grid_options["modulo"]

        if eta_secs < secs_per_day:
            fintime = time.localtime(now + eta_secs)
            etf = "{hours:02d}:{minutes:02d}:{seconds:02d}".format(
                hours=fintime.tm_hour, minutes=fintime.tm_min, seconds=fintime.tm_sec
            )
        else:
            d = int(eta_secs / secs_per_day)
            if d == 1:
                etf = "Tomorrow"
            else:
                etf = "In {} days".format(d)

        # modulo information
        if self.grid_options["modulo"] == 1:
            modulo = ""  # usual case
        else:
            modulo = "%" + str(self.grid_options["modulo"])

        # add up memory use from each thread
        total_mem_use = sum(self.shared_memory["memory_use_per_thread"])

        # make a string to describe the system e.g. M1, M2, etc.
        system_string = ""

        # use the multiplicity if given
        if "multiplicity" in system_dict:
            nmult = int(system_dict["multiplicity"])
        else:
            nmult = 4

        # masses
        for i in range(nmult):
            i1 = str(i + 1)
            if "M_" + i1 in system_dict:
                system_string += (
                    "M{}=".format(i1) + format_number(system_dict["M_" + i1]) + " "
                )

        # separation and orbital period
        if "separation" in system_dict:
            system_string += "a=" + format_number(system_dict["separation"])
        if "orbital_period" in system_dict:
            system_string += "P=" + format_number(system_dict["orbital_period"])

        # do the print
        if self.grid_options["_total_starcount"] > 0:
            self.verbose_print(
                "{opening_colour}{system_number}/{total_starcount}{modulo} {pc_colour}{pc_complete:5.1f}% complete {time_colour}{hours:02d}:{minutes:02d}:{seconds:02d} {ETA_colour}ETA={ETA:7.1f}{units} tpr={tpr:2.2e} {ETF_colour}ETF={ETF} {mem_use_colour}mem:{mem_use:.1f}MB {system_string_colour}{system_string}{closing_colour}".format(
                    opening_colour=self.ANSI_colours["reset"]
                    + self.ANSI_colours["yellow on black"],
                    system_number=system_number,
                    total_starcount=self.grid_options["_total_starcount"],
                    modulo=modulo,
                    pc_colour=self.ANSI_colours["blue on black"],
                    pc_complete=(100.0 * system_number)
                    / (1.0 * self.grid_options["_total_starcount"])
                    if self.grid_options["_total_starcount"]
                    else -1,
                    time_colour=self.ANSI_colours["green on black"],
                    hours=localtime.tm_hour,
                    minutes=localtime.tm_min,
                    seconds=localtime.tm_sec,
                    ETA_colour=self.ANSI_colours["red on black"],
                    ETA=eta,
                    units=units,
                    tpr=tpr,
                    ETF_colour=self.ANSI_colours["blue"],
                    ETF=etf,
                    mem_use_colour=self.ANSI_colours["magenta"],
                    mem_use=total_mem_use,
                    system_string_colour=self.ANSI_colours["yellow"],
                    system_string=system_string,
                    closing_colour=self.ANSI_colours["reset"]
                ),
                self.grid_options["verbosity"],
                1,
            )
        else:
            self.verbose_print(
                "{opening_colour}{system_number}{modulo} {time_colour}{hours:02d}:{minutes:02d}:{seconds:02d} tpr={tpr:2.2e} {mem_use_colour}mem:{mem_use:.1f}MB {system_string_colour}{system_string}{closing_colour}".format(
                    opening_colour=self.ANSI_colours["reset"]
                    + self.ANSI_colours["yellow on black"],
                    system_number=system_number,
                    modulo=modulo,
                    time_colour=self.ANSI_colours["green on black"],
                    hours=localtime.tm_hour,
                    minutes=localtime.tm_min,
                    seconds=localtime.tm_sec,
                    tpr=tpr,
                    mem_use_colour=self.ANSI_colours["magenta"],
                    mem_use=total_mem_use,
                    system_string_colour=self.ANSI_colours["yellow"],
                    system_string=system_string,
                    closing_colour=self.ANSI_colours["reset"]
                ),
                self.grid_options["verbosity"],
                1,
            )

    def vb2print(self, system_dict, cmdline_string):
        print(
            "Running this system now on thread {ID}\n{blue}{cmdline}{reset}\n".format(
                ID=self.process_ID,
                blue=self.ANSI_colours["blue"],
                cmdline=cmdline_string,
                reset=self.ANSI_colours["reset"],
            )
        )

    def verbose_print(self,*args,**kwargs):
        # wrapper for functions.verbose_print to use the correct newline
        newline = kwargs.get("newline",self.grid_options["log_newline"])
        if newline is None:
            newline = "\n"
        kwargs["newline"] = newline
        binarycpython.utils.functions.verbose_print(*args,**kwargs)


    def _boxed(self, *list, colour="yellow on black", boxchar="*", separator="\n"):
        """
        Function to output a list of strings in a single box.

        Args:
            list = a list of strings to be output. If these contain the separator
                   (see below) these strings are split by it.
            separator = strings are split on this, default "\n"
            colour = the colour to be used, usually this is 'yellow on black'
                     as set in the ANSI_colours dict
            boxchar = the character used to make the box, '*' by default

        Note: handles tabs (\t) badly, do not use them!
        """
        strlen = 0
        strings = []
        lengths = []

        # make a list of strings
        if separator:
            for l in list:
                strings += l.split(sep=separator)
        else:
            strings = list

        # get lengths without ANSI codes
        for string in strings:
            lengths.append(len(strip_ansi.strip_ansi(string)))

        # hence the max length
        strlen = max(lengths)
        strlen += strlen % 2
        header = boxchar * (4 + strlen)

        # start output
        out = self.ANSI_colours[colour] + header + "\n"

        # loop over strings to output, padding as required
        for n, string in enumerate(strings):
            if lengths[n] % 2 == 1:
                string = " " + string
            pad = " " * int((strlen - lengths[n]) / 2)
            out = out + boxchar + " " + pad + string + pad + " " + boxchar + "\n"
        # close output and return
        out = out + header + "\n" + self.ANSI_colours["reset"]
        return out


    def _get_stream_logger(self, level=logging.DEBUG):
        """Return logger with configured StreamHandler."""
        stream_logger = logging.getLogger("stream_logger")
        stream_logger.handlers = []
        stream_logger.setLevel(level)
        sh = logging.StreamHandler(stream=sys.stdout)
        sh.setLevel(level)
        fmt = "[%(asctime)s %(levelname)-8s %(processName)s] --- %(message)s"
        formatter = logging.Formatter(fmt)
        sh.setFormatter(formatter)
        stream_logger.addHandler(sh)

        return stream_logger

    def _clean_up_custom_logging(self, evol_type):
        """
        Function to clean up the custom logging.
        Has two types:
            'single':
                - removes the compiled shared library
                    (which name is stored in grid_options['_custom_logging_shared_library_file'])
                - TODO: unloads/frees the memory allocated to that shared library
                    (which is stored in grid_options['custom_logging_func_memaddr'])
                - sets both to None
            'multiple':
                - TODO: make this and design this
        """

        if evol_type == "single":
            self.verbose_print(
                "Cleaning up the custom logging stuff. type: single",
                self.grid_options["verbosity"],
                1,
            )

            # TODO: Explicitly unload the library

            # Reset the memory adress location
            self.grid_options["custom_logging_func_memaddr"] = -1

            # remove shared library files
            if self.grid_options["_custom_logging_shared_library_file"]:
                remove_file(
                    self.grid_options["_custom_logging_shared_library_file"],
                    self.grid_options["verbosity"],
                )
                self.grid_options["_custom_logging_shared_library_file"] = None

        if evol_type == "population":
            self.verbose_print(
                "Cleaning up the custom logging stuffs. type: population",
                self.grid_options["verbosity"],
                1,
            )

            # TODO: make sure that these also work. not fully sure if necessary tho.
            #   whether its a single file, or a dict of files/mem addresses

        if evol_type == "MC":
            pass