/**
 * The npm_crafty server module. Contains the Server API.
 * @module "npm_crafty.server"
 * @requires module:"npm_crafty.net"
 * @requires module:"npm_crafty.matchmaking"
 */

var craftyLib = require('craftyjs');
var craftyNet = require('./npm_crafty.net.js');
var matchmaker = require('./npm_crafty.matchmaking.js');

/**
 * Construct a Crafty server instance with net features enabled.
 *

 * Each server instance shall have an unique room name.
 * Only client and server instances within the same room can talk to each other.
 *

 * This method also set's the server instance's [label]{@link module:"npm_crafty.net"#PeerLabel} to "SERVER".
 *
 * @memberof module:"npm_crafty.server"~Server#
 * @param {!module:"npm_crafty.net"#RoomName} room - the room to use for this server instance
 * @returns {!external:CraftyJS.Crafty} the newly created crafty client instance with net features enabled
 * @see external:CraftyJS.Crafty#define
 *
 * @example
 * var Server = require('npm_crafty')(io.sockets);
 * var Crafty = Server.createInstance("Room1");
 */
var createInstance = function(room, sockets) {
	// create a new crafty instance
	var Crafty = craftyLib();
	// add net features to crafty
	craftyNet.__addNet(Crafty, "SERVER", room);

	// initialize server send list
	craftyNet.__setServerOutputSockets(Crafty, sockets);

	return Crafty;
};

/**
 * Add a client socket to the Crafty server instance.
 * The Crafty net features will use this socket to communicate with the client.
 *
 * @memberof module:"npm_crafty.server"~Server#
 * @param {!external:CraftyJS.Crafty} Crafty - the Crafty server instance to bind the socket to
 * @param {!external:Socket} socket - the socket to use for communication with a client
 * @returns {!external:CraftyJS.Crafty} the Crafty instance for method chaining
 *
 * @example
 * var Server = require('npm_crafty')(io.sockets);
 * var Crafty = Server.createInstance("Room1");
 * Server.addClient(Crafty, socket);
 *
 * @see module:"npm_crafty.server"~Server#removeClient
 */
var addClient = function(Crafty, socket) {
	// add client to receive list
	craftyNet.__setInputSocket(Crafty, socket);
	// add client to send list
	craftyNet.__addOutputSocket(Crafty, socket);

	return Crafty;
};

/**
 * Remove a client socket from the Crafty server instance.
 * The Crafty net features will no longer use this socket to communicate with the client.
 *
 * @memberof module:"npm_crafty.server"~Server#
 * @param {!external:CraftyJS.Crafty} Crafty - the Crafty server instance to unbind the socket from
 * @param {!external:Socket} socket - the socket to no longer use for communication with a client
 * @returns {!external:CraftyJS.Crafty} the Crafty instance for method chaining
 *
 * @example
 * var Server = require('npm_crafty')(io.sockets);
 * var Crafty = Server.createInstance("Room1");
 * Server.addClient(Crafty, socket);
 * Server.removeClient(Crafty, socket);
 *
 * @see module:"npm_crafty.server"~Server#addClient
 */
var removeClient = function(Crafty, socket) {
	// add client to receive list
	craftyNet.__unsetInputSocket(Crafty, socket);
	// add client to send list
	craftyNet.__removeOutputSocket(Crafty, socket);
};

/**
 * The Server API.
 *

 * Can be constructed with this constructor or by calling the [default setup method]{@link module:"npm_crafty.server"~Server.setupDefault}.
 * After construction, various methods are available for creating a crafty server instance with net features and for connecting these net features to socket.io.
 *

 * Also offers a static method for [automatic matchmaking](module:"npm_crafty.server"~Server.MatchMaker).
 *

 * The sockets parameter contains the Socket.IO namespace to use for all Crafty related data.
 *
 * @class
 * @param {!external:Sockets} sockets - the socket namespace to use for all Crafty-related communication
 * @property {object} [app] - the express application instance; only available after calling the [default setup method]{@link module:"npm_crafty.server"~Server.setupDefault}
 * @property {object} [server] - the http server instance; only available after calling the [default setup method]{@link module:"npm_crafty.server"~Server.setupDefault}
 * @property {object} [io] - the socket.io instance; only available after calling the [default setup method]{@link module:"npm_crafty.server"~Server.setupDefault}
 *
 * @example
 * var Server = require('npm_crafty')(io.of("/crafty"));
 */
function Server(sockets) {
	return {
		createInstance: function(room) {
			return createInstance(room, sockets);
		},
		addClient: addClient,
		removeClient: removeClient
	};
}

/**
 * Setup a default node server, which will serve npm_crafty's required files (it will reply to GET requests).

 * Each callback will be called at the appropriate times.
 * Communication will be set to the default '/' namespace.
 *
 * @memberof module:"npm_crafty.server"~Server
 * @param {!function()} immediateFN - will be called immediately once all libraries are loaded
 * @param {!module:"npm_crafty.net"#SocketCallback} connectFN - will be called when a client connects to the server
 * @param {!module:"npm_crafty.net"#SocketCallback} disconnectFN - will be called when a client disconnects from the server
 * @param {number} [port=80] - the port to use (defaults to process.env.PORT; falls back to standard HTTP port 80)
 * @returns {!module:"npm_crafty.server"~Server} - the ServerAPI object, which in addition to the available methods also contains the app, server and io property
 *
 * @example
 * var Crafty;
 * var Server = require('npm_crafty').setupDefault( function () { //immediate callback
 *     Crafty = Server.createInstance("Room1");
 * }, function (socket) { //connect callback
 *     Server.addClient(Crafty, socket);
 * }, function (socket) { //disconnect callback
 * });
 */
var setupDefault = function(immediateFN, connectFN, disconnectFN, port) {
	var app = require('express')(),
		server = require('http').createServer(app),
		io = require('socket.io').listen(server),
		path = require('path'),
		browserify = require('browserify-middleware');

	io.set('log level', 2);
	server.listen(port || process.env.PORT || 80);

	var browserifyOptions = {};
	browserifyOptions[path.join(__dirname + '/npm_crafty.client.js')] = {expose: 'npm_crafty'};
	app.get('/npm_crafty.js', browserify([browserifyOptions]));

	app.get('/crafty_client.js', function (req, res) {
		res.sendfile(path.join(__dirname + '/crafty_client.js'));
	});

	io.sockets.on('connection', function (socket) {
		console.log("Connected ", socket.id);
		connectFN(socket);

		socket.on('disconnect', function (arg) {
			console.log("Disconnected ", socket.id);
			disconnectFN(socket);
		});
	});
	process.nextTick(immediateFN);

	var _server = new Server(io.sockets);
	_server.app = app;
	_server.server = server;
	_server.io = io;
	return _server;
};

Server.setupDefault = setupDefault;
Server.Matchmaker = matchmaker.ServerMatchmaker;
module.exports = Server;


var util = require("util"),
	crudInterfaces = {
		"mysql": __dirname + "/no-mysql-crud"
	},
	conversionFunctionsIn = {
		"bigint": function (i) {
			return i;
		},
		"bit": function (i) {
			return i;
		},
		"decimal": function (n) {
			return n;
		},
		"int": function (i) {
			return i;
		},
		"money": function (n) {
			return n;
		},
		"numeric": function (n) {
			return n;
		},
		"smallint": function (i) {
			return i;
		},
		"smallmoney": function (n) {
			return n;
		},
		"tinyint": function (i) {
			return i;
		},
		"float": function (i) {
			return i;
		},
		"real": function (i) {
			return i;
		},
		"date": function (n) {
			return n ? n : null;
		},
		"datetime": function (n) {
			return n ? n.replace(/T/gi, " ").replace(/Z/gi, "") : null;
		},
		"datetime2": function (n) {
			return n ? n.replace(/T/gi, " ").replace(/Z/gi, "") : null;
		},
		"datetimeoffset": function (n) {
			return n ? n : null;
		},
		"smalldatetime": function (n) {
			return n ? n : null;
		},
		"time": function (n) {
			return n ? n : null;
		},
		"char": function (t) {
			return t;
		},
		"nchar": function (t) {
			return t;
		},
		"varchar": function (t) {
			return t;
		},
		"nvarchar": function (t) {
			return t;
		},
		"text": function (t) {
			return t;
		},
		"ntext": function (t) {
			return t;
		},
		"binary": function (i) {
			return i;
		},
		"varbinary": function (i) {
			return i;
		},
		"image": function (i) {
			return i;
		},
		"uniqueidentifier": function (t) {
			return t;
		}
	},
	conversionFunctionsOut = {
		"bigint": function (i) {
			return i;
		},
		"bit": function (i) {
			return i;
		},
		"decimal": function (n) {
			return n;
		},
		"int": function (i) {
			return i;
		},
		"money": function (n) {
			return n;
		},
		"numeric": function (n) {
			return n;
		},
		"smallint": function (i) {
			return i;
		},
		"smallmoney": function (n) {
			return n;
		},
		"tinyint": function (i) {
			return i;
		},
		"float": function (i) {
			return i;
		},
		"real": function (i) {
			return i;
		},
		"date": function (n) {
			return n ? n : null;
		},
		"datetime": function (n) {
			if (typeof (n) === "string") {
				n = n.replace(/ /gi, "T");
				n = n.indexOf("Z") === -1 ? n + "Z" : n;
			}

			return n ? n : null;
		},
		"datetime2": function (n) {
			if (typeof (n) === "string") {
				n = n.replace(/ /gi, "T");
				n = n.indexOf("Z") === -1 ? n + "Z" : n;
			}

			return n ? n : null;
		},
		"datetimeoffset": function (n) {
			return n ? n : null;
		},
		"smalldatetime": function (n) {
			return n ? n : null;
		},
		"time": function (n) {
			return n ? n : null;
		},
		"char": function (t) {
			return t;
		},
		"nchar": function (t) {
			return t;
		},
		"varchar": function (t) {
			return t;
		},
		"nvarchar": function (t) {
			return t;
		},
		"text": function (t) {
			return t;
		},
		"ntext": function (t) {
			return t;
		},
		"binary": function (i) {
			return i;
		},
		"varbinary": function (i) {
			return i;
		},
		"image": function (i) {
			return i;
		},
		"uniqueidentifier": function (t) {
			return t;
		}
	};

function isNumber(i) {
	return !Number.isNaN(Number(i)) && i !== null;
}

function _nullable(pk, col, prop) {
	if ((prop !== null) || (col.nullable && prop === null) || (prop === null && col.columnName === pk))
		return prop;

	throw new Error(util.format("Column %s of type %s is not nullable.", col.columnName, col.type));
}

function _scrubDatum(schema, datum) {
	for (var p in datum) {

		try {
			var prop = datum[p],
				col = schema.columns[p],
				cfn;

			if (!col) throw new Error(util.format("%s is not a column in the data table.  Check  your spelling and try again.", p));

			cfn = conversionFunctionsIn[col.type];

			if (cfn) {
				prop = _nullable(schema.primaryKey, col, cfn(prop));
			} else {
				prop = _nullable(schema.primaryKey, col, prop);
			}

			datum[p] = prop;

		} catch (err) {
			console.error(err);
		}
	}

	return datum;
}

function _transformDatum(schema, datum) {
	if (!schema.columns) return datum;

	for (var p in datum) {
		try {
			var prop = datum[p],
				col = schema.columns[p],
				cfn;

			if (!col) throw new Error(util.format("%s is not a column in the data table %s.  Check  your spelling and try again.", p, schema.entityName));

			cfn = conversionFunctionsOut[col.type];

			if (cfn) {
				prop = _nullable(schema.primaryKey, col, cfn(prop));
			} else {
				prop = _nullable(schema.primaryKey, col, prop);
			}

			datum[p] = prop;

		} catch (err) {
			console.error(err);
		}
	}
	return datum;
}

