Show:
import WebSocket from 'isomorphic-ws';
import _debug from 'debug';
import EventEmitter from 'events';
import SetList from './SetList.js';
import SongStates from './SongStates.js';
import KeyRanges from './KeyRanges.js';
import ShowNotes from './ShowNotes.js';
import Variables from './Variables.js';
import OnscreenKeyboard from './OnscreenKeyboard.js';
import Bindings from './Bindings.js';
import Bindings4 from './Bindings4.js';
import Commands from './Commands.js';
import Song from './Song.js';
import Transport from './Transport.js';
import Application from './Application.js';
import Engine from './Engine.js';

const debug = _debug('Cantabile');

/**
* Represents a connection to Cantabile.
* 
* @class Cantabile
* @extends EventEmitter
* @constructor
* @param {String} [host] The host to connect to. This can be either <baseaddress> or http://<baseaddress> or ws://<baseaddress>
* When running in a browser, the defaults to `${window.location.host}`.  In other environments it defaults to 
`localhost:35007`.  
*/
class Cantabile extends EventEmitter
{
	constructor(host)
	{
		super();

		this.setMaxListeners(30);

		this.host = host;

		this.shouldConnect = false;
		this._nextRid = 1;
		this._pendingResponseHandlers = {};
		this._endPointEventHandlers = {};
		this._setState("disconnected");

		/**
		 * Gets the {{#crossLink "SetList"}}{{/crossLink}} object
		 *
		 * @property setList
		 * @type {SetList}
		 */
		this.setList = new SetList(this);

		/**
		 * Gets the {{#crossLink "SongStates"}}{{/crossLink}} for the current song
		 *
		 * @property songStates
		 * @type {SongStates}
		 */
		this.songStates = new SongStates(this);

		/**
		 * Gets the currently active {{#crossLink "KeyRanges"}}{{/crossLink}}
		 *
		 * @property keyRanges
		 * @type {KeyRanges}
		 */
		this.keyRanges = new KeyRanges(this);

		/**
		 * Gets the current {{#crossLink "ShowNotes"}}{{/crossLink}}
		 *
		 * @property showNotes
		 * @type {ShowNotes}
		 */
		this.showNotes = new ShowNotes(this);

		/**
		 * Provides access to {{#crossLink "Variables"}}{{/crossLink}} expansion facilities
		 *
		 * @property variables
		 * @type {Variables}
		 */
		 this.variables = new Variables(this);

		/**
		 * Provides access to controllers managed by Cantabile's {{#crossLink "OnscreenKeyboard"}}{{/crossLink}} device
		 *
		 * @property onscreenKeyboard
		 * @type {OnscreenKeyboard}
		 */
		 this.onscreenKeyboard = new OnscreenKeyboard(this);

		 /**
		 * Provides access to global {{#crossLink "Bindings"}}{{/crossLink}} points
		 *
		 * @property bindings
		 * @type {Bindings}
		 */
		  this.bindings = new Bindings(this);

		 /**
		 * Provides access to global {{#crossLink "Bindings4"}}{{/crossLink}} points
		 *
		 * @property bindings4
		 * @type {Bindings4}
		 */
		  this.bindings4 = new Bindings4(this);

		  /**
		 * Provides access to global {{#crossLink "Commands"}}{{/crossLink}}
		 *
		 * @property commands
		 * @type {Commands}
		 */
		 this.commands = new Commands(this);

		 /**
		 * Provides access to {{#crossLink "Song"}}{{/crossLink}} information about the current song
		 *
		 * @property song
		 * @type {Song}
		 */
		this.song = new Song(this);

		/**
		 * Provides access to master {{#crossLink "Transport"}}{{/crossLink}} controls
		 *
		 * @property transport
		 * @type {Transport}
		 */
		this.transport = new Transport(this);

		/**
		 * Provides access to the {{#crossLink "Application"}}{{/crossLink}} object
		 *
		 * @property application
		 * @type {Application}
		 */
		this.application = new Application(this);

		/**
		 * Provides access to the {{#crossLink "Engine"}}{{/crossLink}} object
		 *
		 * @property engine
		 * @type {Engine}
		 */
		 this.engine = new Engine(this);
		}

