var markup = require('ls-markup');
var APIEndpoints = require('libresignage/api/APIEndpoints');
var Assert = require('assert');
var Animations = require('libresignage/slide/Animations');
var SlideAsset = require('libresignage/slide/SlideAsset');

/**
* A class representing a LibeSignage slide.
*
* @memberof module:libresignage/slide
*/
class Slide {
	/**
	* Construct a Slide object.
	*
	* @constructor
	*
	* @param {APIInterface} api An initialized API object.
	*
	* @throws {AssertionError} If api == null.
	*/
	constructor(api) {
		Assert.ok(api != null, "API object can't be null.");

		this.LOCK_RENEWAL_HEADROOM = 30;

		this.data = {};
		this.lock_ren_timeout = null;
		this.api = api;
		this.html = null;
	}

	/**
	* Load a slide.
	*
	* @param {string}  id         The ID of the slide to load.
	* @param {boolean} lock       Whether to automatically lock the slide.
	* @param {boolean} lock_renew Whether to setup automatic lock renewal.
	*/
	async load(id, lock, lock_renew) {
		// If a slide is already loaded and locked release that one first.
		if (
			this.data != null
			&& this.data.id != null
		) { await this.lock_release(null); }

		this.data = {'id': id};
		if (lock) {
			try {
				await this.lock_acquire(lock_renew);
			} catch (e) {
				this.data = {};
				throw e;
			}
		}
		await this.fetch();
	}

	/**
	* Fetch the slide data for a slide. This function expects
	* the 'id' value in this.data to be already set, ie.
	* Slide.load() must be called first.
	*
	* @throws {AssertionError} If Slide.load() hasn't been called.
	*/
	async fetch() {
		Assert.ok(this.has('id'), "Slide id not set.");
		console.log(`Slide: Fetch '${this.data.id}'.`);
		let resp = await this.api.call(
			APIEndpoints.SLIDE_GET,
			{ 'id': this.data.id },
		);
		this.set_from_api(resp.slide);
	}

	/**
	* Release the current lock on this slide and disable
	* automatic lock renewal.
	*
	* @throws {AssertionError} If Slide.load() hasn't been called.
	*/
	async lock_release() {
		Assert.ok(this.has('id'), "Slide not loaded.");
		console.log(`Slide: Release lock on '${this.data.id}'.`);

		clearTimeout(this.lock_ren_timeout);
		this.lock_ren_timeout = null;
		this.data.lock = {};

		return this.api.call(
			APIEndpoints.SLIDE_LOCK_RELEASE,
			{ 'id': this.data.id }
		);
	}

	/**
	* Attempt to acquire a lock on this slide.
	*
	* @param {boolean} renew Whether to setup automatic lock renewal.
	*
	* @throws {AssertionError} If Slide.load() hasn't been called.
	*/
	async lock_acquire(renew) {
		Assert.ok(this.has('id'), "Slide not loaded.");
		console.log(`Slide: Acquire lock on '${this.data.id}'.`)

		let ren_t = 0;
		let resp = await this.api.call(
			APIEndpoints.SLIDE_LOCK_ACQUIRE,
			{ 'id': this.data.id }
		);
		this.data.lock = resp.lock;

		if (renew) {
			ren_t = (
				this.data.lock.expire
				- this.LOCK_RENEWAL_HEADROOM
				- Math.round(Date.now()/1000)
			);

			console.log(
				`Slide: Lock renewal on ${this.data.id} ` +
				`in ${ren_t} seconds.`
			);
			this.lock_ren_timeout = setTimeout(
				async () => {
					try {
						await this.lock_acquire(true);
					} catch (e) {
						this.lock_ren_timeout = null;
						throw e;
					}
				}, ren_t*1000
			);
		}
	}

	/**
	* Check whether a slide is locked by the current session.
	*
	* @return {boolean} True if the slide is locked by current
	*                   session, false otherwise.
	*/
	is_locked_from_here() {
		return (
			this.data != null
			&& this.data.lock != null
			&& this.data.lock.session_id === this.api.get_session().get_id()
			&& this.data.lock.expire >= Date.now()/1000
		);
	}

	/**
	* Check whetehr a slide is owned by the current user.
	*
	* @return {boolean} True if the slide is owned by the current
	*                   user, false otherwise.
	*/
	is_owned_by_me() {
		let user = this.api.get_session().get_user().get_user();
		return(
			this.data != null
			&& this.data.owner === user
		);
	}

	/**
	* Check whether the current user can collaborate on a slide.
	*
	* @return {boolean} True if the current user can
	*                   collaborate, false otherwise.
	*/
	can_collaborate() {
		let user = this.api.get_session().get_user().get_user();
		return (
			this.data != null
			&& this.data.collaborators.includes(user)
		);
	}

	/**
	* Save a slide.
	*
	* @throws {AssertionError} If the slide is not locked.
	*/
	async save() {
		Assert.ok(
			this.data.id == null || this.is_locked_from_here(),
			"Can't save an unlocked slide."
		);
		console.log(`Slide: Save '${this.data.id}'.`);
		let resp = await this.api.call(
			APIEndpoints.SLIDE_SAVE,
			this.data
		);
		this.set_from_api(resp.slide);
	}