function _resolveBodyData(req) {
	try {
		var b = typeof (req.body) === "string" ? JSON.parse(req.body) : req.body;

		if (b._id) {
			delete b._id;
		}

		req.body = b;
		return req;

	} catch (err) {
		console.error(err);
		return req;
	}
}

//HTTP Aliases
function _get(crud, schema, req) {
	return _read(crud, schema, req);
}

function _getOne(crud, schema, req) {
	return _one(crud, schema, req); //TODO: make this support ODATA filter also.
}

function _putByPrimaryKey(crud, schema, req) {

	var filter = {},
		data = _scrubDatum(schema, _resolveBodyData(req).body),
		routeID = (req.params && req.params.id) || data[schema.primaryKey];

	filter[schema.primaryKey] = routeID;

	return _update(crud, schema, data, filter);

}

function _post(crud, schema, req) {
	var data = schema.storageType === "gcs" ? req : req.body;
	return _create(crud, schema, data)
		.then(function (result) {
			return result || data; //??? what does mydata do?
		});
}

function _delete(crud, schema, req) {
	return _destroy(crud, schema, req); //TODO: need to resolve filter from req here.
}

//CRUD Aliases
function _create(crud, schema, data) {
	if (data._id) delete data._id; //This is a mongodb thing.

	if (data[schema.primaryKey]) delete data[schema.primaryKey];

	if (schema.storageType !== "gcs") {
		data = _scrubDatum(schema, data);
	}

	return crud.execute(schema, crud.operations.CREATE, data)
		.then(function (result) {
			if (schema.storageType === "gcs") {
				return result;
			} else {
				data[schema.primaryKey] = result.insertId;

				var r = _transformDatum(schema, data);
				return r;
			}
		});
}

function _read(crud, schema, filter) {
	return crud.execute(schema, crud.operations.READ, null, filter)
		.then(function (results) {

			if (results.value) {
				results.value = results.value.map(function (datum) {
					return _transformDatum(schema, datum);
				});
			} else {
				if (Array.isArray(results)) {
					results = results.map(function (datum) {
						return _transformDatum(schema, datum);
					});
				} else {
					return results;
				}
			}

			return results;
		});
}

function _readSP(crud, schema, payload, filter, multi) {
	return crud.execute(schema, crud.operations.EXECSP, payload, filter, multi)
		.then(function (results) {

			if (typeof results === "object") {
				if (results.length) {
					results = results.map(function (datum) {
						return _transformDatum(schema, datum);
					});

					if (schema.activeUri && schema.activeUri.returnOne) {
						return results[0]; //Always returns the first result item or undefined.
					} else {
						return results;
					}
				}
			}

			return results;

		});
}

function _writeSP(crud, schema, payload, filter) {
	return _readSP(crud, schema, payload, filter)
		.then(function (results) {
			return results.length > 0 ? results[0] : results;
		});
}

function _one(crud, schema, filter) {
	return _read(crud, schema, filter)
		.then(function (results) {
			return results.length > 0 ? _transformDatum(schema, results[0]) : null;
		});
}

function _update(crud, schema, data, filter) {
	return crud.execute(schema, crud.operations.UPDATE, data, filter)
		.then(function (result) {
			return _transformDatum(schema, data);
		});
}

function _destroy(crud, schema, filter) {
	return crud.execute(schema, crud.operations.DELETE, null, filter);
}

function _wrapSchema(crud, schema) {
	return {
		read: _read.bind(null, crud, schema),
		readSP: _readSP.bind(null, crud, schema),
		one: _one.bind(null, crud, schema),
		update: _update.bind(null, crud, schema),
		create: _create.bind(null, crud, schema),
		writeSP: _writeSP.bind(null, crud, schema),
		destroy: _destroy.bind(null, crud, schema),
		get: _get.bind(null, crud, schema),
		getOne: _getOne.bind(null, crud, schema),
		putByPrimaryKey: _putByPrimaryKey.bind(null, crud, schema),
		post: _post.bind(null, crud, schema),
		delete: _delete.bind(null, crud, schema)
	};
}

module.exports = function (crudType, sqlConnInfo) {

	var crud;

	//console.log("crudType", typeof (crudType));

	if (typeof (crudType) === "string") {
		crud = require(crudInterfaces[crudType])(sqlConnInfo);
	} else if (typeof (crudType) === "object") {
		crud = crudType
	} else {
		throw new Error("Unsupported crudType");
	}

	if (!crud) throw new Error(util.format("Invalid CRUD provider, %s.", crudType));

	return {
		//HTTP Exports
		get: _get.bind(null, crud),
		getOne: _getOne.bind(null, crud),
		putByPrimaryKey: _putByPrimaryKey.bind(null, crud),
		post: _post.bind(null, crud),
		delete: _delete.bind(null, crud),

		//CRUD Exports
		read: _read.bind(null, crud),
		readSP: _readSP.bind(null, crud),
		writeSP: _writeSP.bind(null, crud),
		one: _one.bind(null, crud),
		update: _update.bind(null, crud),
		create: _create.bind(null, crud),
		destroy: _destroy.bind(null, crud),

		//Testing interface
		_crud: crud,

		wrapSchema: _wrapSchema.bind(null, crud),

		transformDatum: _transformDatum
	};

};


'use strict'; // eslint-disable-line
// TODO remove when prism will support bower packaging (there are discussions on github)
const _ = require('lodash');
const fs = require('fs');
const path = require('path');
const gulp = require('gulp');
const concat = require('gulp-concat');
const git = require('gulp-git');
const header = require('gulp-header');
const wrapper = require('gulp-wrapper');
const runSequence = require('run-sequence');
const toposort = require('obj-toposort');

// replaces {id} tokens
function replace(format, data) {
  return format.replace(/{(\w+)}/g, (m, name) => (data[name] ? data[name] : ''));
}

function loadComponents() {
  // make prism/components.js to be a module so we could require it
  let src = fs.readFileSync('prism/components.js', 'utf8');
  src += '\nmodule.exports = components;\n';
  const out = './prism-components.js';
  fs.writeFileSync(out, src, 'utf8');
  const data = require(out); // eslint-disable-line
  fs.unlinkSync(out);
  return data;
}

// exclude languages that we won't use
const excludedLanguages = [];

function loadLanguages() {
  const components = loadComponents();

  const langs = _.toPairs(components.languages)
    .map((p) => {
      const obj = p[1];
      obj.id = p[0].toLowerCase();
      obj.deps = obj.require ? [obj.require] : [];
      return obj;
    })
    .filter(t => t.id !== 'meta' && excludedLanguages.indexOf(t.id) < 0);

  // order languages by "require"
  return toposort(langs).filter(_.identity);
}

// task to build prism package
gulp.task('default', () => runSequence('pull', ['css', 'js'], 'test'));

gulp.task('test', () => {
  const Prism = require('./src/prism.js'); // eslint-disable-line

  function highlight(code, lang) {
    const g = Prism.languages[lang];
    if (g) {
      return Prism.highlight(code, g, lang);
    }
    return code;
  }

  const s = highlight('fuction() {\n\tconsole.log("hi");\n}', 'js');
  console.log(s);
});

gulp.task('pull', (done) => {
  if (fs.existsSync('prism')) {
    git.pull('origin', 'gh-pages', {
      cwd: path.join(__dirname, 'prism'),
    }, (err) => {
      if (err) throw err;
      done();
    });
  } else {
    git.clone('https://github.com/prismjs/prism', (err) => {
      if (err) throw err;
      done();
    });
  }
});

gulp.task('css', () => {
  // lets try okaidia.css
  gulp.src('prism/themes/*.css')
    .pipe(gulp.dest('./themes'));
});

gulp.task('js', () => {
  const components = loadComponents();
  const langs = require('./languages.json'); // eslint-disable-line

  // TODO add plugins
  const langpath = components.languages.meta.path;
  const glob = [components.core.meta.path].concat(
    langs.map(lang => replace(langpath, { id: lang }) + '.js') // eslint-disable-line
  ).map(p => path.join('prism', p));

  console.log(glob);

  const head = fs.readFileSync('./header.js', 'utf8');
  const foot = fs.readFileSync('./footer.js', 'utf8');
  const banner =
    '\n/* **********************************************\n' +
    '     Begin <%= file.relative %>\n' +
    '********************************************** */\n\n';

  return gulp.src(glob)
    .pipe(header(banner))
    .pipe(concat('prism.js'))
    .pipe(wrapper({ header: head, footer: foot }))
    .pipe(gulp.dest('./src'));
});

gulp.task('dump', () => {
  const langs = loadLanguages();
  const ids = langs.map(t => t.id);
  fs.writeFileSync('languages.json', JSON.stringify(ids, null, 2));
});


'use strict';

var esprimaTools = require('browserify-esprima-tools');

var testNode     = require('./ast-tests'),
    inferAngular = require('./infer-angular'),
    processNode  = require('./process-node');

/**
 * Create an updater function for the esprima transform with the given error handler
 * @param {function} [errorFn] An optional error handler function
 * @returns {function(string, object):object} A method that transforms the esprima tree
 */
function getUpdater(errorFn) {
  errorFn = errorFn || new Function();
  return updater;

  /**
   * The updater function for the esprima transform
   * @param {string} file The filename for the Browserify transform
   * @param {object} ast The esprima syntax tree
   * @returns {object} The transformed esprima syntax tree
   */
  function updater(file, ast) {
    if (ast.comments) {
      ast.comments
        .filter(testDocTag)
        .map(getAnnotatedNode)
        .concat(inferAngular(ast))    // find the items that are not explicitly annotated
        .filter(testFirstOccurrence)  // ensure unique values
        .forEach(processNode);
    } else {
      errorFn('Esprima AST is required to have top-level comments array');
    }
    return ast;
  }

  /**
   * Get the node that is annotated by the comment or throw if not present.
   * @throws {Error} Where comment does not annotate a node
   * @param {object} comment The comment node
   */
  function getAnnotatedNode(comment) {

    // find the first function declaration or expression following the annotation
    var result;
    if (comment.annotates) {
      var candidateTrees;

      // consider the context the block is in (i.e. what is its parent)
      var parent = comment.annotates.parent;

      // consider nodes from the annotated node forward
      //  include the first non-generated node and all generated nodes preceding it
      if (testNode.isBlockOrProgram(parent)) {
        var body = parent.body;
        var index = body.indexOf(comment.annotates);
        var candidates = body.slice(index);
        var length = candidates.map(testNode.isGeneratedCode).indexOf(false) + 1;
        candidateTrees = candidates.slice(0, length || candidates.length);
      }
      // otherwise we can only consider the given node
      else {
        candidateTrees = [comment.annotates];
      }

      // try the nodes
      while (!result && candidateTrees.length) {
        result = esprimaTools
          .orderNodes(candidateTrees.shift())
          .filter(testNode.isFunctionNotIFFE)
          .shift();
      }
    }

    // throw where not valid
    if (result) {
      return result;
    } else {
      errorFn('Doc-tag @ngInject does not annotate anything');
    }
  }
}

module.exports = getUpdater;

/**
 * Test the comment content for the <code>@ngInject</code> doctag.
 * @param {object} comment The comment node
 */
function testDocTag(comment) {
  return /@ngInject/i.test(comment.value);
}