	/**
	 * The current connection state, either "connecting", "connected" or "disconnected"
	 *
	 * @property state
	 * @type {String} 
	 */
	get state()
	{
		return this._state;
	}

	/**
	 * Initiate connection and retry if fails
	 * @method connect
	 */
	connect()
	{
		this.shouldConnect = true;
		this._internalConnect();
	}

	/**
	 * Disconnect and stop retries
	 * @method disconnect
	 */
	disconnect()
	{
		this.shouldConnect = false;
		this._internalDisconnect();
	}

	/**
	 * Stringify an object as a JSON message and send it to the server
	 *
	 * @method send
	 * @param {object} obj The object to send
	 */
	send(obj)
	{
		debug('SEND: %j', obj);
		this._ws.send(JSON.stringify(obj));
	}

	/**
	 * Stringify an object as a JSON message, send it to the server and returns 
	 * a promise which will resolve to the result.
	 *
	 * @method request
	 * @param {object} obj The object to send
	 * @return {Promise|object}
	 */
	request(message)
	{
		return new Promise(function(resolve, reject) {

			// Tag the message with the request id
			message.rid = this._nextRid++;

			// Store in the response handler map
			this._pendingResponseHandlers[message.rid] = {
				message: message,
				resolve: resolve,
				reject: reject,
			};

			// Send the request
			this.send(message);
		}.bind(this));
	}

	/**
	 * Returns a promise that will be resolved when connected
	 * 
	 * @example
	 * 
	 *     let C = new CantabileApi();
	 *     await C.untilConnected();
	 *
	 * @method untilConnected
	 * @return {Promise}
	 */
	untilConnected()
	{
		if (this._state == "connected")
		{
			return Promise.resolve();		
		}
		else
		{
			return new Promise((resolve, reject) => {
				if (!this.pendingConnectPromises)
					 this.pendingConnectPromises = [resolve];
				else
					this.pendingConnectPromises.push(resolve);
			});
		}
	}

	// PRIVATE:

	// Internal helper to change state, log it and fire event
	_setState(value)
	{
		if (this._state != value)
		{
			this._state = value;
			this.emit('stateChanged', value);
			this.emit(value);
			debug(value);

			if (this._state == "connected")
			{
				if (this.pendingConnectPromises)
				{
					for (let i=0; i<this.pendingConnectPromises.length; i++)
					{
						this.pendingConnectPromises[i]();
					}
					this.pendingConnectPromises = null;
				}
			}
		}
	}

	/**
	 * The current host
	 *
	 * @property host
	 * @type {String} 
	 */
    get host()
	{
		return this._host;
	}

	set host(value)
	{
		if (!value && process.browser)
			value = window.location.host
		if (!value)
			value = "localhost"

		// Crack protocol
		let secure = false;
		if (value.startsWith("https://"))
		{
			secure = true;
			value = value.substring(8);
		}
		else if (value.startsWith("wss://"))
		{
			secure = true;
			value = value.substring(6);
		}
		else if (value.startsWith("http://"))
		{
			value = value.substring(7);
		}
		else if (value.startsWith("ws://"))
		{
			value = value.substring(5);
		}

		// Remove trailing slashes
		while (value.endsWith('/'))
			value = value.substring(0, value.length - 1);

		// Remove socket url
		if (value.endsWith("/api/socket"))
			value = value.substring(0, value.length - 11);

		// Ensure port
		if (value.indexOf(':') < 0)
		{
			let slashPos = value.indexOf('/');
			if (slashPos < 0)
				value += ":35007";
			else
				value = value.substring(0, slashPos) + ':35007' + value.substring(slashPos);
		}

		// Build final http and ws url
		this._host = (secure ? "https://" : "http://") + value;
		this._socketUrl = (secure ? "wss://" : "ws://") + value + "/api/socket/";
	}

	/**
	 * The base socket url
	 *
	 * @property socketUrl
	 * @type {String}
	 */
	 get socketUrl()
	{
		return this._socketUrl;
	}

	/**
	 * The base host url
	 *
	 * @property hostUrl
	 * @type {String}
	 */
	 get hostUrl()
	{
		return this._host;
	}
	set hostUrl(value)
	{
		throw new Error("The `hostUrl` property is read-only, use `host` instead");
	}

