import {iTenderStatus} from "./iTenderStatus";
import Container from "./database/Container";
import {IContainer} from "./database/IContainer";
import Drink from "./database/Drink";
import {IDrink} from "./database/IDrink";
import debug from "debug";
import {WebSocketHandler} from "./WebSocketHandler";
import {IJob} from "./database/IJob";
import {Utils} from "./Utils";
import {WebSocketPayload} from "./WebSocketPayload";
import {WebSocketEvent} from "./WebSocketEvent";
import Job from "./database/Job";
import {IIngredient} from "./database/IIngredient";
import Ingredient from "./database/Ingredient";
import {clearInterval} from "timers";
import {RejectReason} from "./RejectReason";
import axios from "axios";
import GPIO from "rpi-gpio";
import {MyGPIO} from "./MyGPIO";
import {SensorHelper} from "./SensorHelper";
import {SensorType} from "./SensorType";

const isPI = require("detect-rpi");

const log = debug("itender:station");
const mixLog = debug("itender:mix");


/**
 * The main class of the itender, here a located all main features of the system, like starting pumps, firing events and stuff
 */
export class iTender {
    private static secondsPer100ml: number = 35.3335;

    static get drinks(): IDrink[] {
        return this._drinks;
    }

    /**
     * The current job of the itender
     */
    static get currentJob(): IJob | null {
        return this._currentJob;
    }

    private static _status: iTenderStatus = iTenderStatus.STARTING;
    private static _currentJob: IJob | null = null;
    private static _jobCheckInterval: NodeJS.Timer;
    private static _internetConnection: boolean = false;

    private static _jobTimers: NodeJS.Timeout[] = [];

    /**
     * Returns true if internet connection is active
     */
    static get internetConnection(): boolean {
        return this._internetConnection;
    }

    private static _drinks: IDrink[];

    static setStatus(status: iTenderStatus) {
        this._status = status;
        if (WebSocketHandler.ws && WebSocketHandler.ws.readyState == 1)
            WebSocketHandler.sendStatus().then().catch(console.error);
        log("Status is now " + status);
    }

    static get status(): iTenderStatus {
        return this._status;
    }


    /**
     * This method is fired if the user likes to mix a drink
     * @param data
     */
    static onReceiveFill(data: { drink: IDrink, amounts?: { ingredient: String, amount: number }[], amount?: number }): Promise<IJob> {
        return new Promise(async (resolve, reject) => {
            mixLog("Receiving fill");
            let drink = await Drink.findById(data.drink).populate("ingredients.type");
            if (!drink) {
                reject();
                return;
            }

            const job = new Job();

            let amounts: { ingredient: IIngredient, amount: number, container?: IContainer }[] = [];
            job.completeAmount = 0;
            if (data.amounts) {
                for (let x of data.amounts) {
                    let ingredient = await Ingredient.findById(x.ingredient);
                    if (!ingredient) continue;

                    amounts.push({ingredient: ingredient, amount: x.amount});
                    job.completeAmount += x.amount;
                }
            } else if (data.amount) {
                let sum = 0;
                for (let x of (drink.ingredients as { type: IIngredient, amount: number }[])) {
                    sum += x.amount;
                }

                let factor = sum / data.amount;

                for (let x of (drink.ingredients as { type: IIngredient, amount: number }[])) {
                    amounts.push({ingredient: x.type, amount: x.amount * factor})
                    job.completeAmount += x.amount;
                }


            } else {
                for (let x of (drink.ingredients as { type: IIngredient, amount: number }[])) {
                    amounts.push({ingredient: x.type, amount: x.amount});
                    job.completeAmount += x.amount;
                }
            }

            job.estimatedTime = 0;
            let tolerance = 3;
            for (let x of amounts) {
                let c = await Container.findOne({$and: [{enabled: true}, {filled: {$gt: x.amount + tolerance}}, {content: x.ingredient}]});
                if (!c) {
                    mixLog("Not enough ingredients!");
                    reject(RejectReason.NOT_ENOUGH_INGREDIENTS);
                    return;
                }
                x.container = c;
                let estimated = iTender.secondsPer100ml / 100 * x.amount;
                if (job.estimatedTime < estimated)
                    job.estimatedTime = estimated;

            }

            console.log(amounts);
            job.drink = drink
            job.amounts = amounts as { ingredient: IIngredient, amount: number, container: IContainer }[];
            if (job.estimatedTime < 0.5) {
                job.estimatedTime = 1;
            }
            await job.save()
            resolve(job);

            await this.startFill(job);
        });


    }


