index.js

const net = require('net'),
	randomstring = require('randomstring');

// noinspection JSUnusedGlobalSymbols
/**
 * Client class that contains methods for communicating with Streamduck daemon
 */
class StreamduckClient {
	constructor(protocol) {
		this.protocol = protocol;
	}

	/**
	 * Button object
	 * @typedef {Object.<string, any>} Button
	 */

	/**
	 * Value object
	 * @typedef {{name: string, display_name: string, description: string, path: string, ty: any, value: any}} Value
	 */

	/**
	 * Panel object
	 * @typedef {{display_name: string, data: any, buttons: Object.<string, Button>}} Panel
	 */

	/**
	 * Device Config object
	 * @typedef {{vid: number, pid: number, serial: string, brightness: number, layout: Panel, images: Object.<string, string>, plugin_data: Object.<string, any>}} DeviceConfig
	 */


	/**
	 * Event listener
	 * @callback EventListener
	 * @param {Object} event Event data
	 */

	/**
	 * Adds event listener
	 * @param {EventListener} listener Listener to add
	 */
	add_event_listener(listener) {
		this.protocol.add_event_listener(listener)
	}

	/**
	 * Checks if protocol is currently connected to daemon
	 * @returns {boolean} Connection status
	 */
	is_connected() {
		return this.protocol.connected()
	}

	/**
	 * Retrieves version of socket API that daemon is using
	 * @returns {Promise<string>} Version of the daemon
	 */
	version() {
		return this.protocol.request(
			{
				ty: "socket_version"
			}
		).then(data => data.version);
	}

	/**
	 * Retrieves device list currently recognized by daemon
	 * @returns {Promise<Array.<{device_type: ("Unknown"|"Mini"|"Original"|"OriginalV2"|"XL"|"MK2"), serial_number: string, managed: boolean, online: boolean}>>} Device list
	 */
	device_list() {
		return this.protocol.request(
			{
				ty: "list_devices"
			}
		).then(data => data.devices)
	}

	/**
	 * Retrieves data of specific device
	 * @param {string} serial_number Serial number of the device
	 * @returns {Promise<?{device_type: ("Unknown"|"Mini"|"Original"|"OriginalV2"|"XL"|"MK2"), serial_number: string, managed: boolean, online: boolean}>} Device Data, null if device wasn't found
	 */
	get_device(serial_number) {
		return this.protocol.request(
			{
				ty: "get_device",
				data: {
					serial_number
				}
			}
		).then(data => {
			if (data.Found) {
				return data.Found;
			} else {
				return null;
			}
		})
	}

	/**
	 * Adds device into managed list
	 * @param {string} serial_number Serial number of the device
	 * @returns {Promise<"NotFound"|"AlreadyRegistered"|"Added">} Result of the operation 
	 */
	add_device(serial_number) {
		return this.protocol.request(
			{
				ty: "add_device",
				data: {
					serial_number
				}
			}
		)
	}

	/**
	 * Removes device from managed list
	 * @param {string} serial_number Serial number of the device
	 * @returns {Promise<"NotRegistered"|"Removed">} Result of the operation 
	 */
	remove_device(serial_number) {
		return this.protocol.request(
			{
				ty: "remove_device",
				data: {
					serial_number
				}
			}
		)
	}

	/**
	 * Reloads all device configs, all unsaved changes will be lost doing this
	 * @returns {Promise<"ConfigError"|"Reloaded">} Result of the operation 
	 */
	reload_device_configs() {
		return this.protocol.request(
			{
				ty: "reload_device_configs",
			}
		)
	}

	/**
	 * Reloads device config of specified device, all unsaved changes will be lost doing this
	 * @param {string} serial_number Serial number of the device
	 * @returns {Promise<"ConfigError"|"DeviceNotFound"|"Reloaded">} Result of the operation 
	 */
	reload_device_config(serial_number) {
		return this.protocol.request(
			{
				ty: "reload_device_config",
				data: {
					serial_number
				}
			}
		)
	}

	/**
	 * Saves all device configs
	 * @returns {Promise<"ConfigError"|"Saved">} Result of the operation
	 */
	save_device_configs() {
		return this.protocol.request(
			{
				ty: "save_device_configs",
			}
		)
	}