	set socketUrl(value)
	{
		throw new Error("The `socketUrl` property has been deprecated, use `host` instead");
	}


	// Internal helper to actually perform the connection
	_internalConnect()
	{
		if (!this.shouldConnect)
			return;

		// Already connected?
		if (this._ws)
			return;

		this._setState("connecting");

		// Work out socket url
		let socketUrl = this.socketUrl;

		// Create the socket and hook up handlers
		debug("Opening web socket '%s'", socketUrl);
		this._ws =  new WebSocket(socketUrl);
		this._ws.onerror = this._onSocketError.bind(this);
		this._ws.onopen = this._onSocketOpen.bind(this);
		this._ws.onclose = this._onSocketClose.bind(this);
		this._ws.onmessage = this._onSocketMessage.bind(this);
	}

	// Internal helper to disconnect
	_internalDisconnect()
	{
		if (this.state == "connected")
			this._setState("disconnected");

		// Already disconnected?
		if (!this._ws)
			return;

		this._ws.close();
		delete this._ws;
	}

	// Internal helper to retry connection every 1 second
	_internalReconnect()
	{
		if (this.shouldConnect && !this.timeoutPending)
		{
			this.timeoutPending = true;
			this._setState("connecting");
			setTimeout(function() {
				this.timeoutPending = false;
				this._internalConnect();
			}.bind(this), 1000);
		}
	}

	// Socket onerror handler
	_onSocketError(evt)
	{
		// Disconnect
		this._internalDisconnect();

		// Try to reconnect...
		this._internalReconnect();
	}

	// Socket onopen handler
	_onSocketOpen()
	{
		this._setState("connected");
	}

	// Socket onclose handler
	_onSocketClose()
	{
		if (this._ws)
		{
			this._setState("disconnected");
			delete this._ws;

			// Reject any pending requests
			/*
			var pending = this._pendingResponseHandlers;
			console.log(pending);
			this._pendingResponseHandlers = {};
			for (let key in pending) 
			{
				debugger;
				console.log("===> disconnecting", key);
			  	pending[key].reject(new Error("Disconnected"));
			}
			*/
		}

		// Try to reconnect...
		this._internalReconnect();
	}

	// Socket onmessage handler
	_onSocketMessage(msg)
	{
		msg = JSON.parse(msg.data);

		debug('RECV: %j', msg);

		// Request response?
		if (msg.rid)
		{
			// Find the handler
			let handlerInfo = this._pendingResponseHandlers[msg.rid];
			if (!handlerInfo)
			{
				debug('ERROR: received response for unknown rid:', msg.rid)
				return;
			}

			// Remove from pending map
			delete this._pendingResponseHandlers[msg.rid];

			// Resolve reject
			if (msg.status >= 200 && msg.status < 300)
				handlerInfo.resolve(msg);
			else
				handlerInfo.reject(new Error(`${msg.status} - ${msg.statusDescription}`));
		}

		// Event message?
		if (msg.epid && msg.eventName)
		{
			var ep = this._endPointEventHandlers[msg.epid];
			if (ep)
			{
				ep._dispatchEventMessage(msg.eventName, msg.data);
			}
			else
			{
				debug(`ERROR: No event handler found for end point ${msg.epid}`)
			}
		}
	}


	_registerEndPointEventHandler(epid, endPoint)
	{
		this._endPointEventHandlers[epid] = endPoint;
	}

	_revokeEndPointEventHandler(epid)
	{
		delete this._endPointEventHandlers[epid];
	}

}

/**
 * Fired when the {{#crossLink "Cantabile/state:property"}}{{/crossLink}} property value changes
 *
 * @event stateChanged
 * @param {String} state The new connection state ("connecting", "connected" or "disconnected")
 */
const eventStateChanged = "stateChanged";

/**
 * Fired when entering the connected state
 *
 * @event connected
 */
const eventConnected = "connected";

/**
 * Fired when entering the connecting state
 *
 * @event connecting
 */
const eventConnecting = "connecting";

/**
 * Fired when entering the disconnected state
 *
 * @event disconnected
 */
const eventDiconnected = "disconnected";




export default Cantabile;