import numpy as np
import pandas as pd
import fillerfunctions as ff
import preprocessingO as ppo
import coolingsystems as cs
from energyanalysis import propertyCalculator
from helpers import d2df
from helpers import polyvalHelperFunction

def auxEngineProcessing(raw, processed, dict_structure, CONSTANTS, hd):
    # This script summarizes all the functions that calculate the required data for the Main Engines different flows
    # Reading existing values
    processed = readAuxEnginesExistingValues(raw, processed, CONSTANTS, hd)
    processed = ppo.engineStatusCalculation("AuxEngines", raw, processed, CONSTANTS, hd, dict_structure)
    # Calculating the cooling flows, so that they can be later assigned easily
    processed = cs.coolingFlows(processed, CONSTANTS, "AuxEngines")
    processed = ff.systemFill(processed, dict_structure, CONSTANTS, "AuxEngines", "AE-1.1")
    processed = ff.systemFill(processed, dict_structure, CONSTANTS, "AuxEngines", "AE-1.2")
    # Calculating the power, including the generator efficiency
    processed = auxEnginePowerCalculation(processed, CONSTANTS)
    # Calculating engine load, that is used many times later on
    processed = ppo.engineLoadCalculation("AuxEngines", raw, processed, CONSTANTS, hd)
    # Calculating the auxiliary engines fuel flows
    processed = auxEngineFuelFlowCalculation(raw, processed, CONSTANTS)
    # Calculate air and exhaust gas flows in the main engines
    processed = auxEngineAirFlowCalculation(raw, processed, CONSTANTS)
    processed = ff.systemFill(processed, dict_structure, CONSTANTS, "AuxEngines", "AE-2.1")
    processed = ff.systemFill(processed, dict_structure, CONSTANTS, "AuxEngines", "AE-2.2")
    processed = propertyCalculator(processed, dict_structure, CONSTANTS, ["AE1", "AE2", "AE3", "AE4"])
    processed = auxEngineAirFlowPostCalculation(processed, CONSTANTS)
    # Calculating cooling flows
    processed = cs.engineCoolingSystemsCalculation(processed, CONSTANTS, "AuxEngines")
    processed = ff.systemFill(processed, dict_structure, CONSTANTS, "AuxEngines", "AE-3.1")
    processed = ff.systemFill(processed, dict_structure, CONSTANTS, "AuxEngines", "AE-3.2")
    return processed

