import { filter } from 'lodash'
import { cursorToOffset } from 'graphql-relay'
import { wrap, cursorToObj, maybeQuote } from '../util'
import idx from 'idx'

export function joinPrefix(prefix) {
  return prefix.slice(1).map(name => name + '__').join('')
}

export function generateCastExpressionFromValueType(key, val) {
  const castTypes = {
    string: 'TEXT'
  }
  const type = castTypes[typeof val] || null

  if (type) {
    return `CAST(${key} AS ${type})`
  }
  return key
}

function doubleQuote(str) {
  return `"${str}"`
}

export function thisIsNotTheEndOfThisBatch(node, parent) {
  return (!node.sqlBatch && !(idx(node, _ => _.junction.sqlBatch))) || !parent
}

export function whereConditionIsntSupposedToGoInsideSubqueryOrOnNextBatch(node, parent) {
  return !node.paginate && (!(node.sqlBatch || (idx(node, _ => _.junction.sqlBatch))) || !parent)
}

export function keysetPagingSelect(table, whereCondition, order, limit, as, options = {}) {
  let { joinCondition, joinType, extraJoin, q } = options
  q = q || doubleQuote
  whereCondition = filter(whereCondition).join(' AND ') || 'TRUE'
  if (joinCondition) {
    return `\
${joinType || ''} JOIN LATERAL (
  SELECT ${q(as)}.*
  FROM ${table} ${q(as)}
  ${extraJoin ? `LEFT JOIN ${extraJoin.name} ${q(extraJoin.as)}
    ON ${extraJoin.condition}` : ''}
  WHERE ${whereCondition}
  ORDER BY ${orderColumnsToString(order.columns, q, order.table)}
  LIMIT ${limit}
) ${q(as)} ON ${joinCondition}`
  }
  return `\
FROM (
  SELECT ${q(as)}.*
  FROM ${table} ${q(as)}
  WHERE ${whereCondition}
  ORDER BY ${orderColumnsToString(order.columns, q, order.table)}
  LIMIT ${limit}
) ${q(as)}`
}

export function offsetPagingSelect(table, pagingWhereConditions, order, limit, offset, as, options = {}) {
  let { joinCondition, joinType, extraJoin, q } = options
  q = q || doubleQuote
  const whereCondition = filter(pagingWhereConditions).join(' AND ') || 'TRUE'
  if (joinCondition) {
    return `\
${joinType || ''} JOIN LATERAL (
  SELECT ${q(as)}.*, count(*) OVER () AS ${q('$total')}
  FROM ${table} ${q(as)}
  ${extraJoin ? `LEFT JOIN ${extraJoin.name} ${q(extraJoin.as)}
    ON ${extraJoin.condition}` : ''}
  WHERE ${whereCondition}
  ORDER BY ${orderColumnsToString(order.columns, q, order.table)}
  LIMIT ${limit} OFFSET ${offset}
) ${q(as)} ON ${joinCondition}`
  }
  return `\
FROM (
  SELECT ${q(as)}.*, count(*) OVER () AS ${q('$total')}
  FROM ${table} ${q(as)}
  WHERE ${whereCondition}
  ORDER BY ${orderColumnsToString(order.columns, q, order.table)}
  LIMIT ${limit} OFFSET ${offset}
) ${q(as)}`
}

export function orderColumnsToString(orderColumns, q, as) {
  const conditions = []
  for (let column in orderColumns) {
    conditions.push(`${as ? q(as) + '.' : ''}${q(column)} ${orderColumns[column]}`)
  }
  return conditions.join(', ')
}

// find out what the limit, offset, order by parts should be from the relay connection args if we're paginating
export function interpretForOffsetPaging(node, dialect) {
  const { name } = dialect
  if (idx(node, _ => _.args.last)) {
    throw new Error('Backward pagination not supported with offsets. Consider using keyset pagination instead')
  }

  const order = {}
  if (node.orderBy) {
    order.table = node.as
    order.columns = node.orderBy
  } else {
    order.table = node.junction.as
    order.columns = node.junction.orderBy
  }

  let limit = [ 'mariadb', 'mysql', 'oracle' ].includes(name) ? '18446744073709551615' : 'ALL'
  let offset = 0
  if (idx(node, _ => _.args.first)) {
    limit = parseInt(node.args.first, 10)
    // we'll get one extra item (hence the +1). this is to determine if there is a next page or not
    if (node.paginate) {
      limit++
    }
    if (node.args.after) {
      offset = cursorToOffset(node.args.after) + 1
    }
  }
  return { limit, offset, order }
}

export function interpretForKeysetPaging(node, dialect) {
  const { name } = dialect

  let sortTable
  let sortKey
  let descending
  const order = { columns: {} }
  if (node.sortKey) {
    sortKey = node.sortKey
    descending = sortKey.order.toUpperCase() === 'DESC'
    sortTable = node.as
    // flip the sort order if doing backwards paging
    if (idx(node, _ => _.args.last)) {
      descending = !descending
    }
    for (let column of wrap(sortKey.key)) {
      order.columns[column] = descending ? 'DESC' : 'ASC'
    }
    order.table = node.as
  } else {
    sortKey = node.junction.sortKey
    descending = sortKey.order.toUpperCase() === 'DESC'
    sortTable = node.junction.as
    // flip the sort order if doing backwards paging
    if (idx(node, _ => _.args.last)) {
      descending = !descending
    }
    for (let column of wrap(sortKey.key)) {
      order.columns[column] = descending ? 'DESC' : 'ASC'
    }
    order.table = node.junction.as
  }

  let limit = [ 'mariadb', 'mysql', 'oracle' ].includes(name) ? '18446744073709551615' : 'ALL'
  let whereCondition = ''
  if (idx(node, _ => _.args.first)) {
    limit = parseInt(node.args.first, 10) + 1
    if (node.args.after) {
      const cursorObj = cursorToObj(node.args.after)
      validateCursor(cursorObj, wrap(sortKey.key))
      whereCondition = sortKeyToWhereCondition(cursorObj, descending, sortTable, dialect)
    }
    if (node.args.before) {
      throw new Error('Using "before" with "first" is nonsensical.')
    }
  } else if (idx(node, _ => _.args.last)) {
    limit = parseInt(node.args.last, 10) + 1
    if (node.args.before) {
      const cursorObj = cursorToObj(node.args.before)
      validateCursor(cursorObj, wrap(sortKey.key))
      whereCondition = sortKeyToWhereCondition(cursorObj, descending, sortTable, dialect)
    }
    if (node.args.after) {
      throw new Error('Using "after" with "last" is nonsensical.')
    }
  }

  return { limit, order, whereCondition }
}

// the cursor contains the sort keys. it needs to match the keys specified in the `sortKey` on this field in the schema
export function validateCursor(cursorObj, expectedKeys) {
  const actualKeys = Object.keys(cursorObj)
  const expectedKeySet = new Set(expectedKeys)
  const actualKeySet = new Set(actualKeys)
  for (let key of actualKeys) {
    if (!expectedKeySet.has(key)) {
      throw new Error(`Invalid cursor. The column "${key}" is not in the sort key.`)
    }
  }
  for (let key of expectedKeys) {
    if (!actualKeySet.has(key)) {
      throw new Error(`Invalid cursor. The column "${key}" is not in the cursor.`)
    }
  }
}

// take the sort key and translate that for the where clause
function sortKeyToWhereCondition(keyObj, descending, sortTable, dialect) {
  const { name, quote: q } = dialect
  const sortColumns = []
  const sortValues = []
  for (let key in keyObj) {
    sortColumns.push(`${q(sortTable)}.${q(key)}`)
    sortValues.push(maybeQuote(keyObj[key], name))
  }
  const operator = descending ? '<' : '>'
  return name === 'oracle'
    ? recursiveWhereJoin(sortColumns, sortValues, operator)
    : `(${sortColumns.join(', ')}) ${operator} (${sortValues.join(', ')})`
}

function recursiveWhereJoin(columns, values, op) {
  const condition = `${columns.pop()} ${op} ${values.pop()}`
  return _recursiveWhereJoin(columns, values, op, condition)
}

function _recursiveWhereJoin(columns, values, op, condition) {
  if (!columns.length) {
    return condition
  }
  const column = columns.pop()
  const value = values.pop()
  condition = `(${column} ${op} ${value} OR (${column} = ${value} AND ${condition}))`
  return _recursiveWhereJoin(columns, values, op, condition)
}


/* globals React: false */

var Reflux = require('reflux-core');

/**
 * Reflux.defineReact function where you can manually supply
 * the React object in order to create in case Reflux needs to load before
 * React or there is a modular environment where there won't be a global
 * React variable.
 * @note The third param is for internal usage only.
 */
