import {iTenderStatus} from "./iTenderStatus";
import Container from "./database/Container";
import {HCSR04} from "hc-sr04";
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 {HX711} from "./HX711";
import {SensorType} from "./SensorType";
import Job from "./database/Job";
import {IIngredient} from "./database/IIngredient";
import Ingredient from "./database/Ingredient";
import {clearInterval} from "timers";
import {RejectReason} from "./RejectReason";
import {Settings} from "./Settings";
import GPIO from "rpi-gpio";
import axios from "axios";

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

export class iTender {
    static get containers(): { container: IContainer, sensor: HCSR04 | HX711, pump: null }[] {
        return this._containers;
    }

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

    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;

    static get internetConnection(): boolean {
        return this._internetConnection;
    }

    /**
     * @Deprecated
     * @private
     */
    private static _containers: { container: IContainer, sensor: HCSR04 | HX711, pump: null }[] = [];
    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;
    }

    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 }[] = [];
            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});
                }
            } 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})
                }


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

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

            }

            job.drink = drink
            job.amounts = amounts as { ingredient: IIngredient, amount: number, container: IContainer }[];
            await job.save()
            resolve(job);

            await this.startFill(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);

        let timers: NodeJS.Timeout[] = [];
        for (let x of job.amounts) {
            // Start pump here
            await GPIO.setup(x.container.pumpPin, GPIO.DIR_OUT);
            await GPIO.write(x.container.pumpPin, true);

            let waitTime = (Settings.get("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(() => {
                // Stop pump here
                GPIO.write(x.container.pumpPin, false);

                // Remove from list of timers
                let arr: NodeJS.Timer[] = [];
                for (let i = 0; i < timers.length; i++) {
                    if (timers[i] != timer)
                        arr.push(timers[i]);
                }
                mixLog(`Stopping output of pump ${x.container.pumpPin}`)
                timers = arr;

            }, waitTime);
            timers.push(timer);
        }

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

            if (timers.length != 0)
                return;

            clearInterval(iTender._jobCheckInterval);
            job.endAt = new Date();
            job.successful = true;
            await job.save();
            mixLog("Job successful");
            iTender.setStatus(iTenderStatus.READY);
        }, 500);
    }

    static cancelFill() {
        if (!this._currentJob)
            return;
        clearInterval(this._jobCheckInterval);
        this._currentJob.successful = false;
        this._currentJob.endAt = new Date();
        this._currentJob.save();


        for (let x of this._currentJob.amounts) {
            // stop pump pin
            //x.container.pumpPin
            GPIO.write(x.container.pumpPin, false);
        }

        iTender.setStatus(iTenderStatus.READY);

    }

    static measureContainers(): Promise<void> {
        log("Measuring containers...");

        return new Promise(async resolve => {
            for (let c of (await Container.find({}))) {
                try {
                    let rand = Math.random() * 5;
                    if (c.sensorType == SensorType.ULTRASOUND) {
                        let sensor = new HCSR04(c.sensorPin1, c.sensorPin2);

                        c.rawData = sensor.distance();
                        c.rawData = rand;
                    } else if (c.sensorType == SensorType.LOADCELL) {
                        let sensor = new HX711(c.sensorPin1, c.sensorPin2);

                        c.rawData = sensor.measure();
                        c.rawData = rand;
                    }

                    if (c.sensorFilledMax && c.sensorFilledMin) {
                        c.filled = c.rawData * c.sensorFilledMax / 100;
                    }

                } catch (e) {
                    c.filled = -1;
                    c.rawData = 0;
                }

                await c.save();
            }
            log("Containers measured!");
            resolve();

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

    static refreshDrinks(): Promise<void> {
        this.setStatus(iTenderStatus.REFRESHING);
        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);
                }

            }
            log("Drinks refreshed!");
            iTender.setStatus(iTenderStatus.READY);
            await WebSocketHandler.sendDrinks();
            resolve();

        });
    }


    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[];
                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.findOne({name: remote.name});
                    if (!ingredient)
                        ingredient = new Ingredient();

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



                const requestDrinks = await axios.get("https://itender.iif.li/api/drinks");
                let serverDrinks = requestDrinks.data as IDrink[];
                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.findOne({name: remote.name});
                    if (!drink)
                        drink = new Drink();

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

                    await drink.save();


                    // Download thumbnail
                    Utils.downloadImage("https://itender.iif.li/images/" + remote._id + ".png", "./public/images/" + drink._id + ".png").then(filepath => {
                        log("Drink " + remote.name + "'s Thumbnail downloaded to " + filepath);
                    }).catch(e => {
                        log("Drink " + remote.name + " failed to download thumbnail!\n" + e);
                    });

                }

            } catch (e) {
                console.error(e);
            }

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

    private static interval;

    public static toggleTare(state: boolean) {
        clearInterval(iTender.interval);
        if (state)
            this.interval = setInterval(async () => {
                await this.measureContainers();
            }, 500);
    }
}