def readAuxEnginesExistingValues(raw, processed,CONSTANTS,hd):
    print("Started reading raw values for auxiliary engines...", end="", flush=True)
    for system in CONSTANTS["General"]["NAMES"]["AuxEngines"]:
        # Reading main engines exhaust gas temperature, TC inlet and outlet
        processed[d2df(system,"Turbine","Mix_in","T")] = raw[hd[system + "-TC_EG_T_IN1"]] + 273.15
        processed[d2df(system,"Turbine","Mix_out","T")] = raw[hd[system + "-TC_EG_T_OUT"]] + 273.15
        # Reading main engines exhaust gas temperature, after HRSG
        processed[d2df(system,"HRSG","Mix_out","T")] = raw[hd[system + "-EGB_EG_T_OUT"]] + 273.15
        # Reading the HT temperature before and after the main engine
        processed[d2df(system,"JWC","HTWater_in","T")] = raw[hd[system + "-HT_FW_T_IN"]] + 273.15
        #processed[d2df(system, "CAC_HT", "HTWater_out", "T")] = raw[hd[system + "-HT_FW_T_OUT2"]] + 273.15
        # Reading the LT temperature before the main engine
        processed[d2df(system,"CAC_LT","LTWater_in","T")] = raw[hd[system + "-LT-CAC_FW_T_IN"]] + 273.15
        # Reading the Lubricating oil temperature before and after the Lubricating oil cooler
        processed[d2df(system,"LOC","LubOil_out","T")] = raw[hd[system + "-LOC_OIL_T_OUT"]] + 273.15
        # Reading fuel oil temperature before injection
        processed[d2df(system,"Cyl","FuelPh_in","T")] = raw[hd[system + "-CYL_FUEL_T_IN"]] + 273.15
        # Reading charge air temperature.
        processed[d2df(system,"CAC_LT","Air_out","T")] = raw[hd[system + "-CAC_AIR_T_OUT"]] + 273.15
        # Reading power output
        processed[d2df(system,"AG","Power_out","Edot")] = raw[hd[system + "_POWER_Wdot_OUT"]]
        # Reading Engine rpm
        # processed[d2df(system, "Cyl", "Power_out", "omega")] = raw[hd[system + "__RPM_"]]
        # Reading the pressure in the cooling flows
        processed[d2df(system, "CAC_LT", "LTWater_in", "p")] = (raw[hd[system + "-LT-CAC_FW_P_IN"]] + 1.01325) * 100000
        processed[d2df(system, "JWC", "HTWater_in", "p")] = raw[hd[system + "-HT-JWC_FW_P_IN"]] + 273.15
        processed[d2df(system, "Comp", "Air_out", "p")] = (raw[hd[system + "-CAC_AIR_P_OUT"]] + 1.01325) * 100000
        # Reading the pressure of the lubricating oil
        processed[d2df(system, "LOC", "LubOil_out", "p")] = (raw[hd[system + "-LOC_OIL_P_IN"]] + 1.01325) * 100000
        # Reading the pressure in the cooling flows
        processed[d2df(system, "CAC_LT", "LTWater_in", "p")] = (raw[hd[system + "-LT-CAC_FW_P_IN"]] + 1.01325) * 100000
        processed[d2df(system, "JWC", "HTWater_in", "p")] = (raw[hd[system + "-HT-JWC_FW_P_IN"]] + 1.01325) * 100000
        # Reading turbocharger speed
        processed[d2df(system, "TCshaft", "Power_in", "omega")] = raw[hd[system + "-TC__RPM_"]]
        processed[d2df(system, "TCshaft", "Power_out", "omega")] = raw[hd[system + "-TC__RPM_"]]
        processed[d2df(system, "Turbine", "Power_out", "omega")] = raw[hd[system + "-TC__RPM_"]]
        processed[d2df(system, "Compressor", "Power_in", "omega")] = raw[hd[system + "-TC__RPM_"]]
        # TEMPORARY: Filter on fuel temperature
        if system == "AE3":
            processed.loc[
                processed[d2df(system, "Cyl", "FuelPh_in", "T")] < 300, d2df(system, "Cyl", "FuelPh_in", "T")] = 300
        else:
            processed.loc[
                processed[d2df(system, "Cyl", "FuelPh_in", "T")] < 350, d2df(system, "Cyl", "FuelPh_in", "T")] = 350
        print("...done!")
    return processed

def auxEnginePowerCalculation(processed, CONSTANTS):
    print("Started calculating auxiliary engine power...", end="", flush=True)
    # Calculating the power of the auxiliary engines
    for system in CONSTANTS["General"]["NAMES"]["AuxEngines"]:
        load = processed[d2df(system,"AG","Power_out","Edot")] / CONSTANTS["AuxEngines"]["MCR"]
        eta_AG =  CONSTANTS["AuxEngines"]["AG"]["ETA_DES"] - CONSTANTS["AuxEngines"]["AG"]["A"] * np.exp(
            -CONSTANTS["AuxEngines"]["AG"]["k"] * (load))
        processed[d2df(system,"AG","Power_in","Edot")] = processed[d2df(system,"AG","Power_out","Edot")] / eta_AG
        processed[d2df(system,"AG","Losses_out","Edot")] = processed[d2df(system,"AG","Power_in","Edot")] - processed[d2df(system,"AG","Power_out","Edot")]
        processed[d2df(system,"Cyl","Power_out","Edot")] = processed[d2df(system,"AG","Power_in","Edot")]
    print("...done!")
    return processed

