__all__ = ['StopWatch']

import re
import time
import csv
from io import StringIO

from collections import OrderedDict, defaultdict


class StopWatch:
    __START_PROBE_TAG = 'start'
    __STOP_PROBE_TAG = 'stop'
    __DEFAULT_CLICK_TAG_FORMAT = 'click-{:d}'
    __CUSTOM_AUTOINCREMENT_TAG_FORMAT = re.compile(r'{{(.*?)}}')
    __CUSTOM_AUTOINCREMENT_TAG_QUICK_REPLACE = lambda tag: '{{' + str(tag) + '}}'
    __CUSTOM_NONINCREMENTAL_TAG_FORMAT = re.compile(r'{(.*?)}')
    __CUSTOM_NONINCREMENTAL_TAG_QUICK_REPLACE = lambda tag: '{' + str(tag) + '}'

    __CSV_TAG_HEADLINE = 'Tag'
    __CSV_ELAPSED_TIME_HEADLINE = 'Elapsed Time'

    def __init__(self, debug=False, autostart=False, custom_tags=True, rounding_precision=None, logger=None):
        """
        StopWatch initialization
        :param debug: Whether StopWatch is in debug mode or not. At the moment it only sets the default's logging level
        :param autostart: Starts the StopWatch automatically if set to 'True'
        :param custom_tags: Whether custom_tags are enabled or disabled (to gain performance)
        :param rounding_precision: Number of decimal digits to be stored. None means all.
        """
        self.probes = OrderedDict()
        self.probes_n = 0
        self.debug_mode = debug
        self.custom_tags = defaultdict(lambda: 0)
        self.custom_tags_enabled = custom_tags
        self.rounding_precision = rounding_precision
        self.logger = logger or self.__get_default_logger()

        if autostart:
            self.start()

    def start(self):
        """
        Starts the stopwatch
        :return:
        """
        if StopWatch.__START_PROBE_TAG in self.probes:
            self.logger.warning("Start an already started StopWatch, probes will be messed up.")
        self.probes[StopWatch.__START_PROBE_TAG] = time.time()
        self.logger.info("StopWatch started at {}".format(self.probes[StopWatch.__START_PROBE_TAG]))

    def stop(self):
        """
        Stops the stopwatch
        :return:
        """
        self.probes[StopWatch.__STOP_PROBE_TAG] = time.time()
        self.logger.info("StopWatch stopped at {}".format(self.probes[StopWatch.__STOP_PROBE_TAG]))

    def click(self, tag=None):
        """
        Stores elapsed time from the start of the stopwatch time untill this moment.
        If StopWatch wasn't already running it starts it.
        :param tag: Probe's tag. If not present it will choose 'default'
        :return:
        """
        aux = time.time()

        if not tag:
            tag = StopWatch.__DEFAULT_CLICK_TAG_FORMAT.format(self.probes_n)
            self.probes_n += 1
            self.logger.debug("Tag was not given, created one instead: {}".format(tag))
        else:
            self.logger.debug("Given tag {}".format(tag))

        if StopWatch.__START_PROBE_TAG not in self.probes:
            self.probes[StopWatch.__START_PROBE_TAG] = aux
            self.logger.debug("Click before start, auto-starting...")
            self.logger.warning("StopWatch clicked before being started, auto-starting...")

        if self.custom_tags_enabled:
            tag = self.__populate_custom_tag(tag)

        self.probes[tag] = aux - self.probes[StopWatch.__START_PROBE_TAG]

        self.logger.debug("Using tag {} to store probe".format(tag))

        if self.rounding_precision is not None:
            self.logger.debug("Rounding probe up to {} decimals".format(self.rounding_precision))
            self.probes[tag] = round(self.probes[tag], self.rounding_precision)



        self.logger.info("StopWatch clicked. Tag - {tag} - elapsed time since start: {elapsed}"
                         .format(tag=tag, elapsed=self.probes[tag]))

        return self.probes[tag]

    def pprint_probes(self, exponential_format=False):
        """
        Pretty prints all probes registered up to now.
        :param exponential_format: Whether exponential float representation format will be used or not
        :return:
        """
        float_format = "{:s}\t\t{:g}" if exponential_format else "{:s}\t\t{:f}"
        maxlen = len(max(self.probes.keys(), key=len))
        print("\n\n{:s}\t\t{:s}\n".format("TAG".ljust(maxlen, " "), "ELAPSED TIME"))

        for tag, elapsed_time in self.probes.items():
            print(float_format.format(tag.ljust(maxlen, " "), elapsed_time))

    def csv_dump(self, filepath="", exponential_format=False):
        """
        Dumps all probe information to a csv file
        :param filepath: Path to the taget file
        :param exponential_format: Whether exponential float representation format will be used or not
        :return:
        """
        with open(filepath, 'w', newline='') as csvfile:
            self.__write_csv(csvfile, exponential_format=exponential_format)

    def csv_dumps(self, exponential_format=False):
        """
        Dumps all probe information into a csv formatted string
        :param exponential_format: Whether exponential float representation format will be used or not
        :return: String with csv encoded information about the probes
        """
        result = self.__write_csv(StringIO(), exponential_format=exponential_format)
        return result.getvalue()

    def __write_csv(self, file, exponential_format=False):
        """
        Writes all probe information to a generic file
        :param file: Input file
        :param exponential_format: Whether exponential float representation format will be used or not
        :return: File used to write all information
        """
        float_format = "{:g}" if exponential_format else "{:f}"
        fieldnames = [StopWatch.__CSV_TAG_HEADLINE, StopWatch.__CSV_ELAPSED_TIME_HEADLINE]
        writer = csv.DictWriter(file, fieldnames=fieldnames)
        writer.writeheader()
        for tag, elapsed_time in self.probes.items():
            writer.writerow({StopWatch.__CSV_TAG_HEADLINE: tag,
                             StopWatch.__CSV_ELAPSED_TIME_HEADLINE: float_format.format(elapsed_time)})
        return file

    def __populate_custom_tag(self, tag):
        """
        Populates a tag with custom tags if available
        :param tag: Unpopulated tag
        :return: Populated tag
        """
        self.logger.debug("Populating custom tags...")
        tag = self.__populate_custom_autoincrement_tag(tag)
        tag = self.__populate_custom_nonincremental_tag(tag)
        return tag

    def __populate_custom_autoincrement_tag(self, tag):
        """
        Checks if autoincrement custom tags are present and populates them
        :param tag: Unpopulated tag
        :return: Populated tag
        """
        found_tags = StopWatch.__CUSTOM_AUTOINCREMENT_TAG_FORMAT.findall(tag)
        if not found_tags:
            return tag
        self.logger.debug("Found autoincremental tag/s{} in tag {}".format(found_tags, tag))
        for found_tag in found_tags:
            tag = tag.replace(StopWatch.__CUSTOM_AUTOINCREMENT_TAG_QUICK_REPLACE(found_tag),
                              str(self.custom_tags[found_tag]))
            self.custom_tags[found_tag] += 1
        return tag

    def __populate_custom_nonincremental_tag(self, tag):
        """
        Checks for non incremental custom tags and populates them
        :param tag:Unpopulated tag
        :return: Populated tag
        """
        found_tags = StopWatch.__CUSTOM_NONINCREMENTAL_TAG_FORMAT.findall(tag)
        if not found_tags:
            return tag
        self.logger.debug("Found non-incremental tag/s{} in tag {}".format(found_tags, tag))
        for found_tag in found_tags:
            tag = tag.replace(StopWatch.__CUSTOM_NONINCREMENTAL_TAG_QUICK_REPLACE(found_tag),
                              str(self.custom_tags[found_tag]))
        return tag

    def __get_default_logger(self):
        import logging
        logging.basicConfig(level=logging.DEBUG - 1 if self.debug_mode else logging.INFO - 1)
        logger = logging.getLogger("StopWatch")
        logger.debug("Logger set to debug mode")
        return logger

    @property
    def elapsed_time(self):
        """
        Elapsed time from starting time to finished time. If the stopwatch is still running it takes now as the finished
        time.
        :return: Elapsed time
        """
        aux = time.time()
        return self.probes.get(StopWatch.__STOP_PROBE_TAG, aux) - self.probes.get(StopWatch.__START_PROBE_TAG, aux)