var _react, _defined = false;
function defineReact(react, noLongerUsed, extend)
{
	var proto, _extend;

	// if no Reflux object is yet available then return and just wait until defineReact is called manually with it
	try {
		_react  = react  || _react  || React;
		_extend = extend || _react.Component;
	} catch (e) {
		return;
	}

	// if Reflux and React aren't present then ignore, wait until they are properly present
	// also ignore if it's been called before UNLESS there's manual extending happening
	if (!_react || !_extend || (_defined && !extend)) {
		return;
	}

	// ----------- BEGIN Reflux.Component ------------
	/**
	 * Reflux.Component:
	 * An implementation for idiomatic React.js classes that mix with
	 * Reflux stores. To utilize extend Reflux.Component instead of
	 * React.Component. Then you may hook any Reflux store that has a
	 * `this.state` property containing its state values to the component
	 * via `this.store` or an Array of Reflux stores via `this.stores` in
	 * the component's constructor (similar to how you assign initial state
	 * in the constructor in ES6 style React). The default values of the
	 * stores will automatically reflect in the component's state, and any
	 * further `trigger` calls from that store will update properties passed
	 * in the trigger into the component automatically.
	 */
	var RefluxComponent = function(props, context, updater) {
		_extend.call(this, props, context, updater);
	};

	// equivalent of `extends React.Component` or other class if provided via `extend` param
	Reflux.utils.inherits(RefluxComponent, _extend);

	proto = RefluxComponent.prototype;

	/**
	 * this.storeKeys
	 * When this is a falsey value (null by default) the component mixes in
	 * all properties from the stores attached to it and updates on changes
	 * from all of them. When set to an array of string keys it will only
	 * utilized state property names of those keys in any store attached. This
	 * lets you choose which parts of stores update the component on a component-
	 * by-component basis. If using this it is best set in the constructor.
	 */
	proto.storeKeys = null;

	// on the mounting of the component that is where the store/stores are attached and initialized if needed
	proto.componentWillMount = function () {
		// if there is a this.store then simply push it onto the this.stores array or make one if needed
		if (this.store) {
			if (Array.isArray(this.stores)) {
				this.stores.unshift(this.store);
			} else {
				this.stores = [this.store];
			}
		}

		if (this.stores) {
			this.__storeunsubscribes__ = this.__storeunsubscribes__ || [];
			var sS = this.setState.bind(this);
			// this handles the triggering of a store, checking what's updated if proto.storeKeys is utilized
			var onStoreTrigger = function(obj){
				var updateObj = filterByStoreKeys(this.storeKeys, obj);
				if (updateObj) {
					sS(updateObj);
				}
			}.bind(this);
			// for each store in this.stores...
			for (var i = 0, ii = this.stores.length; i < ii; i++) {
				var str = this.stores[i];
				// if's a function then we know it's a class getting passed, not an instance
				if (typeof str === 'function') {
					var storeId = str.id;
					// if there is NOT a .singleton property on the store then this store has not been initialized yet, so do so
					if (!str.singleton) {
						str.singleton = new str();
						if (storeId) {
							Reflux.stores[storeId] = str.singleton;
						}
					}
					// before we weren't sure if we were working with an instance or class, so now we know an instance is created set it
					// to the variables we were using so that we can just continue on knowing it's the instance we're working with
					this.stores[i] = str = str.singleton;
					// the instance should have an .id property as well if the class does, so set that here
					str.id = storeId;
					// if there is an id and there is a global state property for this store then merge
					// the properties from that global state into the default state of the store AND then
					// set the global state to that new state (since it may have previously been partial)
					if (storeId && Reflux.GlobalState[storeId]) {
						for (var key in Reflux.GlobalState[storeId]) {
							str.state[key] = Reflux.GlobalState[storeId][key];
						}
						Reflux.GlobalState[storeId] = str.state;
					// otherwise (if it has an id) set the global state to the default state of the store
					} else if (storeId) {
						Reflux.GlobalState[storeId] = str.state;
					}
					// if no id, then no messing with global state
				}
				// listen/subscribe for the ".trigger()" in the store, and track the unsubscribes so that we can unsubscribe on unmount
				if (!Reflux.serverMode) {
					this.__storeunsubscribes__.push(str.listen(onStoreTrigger));
				}
				// run set state so that it mixes in the props from the store with the component
				var updateObj = filterByStoreKeys(this.storeKeys, str.state);
				if (updateObj) {
					this.setState(updateObj);
				}
			}
		}

		// mapStoreToState needs to know if is ready to map or must wait
		this.__readytomap__ = true;
		// if there are mappings that were delayed, do them now
		var dmaps = this.__delayedmaps__;
		if (dmaps) {
			for (var j=0,jj=dmaps.length; j<jj; j++) {
				dmaps[j].func( dmaps[j].state );
			}
		}
		this.__delayedmaps__ = null;
	};

	// on the unmount phase of the component unsubscribe that which we subscribed earlier to keep our garbage trail clean
	proto.componentWillUnmount = function () {
		if (this.__storeunsubscribes__) {
			for (var i = 0, ii = this.__storeunsubscribes__.length; i < ii; i++) {
				this.__storeunsubscribes__[i]();
			}
		}
		this.__readytomap__ = false;
	};

	/**
	 * this.mapStoreToState
	 * This function allow you to supply map the state of a store to the
	 * state of this component manually via your own logic. This method
	 * is completely separate from this.store/this.stores and/or this.storeKeys.
	 * Call this function with an ES6 store (class or singleton instance) as the
	 * first argument and your filter function as the second. Your filter function
	 * will receive an object of the parts of the ES6 store being updated every
	 * time its setState is called. Your filter function then returns an object
	 * which will be merged with the component state (IF it has any properties at all,
	 * should you return a blank object the component will not rerender).
	 */
	proto.mapStoreToState = function(store, filterFunc)
	{
		// make sure we have a proper singleton instance to work with
		if (typeof store === 'function') {
			if (store.singleton) {
				store = store.singleton;
			} else {
				store = Reflux.initStore(store);
			}
		}

		// we need a closure so that the called function can remember the proper filter function to use, so function gets defined here
		var self = this;
		function onMapStoreTrigger(obj) {
			// get an object
			var update = filterFunc.call(self, obj);
			// if no object returned from filter functions do nothing
			if (!update) {
				return;
			}
			// check if the update actually has any mapped props
			/*jshint unused: false */
			var hasProps = false;
			for (var check in update) {
				hasProps = true;
				break;
			}
			// if there were props mapped, then update via setState
			if (hasProps) {
				self.setState(update);
			}
		}

		// add the listener to know when the store is triggered
		this.__storeunsubscribes__ = this.__storeunsubscribes__ || [];
		this.__storeunsubscribes__.push(store.listen(onMapStoreTrigger));

		// now actually run onMapStoreTrigger with the full store state so that we immediately have all store state mapped to component state
		if (this.__readytomap__) {
			onMapStoreTrigger(store.state);
		} else {
			this.__delayedmaps__ = this.__delayedmaps__ || [];
			this.__delayedmaps__.push({func:onMapStoreTrigger, state:store.state});
		}
	};

	/**
	 * Reflux.Component.extend(OtherClass)
	 * This allows you to get classes that extend off of another React.Component
	 * inheriting class. For example if you're using a third party that uses
	 * components that allow `class MyComponent extends LibComponent` (where LibComponent
	 * itself extends React.Component) and you want to use that component with ES6 then
	 * you can make a class `var MyDualComponent = Reflux.Component.extend(LibComponent);`
	 * then you can use `class MyComponent extends MyDualComponent` to get the benefits
	 * of both libraries.
	 */
	RefluxComponent.extend = function(clss) {
		return defineReact(null, null, clss);
	};

	// if is being manually called with an `extend` argument present then just return the created class
	if (extend) {
		return RefluxComponent;
	}

	// otherwise set as Reflux.Component and continue with other normal definitions
	Reflux.Component = RefluxComponent;

	// also set Reflux.PureComponent (if it exists) using the .extend feature
	if (_react.PureComponent) {
		Reflux.PureComponent = RefluxComponent.extend(_react.PureComponent);
	}

	// ------------ END Reflux.Component ------------

	// --------- BEGIN Reflux.Store ------------
	/**
	 * Reflux.Store:
	 * Also implements optional Reflux.Store class that is idiomatic with
	 * the React ES6 style. You extend Reflux.Store and then the rest works
	 * the same as createStore, except the constructor instead of init, and
	 * it holds state in a state property, and a .setState method is available
	 * which automatically updates state and does a trigger. Then when using
	 * with this.store or this.stores in an ES6 component just plass the class,
	 * it will deal with a singleton instantiation of the class automatically.
	 */
	var RefluxStore = function() {
		// extending doesn't really work well here, so instead we create an internal instance
		// and just loop through its properties/methods and make a getter/setter for each
		// that will actually be getting and setting on that internal instance.
		this.__store__ = Reflux.createStore();
		this.state = {};
		var self = this;
		for (var key in this.__store__) {
			/*jshint loopfunc: true */
			(function (prop) {
				Object.defineProperty(self, prop, {
					get: function () { return self.__store__[prop]; },
					set: function (v) { self.__store__[prop] = v; }
				});
			})(key);
		}
	};

	proto = RefluxStore.prototype;

	// this defines the listenables property, mostly intended to be set as `this.listenables` in the constructor of the store
	// it is essentially a shortcut to the `listenToMany` method
	Object.defineProperty(proto, "listenables", {
		get: function () {
			return this.__listenables__;
		},
		set: function (v) {
			var Combined = {};
			if (Array.isArray(v)){
				v.forEach(function(obj) {
					for (var key in obj) {
						Combined[key] = obj[key];
					}
				});
			} else {
				Combined = v;
			}
			this.__listenables__ = Combined;
			this.listenToMany(Combined);
		},
		enumerable: true,
		configurable: true
	});

	// allows simple usage of `this.setState(obj)` within the store to both update the state and trigger the store to update
	// components that it is attached to in a simple way that is idiomatic with React
	proto.setState = function (obj) {
		// Object.assign(this.state, obj); // later turn this to Object.assign and remove loop once support is good enough
		for (var key in obj) {
			this.state[key] = obj[key];
		}
		// if there's an id (i.e. it's being tracked by the global state) then make sure to update the global state
		if (this.id) {
			Reflux.GlobalState[this.id] = this.state;
		}
		// trigger, because any component it's attached to is listening and will merge the store state into its own on a store trigger
		this.trigger(obj);
	};

	// this is a static property so that other code can identify that this is a Reflux.Store class
	// has issues specifically when using babel to transpile your ES6 stores for IE10 and below, not documented and shouldn't use yet
	Object.defineProperty(RefluxStore, "isES6Store", {
		get: function () {
			return true;
		},
		enumerable: true,
		configurable: true
	});

	// allows a shortcut for accessing MyStore.singleton.state as MyStore.state (since common usage makes a singleton)
	Object.defineProperty(RefluxStore, "state", {
		get: function () {
			if (!this.singleton) {
				throw new Error('Reflux.Store.state is inaccessible before the store has been initialized.');
			}
			return this.singleton.state;
		},
		enumerable: true,
		configurable: true
	});

	/* NOTE:
	If a Reflux.Store definition is given a static id property and used
	properly within a Reflux.Component or with Reflux.initStore then
	it will be added to the Reflux.GlobalState object which automatically tracks the
	current state of all such defined stores in the program. */

	Reflux.Store = RefluxStore;
	// ----------- END Reflux.Store -------------

	// --------- BEGIN Reflux Static Props/Methods ------------
	/**
	 * Reflux.GlobalState is where data is stored for any Reflux.Store that has a static id property. Each store's
	 * state will be on the Reflux.GlobalState object with the id as the key. So a store with the id "MyStore" and
	 * a state {"color":"red"} will end up with a Reflux.GlobalState of {"MyStore":{"color":"red"}}
	 * Reflux.GlobalState is an accessible part of the API. However, keep in mind that non-primitive properties you
	 * read off of it will continue to mutate and you can only manually mutate Reflux.GlobalState BEFORE any component
	 * mounting of components with ES6 stores. For more functionality look to Reflux.setGlobalState to change the global
	 * state at any point, and Reflux.getGlobalState to return a deep clone of the Reflux.GlobalState object which will
	 * not continue to mutate as Reflux.GlobalState continues to mutate.
	 */
	Reflux.GlobalState = Reflux.GlobalState || {};

	/**
	 * Reflux.stores
	 * All initialized stores that have an id will have a reference to their singleton stored here with the key being the id.
	 */
	Reflux.stores = {};

	/**
	 * Reflux.getGlobalState takes no arguments, and returns a deep clone of Reflux.GlobalState
	 * which will not continue to mutate as Reflux.GlobalState does. It can essentially store
	 * snapshots of the global state as the program goes for saving or for in-app time travel.
	 */
	Reflux.getGlobalState = function() {
		return clone(Reflux.GlobalState);
	};

	/**
	 * Reflux.setGlobalState takes one argument that is a representation of the a possible
	 * global state. It updates all stores in the program to represent data in that given state.
	 * This includes triggering those stores so that that state is represented in any Reflux.Component
	 * instances they are attached to. Partial states may be given to it, and only the represented
	 * stores/state values will be updated.
	 */
	Reflux.setGlobalState = function(obj) {
		for (var storeID in obj) {
			if (Reflux.stores[storeID]) {
				Reflux.stores[storeID].setState(obj[storeID]);
			} else {
				Reflux.GlobalState[storeID] = obj[storeID];
			}
		}
	};

	/**
	 * Reflux.initStore takes one argument (a class that extends Reflux.Store) and returns a singleton
	 * intance of that class. Its main functionality is to be able to mimic what happens to stores attached to
	 * this.store or this.stores during the mounting phase of a component without having to actually attach the
	 * store to a component in order to work properly with the global state.
	 */
	// Reflux.initializeGlobalStore is kept for backwards compatibility, but deprecated since the function is
	// now for more broad instantiation of globally stored AND non-globally stored classes
	Reflux.initializeGlobalStore = Reflux.initStore = function(str) {
		var storeId = str.id;
		// if they're initializing something twice then we're done already, return it
		if (str.singleton) {
			return str.singleton;
		}
		// if no id then it's easy: just make new instance and set to singleton
		if (!storeId) {
			str.singleton = new str();
			return str.singleton;
		}
		// create the singleton and assign it to the class's singleton static property
		var inst = str.singleton = new str();
		// store it on the Reflux.stores array to be accessible later
		Reflux.stores[storeId] = inst;
		// the singleton instance itself should also have the id property of the class
		inst.id = storeId;
		// if the global state has something set for this id, copy it to the state and then
		// make sure to set the global state to the end result, since it may have only been partial
		if (Reflux.GlobalState[storeId]) {
			for (var key in Reflux.GlobalState[storeId]) {
				inst.state[key] = Reflux.GlobalState[storeId][key];
			}
			Reflux.GlobalState[storeId] = inst.state;
		// otherwise just set the global state to the default state of the class
		} else {
			Reflux.GlobalState[storeId] = inst.state;
		}
		// returns the singleton itself, though it will also be accessible as as `MyClass.singleton`
		return inst;
	};
	// --------- END Reflux Static Props/Methods ------------

	// so it knows not to redefine Reflux static stuff and stores if called again
	_defined = true;
}