def auxEngineFuelFlowCalculation(raw, processed, CONSTANTS):
    print("Started calculating auxiliary engine fuel flows...", end="", flush=True)
    # Proceeding with the auxiliary engines
    for system in CONSTANTS["General"]["NAMES"]["AuxEngines"]:
        # First we calculate the ISO break specific fuel consumption (BSFC)
        bsfc_iso = processed[system+":"+"load"].apply(ppo.polyvalHelperFunction, args=(CONSTANTS["AuxEngines"][
                                                                          "POLY_LOAD_2_ISO_BSFC"],))
        (bsfc, LHV) = ppo.bsfcISOCorrection(bsfc_iso, processed[d2df(system,"Cyl","Air_in","T")], processed[d2df(system,"CAC_LT",
                "LTWater_in","T")], processed[d2df(system,"Cyl","FuelPh_in","T")], CONSTANTS)
        processed[d2df(system,"Cyl","FuelPh_in","mdot")] = bsfc * processed[d2df(system,"Cyl","Power_out","Edot")] / 3600 / 1000
        processed[d2df(system,"Cyl","FuelCh_in","Edot")] = processed[d2df(system,"Cyl","FuelPh_in","mdot")] * LHV
    print("...done!")
    return processed

def auxEngineAirFlowCalculation(raw, processed, CONSTANTS):
    print("Started calculating auxiliary engine air and exhaust flows...", end="", flush=True)
    # This function calculates the different air and exhaust gas flows in the main engines, taking into account the
    # presence of air bypass and exhaust wastegate valves
    for system in CONSTANTS["General"]["NAMES"]["AuxEngines"]:
        # Calculating the compressor's compression ratio
        beta_comp = processed[d2df(system,"Comp","Air_out","p")] / processed[d2df(system,"Comp","Air_in","p")]
        # Calculating the compressor isentropic efficiency
        comp_isentropic_efficiency = beta_comp.apply(ppo.polyvalHelperFunction, args=(CONSTANTS["AuxEngines"]["POLY_PIN_2_ETA_IS"],))
        # Calculating the temperature after the compressor, based on ideal gas assumption
        T_Comp_out_iso = processed[d2df(system, "Comp", "Air_in", "T")] * beta_comp ** ((CONSTANTS["General"]["K_AIR"] - 1) / CONSTANTS["General"]["K_AIR"])
        T_Comp_out = processed[d2df(system, "Comp", "Air_in", "T")] + (T_Comp_out_iso - processed[d2df(system, "Comp", "Air_in", "T")]) / comp_isentropic_efficiency
        #### NOTE: HERE WE MAKE THE ASSUMPTION THAT THE COMPRESSOR OUTLET TEMPERATURE CANNOT BE LOWER THAN THE CYLINDER INLET TEMPERATURE
        T_Comp_out[T_Comp_out < processed[d2df(system, "Cyl", "Air_in", "T")]] = processed.loc[T_Comp_out < processed[d2df(system, "Cyl", "Air_in", "T")], d2df(system, "Cyl", "Air_in", "T")]
        processed[d2df(system, "Comp", "Air_out", "T")] = T_Comp_out
        # Calculating the air inflow aspired by the cylinder: calculated as inlet air density times the maximum volume,
        # times the engine speed
        processed[d2df(system,"Cyl","Air_in","mdot")] = CONSTANTS["AuxEngines"]["V_MAX"] * (
            processed[d2df(system,"Comp","Air_out","p")]) / (
            CONSTANTS["General"]["R_AIR"] * processed[d2df(system,"Cyl","Air_in","T")]) * (
            processed[d2df(system,"Cyl","Power_out","omega")] / 60 / 2 * CONSTANTS["General"]["ETA_VOL"]) * (
            CONSTANTS["AuxEngines"]["N_CYL"])
        # Exhaust gas flow after the cylinders
        processed[d2df(system,"Cyl","EG_out","mdot")] = processed[d2df(system,"Cyl","Air_in","mdot")] + processed[d2df(system,"Cyl","FuelPh_in","mdot")]
        # Here we calculate the bypass mass flow. THIS NEEDS TO BE CHECKED FOR CONSISTENCY (i.e. the bypass mass flow
        #  should always be kind of low, and anyway only be seen at low to medium load).
        # The equation is the result of a mass and energy balance over the whole engine
        # dT_comp = processed[d2df(system,"Comp","Air_out","T")] - processed[d2df(system,"Comp","Air_in","T")]
        # dT_turb = processed[d2df(system,"Turbine","Mix_in","T")] - processed[d2df(system,"Turbine","Mix_out","T")]
        # processed[d2df(system, "BPsplit", "BP_out", "mdot")] = (processed[d2df(system, "Cyl", "Air_in", "mdot")] * CONSTANTS["General"]["CP_AIR"] * dT_comp -
        #     processed[d2df(system,"Cyl","EG_out","mdot")] * CONSTANTS["General"]["CP_EG"] * dT_turb * CONSTANTS["MainEngines"]["ETA_MECH_TC"]) / (
        #     CONSTANTS["General"]["CP_AIR"] * (CONSTANTS["MainEngines"]["ETA_MECH_TC"] * dT_turb - dT_comp))
        processed.loc[:, d2df(system, "BPmerge", "BP_in", "mdot")] = 0
        # The air mass flow going through the compressor is equal to the sum of the air flow through the bypass valve and
        # to the cylinders
        processed[d2df(system,"BPsplit","Air_in","mdot")] = processed[d2df(system, "BPmerge", "BP_in", "mdot")] + processed[d2df(system,"Cyl","Air_in","mdot")]
        # The flow through the turbine is equal to the sum of the bypass flow and the exhaust coming from the cylinders
        processed[d2df(system,"BPmerge","Mix_out","mdot")] = processed[d2df(system,"BPmerge","BP_in","mdot")] + processed[d2df(system,"Cyl","EG_out","mdot")]
        # Calculating the temperature of the mixture after the merge between bypass and exhaust gas from the cylinders
        processed[d2df(system,"Cyl","EG_out","T")] = ((processed[d2df(system,"Cyl","EG_out","mdot")] * CONSTANTS["General"]["CP_EG"] +
            processed[d2df(system,"BPmerge","BP_in","mdot")] * CONSTANTS["General"]["CP_AIR"]) * processed[d2df(system,"Turbine","Mix_in","T")] -
            processed[d2df(system,"BPmerge","BP_in","mdot")] * CONSTANTS["General"]["CP_AIR"] * processed[d2df(system, "Comp", "Air_out", "T")]) / (
            processed[d2df(system,"Cyl","EG_out","mdot")] * CONSTANTS["General"]["CP_EG"])
        # Assiging the mass flow values for the HRSGs otherwise it makes a mess
        processed[d2df(system, "HRSG", "Mix_in", "mdot")] = processed[d2df(system, "BPmerge", "Mix_out", "mdot")]
        processed[d2df(system, "HRSG", "Mix_out", "mdot")] = processed[d2df(system, "BPmerge", "Mix_out", "mdot")]
    print("...done!")
    return processed