	/**
	 * Saves device config of specified device
	 * @param {string} serial_number Serial number of the device
	 * @returns {Promise<"ConfigError"|"DeviceNotFound"|"Saved">} Return of the operation
	 */
	save_device_config(serial_number) {
		return this.protocol.request(
			{
				ty: "save_device_config",
				data: {
					serial_number
				}
			}
		)
	}

	/**
	 * Gets device config of specified device
	 * @param {string} serial_number Serial number of the device
	 * @returns {Promise<?DeviceConfig>} Device config, null if device wasn't found
	 */
	get_device_config(serial_number) {
		return this.protocol.request(
			{
				ty: "get_device_config",
				data: {
					serial_number
				}
			}
		).then(data => {
			if (data.Config) {
				return data.Config;
			} else {
				return null;
			}
		})
	}

	/**
	 * Exports device config from daemon
	 * @param {string} serial_number Serial number of the device
	 * @returns {Promise<?string>} String that represents device config, null if device wasn't found
	 */
	export_device_config(serial_number) {
		return this.protocol.request(
			{
				ty: "export_device_config",
				data: {
					serial_number
				}
			}
		).then(data => {
			if (data.Exported) {
				return data.Exported;
			} else {
				return null;
			}
		})
	}


	/**
	 * Imports device config from string into daemon
	 * @param {string} serial_number Serial number of the device
	 * @param {string} config String that represents device config
	 * @returns {Promise<"DeviceNotFound"|"InvalidConfig"|"FailedToSave"|"Imported">} Result of the operation. "FailedToSave" if error happened while saving config on daemon's end
	 */
	import_device_config(serial_number, config) {
		return this.protocol.request(
			{
				ty: "import_device_config",
				data: {
					serial_number,
					config
				}
			}
		)
	}

	/**
	 * Gets current brightness of specified device
	 * @param {string} serial_number Serial number of the device
	 * @returns {Promise<?number>} Brightness value, or null if device wasn't found
	 */
	get_brightness(serial_number) {
		return this.protocol.request(
			{
				ty: "get_brightness",
				data: {
					serial_number
				}
			}
		).then(data => {
			if (data.Brightness) {
				return data.Brightness;
			} else {
				return null;
			}
		})
	}

	/**
	 * Sets brightness of specified device
	 * @param {string} serial_number Serial number of the device
	 * @param {number|string} brightness Brightness value (integer 0-100 or higher)
	 * @returns {Promise<"DeviceNotFound"|"Set">} Result of the operation 
	 */
	set_brightness(serial_number, brightness) {
		return this.protocol.request(
			{
				ty: "set_brightness",
				data: {
					serial_number,
					brightness: parseInt(brightness)
				}
			}
		)
	}

	/**
	 * Lists images of specified device
	 * @param {string} serial_number Serial number of the device
	 * @returns {Promise<?Object.<string, string>>} Image map, null if device wasn't found
	 */
	list_images(serial_number) {
		return this.protocol.request(
			{
				ty: "list_images",
				data: {
					serial_number
				}
			}
		).then(data => {
			if (data.Images) {
				return data.Images;
			} else {
				return null;
			}
		})
	}

	/**
	 * Adds images of specified device
	 * @param {string} serial_number Serial number of the device
	 * @param {string} image_data Image encoded in Base64
	 * @returns {Promise<?string>} Image identifier, null if invalid image or device wasn't found
	 */
	add_image(serial_number, image_data) {
		return this.protocol.request(
			{
				ty: "add_image",
				data: {
					serial_number,
					image_data
				}
			},
			8640000000
		).then(data => {
			if (data.Added) {
				return data.Added;
			} else {
				return null;
			}
		})
	}

	/**
	 * Removes images from specified device
	 * @param {string} serial_number Serial number of the device
	 * @param {string} image_identifier Image identifier
	 * @returns {Promise<"NotFound"|"Removed">} Result of the operation
	 */
	remove_image(serial_number, image_identifier) {
		return this.protocol.request(
			{
				ty: "remove_image",
				data: {
					serial_number,
					image_identifier
				}
			}
		)
	}

	/**
	 * Lists names of fonts loaded by daemon
	 * @returns {Promise<Array.<string>>} Font name list
	 */
	list_fonts() {
		return this.protocol.request(
			{
				ty: "list_fonts"
			}
		).then(data => data.font_names)
	}