/**
 * Test whether the given value is the first occurance in the array.
 * @param {*} value The value to test
 * @param {number} i The index of the value in the array
 * @param {Array} array The array the value is within at the given index
 */
function testFirstOccurrence(value, i, array) {
  return (array.indexOf(value) === i);
}


/*
	Kung Fig common

	Copyright (c) 2015 - 2018 Cédric Ronvel

	The MIT License (MIT)

	Permission is hereby granted, free of charge, to any person obtaining a copy
	of this software and associated documentation files (the "Software"), to deal
	in the Software without restriction, including without limitation the rights
	to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
	copies of the Software, and to permit persons to whom the Software is
	furnished to do so, subject to the following conditions:

	The above copyright notice and this permission notice shall be included in all
	copies or substantial portions of the Software.

	THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
	IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
	FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
	AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
	LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
	OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
	SOFTWARE.
*/

"use strict" ;

var parsers = {} ;
module.exports = parsers ;

// Unquoted strings should at least contains a non-whitespace char
parsers.parseUnquotedString = function parseUnquotedString( str , runtime ) {
	var l = runtime.iEndOfLine !== undefined ? runtime.iEndOfLine : str.length ;
	var v = str.slice( runtime.i , l ).trim() || undefined ;
	runtime.i = l ;
	return v ;
} ;

parsers.parseQuotedString = function parseQuotedString( str , runtime ) {
	var c , j = runtime.i , v = '' ,
		l = runtime.iEndOfLine !== undefined ? runtime.iEndOfLine : str.length ;

	for ( ; j < l ; j ++ ) {
		c = str.charCodeAt( j ) ;

		// This construct is intended: this is much faster (15%)
		if ( c === 0x22 || c === 0x5c || c <= 0x1f ) {
			if ( c === 0x22	) {
				// double quote = end of the string
				v += str.slice( runtime.i , j ) ;
				runtime.i = j + 1 ;
				return v ;
			}
			else if ( c === 0x5c ) {
				// backslash
				v += str.slice( runtime.i , j ) ;
				runtime.i = j + 1 ;
				v += parseBackSlash( str , runtime ) ;
				j = runtime.i - 1 ;
			}
			else if ( c <= 0x1f ) {
				// illegal
				throw new SyntaxError( "Unexpected control char 0x" + c.toString( 16 ) + " (" + locationStr( runtime ) + ")" ) ;
			}
		}
	}

	throw new SyntaxError( "Unexpected end of line/string, expecting a double-quote (" + locationStr( runtime ) + ")" ) ;
} ;

// Skip a quoted string, without interpreting it
parsers.skipQuotedString = function skipQuotedString( str , runtime ) {
	var c ,
		l = runtime.iEndOfLine !== undefined ? runtime.iEndOfLine : str.length ;

	for ( ; runtime.i < l ; runtime.i ++ ) {
		c = str.charCodeAt( runtime.i ) ;

		// This construct is intended: this is much faster (15%)
		if ( c === 0x22 || c === 0x5c || c <= 0x1f ) {
			if ( c === 0x22	) {
				// double quote = end of the string
				runtime.i ++ ;
				return ;
			}
			else if ( c === 0x5c ) {
				// backslash
				runtime.i ++ ;
			}
			else if ( c <= 0x1f ) {
				// illegal
				throw new SyntaxError( "Unexpected control char 0x" + c.toString( 16 ) + " (" + locationStr( runtime ) + ")" ) ;
			}
		}
	}

	throw new SyntaxError( "Unexpected end of line/string, expecting a double-quote (" + locationStr( runtime ) + ")" ) ;
} ;

var parseBackSlashLookup =
( function() {
	var c = 0 , lookup = new Array( 0x80 ) ;

	for ( ; c < 0x80 ; c ++ ) {
		if ( c === 0x62 ) { // b
			lookup[ c ] = '\b' ;
		}
		else if ( c === 0x66 ) { // f
			lookup[ c ] = '\f' ;
		}
		else if ( c === 0x6e ) { // n
			lookup[ c ] = '\n' ;
		}
		else if ( c === 0x72 ) { // r
			lookup[ c ] = '\r' ;
		}
		else if ( c === 0x74 ) { // t
			lookup[ c ] = '\t' ;
		}
		else if ( c === 0x5c ) { // backslash
			lookup[ c ] = '\\' ;
		}
		else if ( c === 0x2f ) { // slash
			lookup[ c ] = '/' ;
		}
		else if ( c === 0x22 ) { // double-quote
			lookup[ c ] = '"' ;
		}
		else {
			lookup[ c ] = '' ;
		}
	}

	return lookup ;
} )() ;

function parseBackSlash( str , runtime ) {
	var v , c = str.charCodeAt( runtime.i ) ;

	if ( runtime.i >= str.length ) { throw new SyntaxError( "Unexpected end" ) ; }

	if ( c === 0x75 ) { // u
		runtime.i ++ ;
		v = parseUnicode( str , runtime ) ;
		return v ;
	}
	else if ( ( v = parseBackSlashLookup[ c ] ).length ) {
		runtime.i ++ ;
		return v ;
	}

	throw new SyntaxError( 'Unexpected token: "' + str[ runtime.i ] + '" (' + locationStr( runtime ) + ")" ) ;
}

function parseUnicode( str , runtime ) {
	if ( runtime.i + 3 >= str.length ) { throw new SyntaxError( "Unexpected end" ) ; }

	var match = str.slice( runtime.i , runtime.i + 4 ).match( /[0-9a-f]{4}/ ) ;

	if ( ! match ) { throw new SyntaxError( "Unexpected " + str.slice( runtime.i , runtime.i + 4 ) + " (" + locationStr( runtime ) + ")" ) ; }

	runtime.i += 4 ;

	// Or String.fromCodePoint() ?
	return String.fromCharCode( Number.parseInt( match[ 0 ] , 16 ) ) ;
}