def auxEngineAirFlowPostCalculation(processed, CONSTANTS):
    print("Started calculating auxiliary engine air and exhaust flows...", end="", flush=True)
    # This function calculates the different air and exhaust gas flows in the main engines, taking into account the
    # presence of air bypass and exhaust wastegate valves
    for system in CONSTANTS["General"]["NAMES"]["AuxEngines"]:
        # Calculating the turbine's power outpput to the compressor
        processed[d2df(system, "Turbine", "Power_out", "Edot")] = processed[d2df(system, "BPmerge", "Mix_out", "mdot")] * (
            processed[d2df(system, "Turbine", "Mix_in", "h")] - processed[d2df(system, "Turbine", "Mix_out", "h")])
        # Calculating the compressor's power input.
        processed[d2df(system, "Comp", "Power_in", "Edot")] = processed[d2df(system, "BPsplit", "Air_in", "mdot")] * (
            processed[d2df(system, "Comp", "Air_out", "h")] - processed[d2df(system, "Comp", "Air_in", "h")])
        # Losses at the TC shaft level are calculated
        etaTcMech = (processed[d2df(system, "TCshaft", "Power_in", "omega")]/CONSTANTS["AuxEngines"]["RPM_TC_DES"]).apply(polyvalHelperFunction,args=(CONSTANTS["MainEngines"]["POLY_TC_RPM_2_ETA_MECH"],))
        processed[d2df(system, "TCshaft", "Losses_out", "Edot")] = processed[d2df(system, "Turbine", "Power_out", "Edot")] * (1 - etaTcMech)

    return processed


