'use strict';

import { v4 as uuid } from 'uuid';
import Log = require('log');
import { powerSaveBlocker } from 'electron'


/** @module lib/sequencer **/

let seq : Sequencer;

class Sequencer {
	private running : boolean = false;
	private paused : boolean = false;

	private grid : any[] = [];
	private gridLoops : number = 1;

	private arr : any[] = []; //store sequence from gui
	private loops : number = 1;

	private cfg : any;
	private cmd : any;
	private CMDS : any = {};
	private ipc : any;
	private ui : any;
	private log : any;
	private id : string = 'sequence';
	private alerted : boolean = false;
	private psbId : any;

	/**
	 * @constructor
	 * Create a new sequencer and assign command and UI as private sub-classes
	 *
	 * @param {object} cfg Configuration object
	 * @param {object} cmd Shared command class
	 * @param {object} ui Electron UI, browser window
	 **/

	constructor (cfg : any, cmd : any, ui : any) {
		this.cfg = cfg;
		this.cmd = cmd;
		this.ui = ui;
		this.cmds(cfg.cmd);
		this.init();
	}

	/**
	 * Take configuration object and assign all commands as keys
	 * in the internal CMDS object.
	 *
	 * @param {object} obj Configuration object
	 **/
	private cmds (obj : any) {
		let keys : string[] = Object.keys(obj);
		for (let key of keys) {
			this.CMDS[obj[key]] = key;
		}
		//
		//
	}

	/**
	 * Initialize the class by requiring ipcMain from electron
	 * and creating logger.
	 *
	 **/
	private async init () {
		this.log = await Log({ label : this.id })
		this.ipc = require('electron').ipcMain;
		this.listen();
	}

	/**
	 * Bind ipc listener to channel 'sequencer' or current id of
	 * class.
	 **/ 
	private listen () {
		this.ipc.on(this.id, this.listener.bind(this));
	}

	/**
	 * Listener callback function. Called whenever ipc
	 * message is sent to channel 'sequencer'.
	 *
	 * @param {object} event IPC message event
	 * @param {object} arg Arguments provided in message
	 **/
	private async listener (event : any, arg : any) {
		if (arg && arg.start) {
			this.start(arg);
		} else if (arg && arg.stop) {
			this.stop();
		} else if (arg && arg.pause) {
			this.pause();
		} else if (arg && arg.set) {
			this.setSteps(arg.set);
		} else if (arg && arg.unset) {
			this.unsetSteps(arg.unset);
		} else if (arg && arg.loops) {
			this.setLoops(arg.loops);
		}
		event.returnValue = true;
	}

	/**
	 * Sets the value of the loops in the grid sequence
	 * to value sent by UI in ipc message.
	 *
	 * @param {integer} count Number of loops to set grid sequence to
	 **/
	public setLoops (count : number) {
		this.gridLoops = count;
		this.log.info(`Set loop count to ${count}`);
	}

	/**
	 * Sets multiple steps at once
	 *
	 * @param {array} steps Array of steps to set or update
	 **/
	public setSteps (steps : any[]) {
		for (let step of steps) {
			this.grid[step.x] = step;
		}
	}

    /**
     * Resets multiple steps to default 'undefined' state
     *
     * @param {array} steps Array containing the x location of steps to unset
     **/
	public unsetSteps (steps : number[]) {
		for (let x of steps) {
			this.grid[x] = undefined;
		}
	}

	/**
	 * Starts a sequence with the existing grid sequence, 
	 * or if one is provided in the arg object, starts
	 * that sequence.
	 *
	 * @param {object} arg Arguments from ipc message
	 **/
	public async start (arg : any) {
		const psbId : number = powerSaveBlocker.start('prevent-display-sleep');
		let startTime : number = +new Date();
		let ms : number;

		if (arg && arg.arr) {
			this.arr = arg.arr; //overwrite sequence
		} else {
			this.arr = this.grid;
		}

		if (arg && arg.loops) {
			this.loops = arg.loops; //overwrite loops
		} else {
			this.loops = this.gridLoops;
		}
		
		this.running = true;
		this.paused = false;

		//start sequence
		this.log.info(`Starting sequence...`);
		this.ui.send(this.id, { start : true });

		if (this.cmd.proj.filmout.state.enabled === true) {
			await this.cmd.proj.filmout.display.open();
		}

		for (let x = 0; x < this.loops; x++) {
			//start loop
			this.log.info(`Starting loop ${x + 1}`);
			this.ui.send(this.id, { loop : x, start : true });

			for (let y = 0; y < this.arr.length; y++) {
				//start step
				if (!this.running) {
					break;
				}
				
				//UI initiates pause, not planned
				while (this.paused) {
					await delay(42);
				}

				if (typeof this.arr[y] === 'undefined') {
					continue;
				}

				this.log.info(`Starting step ${y + 1} of loop ${x + 1}`);
				this.ui.send(this.id, { step : y, loop : x, start : true });
					
				await this.step(y);

				//end step
				this.log.info(`Ended step ${y + 1} of loop ${x + 1}`);
				this.ui.send(this.id, { step : y,  loop : x, stop : true });
			}
			if (!this.running) {
				break;
			}
			//end loop
			this.log.info(`Ended loop ${x + 1}`);
			this.ui.send(this.id, { loop : x, stop : true });
		}

		if (this.cmd.proj.filmout.state.enabled === true) {
			await this.cmd.proj.filmout.display.close();
		}

		powerSaveBlocker.stop(psbId)
		this.psbId = null

		ms = ( +new Date() ) - startTime;
		//end sequence
		this.log.info(`Ended sequence`);
		this.ui.send(this.id, { stop : true, ms });
	}

	/**
	 * Pauses sequence from UI.
	 **/
	public pause () {
		this.paused = true;
	}

	/**
	 * Stops the sequence
	 **/
	public stop () {
		if (this.cmd.proj.filmout.state.enabled === true) {
			this.cmd.proj.filmout.display.close();
		}
		this.running = false;
		if (this.psbId) {
			powerSaveBlocker.stop(this.psbId);
		}
		//clear?

	}

	/**
	 * Execute command @ step x. Wrapper with try catch.
	 *
	 * @param {integer} x Step to execute command at
	 **/
	private async step ( x: number) {
		try {
			await this.cmdExec(x)
		} catch (err) {
			throw err;
		}
	}

	/**
	 * Locate step @ position x and execute the command.
	 * 
	 * @param {integer} x Step to execute command at
	 **/
	private async cmdExec (x : number) {
		const cmdOriginal : string = this.arr[x].cmd;
		const cmd : string = this.CMDS[cmdOriginal];
		this.log.info(`CMD: '${cmdOriginal}' -> ${cmd}`);
		//I wrote this when I was very tired and delirious
		return await this.cmd[cmd]();
	}
}

module.exports = function (cfg : any, cmd : any, ui : any) {
	return new Sequencer(cfg, cmd, ui);
}