	/**
	* Remove a slide. If 'id' is supplied, the corresponding
	* slide is removed. Otherwise the currently loaded slide
	* is removed.
	*
	* @param {string|null} id The ID of the slide to remove or null.
	*
	* @throws {Error} if no slide ID is specified implicitly or explicitly.
	*/
	async remove(id) {
		if (id == null) {
			if (this.data.id) {
				id = this.data.id;
			} else {
				throw new Error('No slide ID specified for removal.');
			}
		}

		console.log(`Slide: Remove '${id}'.`);
		await this.api.call(
			APIEndpoints.SLIDE_RM,
			{ 'id': id }
		);
	}

	/**
	* Duplicate a slide.
	*
	* @return {Slide} The duplicate Slide object.
	*/
	async dup() {
		let resp = await this.api.call(
			APIEndpoints.SLIDE_DUP,
			{'id': this.get('id')}
		);
		let s = new Slide(this.api);
		s.set(resp['slide']);
		return s;
	}

	/**
	* Remove and assert from a slide.
	*
	* @param {string} name The name of the asset to remove.
	*
	* @throws {AssertionError} If the slide is not saved.
	* @throws {AssertionError} If the slide is not locked.
	*/
	async remove_asset(name) {
		Assert.ok(this.data.id != null, "Unsaved slide.");
		Assert.ok(this.is_locked_from_here(), "Slide not locked.");

		await this.api.call(
			APIEndpoints.SLIDE_REMOVE_ASSET,
			{ id: this.data.id, name: name }
		);
		await this.fetch();
	}

	/**
	* Upload assets to a slide.
	*
	* @param {object} files An object acquired from a HTML file input.
	*
	* @throws {AssertionError} If the slide is not saved.
	* @throws {AssertionError} If the slide is not locked.
	*/
	async upload_assets(files) {
		Assert.ok(this.data.id != null, "Unsaved slide.");
		Assert.ok(this.is_locked_from_here(), "Slide not locked");

		let data = new FormData();
		if (files.length) {
			for (let i = 0; i < files.length; i++) {
				data.append(i, files[i]);
			}
			data.append('body', JSON.stringify({
				'id': this.data.id
			}));
			await this.api.call(
				APIEndpoints.SLIDE_UPLOAD_ASSET,
				data
			);
			await this.fetch();
		}
	}

	/**
	* Set the data of a Slide from received API data.
	* This function wraps Slide.set() and adds some additional
	* functionality.
	*
	* @param {object} data The data received from an API endpoint.
	*/
	set_from_api(data)  {
		this.set(data);

		// Manually create asset objects mapped to asset names.
		this.data.assets = {};
		for (let a of data.assets) {
			this.data.assets[a.filename] = new SlideAsset(
				data.id,
				a.mime,
				a.filename,
				a.has_thumb,
				a.hash
			);
		}
	}

	/**
	* Set the data of a Slide.
	*
	* @param {object} data The data to set.
	*/
	set(data) {
		Object.assign(this.data, data);
	}

	/**
	* Transpile the markup of a slide into HTML.
	*/
	transpile() {
		this.html = markup.parse(this.get('markup'));
	}

	/**
	* Get the transpiled HTML of a Slide.
	*
	* @returns {string} The HTML of the Slide.
	*/
	get_html() { return this.html; }

	/**
	* Get a data field of a slide.
	*
	* @param {string} key The key of the data to get.
	*
	* @return {*} The requested data.
	*/
	get(key) { return this.data[key]; }

	/**
	* Get the data object of a slide.
	*
	* @return {object} The Slide data object.
	*/
	get_data() { return this.data; }

	/**
	* Check whether a slide has a data key.
	*
	* @param {string} key The key to check.
	*
	* @return {boolean} True if the key exists, false otherwise.
	*/
	has(key) {
		return (
			this.data != null
			&& key in this.data
			&& this.data[key] != null
		);
	}

	/**
	* Get the hide animation CSS classes of a slide.
	*
	* @return {string[]} The corresponding CSS classes.
	*/
	anim_hide() {
		return [Animations[this.get('animation')].hide];
	}

	/**
	* Get the reversed hide animation CSS classes of a slide.
	*
	* @return {string[]} The corresponding CSS classes.
	*/
	anim_hide_rev() {
		return [this.anim_hide(), 'anim-reverse'];
	}

	/**
	* Get the show animation CSS classes of a slide.
	*
	* @return {string[]} The corresponding CSS classes.
	*/
	anim_show() {
		return [Animations[this.get('animation')].show];
	}

	/**
	* Get the reversed show animation CSS classes of a slide.
	*
	* @return {string[]} The corresponding CSS classes.
	*/
	anim_show_rev() {
		return [this.anim_show(), 'anim-reverse'];
	}
}
module.exports = Slide;
