#!/usr/bin/env python3

import argparse
import foundation
import json
import os
import sys

from foundation import (config,
                        logger,
                        util,)
from dateutil import parser

########################################
#  Base support class.
########################################


class ResultVisitor:
    """
    Super class for all possible visitors of benchmark results.
    """

    def __init__(self):
        pass

    def handleResults(self, results):
        """
        Handle results (which is a decoded python dictionary)
        """
        pass

    def storeResults(self, dataset_dir):
        """
        Store results to a JSON file.
        """
        pass

########################################
#  Parser helpers.
########################################


def configureArgumentParser():
    parser = argparse.ArgumentParser(
        description="Cycles benchmark parser script.")
    parser.add_argument("-b", "--benchmark-dir",
                        help="Directory with benchmark results",
                        default="")
    parser.add_argument("-d", "--dataset-dir",
                        help="Directory where datasets will be stored",
                        default="")
    return parser

########################################
#  Configuration helpers.
########################################


def injectDefaultConfiguration(config):
    """
    For a specified configuration object, set all possible properties to their
    default value.
    """
    root_dir = util.getBundleRootDirectory()
    section = {"benchmark_dir": "",
               "dataset_dir": ""}
    config['parser'] = section


def injectArgparseConfiguration(config, args):
    """
    Override settings wit harguments passed from the command line.
    """
    section = config['parser']
    if args.benchmark_dir:
        section['benchmark_dir'] = args.benchmark_dir
    if args.dataset_dir:
        section['dataset_dir'] = args.dataset_dir


def readConfiguration(args):
    """
    Read configuration file and return BenchmarkConfig with all the settings
    we will need to use.
    """
    config = foundation.config.BenchmarkConfig()
    injectDefaultConfiguration(config)
    config.readGlobalConfig("parser")
    injectArgparseConfiguration(config, args)
    return config


def checkConfiguration(config):
    """
    Check whether configuration is complete and usable.
    """
    logger.INFO("Validating configuration...")
    section = config['parser']
    # Check whether directories are correct.
    if not os.path.exists(section["benchmark_dir"]):
        logger.INFO("    Benchmark directory does not exist.")
        return False
    if not os.path.exists(section["dataset_dir"]):
        logger.INFO("    Dataset directory does not exist.")
        return False
    return True


########################################
#  Results iteration implementation.
########################################

def visitBencharkResult(directory, visitors):
    """
    Take all actions needed when new benchmark result is found.
    """
    results_filename = os.path.join(directory, "results.json")
    if not os.path.exists(results_filename):
        return
    with open(results_filename) as results_file:
        results = json.load(results_file)
    # Check results are usable.
    if 'stats' not in results:
        return
    for visitor in visitors:
        visitor.handleResults(results)


def iterateBenchmarks(directory, visitors):
    """
    Iterate over all benchmar results for a specific configuration.
    """
    for filename in sorted(os.listdir(directory)):
        full_filename = os.path.join(directory, filename)
        if os.path.isdir(full_filename):
            visitBencharkResult(full_filename, visitors)


def iterateBenchmarksRoot(directory, visitors):
    """
    Iterate over all benchmark results, startting from top level where all
    benchmarks machines are storing their results.
    """
    for filename in sorted(os.listdir(directory)):
        full_filename = os.path.join(directory, filename)
        if os.path.isdir(full_filename):
            iterateBenchmarks(full_filename, visitors)

########################################
#  Main logic.
########################################


class LatestResultVisitor(ResultVisitor):
    def __init__(self):
        ResultVisitor.__init__(self)
        self.devices_ = {}

    def copyUsableStats(self, device, results):
        stats = results['stats']
        blender_version = results['blender_version']
        timestamp = util.blenderCommitUnixTimestamp(
            blender_version['build_commit_date'],
            blender_version['build_commit_time'])
        for scene_name in stats:
            stat = stats[scene_name]
            # Ignore benchmark results which crashed or aborted.
            if "result" not in stat or stat['result'] != 'OK':
                continue
            # Ignore benchmark results which ar eolder than existing ones.
            if scene_name in device and \
               "timestamp" in device[scene_name] and \
               timestamp < device[scene_name]["timestamp"]:
                continue
            device_scene_stat = dict(stat)
            device_scene_stat.pop("result")
            device_scene_stat['timestamp'] = timestamp
            device[scene_name] = device_scene_stat

    def handleResults(self, results):
        if 'device_info' not in results or \
           'stats' not in results:
            return
        device_info = results['device_info']
        if "device_name_override" in results:
            device_name = results["device_name_override"]
        else:
            device_name = util.deviceInfoAsString(device_info)
        # If there were no stats for the device,
        if device_name not in self.devices_:
            self.devices_[device_name] = {}
        self.copyUsableStats(self.devices_[device_name], results)

    def storeResults(self, dataset_dir):
        # Firts of all, gather all possible scenes.
        all_scenes = set()
        for device_name, stats in self.devices_.items():
            for scene_name in stats.keys():
                all_scenes.add(scene_name)
        all_scenes = sorted(list(all_scenes))
        # Gather datasets in format of charts.
        datasets = []
        device_index = 0
        for device_name in sorted(self.devices_.keys()):
            stats = self.devices_[device_name]
            data = []
            for scene_name in all_scenes:
                if scene_name not in stats:
                    data.append(None)
                    continue
                scene_stats = stats[scene_name]
                data.append(scene_stats['pipeline_render_time'])
            dataset = {
                "label": device_name,
                "data": data,
            }
            datasets.append(dataset)
            device_index += 1
        # Prepare python dict before converting it to JSON.
        data = {
            "labels": all_scenes,
            "datasets": datasets,
        }
        code = "var data = " + json.dumps(data, sort_keys=True, indent=2) + ";"
        # Save dataset to disk.
        filename = os.path.join(dataset_dir, "latest_snapshot.js")
        with open(filename, "w") as f:
            f.write(code)