    /**
     * Start the internal fill method
     * @param job
     */
    static async startFill(job: IJob) {
        job.startedAt = new Date();
        await job.populate([{path: "amounts.ingredient"}, {path: "amounts.container"}, {path: "drink"}]);
        mixLog("New fill job " + job.drink.name + " will take " + job.estimatedTime + "s");

        this._currentJob = job;
        iTender.setStatus(iTenderStatus.FILLING);


        for (let x of job.amounts) {

            // Start pump here
            try {
                await MyGPIO.setup(x.container.pumpPin, GPIO.DIR_OUT)

                await MyGPIO.write(x.container.pumpPin, true);
            } catch (e) {
                if (isPI()) {
                    log("[ERROR] GPIO I/O Error " + e);
                    // Todo error handling to user
                    await iTender.cancelFill();
                    return;
                } else {
                    log("[WARNING] GPIO I/O Error, but it's normal cause you are not on raspberry");
                }
            }


            let waitTime = (iTender.secondsPer100ml as number) / 100 * x.amount * 1000;
            mixLog(`Starting output of pump ${x.container.pumpPin}`);
            //mixLog(x.ingredient + " takes " + (waitTime / 1000) + "s for " + x.amount + "ml");
            let timer = setTimeout(async () => {
                // Remove from list of timers
                let arr: NodeJS.Timer[] = [];
                for (let i = 0; i < this._jobTimers.length; i++) {
                    if (this._jobTimers[i] != timer)
                        arr.push(this._jobTimers[i]);
                }

                mixLog(`Stopping output of pump ${x.container.pumpPin}`);
                // Stop pump here
                try {
                    await MyGPIO.write(x.container.pumpPin, false);
                } catch (e) {
                    if (isPI()) {
                        log("[ERROR] GPIO I/O Error " + e);
                        await iTender.cancelFill();
                        return;
                    } else {
                        log("[WARNING] GPIO I/O Error, but it's normal cause you are not on raspberry");
                    }
                }

                if (x.container.sensorType == SensorType.NONE) {
                    // V2: Manual measuring
                    x.container.filled = x.container.filled - x.amount;
                    await x.container.save();
                }

                this._jobTimers = arr;

            }, waitTime);
            this._jobTimers.push(timer);
        }

        iTender._jobCheckInterval = setInterval(async () => {

            if (this._jobTimers.length != 0)
                return;

            clearInterval(iTender._jobCheckInterval);
            job.endAt = new Date();
            job.successful = true;

            await job.save();
            mixLog("Job successful");
            setTimeout(() => iTender.setStatus(iTenderStatus.READY), 3000)

        }, 500);
    }


    /**
     * Cancel the fill
     */
    static async cancelFill() {
        if (!this._currentJob || this.status != iTenderStatus.FILLING)
            return;

        clearInterval(this._jobCheckInterval);
        this._currentJob.successful = false;
        this._currentJob.endAt = new Date();
        await this._currentJob.save();

        for (let timer of this._jobTimers) {
            // Clears all the ongoing stop timers
            clearTimeout(timer);
        }

        for (let jobIngredient of this._currentJob.amounts) {
            // stop pump pin
            try {
                await MyGPIO.write(jobIngredient.container.pumpPin, false);
            } catch (e) {

            }

            // ToDo
            let container: IContainer = jobIngredient.container;
            let deltaStartStop = (this._currentJob.endAt.getTime() - this._currentJob.startedAt.getTime()) / 1000;

            // füllmenge - ( ( (stopp-start) / 1000 ) * ( sekunden100ml / 100 ) )
            container.filled = container.filled - (deltaStartStop * (iTender.secondsPer100ml / 100)) // V2: Near the current fill value based on time values from delta start stop
            container.save().then();
        }

        iTender.setStatus(iTenderStatus.READY);
    }


    /**
     * Measure all containers based on their sensor values
     */
    static measureContainers(): Promise<void> {
        log("Measuring containers...");

        return new Promise(async resolve => {
            for (let c of (await Container.find({}))) {
                if (c.sensorType != SensorType.NONE) {
                    let weight = SensorHelper.measure(c);
                    if (!weight) {
                        await WebSocketHandler.send(new WebSocketPayload(WebSocketEvent.ERROR, "Ein Sensor hat beim Austarieren einen ungültigen Wert zurückgegeben.<br>Dies weist auf eine Fehlkonfiguration oder kaputten Sensor hin.<br>Aus Sicherheitsgründen wurde der Sensor für diesen Behälter deaktiviert."));
                        continue;
                    }

                    // V2: New calculation method
                    let newFilled = weight - c.sensorDelta;

                    if (newFilled <= 3 && c.filled != -1) {
                        c.filled = -1;
                        // Container is empty!
                    } else {
                        // Container > 2
                        c.filled = newFilled;
                    }

                    await c.save();
                }

            }
            log("Containers measured!");
            resolve();

            let payload = new WebSocketPayload(WebSocketEvent.CONTAINERS, (await Container.find()));
            await WebSocketHandler.send(payload);
        });
    }


    /**
     * Refresh the drinks to the local variable
     * Check which drinks can be done, based on the current container ingredients
     */
    static refreshDrinks(): Promise<void> {
        log("Refreshing drinks...");
        return new Promise(async resolve => {
            this._drinks = [];
            for (let d of (await Drink.find().populate("ingredients.type"))) {

                let drinkAccept = true;
                for (let i of d.ingredients) {
                    let c = await Container.findOne({content: i.type});

                    if (!c) {
                        drinkAccept = false;
                        break;
                    }
                }
                if (drinkAccept) {
                    this._drinks.push(d);
                }

            }
            resolve();
            log("Drinks refreshed!");
            await WebSocketHandler.sendDrinks();
        });
    }


