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 {RejectReason} from "./RejectReason";
import axios from "axios";
import {Mixer} from "./Mixer";



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



/**
 * 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 {


    /**
     * How many seconds it takes to fill 100ml
     * @private
     */
    static secondsPer100ml: number = 19.3335; // 35.3335

    /**
     * Sensitivity-Factor of the hx711 scales
     * Calculated by (Value /100) Value needs to be measured by hx711 when a known weight is on the sensor,
     * in this example its 100g, so divide it by 100
     */
    static sensitivityFactor: number = 1.0;

    /**
     * Retrieve all drinks in cache
     */
    static get drinks(): IDrink[] {
        return this._drinks;
    }


    /**
     * Current internal status of itender
     * @private
     */
    private static _status: iTenderStatus = iTenderStatus.STARTING;


    /**
     * Current internal connection-status boolean
     * @private
     */
    private static _internetConnection: boolean = false;


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

    /**
     * Drinks in cache
     * @private
     */
    private static _drinks: IDrink[];

    /**
     * Sets the current itender status and sends it to the client
     * @param status
     */
    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);
    }

    /**
     * Returns the internal status of itender app
     */
    static get status(): iTenderStatus {
        return this._status;
    }


    /**
     * This method is fired if the user likes to mix a drink
     * It starts to calculate the ingredients and amounts of each ingredient
     * also calculates the amount of time, the drink will need to be done
     * @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;

            }

            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 Mixer.startFill(job);

        });


    }


    /**
     * 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();
        });
    }


    /**
     * Automatic checkup (all 30 seconds)
     * When no current job is ongoing, skip
     * if current job is active, but the start time was more than 2mins ago, the job gets canceled
     */
    public static async autoCheckup() {
        setInterval(async () => {
            log("Auto Checkup");
            if (!Mixer.currentJob)
                return;
            // Check if         startedTime     plus   2 mins   smaller than    now
            if (Mixer.currentJob.startedAt.getTime() + 1000 * 60 * 2 <= Date.now()) {
                // Job can be declared as stuck!
                await Mixer.cancelFill();
            }
        }, 1000 * 30);
    }


    /**
     * Checks the internet connection
     */
    public static async checkNetwork() {
        this._internetConnection = await Utils.checkInternet();
    }


    /**
     * Refrehs drinks from the cloud (https://itender.iif.li)
     */
    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();
        });
    }


}