class HistoryResultVisitor(ResultVisitor):
    def __init__(self):
        ResultVisitor.__init__(self)
        self.devices_ = {}

    def copyUsableStats(self, device, results):
        stats = results['stats']
        blender_version = results['blender_version']
        timestamp = util.blenderCommitUnixTimestamp(
            blender_version['build_commit_date'],
            blender_version['build_commit_time'])
        for scene_name in stats:
            stat = stats[scene_name]
            # Ignore benchmark results which crashed or aborted.
            if "result" not in stat or stat['result'] != 'OK':
                continue
            device_scene_stat = dict(stat)
            device_scene_stat.pop("result")
            device_scene_stat['timestamp'] = timestamp
            if scene_name not in device:
                device[scene_name] = []
            device[scene_name].append(device_scene_stat)

    def handleResults(self, results):
        if 'device_info' not in results or \
           'stats' not in results:
            return
        device_info = results['device_info']
        if "device_name_override" in results:
            device_name = results["device_name_override"]
        else:
            device_name = util.deviceInfoAsString(device_info)
        # If there were no stats for the device,
        if device_name not in self.devices_:
            self.devices_[device_name] = {}
        self.copyUsableStats(self.devices_[device_name], results)

    def removeDuplicated(self, stats_history):
        new_stats_history = []
        prev_timestamp = None
        for stats in stats_history:
            if stats['timestamp'] == prev_timestamp:
                # TODO(sergey): Average somehow?
                continue
            new_stats_history.append(stats)
            prev_timestamp = stats['timestamp']
        return new_stats_history

    def storeResults(self, dataset_dir):
        # Firts of all, gather all possible scenes.
        all_scenes = set()
        for device_name, stats in self.devices_.items():
            for scene_name in stats.keys():
                all_scenes.add(scene_name)
        all_scenes = sorted(list(all_scenes))
        # Gather datasets in format of lines.
        datasets = []
        device_index = 0
        for device_name in sorted(self.devices_.keys()):
            stats_history = self.devices_[device_name]
            for scene_name in all_scenes:
                if scene_name not in stats_history:
                    continue
                scene_stats_history = stats_history[scene_name]
                data = []
                sorted_scene_stats_history = sorted(
                        scene_stats_history,
                        key=lambda k: k['timestamp'])
                uniq_scene_stats_history = self.removeDuplicated(
                        sorted_scene_stats_history)
                for scene_stats in uniq_scene_stats_history:
                    timestamp = scene_stats['timestamp']
                    data.append({"x": timestamp.strftime("%d/%m/%y %H:%M"),
                                 "y": scene_stats['pipeline_render_time']})
                dataset = {
                    "device_name": device_name,
                    "scene_name": scene_name,
                    "data": data,
                   }
                datasets.append(dataset)
                device_index += 1
        data = {
            "scenes": all_scenes,
            "datasets": datasets,
        }
        code = "var data = " + json.dumps(data, sort_keys=True, indent=2) + ";"
        # Save dataset to disk.
        filename = os.path.join(dataset_dir, "history.js")
        with open(filename, "w") as f:
            f.write(code)


def main():
    parser = configureArgumentParser()
    args = parser.parse_args()
    logger.init()
    # Read configuration file, so we know what we will be doing.
    config = readConfiguration(args)
    if not checkConfiguration(config):
        logger.ERROR("Configuration is not complete or valid, aborting.")
        return False
    # Construct list of all visitors whic hwe want.
    latest_results_visitor = LatestResultVisitor()
    history_results_visitor = HistoryResultVisitor()
    # Do actual parse.
    logger.INFO("Iterating over all benchmark results...")
    visitors = (latest_results_visitor,
                history_results_visitor,)
    iterateBenchmarksRoot(config['parser']['benchmark_dir'], visitors)
    # Store results.
    logger.INFO("Storing results...")
    dataset_dir = config['parser']['dataset_dir']
    for visitor in visitors:
        visitor.storeResults(dataset_dir)

    return True


if __name__ == "__main__":
    if not main():
        sys.exit(1)