	/**
	 * Lists modules daemon has loaded
	 * @returns {Promise<Array.<{name: string, author: string, description: string, version: string, used_features: Array.<Array.<string>>}>>} Module list 
	 */
	list_modules() {
		return this.protocol.request(
			{
				ty: "list_modules"
			}
		).then(data => data.modules)
	}

	/**
	 * Lists components provided by loaded modules
	 * @returns {Promise<Object.<string, Object.<string, {default_looks: Object, description: string, display_name: string}>>>} Map of modules to maps of component names to component definitions
	 */
	list_components() {
		return this.protocol.request(
			{
				ty: "list_components"
			}
		).then(data => data.components)
	}

	/**
	 * Retrieves module values of specified module
	 * @param {string} module_name Module name
	 * @returns {Promise<?Array.<Value>>} Module values, null if module wasn't found
	 */
	get_module_values(module_name) {
		return this.protocol.request(
			{
				ty: "get_module_values",
				data: {
					module_name
				}
			}
		).then(data => {
			if (data.Values) {
				return data.Values;
			} else {
				return null;
			}
		})
	}

	/**
	 * Adds element to module setting for specified module
	 * @param {string} module_name Module name
	 * @param {string} path Path to setting
	 * @returns {Promise<"ModuleNotFound"|"FailedToAdd"|"Added">} Result of the operation
	 */
	add_module_value(module_name, path) {
		return this.protocol.request(
			{
				ty: "add_module_value",
				data: {
					module_name,
					path
				}
			}
		)
	}

	/**
	 * Removes element from module setting for specified module
	 * @param {string} module_name Module name
	 * @param {string} path Path to setting
	 * @param {number|string} index Index of element
	 * @returns {Promise<"ModuleNotFound"|"FailedToRemove"|"Removed">} Result of the operation
	 */
	remove_module_value(module_name, path, index) {
		return this.protocol.request(
			{
				ty: "remove_module_value",
				data: {
					module_name,
					path,
					index: parseInt(index)
				}
			}
		)
	}

	/**
	 * Sets module values for specified module
	 * @param {string} module_name Module name
	 * @param {Value} value Value to set
	 * @returns {Promise<"ModuleNotFound"|"FailedToSet"|"Set">} Result of the operation
	 */
	set_module_value(module_name, value) {
		return this.protocol.request(
			{
				ty: "set_module_value",
				data: {
					module_name,
					value
				}
			},
			8640000000
		)
	}

	/**
	 * Gets screen stack of specified device
	 * @param {string} serial_number Serial number of the device
	 * @returns {Promise<?Array.<Panel>>} Screen stack array, null if device wasn't found
	 */
	get_stack(serial_number) {
		return this.protocol.request(
			{
				ty: "get_stack",
				data: {
					serial_number
				}
			}
		).then(data => {
			if (data.Stack) {
				return data.Stack;
			} else {
				return null;
			}
		})
	}

	/**
	 * Gets screen stack names of specified device
	 * @param {string} serial_number Serial number of the device
	 * @returns {Promise<?Array.<string>>} Screen stack name array, null if device wasn't found
	 */
	get_stack_names(serial_number) {
		return this.protocol.request(
			{
				ty: "get_stack_names",
				data: {
					serial_number
				}
			}
		).then(data => {
			if (data.Stack) {
				return data.Stack;
			} else {
				return null;
			}
		})
	}

	/**
	 * Gets current screen of specified device
	 * @param {string} serial_number Serial number of the device
	 * @returns {Promise<?Panel>} Screen consisting of key indices and button objects, null if device wasn't found
	 */
	get_current_screen(serial_number) {
		return this.protocol.request(
			{
				ty: "get_current_screen",
				data: {
					serial_number
				}
			}
		).then(data => {
			if (data.Screen) {
				return data.Screen;
			} else {
				return null;
			}
		})
	}

	/**
	 * Gets current image rendered on specified device
	 * @param {string} serial_number Serial number of the device
	 * @param {number|string} key Index of the key (0-255)
	 * @returns {Promise<?string>} Base64 png image data, null if device or button wasn't found
	 */
	get_button_image(serial_number, key) {
		return this.protocol.request(
			{
				ty: "get_button_image",
				data: {
					serial_number,
					key: parseInt(key)
				}
			}
		).then(data => {
			if (data.Image) {
				return data.Image;
			} else {
				return null;
			}
		})
	}

