#!/bin/python3
import json
import numpy as np
from query_request import ElasticQuery
from util import log_error
from functions import Functions, ArityType
from graphics import my_grapho
from expr import Expr


def access(data, path):
    output = data
    for i in path:
        output = output[i]
    return output


def _validate_function_arity(f_name, args):
    arity, arity_type = Functions.get_function_arity(f_name)
    args_len = len(args)
    if arity_type == ArityType.EXACTLY:
        if not args_len == arity:
            log_error('function {} expects {} arguments but {} where provided'.format(
                f_name, arity, args_len
            ))
            return False
        return True
    elif arity_type == ArityType.AT_LEAST:
        if args_len < arity:
            log_error('function {} expects at least {} arguments but only {} where provided'.format(
                f_name, arity, args_len
            ))
            return False
        return True
    elif arity_type == ArityType.AT_MOST:
        if args_len > arity:
            log_error('function {} expects at most {} arguments but {} where provided'.format(
                f_name, arity, args_len
            ))
            return False
        return True
    else:  # if something unexpected happens with ArityType
        return False


class Datus:
    def __init__(self, filename):
        self.calculus = {"__aux__": None}
        self.load_config(filename)
        self.has_data = False

    def _add_chunk(self, anchor, elastic_result):
        i = 0
        for doc in elastic_result:
            for field in self.config["fields"]:
                name = field["name"]
                path = field["path"]
                self.calculus[name][anchor + i] = access(doc, path)
            self.calculus["timestamp"][anchor + i] = access(doc, ["_source", "timestamp"])
            i += 1

    def _validate_ref(self, ref):
        if ref not in self.calculus:
            log_error('reference to "{}" does not exist'.format(ref))
            return False
        return True

    def _validate_args(self, f_name, args):
        if not _validate_function_arity(f_name, args):
            return False
        for arg in args:
            if isinstance(arg, str) and not self._validate_ref(arg):
                return False
        return True

    def _validate_function(self, f_name, args):
        if f_name not in Functions.operators:
            log_error('function "{}" does not exist'.format(f_name))
            return False
        return self._validate_args(f_name, args)

    def _validate_new_name(self, name):
        if name in self.calculus:
            log_error('there already exists a calculation named "{}".'.format(name))

    def _alloc_fields(self, size):
        self.calculus["timestamp"] = np.zeros(size, dtype='datetime64[ms]')
        self.calculus["__aux__"] = np.zeros(size)
        for field in self.config["fields"]:
            name = field["name"]
            self._validate_new_name(name)
            self.calculus[name] = np.zeros(size)
        for calculation in self.config["calculus"]:
            name = calculation["name"]
            self._validate_new_name(name)
            if "function" in calculation:
                function = calculation["function"]
                args = calculation["args"]
                self._validate_function(function, args)
                self.calculus[name] = np.zeros(size)
            elif "expr" in calculation:
                try:
                    expr = Expr.parse_str(calculation["expr"])
                    if not expr.validate(self.calculus):
                        log_error("expr '" + name + "' has references to unexisting variables")
                    self.calculus[name] = np.zeros(size)
                except Exception as e:
                    raise Exception(
                        "cannot parse '" + name + "' which is expr: '" + calculation["expr"] + "' due to: '" + str(
                            e) + "'. Abort")
        self.has_data = True

    def clear(self):
        self.calculus = {"__aux__": None}
        self.has_data = False

    def load_config(self, filename):
        with open(filename, 'r') as file:
            self.config = json.loads(file.read())

    def get_fields(self, index, ts_from, ts_until):
        # query elastic to get the data
        query = ElasticQuery()
        query.match_from_timestamp(ts_from, ts_until)
        query.set_size(100)

        while query.next():
            _, result = query.send(index)
            if not self.has_data:
                self._alloc_fields(query.max_entries)
            # print(str(float(query.get_first_doc_number())/query.max_entries)+" %")
            self._add_chunk(query.get_first_doc_number(), result)

    def get_service_info_by_method(self, index, ts_from, ts_until, method):
        # query elastic to get the data
        query = ElasticQuery()
        query.match_from_field('timestamp', ts_from, ts_until)
        # query.match_from_method(method)
        query.set_size(100)

        while query.next():
            _, result = query.send(index)
            if not self.calculus:
                self._alloc_fields(query.max_entries)
            # print(str(float(query.get_first_doc_number())/query.max_entries)+" %")
            self._add_chunk(query.get_first_doc_number(), result)

    def apply_functions(self):
        for calculation in self.config["calculus"]:
            name = calculation["name"]
            if "function" in calculation:
                args = calculation["args"]
                f = Functions.get_function(calculation["function"])
                f(self.calculus, name, args)
            elif "expr" in calculation:
                expr = Expr.parse_str(calculation["expr"])
                self.calculus[name] = expr.eval(self.calculus)

    def plot_all(self):
        for plot in self.config["plot"]:
            y = plot["y"]
            title = plot["title"]
            path = 'images/{}.png'.format(title)
            my_grapho(self.calculus, y, path, title)


if __name__ == "__main__":
    datus = Datus("config.json")
    index_ecosat = "index_Ecosat-1234-5678_metrics".lower()
    index_service = "service-vision-2022.05.30".lower()
    indexes = [index_ecosat]

    ts_from = '2022-05-23T12:25:28'
    ts_until = '2022-05-23T18:25:03'
    # Para los logs
    # ts_from = '2022-05-30T11:37:10'
    # ts_until = '2022-05-30T11:37:49'
    for index in indexes:
        # datus.get_service_info_by_method(index, ts_from, ts_until, 'reduce_bands_by_pca')
        datus.get_fields(index, ts_from, ts_until)
        datus.apply_functions()
        # print(datus.calculus['delta_context_switches'])

        print("bat_heur: " + str(datus.calculus['bat_heur']))
        print("expr_bat_heur: " + str(datus.calculus['expr_bat_heur']))
        print("cpu_time_idle_heur: " + str(datus.calculus['cpu_time_idle_heur']))
        print("expr_cpu_time_idle_heur: " + str(datus.calculus['expr_cpu_time_idle_heur']))
        print("virtual_mem_heur: " + str(datus.calculus['virtual_mem_heur']))
        print("expr_virtual_mem_heur: " + str(datus.calculus['expr_virtual_mem_heur']))
        print("heuristic: " + str(datus.calculus['heuristic']))
        print("heuristic_test: " + str(datus.calculus['heuristic_test']))
        print("expr_heuristic: " + str(datus.calculus['expr_heuristic']))
        print("---------------------------------")

        # datus.plot_all(datus.calculus['context_switches'])
        datus.plot_all()

        # my_grapho(data, list_fill, path_save, title)
        datus.clear()