"""Хелперы для работы на стыке с CV."""

import copy
import logging
import random
from typing import List

import cv2  # type: ignore
import numpy as np
from numpy import ndarray

from bot import cv_operations, settings, templates
from bot.gui import Area, Point, sort_areas_by_distance
from bot.state import State

def search_water_cell(state: State, gray_frame: ndarray) -> List[Point]:
    cv_operations.show_current_frame(state, gray_frame, 'search water cell')

    tpl = templates.client.water_template

    res = cv2.matchTemplate(gray_frame, tpl, cv2.TM_CCOEFF_NORMED)
    loc = np.where(res >= float(0.7))  # noqa: WPS432

    center_points = cv_operations.locations_to_center_points(
        loc,
        cv_operations.template_to_point(tpl),
    )
    tmp_frame = copy.copy(gray_frame)
    cv_operations.mark_point_group(tmp_frame, center_points)
    cv_operations.show_current_frame(state, tmp_frame, 'search water cells')

    return center_points

def choice_cell_for_fishing(state: State, gray_frame: ndarray, character: Point, water_cells: List[Point]) -> Point:
    available_water_cells = list(filter(
        lambda cell: settings.FISHING_DISTANCE_MIN < cell.distance(character) < settings.FISHING_DISTANCE_MAX,
        water_cells,
    ))
    assert available_water_cells, 'Not found available water cells for fishing'
    logging.debug('filtering available cell for fishing %s', len(available_water_cells))
    cv_operations.mark_point_group(gray_frame, available_water_cells)
    cv_operations.show_current_frame(state, gray_frame, 'choice cell for fishing')

    return random.choice(available_water_cells)

def search_character(state: State, gray_frame: ndarray) -> List[Point]:
    """Ищем координаты персонажа."""
    cv_operations.show_current_frame(state, gray_frame, 'search character')

    tpl_point = cv_operations.template_to_point(templates.client.character_template)

    res = cv2.matchTemplate(gray_frame, templates.client.character_template, cv2.TM_CCOEFF_NORMED)
    loc = np.where(res >= float(0.8))  # noqa: WPS432

    characters = cv_operations.locations_to_areas(loc, tpl_point)
    cv_operations.mark_area_group(gray_frame, characters)
    cv_operations.show_current_frame(state, gray_frame, 'search character match')

    return cv_operations.locations_to_center_points(loc, tpl_point)

def search_bobber(state: State, gray_frame: ndarray, character: Point) -> List[Area]:
    """Ищем поплавок на воде."""
    tpl = templates.client.bobber_template

    res = cv2.matchTemplate(gray_frame, tpl, cv2.TM_CCOEFF_NORMED)
    loc = np.where(res >= float(0.55))  # noqa: WPS432
    areas = cv_operations.locations_to_areas(
        loc,
        cv_operations.template_to_point(tpl),
    )

    tmp_frame = copy.deepcopy(gray_frame)
    cv_operations.mark_area_group(tmp_frame, areas)
    cv_operations.show_current_frame(state, tmp_frame, 'search bobber cells')

    return sort_areas_by_distance(areas, character)

def lookup_hooking_game_area(state: State, gray_frame: ndarray) -> List[Area]:
    """Ищем миниигру."""
    tpl = templates.client.hooking_game_template

    res = cv2.matchTemplate(gray_frame, tpl, cv2.TM_CCOEFF_NORMED)
    loc = np.where(res >= float(0.6))  # noqa: WPS432
    areas = cv_operations.locations_to_areas(
        loc,
        cv_operations.template_to_point(tpl),
    )

    cv_operations.mark_area_group(gray_frame, areas)
    cv_operations.show_current_frame(state, gray_frame, 'lookup game area')

    return areas