	/**
	 * Gets current images rendered on specified device
	 * @param {string} serial_number Serial number of the device
	 * @returns {Promise<?Object.<string, string>>} Map consisting of key indices and base64 png image data, null if device wasn't found
	 */
	get_button_images(serial_number) {
		return this.protocol.request(
			{
				ty: "get_button_images",
				data: {
					serial_number
				}
			}
		).then(data => {
			if (data.Images) {
				return data.Images;
			} else {
				return null;
			}
		})
	}

	/**
	 * Retrieves button from current screen of specified device
	 * @param {string} serial_number Serial number of the device
	 * @param {number|string} key Index of the key (0-255)
	 * @returns {Promise<?Button>} Button object, null if device or button wasn't found
	 */
	get_button(serial_number, key) {
		return this.protocol.request(
			{
				ty: "get_button",
				data: {
					serial_number,
					key: parseInt(key)
				}
			}
		).then(data => {
			if (data.Button) {
				return data.Button;
			} else {
				return null;
			}
		})
	}

	/**
	 * Sets button on current screen for specified device, commit the change later with commit_changes in order for this to get saved
	 * @param {string} serial_number Serial number of the device
	 * @param {number|string} key Index of the key (0-255)
	 * @param {Button} button Button object
	 * @returns {Promise<"NoScreen"|"DeviceNotFound"|"Set">} Result of the operation
	 */
	set_button(serial_number, key, button) {
		return this.protocol.request(
			{
				ty: "set_button",
				data: {
					serial_number,
					key: parseInt(key),
					button
				}
			}
		)
	}

	/**
	 * Clears button from current screen for specified device, commit the change later with commit_changes in order for this to get saved
	 * @param {string} serial_number Serial number of the device
	 * @param {number|string} key Index of the key (0-255)
	 * @returns {Promise<"DeviceNotFound"|"FailedToClear"|"Cleared">} Result of the operation
	 */
	clear_button(serial_number, key) {
		return this.protocol.request(
			{
				ty: "clear_button",
				data: {
					serial_number,
					key: parseInt(key)
				}
			}
		)
	}

	/**
	 * Retrieves clipboard status of daemon
	 * @returns {Promise<"Empty"|"Full">} Clipboard status, it's either empty or full
	 */
	clipboard_status() {
		return this.protocol.request(
			{
				ty: "clipboard_status"
			}
		)
	}

	/**
	 * Copies button into daemon's clipboard
	 * @param {string} serial_number Serial number of the device
	 * @param {number|string} key Index of the key (0-255)
	 * @returns {Promise<"DeviceNotFound"|"NoButton"|"Copied">} Result of the operation
	 */
	copy_button(serial_number, key) {
		return this.protocol.request(
			{
				ty: "copy_button",
				data: {
					serial_number,
					key: parseInt(key)
				}
			}
		)
	}

	/**
	 * Pastes button from daemon's clipboard
	 * @param {string} serial_number Serial number of the device
	 * @param {number|string} key Index of the key (0-255)
	 * @returns {Promise<"DeviceNotFound"|"FailedToPaste"|"Pasted">} Result of the operation
	 */
	paste_button(serial_number, key) {
		return this.protocol.request(
			{
				ty: "paste_button",
				data: {
					serial_number,
					key: parseInt(key)
				}
			}
		)
	}

	/**
	 * Creates a new empty button, commit the change later with commit_changes in order for this to get saved
	 * @param {string} serial_number Serial number of the device
	 * @param {number|string} key Index of the key (0-255)
	 * @returns {Promise<"DeviceNotFound"|"FailedToCreate"|"Created">} Result of the operation
	 */
	new_button(serial_number, key) {
		return this.protocol.request(
			{
				ty: "new_button",
				data: {
					serial_number,
					key: parseInt(key)
				}
			}
		)
	}

	/**
	 * Creates a new button from component, commit the change later with commit_changes in order for this to get saved
	 * @param {string} serial_number Serial number of the device
	 * @param {number|string} key Index of the key (0-255)
	 * @param {string} component_name Component name
	 * @returns {Promise<"DeviceNotFound"|"ComponentNotFound"|"FailedToCreate"|"Created">} Result of the operation
	 */
	new_button_from_component(serial_number, key, component_name) {
		return this.protocol.request(
			{
				ty: "new_button_from_component",
				data: {
					serial_number,
					key: parseInt(key),
					component_name
				}
			}
		)
	}