// filters a state object by storeKeys array (if it exists)
// if filtering and obj contains no properties to use, returns false to let the component know not to update
function filterByStoreKeys(storeKeys, obj)
{
	// if there are not storeKeys defined then simply return the whole original object
	if (!storeKeys) {
		return obj;
	}
	// otherwise go through and only update properties that are in the storeKeys array, and return straight false if there are none
	var doUpdate = false;
	var updateObj = {};
	for (var i = 0, ii = storeKeys.length; i < ii; i++) {
		var prop = storeKeys[i];
		if (obj.hasOwnProperty(prop)) {
			doUpdate = true;
			updateObj[prop] = obj[prop];
		}
	}
	return doUpdate ? updateObj : false;
}

// this is utilized by some of the global state functionality in order to get a clone that will
// not continue to be modified as the GlobalState mutates
function clone(frm, to) {
	if (frm === null || typeof frm !== "object") {
		return frm;
	}
	if (frm.constructor !== Object && frm.constructor !== Array) {
		return frm;
	}
	if (frm.constructor === Date || frm.constructor === RegExp || frm.constructor === Function ||
		frm.constructor === String || frm.constructor === Number || frm.constructor === Boolean) {
		return new frm.constructor(frm);
	}
	to = to || new frm.constructor();
	for (var name in frm) {
		to[name] = typeof to[name] === "undefined" ? clone(frm[name], null) : to[name];
	}
	return to;
}

module.exports = defineReact;


var ListenerMethods = require('reflux-core/lib/ListenerMethods');

/**
 * A mixin factory for a React component. Meant as a more convenient way of using the `ListenerMixin`,
 * without having to manually set listeners in the `componentDidMount` method.
 *
 * @param {Action|Store} listenable An Action or Store that should be
 *  listened to.
 * @param {Function|String} callback The callback to register as event handler
 * @param {Function|String} defaultCallback The callback to register as default handler
 * @returns {Object} An object to be used as a mixin, which sets up the listener for the given listenable.
 */
module.exports = function(listenable,callback,initial){
    return {
        /**
         * Set up the mixin before the initial rendering occurs. Import methods from `ListenerMethods`
         * and then make the call to `listenTo` with the arguments provided to the factory function
         */
        componentDidMount: function() {
            for(var m in ListenerMethods){
                if (this[m] !== ListenerMethods[m]){
                    if (this[m]){
                        throw "Can't have other property '"+m+"' when using Reflux.listenTo!";
                    }
                    this[m] = ListenerMethods[m];
                }
            }
            this.listenTo(listenable,callback,initial);
        },
        /**
         * Cleans up all listener previously registered.
         */
        componentWillUnmount: ListenerMethods.stopListeningToAll
    };
};


var ListenerMethods = require('reflux-core/lib/ListenerMethods');