var numberLineRegexp = /^(-?[0-9]+(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?)\s*$/ ;
var numberRegexp = /^(-?[0-9]+(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?)/ ;

parsers.parseNumber = function parseNumber( str , runtime ) {
	var part ;

	if ( runtime.iEndOfLine ) {
		// Line mode: the number should finish the line (KFG number value)

		part = str.slice( runtime.i , runtime.iEndOfLine ) ;

		if ( numberLineRegexp.test( part ) ) {
			runtime.i = runtime.iEndOfLine ;
			return parseFloat( part ) ;
		}

		return parsers.parseUnquotedString( str , runtime ) ;
	}

	// Normal mode

	part = str.slice( runtime.i ) ;

	// Is this needed?
	numberRegexp.lastIndex = 0 ;

	var matches = part.match( numberRegexp ) ;

	if ( matches && matches[ 1 ] ) { return parseFloat( part ) ; }

	// NaN or undefined?
	return undefined ;

} ;

// Used to report errors
function locationStr( runtime , line ) {
	var loc ;
	loc = 'line: ' + ( line !== undefined ? line : runtime.lineNumber ) ;
	if ( runtime.file ) { loc += ' -- file: ' + runtime.file ; }
	return loc ;
}

parsers.locationStr = locationStr ;


var _isString = require('./isString');
var LOCALE_ENUM = require('./internal/enum/localeTypeEnum');
var REGEX_ENUM = require('./internal/enum/regexEnum');

/**
 * 校验参数 `val` 是否为座机号码
 *
 * 若参数 `val` 符合参数 `locale`(默认值:  LOCALE_ENUM.ZHCN) 指定地区的座机号码格式, 则返回 true, 否则返回 false
 *
 * 若参数 `locale` 指定地区不存在, 同样直接返回 false
 *
 * 对于不同地区使用其所在地区的座机号码校验格式
 *
 * 目前, 仅会对座机号码进行号码段和位数进行校验，不做真实性校验
 *
 * @param   {*}       val    待校验的参数
 * @param   {String}  locale 国际化
 * @return  {Boolean} 返回校验结果
 * @version 0.0.5
 * @since   0.0.4
 */
function _isTelephone(val, locale) {
  var key = _isString(locale) ? locale : LOCALE_ENUM.ZHCN;
  var rex = REGEX_ENUM.TELEPHONE_REX[key];

  if (!rex) {
    return false;
  }

  if (!_isString(val)) {
    return false;
  }

  return rex.test(val);
}

module.exports = _isTelephone;


/*!
 * Module dependencies
 */
var util = require('util')
  , Resource = require('./')
  , updateMixin = require('../mixins/update')
  , destroyMixin = require('../mixins/destroy')
  ;

/*!
 * Expose `Macro()`.
 */
exports = module.exports = Macro;

/**
 * Initialize a new `Macro` with the given `parent` and `definition`.
 *
 * @param {Function} parent The parent this resource is attached to.
 * @param {Object} definition The resource definition.
 * @api private
 */
function Macro(parent, definition) {
  var key;
  for (key in updateMixin) {
    this[key] = updateMixin[key];
  }
  this.destroy = destroyMixin;
  Macro.super_.apply(this, arguments);
}
util.inherits(Macro, Resource);

Macro.create = require('../mixins/create');


/**
 * Sort the elements of an array.
 *
 * @example
 * const a = [3, 1, 2, 7, 6, 4, 5, 10, 8, 9];
 * sort(a);
 * // => [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
 *
 * @param {Array} array The array to sort.
 * @param {Function} [comparator] The comparator function to use.
 * @return {Array} The sorted array.
 */
export function sort(array, comparator = compare) {
  if (!array) {
    return [];
  }

  const n = array.length;
  const a = new Array(n);

  for (let i = 0; i < n; i++) {
    a[i] = array[i];
  }

  // First pass: 15-sort the array using quicksort.
  quickSort(a, 0, n - 1, 15, comparator);

  // Second pass: Fully sort the array using insertion sort.
  insertionSort(a, 0, n - 1, comparator);

  return a;
}

/**
 * Quick sort an array.
 *
 * @see https://en.wikipedia.org/wiki/Quicksort
 *
 * @param {Array} a The array to quick sort.
 * @param {number} l The lower index of the sorting.
 * @param {number} h The higher index of the sorting.
 * @param {number} k The sorting cutoff.
 * @param {Function} c The comparator to use.
 * @private
 */
function quickSort(a, l, h, k, c) {
  const s = [];

  let t = 0;

  s[t++] = l;
  s[t++] = h;

  while (t > 0) {
    h = s[--t];
    l = s[--t];

    if (h - l > k) {
      const p = partition(a, l, h, c);

      if (p > l) {
        s[t++] = l;
        s[t++] = p;
      }

      if (p + 1 < h) {
        s[t++] = p + 1;
        s[t++] = h;
      }
    }
  }
}

/**
 * Select the pivot element using the median-of-three rule.
 *
 * @see https://en.wikipedia.org/wiki/Quicksort#Choice_of_pivot
 *
 * @param {Array} a The array to select a pivot in.
 * @param {number} l The lower index of the pivot selection.
 * @param {number} h The higher index of the pivot selection.
 * @param {Function} c The comparator function.
 * @return {number} The selected pivot.
 * @private
 */
function pivot(a, l, h, c) {
  const p = (l + ((h - l) / 2)) | 0;

  if (c(a[h], a[l]) < 0) {
    [a[l], a[h]] = [a[h], a[l]];
  }

  if (c(a[p], a[l]) < 0) {
    [a[l], a[p]] = [a[p], a[l]];
  }

  if (c(a[h], a[p]) < 0) {
    [a[h], a[p]] = [a[p], a[h]];
  }

  return p;
}

/**
 * Partition a subarray according to the Hoare partitioning scheme.
 *
 * @see https://en.wikipedia.org/wiki/Quicksort#Hoare_partition_scheme
 *
 * @param {Array} a The array to partition.
 * @param {number} l The lower index of the partition.
 * @param {number} h The higher index of the partition.
 * @param {Function} c The comparator function.
 * @return {number} The final position of the pivot.
 * @private
 */
function partition(a, l, h, c) {
  const p = a[pivot(a, l, h, c)];

  let i = l - 1;
  let j = h + 1;

  for (;;) {
    do {
      i++;
    } while (c(a[i], p) < 0);

    do {
      j--;
    } while (c(a[j], p) > 0);

    if (i < j) {
      [a[i], a[j]] = [a[j], a[i]];
    } else {
      return j;
    }
  }
}

/**
 * Insertion sort an array.
 *
 * @see https://en.wikipedia.org/wiki/Insertion_sort
 *
 * @param {Array} a The array to insertion sort.
 * @param {number} l The lower index of the sorting.
 * @param {number} h The higher index of the sorting.
 * @param {Function} c The comparator to use.
 * @private
 */
function insertionSort(a, l, h, c) {
  for (let i = l + 1; i <= h; i++) {
    const x = a[i];
    let j = i - 1;

    while (j >= 0 && c(a[j], x) > 0) {
      a[j + 1] = a[j];
      j--;
    }

    a[j + 1] = x;
  }
}

/**
 * Compare two elements.
 *
 * @param {*} a The first element.
 * @param {*} b The second element.
 * @return {number} 1 if `a > b`, -1 if `a < b`, and 0 if `a == b`.
 * @private
 */
function compare(a, b) {
  if (a === b) {
    return 0;
  }

  return a > b ? 1 : -1;
}


/*!
 * Module dependencies
 */
var getResource = require('./resource')
  ;

/*!
 * Expose `search()`.
 */
exports = module.exports = search;

/**
 * Searches a resource.
 *
 * @param {Function} parent The parent this resource should be attached to.
 * @param {String} baseUrl That's needed for sub resources (/api/v2/customers/1/cases).
 * @param {Function} callback The callback called with the new resource or error.
 * @api public
 */
function search(parent, baseUrl, callback) {
  var resource = new (getResource('page'))(parent, {
    _links: { self: { href: baseUrl, 'class': 'page' } }
  });

  if (typeof callback == 'function') return resource.exec(callback);
  return resource;
}


/*
 * Copyright 2012 Karl Düüna <karl.dyyna@gmail.com> All rights reserved.
 */
'use strict';

var utils = require('./utils');
var spawn = require('child_process').spawn;
var EventEmitter = require('events').EventEmitter;
var ShellStream = require('./shellStream');

module.exports.createPipeStream = createPipeStream;

/**
 * Main entrance function, will create PipeStream or joined pipe based on input
 *
 * @param {Object|String|Array} cmd a Stream or Spawn object
 *    or string command to create a spawn object
 *    or an array of the previous types
 * @param {Array} [args=[]] optional array to string command
 * @param {Object} [opts={}] optional options to string command
 * @returns {PipeStream}
 */
function createPipeStream(cmd/*, args, opts*/){
    var proc;
    var command;

    if(Array.isArray(cmd)){
    // We have an array so create a pipe from all elements
    var firstCmd = cmd.shift();
    var open = Array.isArray(firstCmd) ? createPipeStream.apply({}, firstCmd) : createPipeStream(firstCmd);
    cmd.forEach(function(p){
        open = open.pipe(p);
    });
    return open;

    } else if(cmd instanceof EventEmitter){
        // Take the eventemitter as base
        proc = cmd;
        command = 'pre-defined';

    } else if(typeof cmd === 'object'){
        throw new TypeError('Invalid input, expected object type -> EventEmitter');

    } else {
        // We have input for Spawn command, normalize the input and create spawn
        var input = utils.normalizeInput.apply(this, arguments);
        command = utils.getCommand(input);
        proc = spawn.apply({}, input);
    }

    // Create inner pointer for command
    proc._command = command;

    // Check if process is still alive
    if(proc.exitCode){
        throw new Error('Process already dead');
    }

    return PipeStream(proc);
}

/**
 * Function for adding PipeStream functionality to an EventEmitter
 * @param {EventEmitter} self
 *
 * @return {PipeStream}
 */
function PipeStream(self){
    // Inner pipeline handle
    self._pipeline = [self];
    // Inner position handle
    self._nr = 0;
    // Modify object
    wrapMethods(self);
    addFunctions(self);
    connectEvents(self);
    addEventHandlers(self);

    return self;
}

/**
 * Function for wrapping default EventEmitter functions to return itself
 *
 * @param {EventEmitter} self
 * @returns {EventEmitter}
 */
function wrapMethods(self){
    var methods = ['on'];
    var childObjects = ['stdin','stdout','stderr'];

    // Wrap on method
    methods.forEach(function(m){
        var old = self[m];
        self[m] = function(){
            old.apply(self, arguments);
            return self;
        };
    });

    // If its a spawn object then wrap child EventEmitters
    if(utils.isSpawn(self)){
        childObjects.forEach(function(child){
            methods.forEach(function(m){
                var old = self[child][m];
                self[child][m] = function(){
                    old.apply(self[child], arguments);
                    return self;
                };
            });
        });
    }
}

/**
 * Adds extra functionality to EventEmitter
 *
 * @param {EventEmitter} self
 * @returns {EventEmitter}
 */
function addFunctions(self){
    // Function for assigning a process to run after the end of this PipeStream
    self.and = function(){
        self._and = new ShellStream(arguments);
        self.on('exit',function(code){
            if(code !== 0){
                self.emit('error',code);
                return;
            }
            self._and = self._and.run();
        });
        return self._and;
    };

    var oldPipe = self.pipe ? self.pipe.bind(self) : false;
    // Function for adding a new pipe to the pipeline
    self.pipe = function(){
        // Create pipe and add it to the pipeline hierarchy
        self._pipe = createPipeStream.apply({}, arguments);
        self._pipe._parent = self;
        self._addPipe(self._pipe);
        self._pipe._pipeline = self._pipeline;

        // Pipe the necessary data events to the new PipeStream
        if(utils.isSpawn(self)){
            self.stdout.pipe(utils.isSpawn(self._pipe) ? self._pipe.stdin:  self._pipe);
            self.stderr.on('data', function(d){
                self._pipe.emit('error', d);
            });
        } else {
            if(utils.isSpawn(self._pipe)){
                oldPipe(self._pipe.stdin);
                self.on('error',function(d){
                    self._pipe.stderr.emit('data', d);
                });
            } else {
                oldPipe(self._pipe);
                self.on('error',function(d){
                    self._pipe.emit('error', d);
                });
            }
        }
        // return new PipeStream
        return self._pipe;
    };

    // Internal function for appending a PipeStream to pipeline
    Object.defineProperty(self, '_addPipe',{value:function(pipe){
        self._pipeline.push(pipe);
        pipe._nr = self._pipeline.length - 1;
        pipe._error = self._error;
    }});

    // Internal function for destroying the whole pipeline
    Object.defineProperty(self, '_cleanUp', {
        value: function(){
            this._pipeline.forEach(function(p){
                p._error = null; // Make sure error is called only once.
                if(p.kill){
                    p.kill();
                } else if(p.destroy){
                    p.destroy();
                }
            });
        }
    });

    // Function for retrieving the pipeline beginning
    self.first = function(){
        return self.get(0);
    };

    // Function for retrieving the pipeline end
    self.last = function(){
        return self.get(self._pipeline.length -1);
    };

    // Function for retrieving the nth element in pipeline
    self.get = function(n){
        return self._pipeline[n] || false;
    };

    // Function for appending an error handler for all elements in pipeline
    self.error = function(fn){
        self._pipeline.forEach(function (section) {
            section._error = fn;
        });
        return self;
    };
}

/**
 * Function for connecting some events
 *
 * @param {EventEmitter} self
 */
function connectEvents(self){
    if(self.stdout){
        self.stdout.on('data', function(d){
            self.emit('data', d);
        });
        self.stderr.on('data', function(d){
            self.emit('error', d);
        });
    }
}
/**
 * Function for adding internal event handlers
 * @param {EventEmitter} self
 */
function addEventHandlers(self){
    self.on('error',function(d){
        var fn = self._error;
        self._cleanUp();
        if(fn){
            fn('pipeline[' + self._nr + ']:"' + self._command + '" failed with: ' + d.toString());
        }
    });
}


/* Router for requests */

var Q = require('q');
var IS = require('./is.js');
var errors = require('./errors.js');
var flags = require('./flags.js');

/** Internal target resolver
 * @param routes Object prepresenting routes to resources.
 * @param path Path to the resource as an array of keys.
 */
function _resolve(routes, path, req, res) {
	return Q.fcall(function() {

		path = path || [];

		var obj = routes;

		// Resolve promises first
		if(IS.obj(obj) && IS.fun(obj.then)) {
			var p = obj.then(function(ret) {
				return _resolve(ret, path, req, res);
			});
			return p;
		}

		// Resolve functions first
		if(IS.fun(obj)) {
			var p2 = Q.when(obj(req, res)).then(function(ret) {
				return _resolve(ret, path, req, res);
			});
			return p2;
		}

		// If the resource is undefined, return flags.notFound (resulting to a HTTP error 404).
		if(obj === undefined) {
			return flags.notFound;
		}

		// If path is at the end, then return the current resource.
		if(path.length === 0) {
			return obj;
		}

		// Handle arrays
		if(IS.array(obj)) {
			var k = path[0],
			    n = parseInt(path.shift(), 10);
			if(k === "length") {
				return _resolve(obj.length, path.shift(), req, res);
			}
			if(k !== ""+n) {
				return Q.fcall(function() { throw new errors.HTTPError({'code':400, 'desc':'Bad Request'}); });
			}
			return _resolve(obj[n], path.shift(), req, res);
		}

		// Handle objects
		if(IS.obj(obj)) {
			var k2 = path[0];
			if(obj[k2] === undefined) {
				return flags.notFound;
			}
			if(!obj.hasOwnProperty(k2)) {
				return Q.fcall(function() { throw new errors.HTTPError({'code':403, 'desc':'Forbidden'}); });
			}
			return _resolve(obj[path.shift()], path, req, res);
		}

		// Returns notFound because we still have keys in the path but nowhere to go.
		return flags.notFound;
	});
}

/** Constructor */
function Router (routes) {
	this._routes = routes;
}

/** Parse target */
function do_parse_url(url) {
	var s = require('url').parse(url).pathname.replace(/[^a-zA-Z0-9_\-\+\.]+/g, "/").replace(/^\/+/, "");
	if(s.length === 0) { return []; }
	return s.split("/");
}

/** Resolve target URL to an object using routes */
Router.prototype.resolve = function(req, res) {
	return _resolve(this._routes, do_parse_url(req.url), req, res);
};

// Exports
module.exports = Router;

/* EOF */


/*!
 * Module dependencies
 */
var getResource = require('./resource')
  , inflection = require('inflection')
  ;

/*!
 * Expose `link()`.
 */
module.exports = link;

/**
 * Link handles the _links part of the HAL response and sets up the associations.
 * It is used by the client to setup the initial resources like `users`, `cases` and
 * so on, as well as by the resource object itself for sub resources (`cases().replies()`).
 *
 * @param {Object} defintion The defintion of the desk.com API resource.
 * @api public
 */
function link(definition) {
  var key, link, newResource, Resource;

  if ('_links' in definition) {
    for (key in definition._links) {
      if (key === 'self') continue;
      link  = definition._links[key];
      key = key.charAt(0) + inflection.camelize(key).slice(1);

      // resources like next, previous can be null
      if (link === null) {
        this[key] = _buildFunction(null);
        continue;
      }

      // this is a really ugly hack but necessary for sub resources which aren't declared consistently
      if (inflection.singularize(key) !== key && link['class'] === inflection.singularize(key))
        link['class'] = 'page'

      newResource = new (getResource(link['class']))(this, { _links: { self: link }});
      this[key] = _buildFunction(newResource);
    }
  }
}

/**
 * Builds the function for the resource.
 *
 * @param {Object|Null} resource The resource for the getter.
 * @return {Function}
 * @api private
 */
function _buildFunction(resource) {
  return function(callback) {
    if (typeof callback == 'function') {
      if (resource !== null) return resource.exec.call(resource, callback);
      else return callback(null, null);
    }
    return resource;
  }
}


var _isNumber = require('./isNumber');
var _isObject = require('./isObject');
var REGEX_ENUM = require('./internal/enum/regexEnum');

/**
 * 校验参数 `val` 是否为 负整数
 *
 * 该接口存在两种模式, 即严格模式、非严格模式;
 *
 * 默认采用 `非严格模式`
 *
 * 若参数 `val` 为 number 类型, 并且是负整数 则返回 true, 否则返回 false
 * 若参数 `val` 为 string 类型, 并且通过验证为负整数字符串, 则返回 true, 否则返回 false
 * 若参数 `val` 不属于以上两种类型, 则直接返回 false
 *
 * 若参数 `val` 的值为空字符串（''、'   '）, 则直接返回 false
 *
 * 若是参数 `options` 指定了严格模式, 即 options.isStrict = true, 则对于 string 类型直接返回 false
 *
 * e.g.
 *
 * 待校验参数 '-3'
 *  > 若是严格模式, 则该参数值校验后, 返回 false
 *  > 若是非严格模式, 则该参数值校验后, 返回 true
 *
 * @param   {*}       val              待校验的参数
 * @param   {Object}  options          可选参数
 * @param   {Boolean} options.isStrict 是否严格模式
 * @return  {Boolean} 返回校验结果
 * @version 0.0.5
 * @since   0.0.4
 */
function _isNegativeInteger(val, options) {
  var opts = _isObject(options) ? options : {};

  if (opts.isStrict === true) {
    return _isNumber(val) && REGEX_ENUM.NEGATIVE_INTEGER_REX.test(val);
  }

  return REGEX_ENUM.NEGATIVE_INTEGER_REX.test(val);
}

module.exports = _isNegativeInteger;


/**
 * Created by allen.hu on 15/8/20.
 */
'use strict';

var _co = require('co');

var _co2 = _interopRequireDefault(_co);

var _ejs = require('ejs');

var _ejs2 = _interopRequireDefault(_ejs);

var _fs = require('fs');

var _fs2 = _interopRequireDefault(_fs);

var _md = require('md5');

var _md2 = _interopRequireDefault(_md);

var _crypto = require('crypto');

var _crypto2 = _interopRequireDefault(_crypto);

var _querystring = require('querystring');

var _querystring2 = _interopRequireDefault(_querystring);

var _urlencode = require('urlencode');

var _urlencode2 = _interopRequireDefault(_urlencode);

var _lodash = require('lodash');

var _bluebird = require('bluebird');

var _bluebird2 = _interopRequireDefault(_bluebird);

var _request = require('request');

var _request2 = _interopRequireDefault(_request);

var _payload = require('./payload');

function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }

var pub = false;
// algorithm to generate signature
var SHA256 = 'sha256';
// encoding of signature
var ENCODING = 'base64';
// default response content type
var XML_MINE = 'application/xml';
// promisify xml parse and request
var parseString = _bluebird2.default.promisify(require('xml2js').parseString);

/**
 * wrap request
 * @param {String} method
 * @param {String} uri
 * @param {Buffer|String} [body] data to send
 * @param {Object} headers
 */
function nosRequest(_ref) {
    var method = _ref.method;
    var uri = _ref.uri;
    var _ref$body = _ref.body;
    var body = _ref$body === undefined ? '' : _ref$body;
    var _ref$headers = _ref.headers;
    var headers = _ref$headers === undefined ? {} : _ref$headers;

    return new _bluebird2.default(function (resolve, reject) {
        (0, _request2.default)({ method: method, uri: uri, body: body, headers: headers }, function (err, response, body) {
            if (err) {
                return reject(err);
            }
            if (response.statusCode >= 400) {
                parseString(body).then(function (_ref2) {
                    var Error = _ref2.Error;

                    var e = {
                        code: Error.Code.join(''),
                        message: Error.Message.join('')
                    };
                    reject(e);
                }).catch(reject);
            } else if (body) {

                if (typeof body === 'string' && response.headers['content-type'].toLowerCase() === XML_MINE) {
                    parseString(body).then(function (json) {
                        resolve(json);
                    }).catch(reject);
                } else {
                    resolve((0, _lodash.assign)({ body: body, url: headers.url }));
                }
            } else {
                var ret = (0, _lodash.pick)(response.headers, ['content-type', 'x-nos-request-id', 'etag', 'content-range', 'last-modified', 'content-length']);
                (0, _lodash.assign)(ret, { url: headers.url });
                resolve(ret);
            }
        });
    });
}

//see: http://115.236.113.201/doc/nos_user_manual/_build/html/accessControl.html#head
/**
 * generate signature
 * @param secretKey
 * @param method
 * @param contentMd5
 * @param contentType
 * @param date
 * @param canonicalizedHeaders
 * @param canonicalizedResource
 * @return {String} signature
 * @private
 */
function generateSignature(secretKey, method, contentMd5, contentType, date, canonicalizedHeaders, canonicalizedResource) {

    var hmac = _crypto2.default.createHmac(SHA256, secretKey);

    var headers = canonicalizedHeaders.map(function (header) {
        return header.toLowerCase();
    }).sort().join('\n');

    var data = method + '\n' + contentMd5 + '\n' + contentType + '\n' + date + '\n';

    if (headers && headers.length) {
        data += headers + '\n';
    }
    data += canonicalizedResource;
    hmac.update(data);
    return hmac.digest(ENCODING);
}

function authorize(accessKey, secretKey, method, contentMd5, contentType, date, canonicalizedHeaders, canonicalizedResource) {
    return 'NOS ' + accessKey + ':' + generateSignature.apply(null, [].slice.call(arguments, 1));
}

function genResource(bucket, objectKey) {
    var query = arguments.length <= 2 || arguments[2] === undefined ? {} : arguments[2];

    var ret = '/' + bucket + '/' + objectKey;

    if (Object.keys(query).length) {
        ret += '?' + (0, _urlencode2.default)(_querystring2.default.stringify(opts));
    }
    return ret;
}

/**
 * 删除一个对象
 * @param {String} accessKey
 * @param {String} secretKey
 * @param {String} bucket
 * @param {String} objectKey
 * @param {Object} [opts]
 * @returns {Promise}
 */
var del = function del(host, accessKey, secretKey, bucket, objectKey) {
    var opts = arguments.length <= 5 || arguments[5] === undefined ? {} : arguments[5];

    var date = utcDate();
    var resource = genResource(bucket, objectKey, opts);
    var authorization = authorize(accessKey, secretKey, 'DELETE', [], "", date, resource);

    var url = 'http://' + bucket + '.' + host + '/' + objectKey;
    var headers = {
        Date: date,
        Authorization: authorization
    };

    return nosRequest({ method: 'del', uri: url, headers: headers });
};
/**
 * 批量删除对象
 * @param {String} accessKey
 * @param {String} secretKey
 * @param {String} bucket
 * @param {Array} objects  要删除的对象
 * @param {Object} [opts]
 * @param {boolean} [opts.quiet]  是否安静删除
 */
var bulkDel = function bulkDel(host, accessKey, secretKey, bucket, objects) {
    var opts = arguments.length <= 5 || arguments[5] === undefined ? {} : arguments[5];

    if (!objects.length) {
        return _bluebird2.default.reject(new Error('nothing to delete'));
    }

    var date = utcDate();
    var content = (0, _payload.deletePayload)({ objects: objects, quiet: opts.quiet });
    var contentLength = content.length;
    var url = 'http://' + bucket + '.' + host + '/?delete';
    var authorization = authorize(accessKey, secretKey, 'POST', '', '', date, [], '');

    var headers = {
        Date: date,
        Authorization: authorization,
        'Content-Length': contentLength
    };

    return nosRequest({ method: 'post', body: content, headers: headers, uri: url }).then(function (res) {
        if (!opts.quiet) {
            if (res.DeleteResult) {
                var _Error = res.DeleteResult.Error;

                var ret = _Error.map(function (detail) {
                    return {
                        key: detail.Key.join(''),
                        code: detail.Code.join(''),
                        message: detail.Message.join('')
                    };
                });
                return { error: ret };
            } else {
                return {};
            }
        }
        var _res$DeleteResult = res.DeleteResult;
        var _res$DeleteResult$Del = _res$DeleteResult.Deleted;
        var Deleted = _res$DeleteResult$Del === undefined ? [] : _res$DeleteResult$Del;
        var _res$DeleteResult$Err = _res$DeleteResult.Error;
        var Error = _res$DeleteResult$Err === undefined ? [] : _res$DeleteResult$Err;

        var deleted = Deleted.map(function (detail) {
            return detail.Key.join('');
        });
        var error = Error.map(function (detail) {
            return {
                key: detail.Key.join(''),
                code: detail.Code.join(''),
                message: detail.Message.join('')
            };
        });
        return { deleted: deleted, error: error };
    });
};
/**
 * 读取对象内容
 * @param {String} accessKey
 * @param {String} secretKey
 * @param {String} bucket
 * @param {String} objectKey
 * @param {Object} [opts]
 * @param {String} [opts.versionId] 版本
 * @param {String} [opts.modifiedSince]
 * @param {String} [opts.range] 用于断电续传
 * @returns {Promise}
 */
function getObject(host, accessKey, secretKey, bucket, objectKey) {
    var expires = arguments.length <= 5 || arguments[5] === undefined ? 0 : arguments[5];
    var opts = arguments.length <= 6 || arguments[6] === undefined ? {} : arguments[6];

    var date = utcDate();

    if (opts.versionId) url += '?versionId=' + opts.versionId;
    var resource = genResource(bucket, objectKey, (0, _lodash.pick)(opts, ['versionId']));
    var signature = encodeURIComponent(generateSignature(secretKey, 'GET', '', '', expires, [], resource));
    var url = pub ? 'http://' + bucket + '.' + host + '/' + objectKey : 'http://' + host + '/' + bucket + '/' + objectKey + '?NOSAccessKeyId=' + accessKey + '&Expires=' + expires + '&Signature=' + signature;
    var headers = { Date: date };
    if (opts.range) headers.Range = opts.range;
    if (opts.modifiedSince) headers['If-Modified-Since'] = opts.modifiedSince;
    headers['url'] = url;
    return nosRequest({ method: 'get', headers: headers, uri: url });
}

/**
 * 获取对象相关元数据信息
 * @param {String} accessKey
 * @param {String} secretKey
 * @param {String} bucket
 * @param {String} objectKey
 * @param {Object} [opts]
 * @param {String} [opts.modifiedSince]
 * @returns {Promise}
 */
var getMeta = function getMeta(host, accessKey, secretKey, bucket, objectKey) {
    var expires = arguments.length <= 5 || arguments[5] === undefined ? 0 : arguments[5];
    var opts = arguments.length <= 6 || arguments[6] === undefined ? {} : arguments[6];

    var date = utcDate();
    if (opts.versionId) url += '?versionId=' + opts.versionId;
    var resource = genResource(bucket, objectKey, (0, _lodash.pick)(opts, ['versionId']));
    var authorization = authorize(accessKey, secretKey, 'HEAD', '', '', date, [], resource);
    var signature = (0, _urlencode2.default)(generateSignature(secretKey, 'HEAD', '', '', date, [], resource));
    var url = pub ? 'http://' + bucket + '.' + host + '/' + objectKey : 'http://' + host + '/' + bucket + '/' + objectKey + '?NOSAccessKeyId=' + accessKey + '&Expires=' + expires + '&Signature=' + signature;

    var headers = { Date: date };
    if (opts.modifiedSince) headers['If-Modified-Since'] = opts.modifiedSince;
    headers['url'] = url;

    return nosRequest({ method: 'head', uri: url, headers: headers }).then(function (res) {
        var contentType = res['content-type'];
        var lastModified = res['last-modified'];
        var etag = res['etag'];
        var requestId = res['x-nos-request-id'];
        return { contentType: contentType, lastModified: lastModified, etag: etag, requestId: requestId };
    });
};
/**
 * 上传对象
 * @param {String} accessKey
 * @param {String} secretKey
 * @param {String} bucket
 * @param {String} objectKey
 * @param {String} file
 * @param {Object} nosHeader
 *
 */
var upload = function upload(host, accessKey, secretKey, bucket, objectKey, file) {
    var nosHeader = arguments.length <= 6 || arguments[6] === undefined ? {} : arguments[6];

    nosHeader['x-nos-storage-class'] = nosHeader['x-nos-storage-class'] || 'standard';
    var date = utcDate();
    var content = _fs2.default.readFileSync(file);
    var contentLength = content.length;
    var contentMd5 = (0, _md2.default)(content);

    var resource = genResource(bucket, objectKey);
    var canonicalizedHeaders = Object.keys(nosHeader).map(function (key) {
        return key + ':' + nosHeader[key];
    });
    var authorization = authorize(accessKey, secretKey, 'PUT', contentMd5, '', date, canonicalizedHeaders, resource);
    var url = 'http://' + bucket + '.' + host + '/' + objectKey;
    var headers = (0, _lodash.assign)({
        Date: date,
        'Content-Length': contentLength,
        'Content-MD5': contentMd5,
        Authorization: authorization,
        'url': url
    }, nosHeader);
    return nosRequest({ method: 'put', uri: url, body: content, headers: headers });
};
/**
 * 远程拷贝操作，生成一个新的对象
 * @param {String} accessKey
 * @param {String} secretKey
 * @param {String} bucket
 * @param {String} source 源对象
 * @param {String} dest   目标对象
 * @returns {Promise}
 */
var copy = function copy(host, accessKey, secretKey, bucket, source, dest) {
    var date = utcDate();
    var url = 'http://' + bucket + '.' + host + '/' + dest;
    var resource = genResource(bucket, dest);
    var authorization = authorize(accessKey, secretKey, 'PUT', '', '', date, ['x-nos-copy-source:/' + (0, _urlencode2.default)('/' + bucket + '/' + source)], resource);
    var headers = {
        Date: date,
        Authorization: authorization,
        'x-nos-copy-source': (0, _urlencode2.default)('/' + bucket + '/' + source)
    };
    return nosRequest({ method: 'put', uri: url, headers: headers });
};

var move = function move(host, accessKey, secretKey, bucket, source, dest) {
    var date = utcDate();
    var url = 'http://' + bucket + '.' + host + '/' + dest;
    var resource = genResource(bucket, dest);
    var authorization = authorize(accessKey, secretKey, 'PUT', '', '', date, ['x-nos-move-source:' + (0, _urlencode2.default)('/' + bucket + '/' + source)], resource);
    var headers = {
        Date: date,
        Authorization: authorization,
        'x-nos-move-source': (0, _urlencode2.default)('/' + bucket + '/' + source)
    };
    return nosRequest({ method: 'put', uri: url, headers: headers });
};

function utcDate() {
    var date = arguments.length <= 0 || arguments[0] === undefined ? new Date() : arguments[0];

    return date.toUTCString();
}

/**
 *
 * @param accessKey nos accessKey
 * @param secretKey nos secretKey
 * @param bucket nos bucket
 * @example
 * ```
 * var nos = require('node-nos')('accessKey', 'secretKey', 'bucket');
 * var file = 'readme'
 * nos
 *   .upload('key', file)
 *   .then(function(res) {})
 * ```
 */
module.exports = function (p, host, accessKey, secretKey, bucket) {
    pub = p;
    return {
        del: del.bind(null, host, accessKey, secretKey, bucket),
        bulk: bulkDel.bind(null, host, accessKey, secretKey, bucket),
        copy: copy.bind(null, host, accessKey, secretKey, bucket),
        getMeta: getMeta.bind(null, host, accessKey, secretKey, bucket),
        getObject: getObject.bind(null, host, accessKey, secretKey, bucket),
        upload: upload.bind(null, host, accessKey, secretKey, bucket),
        move: move.bind(null, host, accessKey, secretKey, bucket)
    };
};


var _isNumber = require('./isNumber');
var _isObject = require('./isObject');
var REGEX_ENUM = require('./internal/enum/regexEnum');

/**
 * 校验参数 `val` 是否为 非负整数, 即正整数和零
 *
 * 该接口存在两种模式, 即严格模式、非严格模式;
 *
 * 默认采用 `非严格模式`
 *
 * 若参数 `val` 为 number 类型, 并且是非负整数 则返回 true, 否则返回 false
 * 若参数 `val` 为 string 类型, 并且通过验证为非负整数字符串, 则返回 true, 否则返回 false
 * 若参数 `val` 不属于以上两种类型, 则直接返回 false
 *
 * 若参数 `val` 的值为空字符串（''、'   '）, 则直接返回 false
 *
 * 若是参数 `options` 指定了严格模式, 即 options.isStrict = true, 则对于 string 类型直接返回 false
 *
 * e.g.
 *
 * 待校验参数 '3'
 *  > 若是严格模式, 则该参数值校验后, 返回 false
 *  > 若是非严格模式, 则该参数值校验后, 返回 true
 *
 * @param   {*}       val              待校验的参数
 * @param   {Object}  options          可选参数
 * @param   {Boolean} options.isStrict 是否严格模式
 * @return  {Boolean} 返回校验结果
 * @version 0.0.5
 * @since   0.0.4
 */
function _isUnNegativeInteger(val, options) {
  var opts = _isObject(options) ? options : {};

  if (opts.isStrict === true) {
    return _isNumber(val) && REGEX_ENUM.UN_NEGATIVE_INTEGER_REX.test(val);
  }

  return REGEX_ENUM.UN_NEGATIVE_INTEGER_REX.test(val);
}

module.exports = _isUnNegativeInteger;


/**
 * create_markdown.js - Create the "create_markdown" task but only for application
 */

module.exports = function (grunt) {

    var common   = grunt.fcoo.common,
        options  = grunt.fcoo.options,
        paths    = grunt.fcoo.paths,
        _console = grunt.fcoo._console,
        lodash = require('lodash'),
        taskList = [],

        dirList,
        srcList = [],
        fileList = [];

    if (options.isPackage)
        return [];

    //Find all sub-dir of app/markdown that contains a da.md file
    dirList = grunt.file.expand({cwd: paths.app_markdown}, '**/da.md');
    for (var i=0; i<dirList.length; i++ ){
        dirList[i] = dirList[i].replace(/\\/g, "/").split('/')[0];
        srcList.push( dirList[i] + '/**/*.*' );
    }

    //Find all *.md files and create list of {cwd, src} in result
    function getFileList(){
        var result = [],
            fileNameList = ['da.md', 'en.md'],
            cwd, fileName;
        for (var i=0; i<dirList.length; i++ ){
            cwd = paths.temp_dist + dirList[i];
            for (var f in fileNameList){
                fileName = fileNameList[f];
                if ( grunt.file.isFile(cwd + '/' + fileName) )
                    result.push({ cwd: cwd, src: fileName } );
            }
        }
        return result;
    }

    //Update config for 'copy:AppMarkdown_2_TempDist'
    taskList.push( function(){
        var copyOptions = grunt.config('copy');
        copyOptions['AppMarkdown_2_TempDist'].src  = common.srcExclude_(  srcList );
        copyOptions['AppMarkdown_2_TempDist'].dest = paths.temp_dist;
        grunt.config('copy', copyOptions);
    });

    //Copy all markdown-files to TempDist or dev
    taskList.push( 'copy:AppMarkdown_2_TempDist' );

    //Create options for 'copy' to copy all files from node_modules/.../assets/markdown to each of the markdown-directories
    taskList.push( function(){
        var copyOptions         = grunt.config('copy'),
            copyMarkdownOptions = copyOptions['AssetsMarkdown_2_Temp'];

        for (var i=0; i<dirList.length; i++ ){
            //Create options for each directories
            var options = lodash.merge( {}, copyMarkdownOptions, {dest: paths.temp_dist + dirList[i]} );
            copyOptions['markdown_files'+i] = options;
        }

        grunt.config('copy', copyOptions);
    });

    //Add the copy-tasks
    for (var i=0; i<dirList.length; i++ )
        taskList.push( 'copy:markdown_files'+i );

    //Copy da.md to en.md if en.md don't exsists
    //var enMissingList = [];
    taskList.push( function(){
        var fileList = getFileList(),
            list = [],
            cwd, enMissing;
        for (var i=0; i<dirList.length; i++ ){
            cwd = paths.temp_dist + dirList[i];
            enMissing = true;
            for (var j=0; j<fileList.length; j++ )
                if ((fileList[j].cwd == cwd) && (fileList[j].src == 'en.md')){
                    enMissing = false;
                    break;
                }
            if (enMissing){
                //Ajust tast copy:daMd_2_enMd
                var copyOptions = grunt.config('copy');

                //Update options and run task
                copyOptions['daMd_2_enMd'+i] = lodash.merge( {},
                                                    copyOptions['daMd_2_enMd'], {
                                                        src : cwd + '/da.md',
                                                        dest: cwd + '/en.md'
                                                    }
                                                );
                grunt.config('copy', copyOptions);
                grunt.task.run('copy:daMd_2_enMd'+i);
            }
        }
    });

    //Update config for 'markdown'
    taskList.push( function(){
        var markdownOptions = grunt.config('markdown')
            fileList = getFileList();

        for (var i=0; i<fileList.length; i++ ){
            var fileObject = lodash.merge( {}, fileList[i], {dest: fileList[i].cwd, ext: '.html', expand: true } );
            if (fileObject.src == 'da.md')
                markdownOptions.da.files.push( fileObject )
            else
                markdownOptions.en.files.push( fileObject );
        }
        grunt.config('markdown', markdownOptions);
    });

    //Convert all md-files to htlm-files
    taskList.push( 'markdown:da',
                   'markdown:en' );

    //Update options for realFavicon:markdown
    if (dirList.length)
        taskList.push( function(){
            var fileList           = getFileList(),
                realFaviconOptions = grunt.config('realFavicon'),
                markdown           = realFaviconOptions.markdown;

            //Using favicons in parent-dir
            markdown.options.iconsPath = '..';

            markdown.options.html = []; //List of the HTML files where to inject favicon markups
            for (var i=0; i<fileList.length; i++ )
                markdown.options.html.push( fileList[i].cwd + '/' + fileList[i].src.replace(".md", ".html") );

            realFaviconOptions.markdown = markdown;
            grunt.config('realFavicon', realFaviconOptions);
        });

    return taskList;
}


"use strict"
var Url = require('url')
var _ = require('underscore')

module.exports = function(ngin) {
  var SportsModel = ngin.SportsModel
  var Super = SportsModel.prototype
  var config = ngin.config

  /**
   * Scopes the url to the tournament or flight
   *
   * @param {Object} options
   * @returns {String}
   * @api public
   */

  function scopeUrl(options, inst) {
    options = _.extend(_.clone(options || {}), inst)

    if (options && options.flight_id) {
      var route = ngin.Flight.urlRoot() + '/' + options.flight_id + StandingsPreference.urlRoot()
    } else if (options && (options.division_id && options.game_type)) {
      var route = ngin.Division.urlRoot() + '/' + options.division_id + StandingsPreference.urlRoot() + '/' + options.game_type
    } else {
      throw new Error('flight_id or division_id and game_type required to make standings preference api calls')
    }

    var base = config.urls && config.urls.sports || config.url
    return Url.resolve(base, route)
  }

  /**
   * StandingsPreference Class
   *
   * @param {Object} attr
   * @param {Object} options
   * @api public
   */

  var StandingsPreference = SportsModel.extend({

    fetch: function(options, callback) {
      var url = scopeUrl(options, this)
      return Super.fetch.call(this, url, options, callback)
    },

    save: function(options, callback) {
      if (typeof options == 'function') {
        callback = options
        options = {}
      }
      var url = scopeUrl(options, this)
      options.method = options.method || 'PUT'
      return Super.save.call(this, url, options, callback)
    },

    destroy: function(options, callback) {
      options = _.extend(_.clone(options || {}), inst)
      var url = scopeUrl(options)
      return Super.destory(this, url, options, callback)
    }

  }, {

    urlRoot: function() {
      return '/standings_preferences'
    }

  })

  return StandingsPreference

}


"use strict";

const resource = require("./resource");

/**
 * Exposes objects as another value when serialized.
 * `expose` uses the `expose` property of object [resources]{@link resource} internally.
 * @module expose
 * @param {!object} obj The object to expose.
 * @param {any} [val] The value that should be used instead of obj when serialized. If not specified and `obj` is an `Error`, `obj.message` will be made enumerable and obj will get itself as its exposed value.
 * @return {object} The exposed object.
 */
function expose(obj, val) {
	if(arguments.length === 1) {
		if(obj instanceof Error)
			Object.defineProperty(obj, "message", {
				enumerable: true,
				value: obj.message
			});

		val = obj;
	}

	return resource(obj, {
		expose: val
	});
}

function subclassError(error) {
	return class extends error {
		constructor(msg) {
			super(msg);

			expose(this);
		}
	};
}

module.exports = Object.assign(expose, {
	/**
	 * An Error constructor that builds exposed errors.
	 * @class
	 * @extends Error
	 */
	Error: subclassError(Error),

	/**
	 * An TypeError constructor that builds exposed errors.
	 * @class
	 * @extends TypeError
	 */
	TypeError: subclassError(TypeError),

	/**
	 * An ReferenceError constructor that builds exposed errors.
	 * @class
	 * @extends ReferenceError
	 */
	ReferenceError: subclassError(ReferenceError),

	/**
	 * An RangeError constructor that builds exposed errors.
	 * @class
	 * @extends RangeError
	 */
	RangeError: subclassError(RangeError),

	/**
	 * An SyntaxError constructor that builds exposed errors.
	 * @class
	 * @extends SyntaxError
	 */
	SyntaxError: subclassError(SyntaxError),

	/**
	 * An URIError constructor that builds exposed errors.
	 * @class
	 * @extends URIError
	 */
	URIError: subclassError(URIError),

	/**
	 * An EvalError constructor that builds exposed errors.
	 * @class
	 * @extends EvalError
	 */
	EvalError: subclassError(EvalError),

	/**
	 * Checks whether a given object is exposed.
	 * @param {any} object The object to check. This can be any value. The method will always return `false` for non-objects.
	 * @return {boolean} `true` if the object is exposed. `false` if not.
	 */
	isExposed(object) {
		return "expose" in resource(object);
	},

	/**
	 * Returns the value an object is exposed with. `undefined` if the given object is not exposed.
	 * @param {any} object The object to look up. This can be any value. The method will always return `undefined` for non-objects.
	 * @return {any} The exposed value of the given object.
	 */
	getValue(object) {
		return resource(object).expose;
	},

	/**
	 * Exposes the given object so that only the given iterable collection of properties will be serialized.
	 * @param {object} obj The object that should be exposed.
	 * @param {Iterable.<string>} properties An iterable collection of property strings that should be exposed.
	 * @return {object} The exposed object.
	 */
	properties(obj, properties) {
		if(!properties || typeof properties !== "object" || !(Symbol.iterator in properties))
			throw new TypeError("The properties to be exposed have to be iterable.");

		return resource(obj, Object.defineProperty({}, "expose", {
			configurable: true,
			enumerable: true,
			get: properties instanceof Map ? () => {
				const result = {};

				for(const [key, value] of properties)
					result[value] = obj[key];

				return result;
			} : () => {
				const result = {};

				for(const property of properties)
					result[property] = obj[property];

				return result;
			}
		}));
	}
});


'use strict';

var env = require('env-var')
  , url = require('url')
  , log = require('./log')
  , assert = require('assert')
  , VError = require('verror')
  , pkg = require('./config');

/**
 * Returns the app api key required to make a service request
 * @return {String}
 */
function getApiKey () {
  if (isLocal() && pkg.apiKey) {
    log.trace('using apiKey in fhconfig');
    return pkg.apiKey;
  } else {
    log.trace('using api key in FH_APP_API_KEY env var');
    return env('FH_APP_API_KEY').asString();
  }
}

/**
 * Returns the widget id required to make a service request
 * @return {String}
 */
var getWidget = exports.getWidget = function getWidget () {
  if (isLocal() && pkg.appId) {
    log.trace('using appId key in fhconfig');
    return pkg.appId;
  } else {
    log.trace('using appId in FH_WIDGET env var');
    return env('FH_WIDGET').asString();
  }
};

/**
 * Determine if the application is deployed on RHMAP, or locally
 * @param  {Object} env process.env Object
 * @return {Boolean}
 */
var isLocal = exports.isLocal = function isLocal () {
  // This should be suffificient to determine are we running locally or not, but
  // might require more considerations since it is a little crude
  var ret = (
    env('FH_USE_LOCAL_DB').asString() ||
    !env('FH_MILLICORE').asString() ||
    !env('FH_ENV').asString()
  );

  log.trace('are we running locally? %s', ret);

  return ret;
};

/**
 * Returns data in the FH_SERVICE_MAP format if it exists in fhconfig or the
 * older FH_SERVICE_MAP env var
 * @return Object
 */
exports.getServiceMap = function getServiceMap (callback) {
  var err = null
    , ret = null;

  if (pkg && typeof pkg.services === 'object') {
    log.trace('fhconfig "services" entry found. using it for service mapping');
    ret = {};
    Object.keys(pkg.services).forEach(function (guid) {
      ret[guid] = pkg.services[guid].devUrl;
    });
  } else if (env('FH_SERVICE_MAP').asString()) {
    log.trace(
      'fhconfig "services" entry not found. using FH_SERVICE_MAP instead'
    );
    try {
      ret = env('FH_SERVICE_MAP').asJson();
    } catch (e) {
      err = new VError(e, 'cannot parse FH_SERVICE_MAP environment variable');
    }
  } else {
    log.warn(
      'no service mappings found as fhconfig.services, or FH_SERVICE_MAP'
    );
  }

  callback(err, ret);
};

/**
 * Normalise a host string to {HOST}.feedhenry.com
 * @param  {String} host
 * @return {String}
 */
exports.genHost = function genHost () {
  var ret = exports.getDomain();

  assert(
    ret && ret.length >= 0,
    'fhconfig.json domain property must be provided, or FH_MILLICORE env var ' +
    'must be set on the system'
  );

  assert.notEqual(
    ret.indexOf('http'),
    0,
    'fhconfig.json "domain" must not contain a protcol string, just the host' +
    ', i.e "http://" or "https://"'
  );

  var parsedUrl = url.format({
    host: ret,
    protocol: 'https:',
    pathname: '/box/srv/1.1/ide/apps/app/hosts'
  });

  log.trace('generated guid lookup endpoint as "%s"', parsedUrl);

  return parsedUrl;
};

/**
 * Returns the domain this application is running on.
 *
 * Locally it will use the domain in the fhconfig, else it'll check the
 * environment variables, be sure they match to avoid headaches
 * @return {String}
 */
exports.getDomain = function () {
  var ret = null;

  if (isLocal() && pkg.domain) {
    log.trace('using fhconfig "domain" for host generation');
    ret = pkg.domain;
  } else {
    log.trace('using FH_MILLICORE for host generation');
    ret = env('FH_MILLICORE').asString();
  }

  log.trace('determined that domain is "%s"', ret);

  return ret;
};

/**
 * Returns service call headers that are required for secure calls
 * @return {Object}
 */
exports.getServiceCallHeaders = function () {
  var ret = {
    'x-fh-auth-app': getApiKey(),
    'x-request-with': getWidget()
  };

  log.trace('generated service call headers as', ret);

  return ret;
};

/**
 * Return the FH_ENV variable that indicates the env we're running in
 * @return {String}
 */
exports.getEnv = function getEnv () {
  var e = env('FH_ENV').asString();

  if (!e) {
    log.warn(
      'FH_ENV environment variable not set. this is OK during local ' +
      'development, and will default to "dev" unless you need to set it ' +
      'explicitly e.g "export FH_ENV=rht-dev" or similar in bash'
    );

    return 'dev';
  } else {
    return e;
  }
};


const DateTime = require('luxon').DateTime
const jsonPathChecker = require('./json_path_checker')
const TYPES = require('./types')

class Constraint {
  constructor () {
    this.conditions = []
  } // constructor

  addCondition (condition) {
    this.conditions.push(condition)
  } // addCondition

  applies (node, role) {
    return (this.conditions.length === 0) ||
      this.conditions.some(c => c.constraintApplies(node, role))
  } // applies
} // class Constraint

// Verify that there is only one of a selection of fields
class OnlyOneOfConstraint extends Constraint {
  constructor (fields) {
    super()
    this.fields = fields
  } // constructor

  check (node, path, problems) {
    const selectionFields = Object.keys(node).filter(f => this.fields.includes(f))
    if (selectionFields.length > 1) {
      problems.push(`${path} may have only one of ${this.fields}`)
    }
  } // check

  toString () {
    const conds = this.conditions.length ? ` ${this.conditions.length} conditions` : ''
    return `<Node can have one of ${JSON.stringify(this.fields)} fields${conds}>`
  }
} // class OnlyOneOfConstraint

// Verify that array field is not empty
class NonEmptyConstraint extends Constraint {
  constructor (name) {
    super()
    this.name = name
  } // constructor

  check (node, path, problems) {
    const a = node[this.name]
    if (Array.isArray(a) && a.length === 0) {
      problems.push(`${path}.${this.name} is empty, non-empty required`)
    }
  } // check

  toString () {
    const conds = this.conditions.length ? ` ${this.conditions.length} conditions` : ''
    return `<Array field ${this.name} should not be empty${conds}>`
  }
} // class NonEmptyConstraint

// Verify node has the named field, or one of the named fields
class HasFieldConstraint extends Constraint {
  constructor (names) {
    super()
    this.names = Array.isArray(names) ? names : [ names ]
  } // constructor

  check (node, path, problems) {
    const keys = Object.keys(node)
    if (!this.names.some(name => keys.includes(name))) {
      problems.push(`${path} does not have required field ` +
        (this.names.length === 1 ? `"${this.names[0]}"` : `from ${this.names}`)
      )
    } // if ...
  } // check

  toString () {
    const conds = this.conditions.length ? ` ${this.conditions.length} conditions` : ''
    return `<Field ${this.names} should be present${conds}>`
  } // toString
} // HasFieldConstraint

// Verify node does not have the named field
class DoesNotHaveFieldConstraint extends Constraint {
  constructor (name) {
    super()
    this.name = name
  } // constructor

  check (node, path, problems) {
    if (node[this.name]) {
      problems.push(`${path} have forbidden field "${this.name}"`)
    }
  } // check

  toString () {
    const conds = this.conditions.length ? ` ${this.conditions.length} conditions` : ''
    return `<Field ${this.name} should be absent${conds}>`
  } // toString
} // class DoesNotHaveFieldConstructor

// Verify type of a field in a node
class FieldTypeConstraint extends Constraint {
  constructor (name, type, isArray, isNullable) {
    super()
    this.name = name
    this.type = type
    this.isArray = isArray
    this.isNullable = isNullable
  } // constructor

  check (node, path, problems) {
    // type checking is orthogonal to existence checking
    if (!Object.keys(node).includes(this.name)) return

    const value = node[this.name]
    path = `${path}.${this.name}`

    if (value === null) {
      if (!this.isNullable) {
        problems.push(`${path} should be non-null`)
      }
      return
    }

    if (this.isArray) {
      if (Array.isArray(value)) {
        value.forEach((element, index) =>
          this.valueCheck(element, `${path}[${index}]`, problems)
        )
      } else {
        this.report(path, value, 'an Array', problems)
      }
    } else {
      this.valueCheck(value, path, problems)
    }
  } // check

  valueCheck (value, path, problems) {
    const report = message => this.report(path, value, message, problems)

    switch (this.type) {
      case TYPES.object:
        if (typeof value !== 'object' || Array.isArray(value)) report('an Object')
        break
      case TYPES.array:
        if (!Array.isArray(value)) report('an Array')
        break
      case TYPES.string:
        if (typeof value !== 'string') report('a String')
        break
      case TYPES.integer:
        if (!Number.isInteger(value)) report('an Integer')
        break
      case TYPES.boolean:
        if (typeof value !== 'boolean') report('a Boolean')
        break
      case TYPES.float:
      case TYPES.numeric:
        if (typeof value !== 'number') report('numeric')
        break
      case TYPES.JSONPath:
        if (!jsonPathChecker.isPath(value)) report('a JSONPath')
        break
      case TYPES.referencePath:
        if (!jsonPathChecker.isReferencePath(value)) report('a Reference Path')
        break
      case TYPES.timestamp:
        const ts = DateTime.fromISO(value)
        if (!ts.isValid) report('an ISO8601 timestamp') // Spec actually says RFC3339, but close enough
        break
      case TYPES.URI:
        if ((typeof value !== 'string') || (!value.match(/^[a-z]+:/))) report('a URI')
        break
    }
  } // valueCheck

  report (path, value, message, problems) {
    if (typeof value === 'string') {
      value = `"${value}"`
    }
    problems.push(`${path} is ${value} but should be ${message}`)
  } // report

  toString () {
    const conds = this.conditions.length ? ` ${this.conditions.length} conditions` : ''
    const nullable = this.isNullable ? ' (nullable)' : ''
    return `<Field ${this.name} should be of type ${this.type}${conds}>${nullable}`
  }
} // class FieldTypeConstraint

// Verify constraints on values of a named field
class FieldValueConstraint extends Constraint {
  constructor (name, params) {
    super()
    this.name = name
    this.params = new Map(Object.entries(params))
  } // constructor

  check (node, path, problems) {
    // value checking is orthogonal to existence checking
    if (!Object.keys(node).includes(this.name)) return

    const value = node[this.name]

    if (this.params.has('enum')) {
      if (!this.params.get('enum').includes(value)) {
        problems.push(`${path}.${this.name} is "${value}", not one of the allowed values ${this.params.get('enum')}`)
      }

      return // if enum constraints are provided, others are ignored
    } // if enum ...

    if (this.params.has('equal')) {
      if (value !== this.params.get('equal')) {
        problems.push(`${path}.${this.name} is "${value}", but required value is ${this.params.get('equal')}`)
      }
    } // if equal ...

    if (this.params.has('floor')) {
      if (value <= this.params.get('floor')) {
        problems.push(`${path}.${this.name} is "${value}", but allowed floor is ${this.params.get('floor')}`)
      }
    } // if floor ...

    if (this.params.has('min')) {
      if (value < this.params.get('min')) {
        problems.push(`${path}.${this.name} is "${value}", but allowed minimum is ${this.params.get('min')}`)
      }
    } // if min ...

    if (this.params.has('ceiling')) {
      if (value >= this.params.get('ceiling')) {
        problems.push(`${path}.${this.name} is "${value}", but allowed ceiling is ${this.params.get('ceiling')}`)
      }
    } // if ceiling ...

    if (this.params.has('max')) {
      if (value > this.params.get('max')) {
        problems.push(`${path}.${this.name} is "${value}", but allowed max is ${this.params.get('max')}`)
      }
    } // if max ...
  } // check

  toString () {
    const conds = this.conditions.length ? ` ${this.conditions.length} conditions` : ''
    return `<Field ${this.name} has constraints ${JSON.stringify(strMapToObj(this.params))}${conds}>`
  } // toString
} // FieldValueConstraint

function strMapToObj (strMap) {
  const obj = { }
  for (const [ k, v ] of strMap) {
    // We don’t escape the key '__proto__'
    // which can cause problems on older engines
    obj[k] = v
  }
  return obj
} // strMapToObj

module.exports = {
  onlyOneOf: fields => new OnlyOneOfConstraint(fields),
  nonEmpty: name => new NonEmptyConstraint(name),
  hasField: names => new HasFieldConstraint(names),
  doesNotHaveField: name => new DoesNotHaveFieldConstraint(name),
  fieldType: (name, type, isArray, isNullable) => new FieldTypeConstraint(name, type, isArray, isNullable),
  fieldValue: (name, params) => new FieldValueConstraint(name, params)
}


/*!
 * Module dependencies
 */
var resources = {}
  ;

/*!
 * Expose `getResource()`.
 */
exports = module.exports = getResource;

/**
 * Get or require the resource based on the class name.
 *
 * @param {String} name The class name for this resource.
 * @return {Function}
 * @api private
 */
function getResource(name) {
  if (name in resources) return resources[name];
  try { return resources[name] = require('../resource/' + name); }
  catch(err) { return resources[name] = require('../resource'); }
}


var _isObject = require('./isObject');
var _isFunction = require('./isFunction');

/**
 * 校验参数 `val` 是否为纯粹的 object
 *
 * 若参数 `val` 是纯粹的 object, 则返回 true, 否则返回 false
 *
 * @param   {*}       val 待校验的参数
 * @return  {Boolean} 返回校验结果
 * @version 0.0.5
 * @since   0.0.4
 */
function _isPlainObject(val) {
  if (!_isObject(val)) {
    return false;
  }

  var proto = Object.getPrototypeOf(val);

  if (proto === null) {
    return true;
  }

  var ctor = proto.constructor;

  return _isFunction(ctor);
}

module.exports = _isPlainObject;


/* eslint-disable prefer-rest-params */

/* putil-stringify
 ------------------------
 (c) 2017-present Panates
 SQB may be freely distributed under the MIT license.
 For details and documentation:
 https://panates.github.io/putil-stringify/
 */
/**
 *
 * @param obj
 * @param {Function} [replacer]
 * @param {Number|String} [space]
 * @return {string}
 */

function stringify(obj, replacer, space) {

  if (typeof replacer === 'string' || typeof replacer === 'number') {
    // noinspection JSValidateTypes
    space = replacer;
    // noinspection JSValidateTypes
    replacer = undefined;
  }
  const replacerArgs = replacer ? replacer.length : 0;
  if (space && typeof space === 'number') {
    var i = space;
    space = ' ';
    while (i-- > 1)
      space += ' ';
  }

  const toString = function(o, k, v) {
    var z;
    if (replacerArgs === 2) {
      z = replacer(k, v);
      return z === undefined ? z : JSON.stringify(z);
    }
    if (replacerArgs === 3) {
      z = replacer(o, k, v);
      return z === undefined ? z : JSON.stringify(z);
    }
    return JSON.stringify(v);
  };

  var result = '';
  var indent = 0;
  const refs = [];
  const doStringify = function(obj, key, value) {
    // Check circular references
    const arrayOfObject = Array.isArray(value) || isPlainObject(value);
    if (arrayOfObject && refs.indexOf(value) >= 0) {
      result += '"[Circular]"';
      return;
    }
    const getIndent = function(n) {
      if (!space) return '';
      indent += (n || 0);
      var s = '\n';
      var k = indent;
      while (k-- > 0)
        s += space;
      return s;
    };

    var i;
    // serialize array
    if (Array.isArray(value)) {
      refs.push(value);
      result += '[' + getIndent(1);
      i = 0;
      value.forEach(function(v, k) {
        if (Array.isArray(v) || isPlainObject(v)) {
          result += (i ? ',' + getIndent() : '');
          doStringify(value, k, v);
          i++;
        } else {
          const s = toString(value, k, v);
          if (s !== undefined) {
            result += (i ? ',' + getIndent() : '') + s;
            i++;
          }
        }
      });
      result += getIndent(-1) + ']';
      refs[refs.indexOf(value)] = undefined;
      return;
    }
    // serialize object
    if (isPlainObject(value)) {
      refs.push(value);
      result += '{' + getIndent(1);
      i = 0;
      Object.getOwnPropertyNames(value).forEach(function(k) {
        const v = value[k];
        if (Array.isArray(v) || isPlainObject(v)) {
          result += (i ? ',' + getIndent() : '') +
              JSON.stringify(k) + ':' + (space ? ' ' : '');
          doStringify(value, k, v);
          i++;
        } else {
          const s = toString(value, k, v);
          if (s !== undefined) {
            result += (i ? ',' + getIndent() : '') +
                JSON.stringify(k) + ':' + (space ? ' ' : '') + s;
            i++;
          }
        }
      });
      result += getIndent(-1) + '}';
      refs[refs.indexOf(value)] = undefined;
      return;
    }
    // serialize values
    result += toString(obj, key, value);
  };
  doStringify(undefined, undefined, obj);
  return result;
}

const objCtorStr = Function.prototype.toString.call(Object);

function isPlainObject(obj) {
  if (typeof obj === 'object' &&
      Object.prototype.toString.call(obj) === '[object Object]') {
    const proto = Object.getPrototypeOf(obj);
    if (proto) {
      const ctor = Object.prototype.hasOwnProperty.call(proto, 'constructor') &&
          proto.constructor;
      return typeof ctor === 'function' && (ctor instanceof ctor) &&
          Function.prototype.toString.call(ctor) === objCtorStr;
    } else return true;
  }
  return false;
}

module.exports = stringify;