	/**
	 * Adds component onto a button, commit the change later with commit_changes in order for this to get saved
	 * @param {string} serial_number Serial number of the device
	 * @param {number|string} key Index of the key (0-255)
	 * @param {string} component_name Component name
	 * @returns {Promise<"DeviceNotFound"|"FailedToAdd"|"Added">} Result of the operation
	 */
	add_component(serial_number, key, component_name) {
		return this.protocol.request(
			{
				ty: "add_component",
				data: {
					serial_number,
					key: parseInt(key),
					component_name
				}
			}
		)
	}

	/**
	 * Gets component values for component from a button
	 * @param {string} serial_number Serial number of the device
	 * @param {number|string} key Index of the key (0-255)
	 * @param {string} component_name Component name
	 * @returns {Promise<?Array.<Value>>} Component values, null if component or device wasn't found
	 */
	get_component_values(serial_number, key, component_name) {
		return this.protocol.request(
			{
				ty: "get_component_values",
				data: {
					serial_number,
					key: parseInt(key),
					component_name
				}
			}
		).then(data => {
			if (data.Values) {
				return data.Values;
			} else {
				return null;
			}
		})
	}

	/**
	 * Adds element to component value, commit the change later with commit_changes in order for this to get saved
	 * @param {string} serial_number Serial number of the device
	 * @param {number|string} key Index of the key (0-255)
	 * @param {string} component_name Component name
	 * @param {string} path Path to value
	 * @returns {Promise<"DeviceNotFound"|"FailedToAdd"|"Added">} Result of the operation
	 */
	add_component_value(serial_number, key, component_name, path) {
		return this.protocol.request(
			{
				ty: "add_component_value",
				data: {
					serial_number,
					key: parseInt(key),
					component_name,
					path
				}
			}
		)
	}

	/**
	 * Removes element from component value, commit the change later with commit_changes in order for this to get saved
	 * @param {string} serial_number Serial number of the device
	 * @param {number|string} key Index of the key (0-255)
	 * @param {string} component_name Component name
	 * @param {string} path Path to value
	 * @param {number|string} index Index of element
	 * @returns {Promise<"DeviceNotFound"|"FailedToRemove"|"Removed">} Result of the operation
	 */
	remove_component_value(serial_number, key, component_name, path, index) {
		return this.protocol.request(
			{
				ty: "remove_component_value",
				data: {
					serial_number,
					key: parseInt(key),
					component_name,
					path,
					index: parseInt(index)
				}
			}
		)
	}

	/**
	 * Sets component values for a component on a button, commit the change later with commit_changes in order for this to get saved
	 * @param {string} serial_number Serial number of the device
	 * @param {number|string} key Index of the key (0-255)
	 * @param {string} component_name Component name
	 * @param {Value} value Value to set
	 * @returns {Promise<"DeviceNotFound"|"FailedToSet"|"Set">} Result of the operation
	 */
	set_component_value(serial_number, key, component_name, value) {
		return this.protocol.request(
			{
				ty: "set_component_value",
				data: {
					serial_number,
					key: parseInt(key),
					component_name,
					value
				}
			},
			8640000000
		)
	}

	/**
	 * Removes component from a button, commit the change later with commit_changes in order for this to get saved
	 * @param {string} serial_number Serial number of the device
	 * @param {number|string} key Index of the key (0-255)
	 * @param {string} component_name Component name 
	 * @returns {Promise<"DeviceNotFound"|"FailedToRemove"|"Removed">} Result of the operation
	 */
	remove_component(serial_number, key, component_name) {
		return this.protocol.request(
			{
				ty: "remove_component",
				data: {
					serial_number,
					key: parseInt(key),
					component_name
				}
			}
		)
	}

	/**
	 * Pushes a new screen into device's screen stack
	 * @param {string} serial_number Serial number of the device
	 * @param {Panel} screen Screen object consisting of key indices and button objects
	 * @returns {Promise<"DeviceNotFound"|"Pushed">} Result of the operation
	 */
	push_screen(serial_number, screen) {
		return this.protocol.request(
			{
				ty: "push_screen",
				data: {
					serial_number,
					screen
				}
			}
		)
	}