/**
 * A mixin factory for a React component. Meant as a more convenient way of using the `listenerMixin`,
 * without having to manually set listeners in the `componentDidMount` method. This version is used
 * to automatically set up a `listenToMany` call.
 *
 * @param {Object} listenables An object of listenables
 * @returns {Object} An object to be used as a mixin, which sets up the listeners for the given listenables.
 */
module.exports = function(listenables){
    return {
        /**
         * Set up the mixin before the initial rendering occurs. Import methods from `ListenerMethods`
         * and then make the call to `listenTo` with the arguments provided to the factory function
         */
        componentDidMount: function() {
            for(var m in ListenerMethods){
                if (this[m] !== ListenerMethods[m]){
                    if (this[m]){
                        throw "Can't have other property '"+m+"' when using Reflux.listenToMany!";
                    }
                    this[m] = ListenerMethods[m];
                }
            }
            this.listenToMany(listenables);
        },
        /**
         * Cleans up all listener previously registered.
         */
        componentWillUnmount: ListenerMethods.stopListeningToAll
    };
};


'use strict'

var debug = require('debug')('httpsnippet')
var es = require('event-stream')
var MultiPartForm = require('form-data')
var qs = require('querystring')
var reducer = require('./helpers/reducer')
var targets = require('./targets')
var url = require('url')
var validate = require('har-validator/lib/async')

// constructor
var HTTPSnippet = function (data) {
  var entries
  var self = this
  var input = Object.assign({}, data)

  // prep the main container
  self.requests = []

  // is it har?
  if (input.log && input.log.entries) {
    entries = input.log.entries
  } else {
    entries = [{
      request: input
    }]
  }

  entries.forEach(function (entry) {
    // add optional properties to make validation successful
    entry.request.httpVersion = entry.request.httpVersion || 'HTTP/1.1'
    entry.request.queryString = entry.request.queryString || []
    entry.request.headers = entry.request.headers || []
    entry.request.cookies = entry.request.cookies || []
    entry.request.postData = entry.request.postData || {}
    entry.request.postData.mimeType = entry.request.postData.mimeType || 'application/octet-stream'

    entry.request.bodySize = 0
    entry.request.headersSize = 0
    entry.request.postData.size = 0

    validate.request(entry.request, function (err, valid) {
      if (!valid) {
        throw err
      }

      self.requests.push(self.prepare(entry.request))
    })
  })
}

HTTPSnippet.prototype.prepare = function (request) {
  // construct utility properties
  request.queryObj = {}
  request.headersObj = {}
  request.cookiesObj = {}
  request.allHeaders = {}
  request.postData.jsonObj = false
  request.postData.paramsObj = false

  // construct query objects
  if (request.queryString && request.queryString.length) {
    debug('queryString found, constructing queryString pair map')

    request.queryObj = request.queryString.reduce(reducer, {})
  }

  // construct headers objects
  if (request.headers && request.headers.length) {
    // loweCase header keys
    request.headersObj = request.headers.reduce(function (headers, header) {
      headers[header.name.toLowerCase()] = header.value
      return headers
    }, {})
  }

  // construct headers objects
  if (request.cookies && request.cookies.length) {
    request.cookiesObj = request.cookies.reduceRight(function (cookies, cookie) {
      cookies[cookie.name] = cookie.value
      return cookies
    }, {})
  }

  // construct Cookie header
  var cookies = request.cookies.map(function (cookie) {
    return encodeURIComponent(cookie.name) + '=' + encodeURIComponent(cookie.value)
  })

  if (cookies.length) {
    request.allHeaders.cookie = cookies.join('; ')
  }

  switch (request.postData.mimeType) {
    case 'multipart/mixed':
    case 'multipart/related':
    case 'multipart/form-data':
    case 'multipart/alternative':
      // reset values
      request.postData.text = ''
      request.postData.mimeType = 'multipart/form-data'

      if (request.postData.params) {
        var form = new MultiPartForm()

        // easter egg
        form._boundary = '---011000010111000001101001'

        request.postData.params.forEach(function (param) {
          form.append(param.name, param.value || '', {
            filename: param.fileName || null,
            contentType: param.contentType || null
          })
        })

        form.pipe(es.map(function (data, cb) {
          request.postData.text += data
        }))

        request.postData.boundary = form.getBoundary()
        request.headersObj['content-type'] = 'multipart/form-data; boundary=' + form.getBoundary()
      }
      break

    case 'application/x-www-form-urlencoded':
      if (!request.postData.params) {
        request.postData.text = ''
      } else {
        request.postData.paramsObj = request.postData.params.reduce(reducer, {})

        // always overwrite
        request.postData.text = qs.stringify(request.postData.paramsObj)
      }
      break

    case 'text/json':
    case 'text/x-json':
    case 'application/json':
    case 'application/x-json':
      request.postData.mimeType = 'application/json'

      if (request.postData.text) {
        try {
          request.postData.jsonObj = JSON.parse(request.postData.text)
        } catch (e) {
          debug(e)

          // force back to text/plain
          // if headers have proper content-type value, then this should also work
          request.postData.mimeType = 'text/plain'
        }
      }
      break
  }

  // create allHeaders object
  request.allHeaders = Object.assign(request.allHeaders, request.headersObj)

  // deconstruct the uri
  request.uriObj = url.parse(request.url, true, true)

  // merge all possible queryString values
  request.queryObj = Object.assign(request.queryObj, request.uriObj.query)

  // reset uriObj values for a clean url
  request.uriObj.query = null
  request.uriObj.search = null
  request.uriObj.path = request.uriObj.pathname

  // keep the base url clean of queryString
  request.url = url.format(request.uriObj)

  // update the uri object
  request.uriObj.query = request.queryObj
  request.uriObj.search = qs.stringify(request.queryObj)

  if (request.uriObj.search) {
    request.uriObj.path = request.uriObj.pathname + '?' + request.uriObj.search
  }

  // construct a full url
  request.fullUrl = url.format(request.uriObj)

  return request
}

HTTPSnippet.prototype.convert = function (target, client, opts) {
  if (!opts && client) {
    opts = client
  }

  var func = this._matchTarget(target, client)

  if (func) {
    var results = this.requests.map(function (request) {
      return func(request, opts)
    })

    return results.length === 1 ? results[0] : results
  }

  return false
}

HTTPSnippet.prototype._matchTarget = function (target, client) {
  // does it exist?
  if (!targets.hasOwnProperty(target)) {
    return false
  }

  // shorthand
  if (typeof client === 'string' && typeof targets[target][client] === 'function') {
    return targets[target][client]
  }

  // default target
  return targets[target][targets[target].info.default]
}

// exports
module.exports = HTTPSnippet

module.exports.availableTargets = function () {
  return Object.keys(targets).map(function (key) {
    var target = Object.assign({}, targets[key].info)
    var clients = Object.keys(targets[key])

      .filter(function (prop) {
        return !~['info', 'index'].indexOf(prop)
      })

      .map(function (client) {
        return targets[key][client].info
      })

    if (clients.length) {
      target.clients = clients
    }

    return target
  })
}

module.exports.extname = function (target) {
  return targets[target] ? targets[target].info.extname : ''
}


'use strict'

var util = require('util')