    public static async autoCheckup() {
        setInterval(async () => {
            log("Auto Checkup");
            if (!this._currentJob)
                return;
            // Check if         startedTime     plus   2 mins   smaller than    now
            if (this._currentJob.startedAt.getTime() + 1000 * 60 * 2 <= Date.now()) {
                // Job can be declared as stuck!
                this._currentJob.successful = false;
                this._currentJob.endAt = new Date();
                await this._currentJob.save();
                this._currentJob = null;
                this.setStatus(iTenderStatus.READY);
            }
        }, 30000);
    }

    public static async checkNetwork() {
        this._internetConnection = await Utils.checkInternet();
    }


    static refreshFromServer(): Promise<void> {
        return new Promise(async (resolve, reject) => {
            iTender.setStatus(iTenderStatus.DOWNLOADING)
            log("Refreshing drinks from server...");
            try {
                const requestIngredients = await axios.get("https://itender.iif.li/api/ingredients");
                let serverIngredients = requestIngredients.data as IIngredient[];
                if (serverIngredients.length == 0) {
                    log("Got 0 ingredients from the server... aborting.");
                    throw new Error("Got 0 ingredients from the server, invalid");
                }
                log("Got " + serverIngredients.length + " ingredients from server");

                let localIngredients = await Ingredient.find();
                for (let local of localIngredients) {
                    let found = false;
                    for (let remote of serverIngredients) {
                        if (local.name == remote.name) {
                            found = true;
                            break;
                        }
                    }

                    if (!found) {
                        await Ingredient.deleteOne({"_id": local._id});
                        for (let c of (await Container.find({content: local._id}))) {
                            c.content = undefined;
                            c.save();
                        }
                    }
                }

                for (let remote of serverIngredients) {
                    let ingredient = await Ingredient.findById(remote._id);
                    if (!ingredient)
                        ingredient = new Ingredient();

                    ingredient._id = remote._id;
                    ingredient.name = remote.name;
                    await ingredient.save();
                }


                const requestDrinks = await axios.get("https://itender.iif.li/api/drinks");
                let serverDrinks = requestDrinks.data as IDrink[];
                if (serverDrinks.length == 0) {
                    log("Got 0 drinks from the server... aborting.");
                    throw new Error("Got 0 drinks from the server, invalid");
                }
                log("Got " + serverDrinks.length + " drinks from server");


                let localDrinks = await Drink.find();
                for (let local of localDrinks) {
                    let found = false;
                    for (let remote of serverDrinks) {
                        if (local.name == remote.name) {
                            found = true;
                            break;
                        }
                    }

                    if (!found) {
                        Utils.deleteImage(local._id);
                        await Drink.deleteOne({"_id": local._id});
                    }

                }


                for (let remote of serverDrinks) {
                    let drink = await Drink.findById(remote._id);
                    if (!drink)
                        drink = new Drink();

                    drink._id = remote._id;
                    drink.name = remote.name;
                    drink.ingredients = remote.ingredients;

                    await drink.save();


                    // Download thumbnail
                    if (!Utils.checkForImage(remote._id)) {
                        let url = "https://itender.iif.li/images/" + remote._id + ".png";
                        try {
                            await Utils.downloadImage(url, "./public/images/" + drink._id + ".png")
                            log("Drink " + remote.name + "'s Thumbnail downloaded");
                        } catch (e) {
                            log("Drink " + remote.name + " failed to download thumbnail! (" + url + ") | " + e);
                        }
                    }
                }

            } catch (e) {
                console.error("Could not refresh drinks " + e);
                await WebSocketHandler.send(new WebSocketPayload(WebSocketEvent.ERROR, "Beim Aktualisieren der Getränke ist ein Netzwerk-Fehler aufgetreten.<br>Bitte später erneut versuchen!"));
            }

            iTender.setStatus(iTenderStatus.READY);
            resolve();
            await iTender.refreshDrinks();
        });
    }

    public static clearAllRawMeasurements()
    {
        return new Promise<void>(async (resolve, reject) => {
            for (let c of (await Container.find({}))) {
                if (c.sensorType != SensorType.NONE) {
                    c.rawData = -1;
                    await c.save();
                }
            }
            resolve();
        })
    }

    public static measureAllRaw() {
        return new Promise<void>(async (resolve, reject) => {
            for (let c of (await Container.find({}))) {
                if (c.sensorType != SensorType.NONE) {
                    let weight : number | null = c.rawData;
                    if( !c.sensorProxy )
                    {
                        // Check values
                        weight = SensorHelper.measure(c);
                    }

                    if (weight == null || weight > 1000 || weight < 0 ) { //fixme werte
                        // Problem erkannt!
                        return reject("Fehler Sensor (" + c.sensorPin1 + ", " + c.sensorPin2 + ") - Container " + c.slot + 1);
                    }
                    c.rawData = weight;
                    await c.save();
                }
            }
            resolve();
        })
    }
}