	/**
	 * Pops a screen from device's screen stack, unless only one screen remaining
	 * @param {string} serial_number Serial number of the device
	 * @returns {Promise<"DeviceNotFound"|"OnlyOneRemaining"|"Popped">} Result of the operation
	 */
	pop_screen(serial_number) {
		return this.protocol.request(
			{
				ty: "pop_screen",
				data: {
					serial_number
				}
			}
		)
	}

	/**
	 * Pops a screen from device's screen stack, bypassing limit of pop_screen, use this only if you know what you're doing
	 * @param {string} serial_number Serial number of the device
	 * @returns {Promise<"DeviceNotFound"|"Popped">} Result of the operation
	 */
	force_pop_screen(serial_number) {
		return this.protocol.request(
			{
				ty: "force_pop_screen",
				data: {
					serial_number
				}
			}
		)
	}

	/**
	 * Replaces current screen with new one
	 * @param {string} serial_number Serial number of the device
	 * @param {Panel} screen Screen object consisting of key indices and button objects
	 * @returns {Promise<"DeviceNotFound"|"Replaced">} Result of the operation
	 */
	replace_screen(serial_number, screen) {
		return this.protocol.request(
			{
				ty: "replace_screen",
				data: {
					serial_number,
					screen
				}
			}
		)
	}

	/**
	 * Resets stack and pushes a screen
	 * @param {string} serial_number Serial number of the device
	 * @param {Panel} screen Screen object consisting of key indices and button objects
	 * @returns {Promise<"DeviceNotFound"|"Reset">} Result of the operation
	 */
	reset_stack(serial_number, screen) {
		return this.protocol.request(
			{
				ty: "reset_stack",
				data: {
					serial_number,
					screen
				}
			}
		)
	}

	/**
	 * Resets stack and pushes a screen
	 * @param {string} serial_number Serial number of the device
	 * @returns {Promise<"DeviceNotFound"|"Dropped">} Result of the operation
	 */
	drop_stack_to_root(serial_number) {
		return this.protocol.request(
			{
				ty: "drop_stack_to_root",
				data: {
					serial_number
				}
			}
		)
	}

	/**
	 * Commits changes made with screen related functions to device config, so they can be saved by save_device_config. Must be either called after each change, or sequence of changes
	 * @param {serial} serial_number 
	 * @returns {Promise<"DeviceNotFound"|"Committed">} Result of the operation
	 */
	commit_changes(serial_number) {
		return this.protocol.request(
			{
				ty: "commit_changes",
				data: {
					serial_number
				}
			}
		)
	}

	/**
	 * Simulates a key press on the device
	 * @param {string} serial_number Serial number of the device
	 * @param {number|string} key Index of the key (0-255)
	 * @returns {Promise<"DeviceNotFound"|"Activated">} Result of the operation
	 */
	do_button_action(serial_number, key) {
		return this.protocol.request(
			{
				ty: "do_button_action",
				data: {
					serial_number,
					key: parseInt(key)
				}
			}
		)
	}

	/**
	 * Destroys the client
	 */
	destroy() {
		this.protocol.destroy();
	}
}

function buildRequestProtocol(opts) {
	let protocol = {
		pool: {},
		event_listeners: []
	};

	/**
	 * Sends request to daemon
	 * @param data Data to send
	 * @param {number} [timeout] Timeout, defaults to options
	 * @returns {Promise<Object>} Response
	 */
	protocol.request = (data, timeout) => {
		let requester = randomstring.generate();
		data.requester = requester;

		if (opts.client) {
			opts.client.write(JSON.stringify(data) + "\u0004");

			return new Promise((resolve, reject) => {
				let timer = setTimeout(() => {
					reject("Request timed out")
				}, timeout || opts.timeout)

				protocol.pool[requester] = function (data) {
					clearTimeout(timer);
					resolve(data)
				};
			})
		} else {
			return new Promise((_, reject) => reject("Client not connected"));
		}
	}

	protocol.add_event_listener = (func) => {
		protocol.event_listeners.push(func);
	}

	protocol.connected = () => {
		if (opts.client != null) {
			return opts.client.readyState === "open";
		} else {
			return false;
		}
	}

	protocol.destroy = () => {
		opts.destroy();
	}

	return protocol;
}

/**
 * Initializes a new Unix Domain Socket based Streamduck client
 * @param {{timeout: number?, reconnect: boolean?}?} opts Options for client. timeout - Request timeout, default 5000; reconnect - Automatically reconnects to daemon, default true
 * @returns {StreamduckClient} Client that might still be connecting, check with is_connected method
 */