module.exports = {
  /**
   * Use 'strong quoting' using single quotes so that we only need
   * to deal with nested single quote characters.
   * http://wiki.bash-hackers.org/syntax/quoting#strong_quoting
   */
  quote: function (value) {
    var safe = /^[a-z0-9-_/.@%^=:]+$/i

    // Unless `value` is a simple shell-safe string, quote it.
    if (!safe.test(value)) {
      return util.format('\'%s\'', value.replace(/'/g, "'\\''"))
    }

    return value
  },

  escape: function (value) {
    return value.replace(/\r/g, '\\r').replace(/\n/g, '\\n')
  }
}


'use strict'

var util = require('util')

/**
 * Create an string of given length filled with blank spaces
 *
 * @param {number} length Length of the array to return
 * @return {string}
 */
function buildString (length, str) {
  return Array.apply(null, new Array(length)).map(String.prototype.valueOf, str).join('')
}

/**
 * Create a string corresponding to a Dictionary or Array literal representation with pretty option
 * and indentation.
 */
function concatArray (arr, pretty, indentation, indentLevel) {
  var currentIndent = buildString(indentLevel, indentation)
  var closingBraceIndent = buildString(indentLevel - 1, indentation)
  var join = pretty ? ',\n' + currentIndent : ', '

  if (pretty) {
    return '[\n' + currentIndent + arr.join(join) + '\n' + closingBraceIndent + ']'
  } else {
    return '[' + arr.join(join) + ']'
  }
}

module.exports = {
  /**
   * Create a string corresponding to a valid declaration and initialization of a Swift array or dictionary literal
   *
   * @param {string} name Desired name of the instance
   * @param {Object} parameters Key-value object of parameters to translate to a Swift object litearal
   * @param {Object} opts Target options
   * @return {string}
   */
  literalDeclaration: function (name, parameters, opts) {
    return util.format('let %s = %s', name, this.literalRepresentation(parameters, opts))
  },

  /**
   * Create a valid Swift string of a literal value according to its type.
   *
   * @param {*} value Any JavaScript literal
   * @param {Object} opts Target options
   * @return {string}
   */
  literalRepresentation: function (value, opts, indentLevel) {
    indentLevel = indentLevel === undefined ? 1 : indentLevel + 1

    switch (Object.prototype.toString.call(value)) {
      case '[object Number]':
        return value
      case '[object Array]':
        // Don't prettify arrays nto not take too much space
        var pretty = false
        var valuesRepresentation = value.map(function (v) {
          // Switch to prettify if the value is a dictionary with multiple keys
          if (Object.prototype.toString.call(v) === '[object Object]') {
            pretty = Object.keys(v).length > 1
          }
          return this.literalRepresentation(v, opts, indentLevel)
        }.bind(this))
        return concatArray(valuesRepresentation, pretty, opts.indent, indentLevel)
      case '[object Object]':
        var keyValuePairs = []
        for (var k in value) {
          keyValuePairs.push(util.format('"%s": %s', k, this.literalRepresentation(value[k], opts, indentLevel)))
        }
        return concatArray(keyValuePairs, opts.pretty && keyValuePairs.length > 1, opts.indent, indentLevel)
      case '[object Boolean]':
        return value.toString()
      default:
        if (value === null || value === undefined) {
          return ''
        }
        return '"' + value.toString().replace(/"/g, '\\"') + '"'
    }
  }
}


'use strict'

const fp = require('fastify-plugin')
const Busboy = require('busboy')
const kMultipart = Symbol('multipart')
const eos = require('end-of-stream')
const deepmerge = require('deepmerge')

function setMultipart (req, done) {
  // nothing to do, it will be done by the Request.multipart object
  req[kMultipart] = true
  done()
}

function attachToBody (options, req, reply, next) {
  if (req.raw[kMultipart] !== true) {
    next()
    return
  }

  const consumerStream = options.onFile || defaultConsumer
  const body = { }
  const mp = req.multipart((field, file, filename, encoding, mimetype) => {
    body[field] = {
      data: [],
      filename,
      encoding,
      mimetype,
      limit: false
    }

    const result = consumerStream(field, file, filename, encoding, mimetype, body)
    if (result && typeof result.then === 'function') {
      result.catch((err) => {
        // continue with the workflow
        err.statusCode = 500
        file.destroy(err)
      })
    }
  }, function (err) {
    if (!err) {
      req.body = body
    }
    next(err)
  }, options)

  mp.on('field', (key, value) => {
    body[key] = value
  })
}

function defaultConsumer (field, file, filename, encoding, mimetype, body) {
  const fileData = []
  file.on('data', data => { fileData.push(data) })
  file.on('limit', () => { body[field].limit = true })
  file.on('end', () => {
    body[field].data = Buffer.concat(fileData)
  })
}

function fastifyMultipart (fastify, options, done) {
  if (options.addToBody === true) {
    if (typeof options.sharedSchemaId === 'string') {
      fastify.addSchema({
        $id: options.sharedSchemaId,
        type: 'object',
        properties: {
          encoding: { type: 'string' },
          filename: { type: 'string' },
          limit: { type: 'boolean' },
          mimetype: { type: 'string' }
        }
      })
    }

    fastify.addHook('preValidation', function (req, reply, next) {
      attachToBody(options, req, reply, next)
    })
  }
  fastify.addContentTypeParser('multipart', setMultipart)

  fastify.decorateRequest('multipart', multipart)
  fastify.decorateRequest('isMultipart', isMultipart)

  done()

  // handler definition is in multipart-readstream
  // handler(field, file, filename, encoding, mimetype)
  // opts is a per-request override for the options object
  function multipart (handler, done, opts) {
    if (typeof handler !== 'function') {
      throw new Error('handler must be a function')
    }

    if (typeof done !== 'function') {
      throw new Error('the callback must be a function')
    }

    if (!this.isMultipart()) {
      done(new Error('the request is not multipart'))
      return
    }

    const log = this.log

    log.debug('starting multipart parsing')

    const req = this.req

    const busboyOptions = deepmerge.all([{ headers: req.headers }, options || {}, opts || {}])
    const stream = new Busboy(busboyOptions)
    var completed = false
    var files = 0
    var count = 0
    var callDoneOnNextEos = false

    req.on('error', function (err) {
      stream.destroy()
      if (!completed) {
        completed = true
        done(err)
      }
    })

    stream.on('finish', function () {
      log.debug('finished multipart parsing')
      if (!completed && count === files) {
        completed = true
        setImmediate(done)
      } else {
        callDoneOnNextEos = true
      }
    })

    stream.on('file', wrap)

    req.pipe(stream)

    function wrap (field, file, filename, encoding, mimetype) {
      log.debug({ field, filename, encoding, mimetype }, 'parsing part')
      files++
      eos(file, waitForFiles)
      handler(field, file, filename, encoding, mimetype)
    }

    function waitForFiles (err) {
      if (err) {
        completed = true
        done(err)
        return
      }

      if (completed) {
        return
      }

      ++count
      if (callDoneOnNextEos && count === files) {
        completed = true
        done()
      }
    }

    return stream
  }

  function isMultipart () {
    return this.req[kMultipart] || false
  }
}

module.exports = fp(fastifyMultipart, {
  fastify: '>= 0.39.0',
  name: 'fastify-multipart'
})


// Copyright 2018 Quip

// Shims for backwards-compatibility without bumping the min API version.
(function(global) {
    if (!("window" in global)) {
        return
    }
    var exports = module.exports = {};

    // For quiptext, introduced in 0.1.044
    var translationShim = function(text, placeholders) {
        if (text[text.length - 1] == "]" && text.lastIndexOf(" [") != -1) {
            // Remove translation comments
            text = text.substr(0, text.lastIndexOf(" ["));
        }
        var replaceAll = function(str, substr, replacement) {
            return str.replace(
                new RegExp(
                    substr.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, "\\$1"), "g"),
                replacement);
        }
        var localeReplace = function(text, placeholders) {
            for (var key in placeholders) {
                text = replaceAll(text, "%(" + key + ")s", placeholders[key]);
            }
            return text;
        };
        var reactLocaleReplace = function(text, placeholders) {
            var start;
            var expanded = [text];
            for (var key in placeholders) {
                start = expanded;
                expanded = [];
                for (var i = 0; i < start.length; i++) {
                    if (typeof start[i] == "string") {
                        var keyStr = "%(" + key + ")s";
                        var parts = start[i].split(keyStr);
                        var replaced = [];
                        for (var j = 0; j < parts.length - 1; j++) {
                            replaced.push(parts[j]);
                            replaced.push(placeholders[key]);
                        }
                        replaced.push(parts[parts.length - 1]);
                        replaced = replaced.filter(function (str) {
                            return str != "";
                        });
                        expanded.push.apply(expanded, replaced)
                    } else {
                        expanded.push(start[i]);
                    }
                }
            }
            return expanded;
        }
        if (placeholders) {
            var hasReactElements = false;
            for (var key in placeholders) {
                var val = placeholders[key];
                if (typeof val !== "string" && React.isValidElement(val)) {
                    hasReactElements = true;
                    break;
                }
            }
            return (hasReactElements ?
                reactLocaleReplace(text, placeholders) :
                localeReplace(text, placeholders));
        }
        return text;
    }

    // Shims
    if (!window["quiptext"]) {
        window.quiptext = translationShim;
    }

    // Translated color label, until API implements these by default.
    var COLORS_TO_LABEL = {
        "RED": quiptext("Red"),
        "ORANGE": quiptext("Orange"),
        "YELLOW": quiptext("Yellow"),
        "GREEN": quiptext("Green"),
        "BLUE": quiptext("Blue"),
        "VIOLET": quiptext("Violet"),
    };

    exports.localizedColorLabel = function(colorKey)  {
        // Manually localize the color labels until the Quip Client API
        // translates its own strings as well.
        if (colorKey in COLORS_TO_LABEL) {
            return COLORS_TO_LABEL[colorKey];
        }
        return colorKey.charAt(0).toUpperCase() + colorKey.slice(1).toLowerCase()
    }
})(self);


/*
 *
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 *
*/

/* global MediaError */

var argscheck = require('cordova/argscheck'),
    utils = require('cordova/utils');

var mediaObjects = {};

/**
 * This class provides access to the device media, interfaces to both sound and video
 *
 * @constructor
 * @param src                   The file name or url to play
 * @param successCallback       The callback to be called when the file is done playing or recording.
 *                                  successCallback()
 * @param errorCallback         The callback to be called if there is an error.
 *                                  errorCallback(int errorCode) - OPTIONAL
 * @param statusCallback        The callback to be called when media status has changed.
 *                                  statusCallback(int statusCode) - OPTIONAL
 */
var Media = function(src, successCallback, errorCallback, statusCallback) {
    argscheck.checkArgs('SFFF', 'Media', arguments);
    this.id = utils.createUUID();
    mediaObjects[this.id] = this;
    this.src = src;
    this.successCallback = successCallback;
    this.errorCallback = errorCallback;
    this.statusCallback = statusCallback;
    this._duration = -1;
    this._position = -1;

    try {
        this.node = createNode(this);
    } catch (err) {
        Media.onStatus(this.id, Media.MEDIA_ERROR, { code: MediaError.MEDIA_ERR_ABORTED });
    }
};

/**
 * Creates new Audio node and with necessary event listeners attached
 * @param  {Media} media Media object
 * @return {Audio}       Audio element
 */
function createNode (media) {
    var node = new Audio();

    node.onplay = function () {
        Media.onStatus(media.id, Media.MEDIA_STATE, Media.MEDIA_STARTING);
    };

    node.onplaying = function () {
        Media.onStatus(media.id, Media.MEDIA_STATE, Media.MEDIA_RUNNING);
    };

    node.ondurationchange = function (e) {
        Media.onStatus(media.id, Media.MEDIA_DURATION, e.target.duration || -1);
    };

    node.onerror = function (e) {
        // Due to media.spec.15 It should return MediaError for bad filename
        var err = e.target.error.code === MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED ?
            { code: MediaError.MEDIA_ERR_ABORTED } :
            e.target.error;

        Media.onStatus(media.id, Media.MEDIA_ERROR, err);
    };

    node.onended = function () {
        Media.onStatus(media.id, Media.MEDIA_STATE, Media.MEDIA_STOPPED);
    };

    if (media.src) {
        node.src = media.src;
    }

    return node;
}

// Media messages
Media.MEDIA_STATE = 1;
Media.MEDIA_DURATION = 2;
Media.MEDIA_POSITION = 3;
Media.MEDIA_ERROR = 9;

// Media states
Media.MEDIA_NONE = 0;
Media.MEDIA_STARTING = 1;
Media.MEDIA_RUNNING = 2;
Media.MEDIA_PAUSED = 3;
Media.MEDIA_STOPPED = 4;
Media.MEDIA_MSG = ["None", "Starting", "Running", "Paused", "Stopped"];

/**
 * Start or resume playing audio file.
 */
Media.prototype.play = function() {

    // if Media was released, then node will be null and we need to create it again
    if (!this.node) {
        try {
            this.node = createNode(this);
        } catch (err) {
            Media.onStatus(this.id, Media.MEDIA_ERROR, { code: MediaError.MEDIA_ERR_ABORTED });
        }
    }

    this.node.play();
};

/**
 * Stop playing audio file.
 */
Media.prototype.stop = function() {
    try {
        this.pause();
        this.seekTo(0);
        Media.onStatus(this.id, Media.MEDIA_STATE, Media.MEDIA_STOPPED);
    } catch (err) {
        Media.onStatus(this.id, Media.MEDIA_ERROR, err);
    }
};

/**
 * Seek or jump to a new time in the track..
 */
Media.prototype.seekTo = function(milliseconds) {
    try {
        this.node.currentTime = milliseconds / 1000;
    } catch (err) {
        Media.onStatus(this.id, Media.MEDIA_ERROR, err);
    }
};

/**
 * Pause playing audio file.
 */
Media.prototype.pause = function() {
    try {
        this.node.pause();
        Media.onStatus(this.id, Media.MEDIA_STATE, Media.MEDIA_PAUSED);
    } catch (err) {
        Media.onStatus(this.id, Media.MEDIA_ERROR, err);
    }};

/**
 * Get duration of an audio file.
 * The duration is only set for audio that is playing, paused or stopped.
 *
 * @return      duration or -1 if not known.
 */
Media.prototype.getDuration = function() {
    return this._duration;
};

/**
 * Get position of audio.
 */
Media.prototype.getCurrentPosition = function(success, fail) {
    try {
        var p = this.node.currentTime;
        Media.onStatus(this.id, Media.MEDIA_POSITION, p);
        success(p);
    } catch (err) {
        fail(err);
    }
};

/**
 * Start recording audio file.
 */
Media.prototype.startRecord = function() {
    Media.onStatus(this.id, Media.MEDIA_ERROR, "Not supported");
};

/**
 * Stop recording audio file.
 */
Media.prototype.stopRecord = function() {
    Media.onStatus(this.id, Media.MEDIA_ERROR, "Not supported");
};

/**
 * Pause recording audio file.
 */
Media.prototype.pauseRecord = function() {
    Media.onStatus(this.id, Media.MEDIA_ERROR, "Not supported");
};

/**
 * Returns the current amplitude of the current recording.
 */
Media.prototype.getCurrentAmplitude = function() {
    Media.onStatus(this.id, Media.MEDIA_ERROR, "Not supported");
};

/**
 * Resume recording an audio file.
 */
Media.prototype.resumeRecord = function() {
    Media.onStatus(this.id, Media.MEDIA_ERROR, "Not supported");
};

/**
 * Set rate of an autio file.
 */
Media.prototype.setRate = function() {
    Media.onStatus(this.id, Media.MEDIA_ERROR, "Not supported");
};

/**
 * Release the resources.
 */
Media.prototype.release = function() {
    try {
        delete this.node;
    } catch (err) {
        Media.onStatus(this.id, Media.MEDIA_ERROR, err);
    }};

/**
 * Adjust the volume.
 */
Media.prototype.setVolume = function(volume) {
    this.node.volume = volume;
};

/**
 * Audio has status update.
 * PRIVATE
 *
 * @param id            The media object id (string)
 * @param msgType       The 'type' of update this is
 * @param value         Use of value is determined by the msgType
 */
Media.onStatus = function(id, msgType, value) {

    var media = mediaObjects[id];

    if (media) {
        switch(msgType) {
            case Media.MEDIA_STATE :
                if (media.statusCallback) {
                    media.statusCallback(value);
                }
                if (value === Media.MEDIA_STOPPED) {
                    if (media.successCallback) {
                        media.successCallback();
                    }
                }
                break;
            case Media.MEDIA_DURATION :
                media._duration = value;
                break;
            case Media.MEDIA_ERROR :
                if (media.errorCallback) {
                    media.errorCallback(value);
                }
                break;
            case Media.MEDIA_POSITION :
                media._position = Number(value);
                break;
            default :
                if (console.error) {
                    console.error("Unhandled Media.onStatus :: " + msgType);
                }
                break;
        }
    } else if (console.error) {
        console.error("Received Media.onStatus callback for unknown media :: " + id);
    }
};

module.exports = Media;


/*
 *
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 *
*/

/*global Windows:true */

var Media = require('cordova-plugin-media.Media');
var MediaError = require('cordova-plugin-media.MediaError');

var recordedFile;
var tempFolderAppDataBasePath = 'ms-appdata:///temp/',
    localFolderAppDataBasePath = 'ms-appdata:///local/',
    tempFolderFullPath = Windows.Storage.ApplicationData.current.temporaryFolder.path,
    localFolderFullPath = Windows.Storage.ApplicationData.current.localFolder.path;

var PARAMETER_IS_INCORRECT = -2147024809;
var SUPPORTED_EXTENSIONS = ['.mp3', '.wma', '.wav', '.cda', '.adx', '.wm', '.m3u', '.wmx', '.m4a'];
var SUPPORTED_PREFIXES = ['http', 'https', 'rstp'];

var fsTypes = {
    PERSISTENT: 'PERSISTENT',
    TEMPORARY: 'TEMPORARY'
};

module.exports = {
    mediaCaptureMrg:null,

    // Initiates the audio file
    create:function(win, lose, args) {
        var id = args[0];

        var srcUri = processUri(args[1]);

        var createAudioNode = !!args[2];
        var thisM = Media.get(id);

        Media.prototype.node = null;

        var prefix = args[1].split(':').shift();
        var extension = srcUri.extension;
        if (thisM.node === null) {
            if (SUPPORTED_EXTENSIONS.indexOf(extension) === -1 && SUPPORTED_PREFIXES.indexOf(prefix) === -1) {
                if (lose) {
                    lose({ code: MediaError.MEDIA_ERR_ABORTED });
                }
                return false; // unable to create
            }

            // Don't create Audio object in case of record mode
            if (createAudioNode === true) {
                thisM.node = new Audio();
                thisM.node.msAudioCategory = "BackgroundCapableMedia";
                thisM.node.src = srcUri.absoluteCanonicalUri;

                thisM.node.onloadstart = function () {
                    Media.onStatus(id, Media.MEDIA_STATE, Media.MEDIA_STARTING);
                };

                thisM.node.ontimeupdate = function (e) {
                    Media.onStatus(id, Media.MEDIA_POSITION, e.target.currentTime);
                };

                thisM.node.onplaying = function () {
                    Media.onStatus(id, Media.MEDIA_STATE, Media.MEDIA_RUNNING);
                };

                thisM.node.ondurationchange = function (e) {
                    Media.onStatus(id, Media.MEDIA_DURATION, e.target.duration || -1);
                };

                thisM.node.onerror = function (e) {
                    // Due to media.spec.15 It should return MediaError for bad filename
                    var err = e.target.error.code === MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED ?
                        { code: MediaError.MEDIA_ERR_ABORTED } :
                        e.target.error;

                    Media.onStatus(id, Media.MEDIA_ERROR, err);
                };

                thisM.node.onended = function () {
                    Media.onStatus(id, Media.MEDIA_STATE, Media.MEDIA_STOPPED);
                };
            }
        }

        return true; // successfully created
    },

    // Start playing the audio
    startPlayingAudio:function(win, lose, args) {
        var id = args[0];
        //var src = args[1];
        //var options = args[2];

        var thisM = Media.get(id);
        // if Media was released, then node will be null and we need to create it again
        if (!thisM.node) {
            args[2] = true; // Setting createAudioNode to true
            if (!module.exports.create(win, lose, args)) {
                // there is no reason to continue if we can't create media
                // corresponding callback has been invoked in create so we don't need to call it here
                return;
            }
        }

        try {
            thisM.node.play();
        } catch (err) {
            if (lose) {
                lose({code:MediaError.MEDIA_ERR_ABORTED});
            }
        }
    },

    // Stops the playing audio
    stopPlayingAudio:function(win, lose, args) {
        var id = args[0];
        try {
            var thisM = Media.get(id);
            thisM.node.pause();
            thisM.node.currentTime = 0;
            Media.onStatus(id, Media.MEDIA_STATE, Media.MEDIA_STOPPED);
        } catch (err) {
            lose("Failed to stop: "+err);
        }
    },

    // Seeks to the position in the audio
    seekToAudio:function(win, lose, args) {
        var id = args[0];
        var milliseconds = args[1];
        var thisM = Media.get(id);
        try {
            thisM.node.currentTime = milliseconds / 1000;
            win(thisM.node.currentTime);
        } catch (err) {
            lose("Failed to seek: "+err);
        }
    },

    // Pauses the playing audio
    pausePlayingAudio:function(win, lose, args) {
        var id = args[0];
        var thisM = Media.get(id);
        try {
            thisM.node.pause();
            Media.onStatus(id, Media.MEDIA_STATE, Media.MEDIA_PAUSED);
        } catch (err) {
            lose("Failed to pause: "+err);
        }
    },

    // Gets current position in the audio
    getCurrentPositionAudio:function(win, lose, args) {
        var id = args[0];
        try {
            var p = (Media.get(id)).node.currentTime;
            win(p);
        } catch (err) {
            lose(err);
        }
    },

    // Start recording audio
    startRecordingAudio:function(win, lose, args) {
        var id = args[0];
        var srcUri = processUri(args[1]);

        var dest = parseUriToPathAndFilename(srcUri);
        var destFileName = dest.fileName;

        var success = function () {
            Media.onStatus(id, Media.MEDIA_STATE, Media.MEDIA_RUNNING);
        };

        var error = function (reason) {
            Media.onStatus(id, Media.MEDIA_ERROR, reason);
        };

        // Initialize device
        Media.prototype.mediaCaptureMgr = null;
        var thisM = (Media.get(id));
        var captureInitSettings = new Windows.Media.Capture.MediaCaptureInitializationSettings();
        captureInitSettings.streamingCaptureMode = Windows.Media.Capture.StreamingCaptureMode.audio;
        thisM.mediaCaptureMgr = new Windows.Media.Capture.MediaCapture();
        thisM.mediaCaptureMgr.addEventListener("failed", error);

        thisM.mediaCaptureMgr.initializeAsync(captureInitSettings).done(function (result) {
            thisM.mediaCaptureMgr.addEventListener("recordlimitationexceeded", error);
            thisM.mediaCaptureMgr.addEventListener("failed", error);

            // Start recording
            Windows.Storage.ApplicationData.current.temporaryFolder.createFileAsync(destFileName, Windows.Storage.CreationCollisionOption.replaceExisting).done(function (newFile) {
                recordedFile = newFile;
                var encodingProfile = null;
                switch (newFile.fileType) {
                    case '.m4a':
                        encodingProfile = Windows.Media.MediaProperties.MediaEncodingProfile.createM4a(Windows.Media.MediaProperties.AudioEncodingQuality.auto);
                        break;
                    case '.mp3':
                        encodingProfile = Windows.Media.MediaProperties.MediaEncodingProfile.createMp3(Windows.Media.MediaProperties.AudioEncodingQuality.auto);
                        break;
                    case '.wma':
                        encodingProfile = Windows.Media.MediaProperties.MediaEncodingProfile.createWma(Windows.Media.MediaProperties.AudioEncodingQuality.auto);
                        break;
                    default:
                        error("Invalid file type for record");
                        break;
                }
                thisM.mediaCaptureMgr.startRecordToStorageFileAsync(encodingProfile, newFile).done(success, error);
            }, error);
        }, error);
    },

    // Stop recording audio
    stopRecordingAudio:function(win, lose, args) {
        var id = args[0];
        var thisM = Media.get(id);
        var srcUri = processUri(thisM.src);

        var dest = parseUriToPathAndFilename(srcUri);
        var destPath = dest.path;
        var destFileName = dest.fileName;
        var fsType = dest.fsType;

        var success = function () {
            Media.onStatus(id, Media.MEDIA_STATE, Media.MEDIA_STOPPED);
        };

        var error = function (reason) {
            Media.onStatus(id, Media.MEDIA_ERROR, reason);
        };

        thisM.mediaCaptureMgr.stopRecordAsync().done(function () {
            if (fsType === fsTypes.TEMPORARY) {
                if (!destPath) {
                    // if path is not defined, we leave recorded file in temporary folder (similar to iOS)
                    success();
                } else {
                    Windows.Storage.ApplicationData.current.temporaryFolder.getFolderAsync(destPath).done(function (destFolder) {
                        recordedFile.copyAsync(destFolder, destFileName, Windows.Storage.CreationCollisionOption.replaceExisting).done(success, error);
                    }, error);
                }
            } else {
                // Copying file to persistent storage
                if (!destPath) {
                    recordedFile.copyAsync(Windows.Storage.ApplicationData.current.localFolder, destFileName, Windows.Storage.CreationCollisionOption.replaceExisting).done(success, error);
                } else {
                    Windows.Storage.ApplicationData.current.localFolder.getFolderAsync(destPath).done(function (destFolder) {
                        recordedFile.copyAsync(destFolder, destFileName, Windows.Storage.CreationCollisionOption.replaceExisting).done(success, error);
                    }, error);
                }
            }
        }, error);
    },

    // Release the media object
    release:function(win, lose, args) {
        var id = args[0];
        var thisM = Media.get(id);
        try {
            if (thisM.node) {
                thisM.node.onloadedmetadata = null;
                // Unsubscribing as the media object is being released
                thisM.node.onerror = null;
                // Needed to avoid "0x80070005 - JavaScript runtime error: Access is denied." on copyAsync
                thisM.node.src = null;
                delete thisM.node;
            }
        } catch (err) {
            lose("Failed to release: "+err);
        }
    },
    setVolume:function(win, lose, args) {
        var id = args[0];
        var volume = args[1];
        var thisM = Media.get(id);
        thisM.volume = volume;
    }
};

/**
 * Converts a path to Windows.Foundation.Uri basing on App data temporary folder
 * if scheme is not defined, e.g.: path/to/file.m4a -> ms-appdata:///temp/path/to/file.m4a
 * @param  {String} src Input path
 * @return {Object}     Windows.Foundation.Uri
 */
function setTemporaryFsByDefault(src) {
    var uri;
    try {
        uri = new Windows.Foundation.Uri(src);
    } catch (e) {
        if (e.number === PARAMETER_IS_INCORRECT) {
            // Use TEMPORARY fs there is no 'scheme:'
            uri = new Windows.Foundation.Uri(tempFolderAppDataBasePath, src);
        } else {
            throw e;
        }
    } finally {
        return uri;
    }
}

/**
 * Convert native full path to ms-appdata path
 * @param  {Object} uri Windows.Foundation.Uri
 * @return {Object}     ms-appdata Windows.Foundation.Uri
 */
function fullPathToAppData(uri) {
    if (uri.schemeName === 'file') {
        if (uri.rawUri.indexOf(Windows.Storage.ApplicationData.current.localFolder.path) !== -1) {
            // Also remove path' beginning slash to avoid losing folder name part
            uri = new Windows.Foundation.Uri(localFolderAppDataBasePath, uri.rawUri.replace(localFolderFullPath, '').replace(/^[\\\/]{1,2}/, ''));
        } else if (uri.rawUri.indexOf(Windows.Storage.ApplicationData.current.temporaryFolder.path) !== -1) {
            uri = new Windows.Foundation.Uri(tempFolderAppDataBasePath, uri.rawUri.replace(tempFolderFullPath, '').replace(/^[\\\/]{1,2}/, ''));
        } else {
            throw new Error('Not supported file uri: ' + uri.rawUri);
        }
    }

    return uri;
}

/**
 * Converts cdvfile paths to ms-appdata path
 * @param  {Object} uri Input cdvfile scheme Windows.Foundation.Uri
 * @return {Object}     Windows.Foundation.Uri based on App data path
 */
function cdvfileToAppData(uri) {
    var cdvFsRoot;

    if (uri.schemeName === 'cdvfile') {
        cdvFsRoot = uri.path.split('/')[1];
        if (cdvFsRoot === 'temporary') {
            return new Windows.Foundation.Uri(tempFolderAppDataBasePath, uri.path.split('/').slice(2).join('/'));
        } else if (cdvFsRoot === 'persistent') {
            return new Windows.Foundation.Uri(localFolderAppDataBasePath, uri.path.split('/').slice(2).join('/'));
        } else {
            throw new Error(cdvFsRoot + ' cdvfile root is not supported on Windows');
        }
    }

    return uri;
}

/**
 * Prepares media src for internal usage
 * @param  {String} src Input media path
 * @return {Object}     Windows.Foundation.Uri
 */
function processUri(src) {
    // Collapse double slashes (File plugin issue): ms-appdata:///temp//recs/memos/media.m4a => ms-appdata:///temp/recs/memos/media.m4a
    src = src.replace(/([^\/:])(\/\/)([^\/])/g, '$1/$3');

    // Remove beginning slashes
    src = src.replace(/^[\\\/]{1,2}/, '');

    var uri = setTemporaryFsByDefault(src);

    uri = fullPathToAppData(uri);
    uri = cdvfileToAppData(uri);

    return uri;
}

/**
 * Extracts path, filename and filesystem type from Uri
 * @param  {Object} uri Windows.Foundation.Uri
 * @return {Object}     Object containing path, filename and filesystem type
 */
function parseUriToPathAndFilename(uri) {
    // Removing scheme and location, using backslashes: ms-appdata:///local/path/to/file.m4a -> path\\to\\file.m4a
    var normalizedSrc = uri.path.split('/').slice(2).join('\\');

    var path = normalizedSrc.substr(0, normalizedSrc.lastIndexOf('\\'));
    var fileName = normalizedSrc.replace(path + '\\', '');

    var fsType;

    if (uri.path.split('/')[1] === 'local') {
        fsType = fsTypes.PERSISTENT;
    } else if (uri.path.split('/')[1] === 'temp') {
        fsType = fsTypes.TEMPORARY;
    }

    return {
        path: path,
        fileName: fileName,
        fsType: fsType
    };
}

require("cordova/exec/proxy").add("Media",module.exports);


/*
 *
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 *
*/

var argscheck = require('cordova/argscheck'),
    utils = require('cordova/utils'),
    exec = require('cordova/exec');

var mediaObjects = {};

/**
 * This class provides access to the device media, interfaces to both sound and video
 *
 * @constructor
 * @param src                   The file name or url to play
 * @param successCallback       The callback to be called when the file is done playing or recording.
 *                                  successCallback()
 * @param errorCallback         The callback to be called if there is an error.
 *                                  errorCallback(int errorCode) - OPTIONAL
 * @param statusCallback        The callback to be called when media status has changed.
 *                                  statusCallback(int statusCode) - OPTIONAL
 */
var Media = function(src, successCallback, errorCallback, statusCallback) {
    argscheck.checkArgs('sFFF', 'Media', arguments);
    this.id = utils.createUUID();
    mediaObjects[this.id] = this;
    this.src = src;
    this.successCallback = successCallback;
    this.errorCallback = errorCallback;
    this.statusCallback = statusCallback;
    this._duration = -1;
    this._position = -1;
    exec(null, this.errorCallback, "Media", "create", [this.id, this.src]);
};

// Media messages
Media.MEDIA_STATE = 1;
Media.MEDIA_DURATION = 2;
Media.MEDIA_POSITION = 3;
Media.MEDIA_ERROR = 9;

// Media states
Media.MEDIA_NONE = 0;
Media.MEDIA_STARTING = 1;
Media.MEDIA_RUNNING = 2;
Media.MEDIA_PAUSED = 3;
Media.MEDIA_STOPPED = 4;
Media.MEDIA_MSG = ["None", "Starting", "Running", "Paused", "Stopped"];

// "static" function to return existing objs.
Media.get = function(id) {
    return mediaObjects[id];
};

/**
 * Start or resume playing audio file.
 */
Media.prototype.play = function(options) {
    exec(null, null, "Media", "startPlayingAudio", [this.id, this.src, options]);
};

/**
 * Stop playing audio file.
 */
Media.prototype.stop = function() {
    var me = this;
    exec(function() {
        me._position = 0;
    }, this.errorCallback, "Media", "stopPlayingAudio", [this.id]);
};

/**
 * Seek or jump to a new time in the track..
 */
Media.prototype.seekTo = function(milliseconds) {
    var me = this;
    exec(function(p) {
        me._position = p;
    }, this.errorCallback, "Media", "seekToAudio", [this.id, milliseconds]);
};

/**
 * Pause playing audio file.
 */
Media.prototype.pause = function() {
    exec(null, this.errorCallback, "Media", "pausePlayingAudio", [this.id]);
};

/**
 * Get duration of an audio file.
 * The duration is only set for audio that is playing, paused or stopped.
 *
 * @return      duration or -1 if not known.
 */
Media.prototype.getDuration = function() {
    return this._duration;
};

/**
 * Get position of audio.
 */
Media.prototype.getCurrentPosition = function(success, fail) {
    var me = this;
    exec(function(p) {
        me._position = p;
        success(p);
    }, fail, "Media", "getCurrentPositionAudio", [this.id]);
};

/**
 * Start recording audio file.
 */
Media.prototype.startRecord = function() {
    exec(null, this.errorCallback, "Media", "startRecordingAudio", [this.id, this.src]);
};

/**
 * Stop recording audio file.
 */
Media.prototype.stopRecord = function() {
    exec(null, this.errorCallback, "Media", "stopRecordingAudio", [this.id]);
};

/**
 * Pause recording audio file.
 */
Media.prototype.pauseRecord = function() {
    exec(null, this.errorCallback, "Media", "pauseRecordingAudio", [this.id]);
};

/**
* Resume recording audio file.
*/
Media.prototype.resumeRecord = function() {
    exec(null, this.errorCallback, "Media", "resumeRecordingAudio", [this.id]);
};

/**
 * Release the resources.
 */
Media.prototype.release = function() {
    exec(null, this.errorCallback, "Media", "release", [this.id]);
};

/**
 * Adjust the volume.
 */
Media.prototype.setVolume = function(volume) {
    exec(null, null, "Media", "setVolume", [this.id, volume]);
};

/**
 * Adjust the playback rate.
 */
Media.prototype.setRate = function(rate) {
    if (cordova.platformId === 'ios'){
        exec(null, null, "Media", "setRate", [this.id, rate]);
    } else {
        console.warn('media.setRate method is currently not supported for', cordova.platformId, 'platform.');
    }
};

/**
 * Get amplitude of audio.
 */
Media.prototype.getCurrentAmplitude = function(success, fail) {
    exec(function(p) {
        success(p);
    }, fail, "Media", "getCurrentAmplitudeAudio", [this.id]);
};

/**
 * Audio has status update.
 * PRIVATE
 *
 * @param id            The media object id (string)
 * @param msgType       The 'type' of update this is
 * @param value         Use of value is determined by the msgType
 */
Media.onStatus = function(id, msgType, value) {

    var media = mediaObjects[id];

    if (media) {
        switch(msgType) {
            case Media.MEDIA_STATE :
                if (media.statusCallback) {
                    media.statusCallback(value);
                }
                if (value == Media.MEDIA_STOPPED) {
                    if (media.successCallback) {
                        media.successCallback();
                    }
                }
                break;
            case Media.MEDIA_DURATION :
                media._duration = value;
                break;
            case Media.MEDIA_ERROR :
                if (media.errorCallback) {
                    media.errorCallback(value);
                }
                break;
            case Media.MEDIA_POSITION :
                media._position = Number(value);
                break;
            default :
                if (console.error) {
                    console.error("Unhandled Media.onStatus :: " + msgType);
                }
                break;
        }
    } else if (console.error) {
        console.error("Received Media.onStatus callback for unknown media :: " + id);
    }

};

module.exports = Media;

function onMessageFromNative(msg) {
    if (msg.action == 'status') {
        Media.onStatus(msg.status.id, msg.status.msgType, msg.status.value);
    } else {
        throw new Error('Unknown media action' + msg.action);
    }
}

if (cordova.platformId === 'android' || cordova.platformId === 'amazon-fireos' || cordova.platformId === 'windowsphone') {

    var channel = require('cordova/channel');

    channel.createSticky('onMediaPluginReady');
    channel.waitForInitialization('onMediaPluginReady');

    channel.onCordovaReady.subscribe(function() {
        exec(onMessageFromNative, undefined, 'Media', 'messageChannel', []);
        channel.initializationComplete('onMediaPluginReady');
    });
}