def looking_for_nibbles(state: State, gray_frame: ndarray, search_area: Area) -> bool:
    """Определяем, клюёт ли в данный момент."""
    # crop frame to search_area
    gray_frame = cv_operations.crop_frame(gray_frame, search_area)
    cv_operations.show_current_frame(state, gray_frame, 'search nibbles area')

    processed_image = cv2.Canny(gray_frame, threshold1=35, threshold2=15)  # noqa: WPS432
    cv_operations.show_current_frame(state, processed_image, 'search nibbles Canny')
    black_count = np.sum(processed_image == 0)
    bw_factor = np.count_nonzero(processed_image) / black_count
    logging.info('black vs white factor: %s', bw_factor)

    return bw_factor >= settings.FISHING_NIBBLES_THRESHOLD

def search_bobber_in_game(state: State, gray_frame: ndarray) -> List[Point]:
    """Ищем поплавок в миниигре."""
    tpl = templates.client.bobber_in_game_template

    res = cv2.matchTemplate(gray_frame, tpl, cv2.TM_CCOEFF_NORMED)
    loc = np.where(res >= float(0.6))  # noqa: WPS432
    return cv_operations.locations_to_center_points(
        loc,
        cv_operations.template_to_point(tpl),
    )


import numpy
import simpleaudio as sa
import time

class CharlieWhisky:

    def __init__(self, samplerate=22050, dit_duration_ms=180, frequency=1660.0, debug=False):
        self.samplerate = samplerate
        self.frequency = frequency
        self.dit_duration = dit_duration_ms / 1000
        self.dah_duration = self.dit_duration * 3
        self.space_letter_duration = self.dit_duration * 3
        self.space_word_duration = self.dit_duration * 7
        self.words_per_minute = 1200 / dit_duration_ms
        self.debug = debug

        self.letter_dictionary = {"A": "01", "B": "1000", "C": "1010",
                                  "D": "100", "E": "0", "F": "0010",
                                  "G": "110", "H": "0000", "I": "00",
                                  "J": "0111", "K": "101", "L": "0100",
                                  "M": "11", "N": "10", "O": "111",
                                  "P": "0110", "Q": "1101", "R": "010",
                                  "S": "000", "T": "1", "U": "001",
                                  "V": "0001", "W": "011", "X": "1001",
                                  "Z": "1100", ".": "010101", ",": "110011",
                                  "?": "001100", "/": "10010", "@": "011010",
                                  "1": "01111", "2": "00111",
                                  "3": "00011", "4": "00001",
                                  "5": "00000", "6": "10000",
                                  "7": "11000", "8": "11100",
                                  "9": "11110", "0": "11111"
                                  }

    def dit(self):
        t = numpy.linspace(0, self.dit_duration, self.dit_duration * self.samplerate, False)
        sine = numpy.sin(self.frequency * t * 2 * numpy.pi)
        audio = numpy.hstack((sine))
        audio *= 32767 / numpy.max(numpy.abs(audio))
        audio = audio.astype(numpy.int16)
        play_obj = sa.play_buffer(audio, 1, 2, self.samplerate)
        play_obj.wait_done()

    def dah(self):
        t = numpy.linspace(0, self.dah_duration, self.dah_duration * self.samplerate, False)
        sine = numpy.sin(self.frequency * t * 2 * numpy.pi)
        audio = numpy.hstack((sine))
        audio *= 32767 / numpy.max(numpy.abs(audio))
        audio = audio.astype(numpy.int16)
        play_obj = sa.play_buffer(audio, 1, 2, self.samplerate)
        play_obj.wait_done()

    def space_letter(self):
        time.sleep(self.space_letter_duration)

    def space_word(self):
        time.sleep(self.space_word_duration)

    def play_letter(self, letter):
        morse = self.letter_dictionary[letter.upper()]
        if self.debug:
            print(letter.upper(), end=" ", flush=True)
        for bit in morse:
            if bit == "0":
                if self.debug:
                    print(".", end="", flush=True)
                self.dit()
            elif bit == "1":
                if self.debug:
                    print("-", end="", flush=True)
                self.dah()
        if self.debug:
            print(" ")
        self.space_letter()

    def play_word(self, word, repeat=1):
        for letter in word:
            if letter.upper() in self.letter_dictionary.keys():
                self.play_letter(letter)
            elif letter == " ":
                if self.debug:
                    print(" ")
                self.space_word()