exports.newUnixClient = function (opts) {
	let timeout = opts && opts.timeout !== undefined ? opts.timeout : 5000;
	let reconnect = opts && opts.reconnect !== undefined ? opts.reconnect : true;

	let protocol_options = {
		collected_string: "",
		client: null,
		timeout: timeout
	};

	let protocol = buildRequestProtocol(protocol_options);
	let streamduck = new StreamduckClient(protocol);

	let rec = () => {
		protocol_options.client = net.createConnection("/tmp/streamduck.sock")
			.on('connect', async () => {
				let version = await streamduck.version();

				let client_version = "0.2";
				if (version !== client_version)
					console.log(`Daemon version doesn't match this client's version. Daemon is using ${version}, client is using ${client_version}`);

				console.log("Connected to Streamduck");
			})
			.on('data', data => {
				data = data.toString();
				protocol_options.collected_string += data;

				if (protocol_options.collected_string.includes("\u0004")) {
					protocol_options.collected_string.split("\u0004").forEach(json => {
						if (json) {
							try {
								let obj = JSON.parse(json);

								if (obj.ty === "event") {
									for(const listener of protocol.event_listeners) {
										listener(obj.data)
									}
								} else {
									let callback = protocol.pool[obj.requester];

									if (callback) {
										callback(obj.data);
									}

									delete protocol.pool[obj.requester];
								}
							} catch (e) {

							}
						}
					});

					protocol_options.collected_string = "";
				}
			})
			.on('error', _ => {
				if (protocol_options.client) {
					protocol_options.client.destroy()
				}
				protocol_options.client = null;
			})
			.on('close', _ => {
				if (protocol_options.client) {
					protocol_options.client.destroy()
				}
				protocol_options.client = null;
			});
	}

	rec();
	setInterval(() => {
		if (protocol_options.client == null && reconnect) {
			rec();
		}
	}, 2000);

	return streamduck;
}

/**
 * Initializes a new Windows Named Pipes based Streamduck client
 * @param {{timeout: number?, reconnect: boolean?, events: boolean?}?} opts Options for client. timeout - Request timeout, default 5000; reconnect - Automatically reconnects to daemon, default true
 * @returns {StreamduckClient} Client that might still be connecting, check with is_connected method
 */
exports.newWindowsClient = function (opts) {
	let timeout = opts && opts.timeout !== undefined ? opts.timeout : 5000;
	let reconnect = opts && opts.reconnect !== undefined ? opts.reconnect : true;

	let protocol_options = {
		collected_string_request: "",
		collected_string_event: "",
		client: null,
		eventClient: null,
		timeout: timeout
	}

	let protocol = buildRequestProtocol(protocol_options);
	let streamduck = new StreamduckClient(protocol);

	let rec = () => {
		protocol_options.client = net.connect("\\\\.\\pipe\\streamduck", async () => {
			let version = await streamduck.version();

			let client_version = "0.2";
			if (version !== client_version)
				console.log(`Daemon version doesn't match this client's version. Daemon is using ${version}, client is using ${client_version}`);

			console.log("Connected to Streamduck");
		})
			.on('data', data => {
				data = data.toString();
				protocol_options.collected_string_request += data;

				if (protocol_options.collected_string_request.includes("\u0004")) {
					protocol_options.collected_string_request.split("\u0004").forEach(json => {
						if (json) {
							try {
								let obj = JSON.parse(json);

								if (obj.ty === "event") {
									for(const listener of protocol.event_listeners) {
										listener(obj.data)
									}
								} else {
									let callback = protocol.pool[obj.requester];

									if (callback) {
										callback(obj.data);
									}

									delete protocol.pool[obj.requester];
								}
							} catch (e) {

							}
						}
					});

					protocol_options.collected_string_request = "";
				}
			})
			.on('error', _ => {
				if (protocol_options.client) {
					protocol_options.client.destroy()
				}
				protocol_options.client = null;
			})
			.on('close', _ => {
				if (protocol_options.client) {
					protocol_options.client.destroy()
				}
				protocol_options.client = null;
			});
	}

	rec();
	setInterval(() => {
		if (protocol_options.client == null && reconnect) {
			rec();
		}
	}, 2000);

	return streamduck;
}