/**
 * @author: daxingplay <daxingplay@gmail.com>
 * @date: 1/27/15
 */

"use strict";

var path = require('path');
var fs = require('fs');
var utils = require('./utils');
var _ = require('lodash');
var compressor = require('./cssmin').compressor;
var iconv = require('iconv-lite');
var os = require('os');

function CssCombo(cfg){
    var self = this;

    self.imports = [];
    /**
     * 额外进行查找的路径，如：paths: 'path/to/app/utils', path/to/app/public/a.js文件内容：@import "mod.css"
     *  1、现在当前路径下寻找: path/to/app/public/mod.css
     *  2、若找不到，则再查找: path/to/app/utils/mod.css
     */
    self.paths = cfg.paths || [];
    utils.debug = cfg.debug;
    cfg.target = cfg.src || cfg.target;
    cfg.native2ascii = cfg.native2ascii !== false;
    cfg.replaceFont = cfg.replaceFont !== false;
    if(!cfg.target) {
        throw new Error('Please enter css path');
    }else{
        cfg.target = cfg.src = path.resolve(cfg.target);

        if(!cfg.outputEncoding || cfg.outputEncoding == 'gbk' || cfg.outputEncoding == 'GBK' || cfg.outputEncoding == 'gb2312') {
            cfg.outputEncoding = '';
        }

        cfg.linefeed = cfg.linefeed || os.EOL;

        cfg.compress = !!cfg.compress;

        cfg.silent = !!cfg.silent;

        cfg.dest = cfg.output = (cfg.dest || cfg.output) ? path.resolve(cfg.dest) : '';

        cfg.suffix = cfg.suffix || '.combo';

        cfg.sourceDir = path.dirname(cfg.target);

        self.config = cfg;
        //self.build(callback);
    }
}

CssCombo.prototype = {
    error: function(e){
        var self = this;
        return {
            type: e.type || 'Syntax',
            message: e.message,
            filename: path.basename(self.config.target),
            filepath: self.config.target
        };
    },
    isExcluded: function(filename){
        var config = this.config;
        for(var i in config.exclude){
            if(config.exclude[i].test(filename)){
                return true;
            }
        }
        return false;
    },
    isRemoteFile: function(filepath){
        return (/^http(s?):\/\//).test(filepath) || /^\/\//.test(filepath);
    },
    getFileContent: function(file, callback){
        var self = this,
            config = self.config,
            content = '';
        if(!self.isRemoteFile(file)){
            if(!self.isExcluded(file)){
                var filePath = path.resolve(config.base, file);
                if(fs.existsSync(filePath)){
                    var buf = fs.readFileSync(filePath);
                    content = iconv.decode(buf, config.inputEncoding ? config.inputEncoding : utils.detectCharset(buf));
                }else{
                    // 若在当前文件夹找不到，就去paths中寻找，直到找到为止
                    self.paths.forEach(function( pathBase ){
                        if( !content ){
                            var outFilePath = path.resolve( pathBase, file);
                            if( fs.existsSync(outFilePath) ){
                                var buf = fs.readFileSync( outFilePath );
                                content = iconv.decode(buf, config.inputEncoding ? config.inputEncoding : utils.detectCharset(buf));
                            }
                        }
                    });

                    if(!content){
                        utils.log('cannot find file ' + filePath, 'warning');
                    }
                }
            }else{
                utils.log('file excluded: ' + file, 'debug');
            }
            callback && callback(content);
        }else{
            utils.log('Try to get remote file: ' + file, 'debug');
            utils.getRemoteFile(file, function(data, charset){
                content = iconv.decode(data, charset);
                callback && callback(content);
            });
        }
    },
    generateOutput: function(fileContent){
        var self = this,
            config = self.config,
            commentTpl = [
                '/*' + config.linefeed,
                'combined files : ' + config.linefeed,
                config.linefeed
            ];

        for (var i in self.imports) {
            commentTpl.push(self.imports[i] + config.linefeed);
        }

        commentTpl.push('*/' + config.linefeed);

        // join combo comment to file content.
        fileContent = commentTpl.join('') + config.linefeed + fileContent;
        var cssFileExt = config.suffix + '.css';
        if(config.compress){
            utils.log('start compress file.', 'debug');
            fileContent = compressor.cssmin(fileContent);
            cssFileExt = '.css';
        }
        fileContent = iconv.encode(fileContent, config.outputEncoding || 'gbk');
        var comboFile = config.output;
        // if output is not a file name, then generate a file name with .combo.css or .css.
        if(path.extname(config.output) !== '.css'){
            comboFile = path.resolve(config.output, path.basename(config.target).replace(/(\.source)?\.css/, cssFileExt));
            utils.log('dest is dir, I guess the output file is: ' + comboFile, 'debug');
        }

        utils.log('start generate combo file: ' + comboFile, 'debug');

        utils.mkdirSync(path.dirname(comboFile));

//        fs.writeFileSync(comboFile, fileContent, '');
        // if exists, unlink first, otherwise, there may be some problems with the file encoding.
        if(fs.existsSync(comboFile)){
            fs.unlinkSync(comboFile);
        }

        // write file
        var fd = fs.openSync(comboFile, 'w');
        fs.writeSync(fd, fileContent, 0, fileContent.length);
        fs.closeSync(fd);

        utils.log('Successfully generated combo file: ' + comboFile, 'debug');
    },
    // 从内容中提取import列表
    extractImports: function(content) {
        var reg = /@import\s*(url)?\s*[\('"]+([^'"]+)\.css(\?[^\s]*)?\s*['"\)]+\s*[^;]*;/ig;
        var result = reg.exec(content);
        if (result && result[2]) {
            return {
                match: result[0],
                filePath: result[2] + '.css'
            };
        }
        return null;
    },
    analyzeImports: function(content, callback){
        var self = this;
//        utils.log('Analyzing ' + self.config.target, 'debug');
        if(content){
            var result = self.extractImports(content);
            if(result !== null){
                var filePath = result.filePath;
                self.imports.push(filePath);
                self.getFileContent(filePath, function(data){
                    data = self.modifySubImportsPath(data,filePath);
                    data = data.replace(/(['"]|\():/g,'$1');
                    data = data.replace(/(['"])%/g,'$1');
                    content = content.replace(result.match, '\n' + data + '\n');
                    content = self.analyzeImports(content, callback);
                });
            }else{
                callback && callback(content);
            }
        }else{
            utils.log('content empty.', 'debug');
            callback && callback(content);
        }
    },
    // 修改子文件import的相对路径
    // 子文件夹中所有的相对路径都要进行转换
    // 先看一下什么形式的地址需要转换
    // @import "./mods/import.css"
    // background: url("./mods/import.png");
    // list-style: url("./mods/import.jpg");
    // @font-face {
    //      src:url("./mods/import.ttf");
    // }
    modifySubImportsPath: function(content,filePath) {
        'use strict';

        var self = this;

        var regImport = /@import\s*(url)?\(?['"]([^'"%]+)\.css['"]\)?[^;]*;/ig,
            regImageOrFont = /(url)?\(['"]?([^:\)]+\.(png|jpg|gif|jpeg|ttf|eot|woff|svg))([^\)]*)['"]?\)/ig,
            importResult,
            picAndFontResult;

        var importFilePath = path.dirname(path.resolve(self.config.sourceDir,filePath));

        // 替换import
        importResult = regImport.exec(content);
        if (typeof importResult !== 'undefined' && importResult && importResult[2]) {
            var importAbsoluteUrl = path.resolve(importFilePath,importResult[2]);
            // 用%号表示已经替换好的import路径，后续会再去掉百分号,这里替换的时
            // 候要注意全局的替换
            var regimportReplace = new RegExp(importResult[2],'g');
            content = content.replace(regimportReplace, "%" + path.relative(self.config.sourceDir,importAbsoluteUrl));
            return self.modifySubImportsPath(content, filePath);
        }
        // 替换图片和font的路径
        picAndFontResult = regImageOrFont.exec(content);
        if (typeof picAndFontResult !== 'undefined' && picAndFontResult && picAndFontResult[2] && !/^\/\/[^\/]+/.test(picAndFontResult[2])) {
            var regpicReplace = new RegExp(picAndFontResult[2],'g');
            var picAbsolutePath = path.resolve(importFilePath,picAndFontResult[2]);
            //解决win平台下路径的斜杠问题
            var isWin = (process.platform === 'win32');
            var _path = path.relative(self.config.sourceDir,picAbsolutePath);
            if(isWin){
                _path = path.relative(self.config.sourceDir,picAbsolutePath).split(path.sep).join("\/");
            }
            // 用：号表示已经替换好的import路径，后续会再去掉冒号
            content = content.replace(regpicReplace, ":" + _path);
            return self.modifySubImportsPath(content, filePath);
        }
        return content;
    },
    removeCharset: function(str) {
        return (str || '').replace(/@charset\s+['|"][^;"']+?["|']\s*;/g, '');
    },
    analyze: function(callback){
        var self = this,
            config = self.config,
            file = config.target,
            report = {};

        utils.log('start analyze file : ' + file, 'debug');

        config.base = path.dirname(file);

        var data = fs.readFileSync(file);
        utils.log('Cur file is ' + self.config.target, 'debug');
        utils.log('file read: ' + file, 'debug');

        config.inputEncoding = config.inputEncoding ? config.inputEncoding : utils.detectCharset(data);
        var fileContent = iconv.decode(data, config.inputEncoding);
        utils.log('file charset is: ' + config.inputEncoding, 'debug');

        // preserve data url and comment.
        var preservedTokens = [];
//        fileContent = compressor._extractDataUrls(fileContent, preservedTokens);
        fileContent = compressor._extractComments(fileContent, preservedTokens);

        // start analyze file content
        self.analyzeImports(fileContent, function(data){
            utils.log('analyze done.', 'debug');
            // after combo, @charset position may be changed. since the output file encoding is specified, we should remove @charset.
            data = self.removeCharset(data);
            // restore all comments back.
            data = compressor._restoreComments(data, preservedTokens);
            //convert ZH to unicode
            if(config.native2ascii === true){
                data = data.replace(/[\u4E00-\u9FA5\uF900-\uFA2D]/g,function($1){
                    return '\\'+utils.a2u($1).replace('%u','');
                });
            }
            //convert unicode font-family to EN
            if(config.replaceFont === true){
                data = data.replace(/[\'|\"](\\\w{4}.*)[\'|\"]/g,function($1,$2){
                    $2 = $2.trim();
                    var en = utils.unicode2En($2),
                        temp = '';
                    if(_.isArray(en)){
                        for(var i = 0;i<en.length;i++) {
                            temp += '"' + en[i] + '",';
                        }
                        en = temp.slice(0,-1);
                    }else{
                        en = '"' + en + '"';
                    }
                    return $2 ? en : $1;
                });
            }
            //self.generateOutput(data);

            report.imports = self.imports;
            report.target = self.config.target;
            report.output = self.config.output;
            report.content = data;
            callback && callback(null, report);
        });
    },
    build: function(callback){
        var self = this;
        self.analyze(function(err, report){
            self.generateOutput(report.content);
            callback && callback(null, report);
        });
    }
};

module.exports = CssCombo;


/**
 * @copyright Copyright 2016 Kevin Locke <kevin@kevinlocke.name>
 * @license MIT
 */

'use strict';

const Chalk = require('chalk').constructor;
const util = require('util');
const url = require('url');

const InvalidSlugError = require('./invalid-slug-error');
const Shortline = require('./shortline');
const SlugDetectionError = require('./slug-detection-error');
const git = require('./git');

const debug = util.debuglog('travis-status:git-status-checker');

/** Gets the right-trimmed stdout result of execFileP.
 * @param {!Array<string>} result Result Array from execFileP
 * @return {string} The first element of result, right-trimed.
 * @private
 */
function getTrimmedStdout(result) {
  return result[0].trimRight();
}

/** Options for {@link GitStatusChecker}.
 *
 * @typedef {{
 *   err: stream.Writable|undefined,
 *   in: stream.Readable|undefined,
 *   interactive: boolean|undefined,
 *   out: stream.Writable|undefined
 * }} GitStatusCheckerOptions
 * @property {stream.Writable=} err Stream to which errors (and non-output
 * status messages) are written. (default: process.stderr)
 * @property {stream.Readable=} in Stream from which input is read. (default:
 * process.stdin)
 * @property {boolean=} interactive Be interactive and colorful
 * @property {stream.Writable=} out Stream to which output is written.
 * (default: process.stdout)
 */
// var GitStatusCheckerOptions;

/** Creates a status checker for the current git repository.
 *
 * @constructor
 * @param {GitStatusCheckerOptions=} options Options.
 */
function GitStatusChecker(options) {
  if (options && typeof options !== 'object') {
    throw new TypeError('options must be an object');
  }

  options = Object.assign(
    {
      in: process.stdin,
      out: process.stdout,
      err: process.stderr
    },
    options
  );

  if (!options.in || typeof options.in.read !== 'function') {
    throw new TypeError('options.in must be a stream.Readable');
  }
  if (!options.out || typeof options.out.write !== 'function') {
    throw new TypeError('options.out must be a stream.Writable');
  }
  if (!options.err || typeof options.err.write !== 'function') {
    throw new TypeError('options.err must be a stream.Writable');
  }

  this._options = options;
  this._chalk = new Chalk({
    enabled: Boolean(
      options.interactive !== undefined
        ? options.interactive
        : options.out.isTTY
    )
  });
}

/** Message returned when the repository slug is invalid.
 * @const
 */
GitStatusChecker.SLUG_INVALID =
  'GitHub repo name is invalid, it should be on the form \'owner/repo\'';

/** RegExp used to test the validity of a repository slug.
 * Note:  travis.rb only checks for a '/' character, but GitHub currently
 * requires only ASCII letters, numbers, '.', and '-'.  Choose a middle ground
 * which catches likely erroneous names.
 * @const
 */
GitStatusChecker.SLUG_VALID_RE = /^[^\s/]+\/[^\s/]+$/;

/** Git configuration option name in which the Travis CI slug is stored.
 * @const
 */
GitStatusChecker.SLUG_CONFIG_NAME = 'travis.slug';

/** Checks that a repository slug has the correct format.
 * @param {string} slug Slug to check.
 * @return {string} slug.
 * @throws {InvalidSlugError} If slug does not have the correct
 * format.
 * @private
 */
GitStatusChecker.checkSlugFormat = function checkSlugFormat(slug) {
  if (!GitStatusChecker.SLUG_VALID_RE.test(slug)) {
    const err = new InvalidSlugError(GitStatusChecker.SLUG_INVALID);
    err.slug = slug;
    throw err;
  }
  return slug;
};

/** Resolves a named git commit to its hash id.
 * @param {string} commitName Name of the commit (branch, tag, etc.) for which
 * to get the hash.  Can be a hash, which resolves to itself.
 * @return {!Promise<string>} Hash for commitName, or Error if
 * commitName does not name a commit.
 * @private
 */
GitStatusChecker.prototype.resolveHash = function resolveHash(commitName) {
  return git('rev-parse', '--verify', commitName)
    .then(getTrimmedStdout);
};

/** Stores a repository slug as travis.slug in the local git
 * config.
 * @param {string} slug Slug to store.
 * @return {!Promise<string>} Promise with slug.
 * @private
 */
GitStatusChecker.prototype.storeSlug = function storeSlug(slug) {
  try {
    GitStatusChecker.checkSlugFormat(slug);
  } catch (err) {
    return Promise.reject(err);
  }

  return git('config', GitStatusChecker.SLUG_CONFIG_NAME, slug)
    .then(() => slug);
};

/** Tries to store a repository slug as travis.slug in the local
 * git config, prints an error on failure.
 * @param {string} slug Slug to store.
 * @return {!Promise<string>} Promise with slug, never
 * Error.
 * @private
 */
GitStatusChecker.prototype.tryStoreSlug = function tryStoreSlug(slug) {
  const {err} = this._options;
  return this.storeSlug(slug)
    .catch((errStore) => {
      err.write(`Error storing slug in git config: ${errStore}`);
      return slug;
    });
};

/** Prompts the user to confirm that a repository slug is correct.
 * @param {string} slug Slug to prompt about.
 * @return {!Promise<string>} Promise with correct slug.
 * @private
 */
GitStatusChecker.prototype.confirmSlug = function confirmSlug(slug) {
  const shortline = new Shortline({
    input: this._options.in,
    output: this._options.err
  });
  const question = `Detected repository as ${this._chalk.yellow(slug)
  } is this correct? `;
  return shortline.agree(question)
    .then((correct) => {
      if (correct) {
        return slug;
      }

      // Note:  travis.rb accepts an invalid slug here then errors out after
      // storing it in .git/config.  We re-prompt until valid.
      return shortline.ask('Repository slug (owner/name): ', {
        default: slug,
        responses: {
          notValid: GitStatusChecker.SLUG_INVALID
        },
        trim: true,
        validate: GitStatusChecker.SLUG_VALID_RE
      });
    });
};

/** Detects the current branch the current repository.
 * @return {!Promise<string>} The name of the current branch in the current
 * repository, or Error.
 * @private
 */
GitStatusChecker.prototype.detectBranch = function detectBranch() {
  return git('symbolic-ref', '-q', '--short', 'HEAD')
    .then(
      getTrimmedStdout,
      (err) => {
        const errMsg = `Unable to determine current branch: ${
          err.stderr || 'detached HEAD'}`;
        return Promise.reject(new Error(errMsg));
      }
    );
};

/** Gets the path portion of a git URL.
 * @param {string} gitUrl Git URL to parse.
 * @return {string} Path portion of the URL.
 * @private
 */
function gitUrlPath(gitUrl) {
  // Foreign URL for remote helper
  // See transport_get in transport.c
  // Note:  url.parse considers second : as part of path.  So check this first.
  const foreignParts = /^([A-Za-z0-9][A-Za-z0-9+.-]*)::(.*)$/.exec(gitUrl);
  if (foreignParts) {
    return foreignParts[2];
  }

  // Typical URL
  const gitUrlObj = url.parse(gitUrl);
  if (gitUrlObj.protocol) {
    return gitUrlObj.path;
  }

  // SCP-like syntax.  Host can be wrapped in [] to disambiguate path.
  // See parse_connect_url and host_end in connect.c
  const scpParts = /^([^@/]+)@(\[[^]\/]+\]|[^:/]+):(.*)$/.exec(gitUrl);
  if (scpParts) {
    return scpParts[3];
  }

  // Assume URL is a local path
  return gitUrl;
}

/** Detects the repository slug for the current repository.
 * @return {!Promise<string>} The slug value for the current repository.
 * @private
 */
GitStatusChecker.prototype.detectSlug = function detectSlug() {
  const self = this;
  const slugP = this.detectBranch()
    .then((branch) => {
      const configName = `branch.${branch}.remote`;
      return git('config', '--get', configName)
        .then(getTrimmedStdout);
    })
    .catch((err) => {
      debug('Unable to get remote for current branch', err);
      return 'origin';
    })
    .then((remoteName) => git('ls-remote', '--get-url', remoteName)
      .then(getTrimmedStdout)
      .then((remoteUrl) => {
        // ls-remote prints its argument when it doesn't have a URL
        if (remoteUrl === remoteName) {
          return Promise.reject(new SlugDetectionError(
            `No URL for '${remoteName}' remote`
          ));
        }
        return remoteUrl;
      }))
    .then((remoteUrl) => {
      const path = gitUrlPath(remoteUrl);
      const match = /([^/]+\/[^/]+?)(?:\/?\.git)?$/.exec(path);
      return match
        ? match[1]
        : Promise.reject(new SlugDetectionError(
          `Unable to extract slug from URL <${remoteUrl}>`
        ));
    });

  return slugP.then((slug) => {
    if (self._options.interactive) {
      return self.confirmSlug(slug);
    }

    self._options.err.write(`detected repository as ${
      self._chalk.bold(slug)}\n`);
    return slug;
  });
};

/** Loads the repository slug from travis.slug in the git config.
 * @return {!Promise<?string>} The slug value for the current repository, or
 * null if not configured.
 * @private
 */
GitStatusChecker.prototype.loadSlug = function loadSlug() {
  return git('config', '--get', GitStatusChecker.SLUG_CONFIG_NAME)
    .then(
      getTrimmedStdout,
      // git config exits with code 1 if the configuration value is not set
      (err) => (err.code === 1 ? null : Promise.reject(err))
    );
};

/** Finds the repository slug for the current repository by either loading it
 * from the stored configuration or detecting it from the current repository,
 * stores it if confirmed interactively.
 * @return {!Promise<string>} The slug value for the current repository.
 * @private
 */
GitStatusChecker.prototype.findSlug = function findSlug() {
  const self = this;
  return this.loadSlug().then((slug) => {
    if (slug) {
      return slug;
    }

    return self.detectSlug();
  });
};

module.exports = GitStatusChecker;


/* eslint-env node */
'use strict';

var RSVP = require('rsvp');
var glob  = require('glob');
var DeployPluginBase = require('ember-cli-deploy-plugin');
var path = require('path');
var Funnel = require('broccoli-funnel');
var stew = require('broccoli-stew');
var fs = require('fs');
var chalk = require('chalk');

function cleanupRobotsTxt(outputPath) {
  var files = glob.sync(outputPath + path.sep + 'robots-*.txt');
  if (files && files.length) {
    files.forEach(function(path) {
      fs.unlink(path, function(){});
    });
  }
}

module.exports = {
  name: 'ember-cli-deploy-build-plus',

  // Cleanup env specific robots.txt
  postBuild: function(result) {
    cleanupRobotsTxt(result.directory);
  },

  // Pick env specific robots.txt
  treeForPublic: function() {
    var appEnv = this.app.env;
    if (process.env.DEPLOY_TARGET) {
      appEnv = process.env.DEPLOY_TARGET;
    }
    var publicFiles = new Funnel(this.app.trees.public);

    this._requireBuildPackages();

    fs.stat(
      path.join(this.project.root, 'public', 'robots.txt'),
      function(err, stats) {
        if (stats && stats.isFile()) {
          console.log(chalk.yellow('There is a robots.txt in /public and ENV specific robots.txt are ignored!'));
        }
      }
    );

    publicFiles = stew.rename(
      publicFiles,
      'robots-' + appEnv + '.txt',
      'robots.txt'
    );

    return new Funnel(publicFiles, {
      srcDir: '/',
      destDir: '/'
    });
  },

	_requireBuildPackages() {
     if (this._didRequiredBuildPackages === true) {
       return;
     } else {
       this._didRequiredBuildPackages = true;
     }
   },

  createDeployPlugin: function(options) {
    var DeployPlugin = DeployPluginBase.extend({
      name: options.name,
      defaultConfig: {
        environment: 'production',
        outputPath: 'tmp' + path.sep + 'deploy-dist',
        distDir: function(context) {
          return context.distDir;
        }
      },

      setup: function() {
        var outputPath = this.readConfig('outputPath');
        return {
          distDir: outputPath
        };
      },

      build: function(/* context */) {
        var self       = this;
        var distDir    = this.readConfig('distDir');
        var buildEnv   = this.readConfig('environment');

        var Builder  = this.project.require('ember-cli/lib/models/builder');
        var builder = new Builder({
          ui: this.ui,
          outputPath: distDir,
          environment: buildEnv,
          project: this.project
        });

        this.log('building app to `' + distDir + '` using buildEnv `' + buildEnv + '`...', { verbose: true });
        return builder.build()
          .finally(function() {
            return builder.cleanup();
          })
          .then(this._cleanupRobotsTxt.bind(this, distDir))
          .then(this._logSuccess.bind(this, distDir))
          .then(function(files) {
            files = files || [];

            return {
              distFiles: files
            };
          })
          .catch(function(error) {
            self.log('build failed', { color: 'red' });
            return RSVP.reject(error);
          });
      },
      _cleanupRobotsTxt: function(outputPath) {
        cleanupRobotsTxt(outputPath);

        return outputPath;
      },
      _logSuccess: function(outputPath) {
        var self = this;
        var files = glob.sync('**/**/*', { nonull: false, nodir: true, cwd: outputPath });

        if (files && files.length) {
          files.forEach(function(path) {
            self.log('✔  ' + path, { verbose: true });
          });
        }
        self.log('build ok', { verbose: true });

        return RSVP.resolve(files);
      }
    });
    return new DeployPlugin();
  }
};


/**
 * # BalanceSheet
 *
 * Copyright(c) 2012 Stefano Balietti
 * MIT Licensed
 *
 * Keeps a record of
 */

(function(exports, node){

// ## Global scope

exports.BalanceSheet = BalanceSheet;

var	JSUS = node.JSUS,
	NDDB = node.NDDB,
	GameDB = node.GameDB,
	GameBit = node.GameBit;

	PlayerList = node.PlayerList;

////Inheriting from NDDB
//BalanceSheet.prototype = JSUS.clone(NDDB.prototype);
//BalanceSheet.prototype.constructor = BalanceSheet;

var log = console.log;

var isValidSheet = function (sheet) {
	if (!sheet) {
		log('Cannot add a sheet with no name.', 'ERR');
		return false;
	}

	if (this.sheets[sheet]) {
		log('A sheet with the same already exists: ' + sheet);
		return false;
	}
	return true;
}

/**
 * ## BalanceSheet constructor
 *
 * Creates a new instance of BalanceSheet
 *
 */
function BalanceSheet (options) {

// ## Public properties

	this.sheets = {};

	this.sheet = null;

	log = options.log || log;

/**
 * ### BalanceSheet.options
 *
 * Reference to current configuration
 *
 */
	this.options = options || {};

// ## BalanceSheet methods

/**
 * ### BalanceSheet.init
 *
 * Configures the BalanceSheet instance
 *
 * Takes the configuration as an input parameter or
 * recycles the settings in `this.options`.
 *
 * The configuration object is of the type
 *
 * 	var options = {
 * 		returnAt: 'first', // or 'last'
 * 		triggers: [ myFunc,
 * 					myFunc2
 * 		],
 * 	}
 *
 * @param {object} options Optional. Configuration object
 *
 */
BalanceSheet.prototype.init = function (options) {
	this.options = options || this.options;
	if (this.options.returnAt === BalanceSheet.first || this.options.returnAt === BalanceSheet.last) {
		this.returnAt = this.options.returnAt;
	}
	this.resetTriggers();
};

/**
 * ### BalanceSheet.addSheet
 *
 * Adds a new sheet and sets it as default
 *
 * @param {string} sheet The sheet name
 * @param {object} options. Optional. Configuration options for the sheet
 * @param {array} items. Optional. An initial set of items for the sheet
 * @return {boolean} TRUE, if the sheet is added successfully
 */
//BalanceSheet.prototype.addSheet = function (sheet, pl, options) {
//	if (!isValidSheet(sheet)) return false;
//	pl = pl || new PlayerList();
//
//	this.sheets[sheet] = pl;
//
//	if (!this.initSheet(sheet, options)) {
//		return false;
//	}
//
//	this.sheet = sheet;
//	return true;
//};

//BalanceSheet.prototype.initSheet = function (sheet, options) {
//	if  (!isValidSheet(sheet)) return false;
//	this.sheets[sheet].each(function(p){
//		if (!p.__balance) {
//			p.__balance = 0;
//		}
//	});
//};

BalanceSheet.prototype.updateBalance = function (player, amount) {
	if (!player || !amount) return;
	if (!this.sheet) {
		log('No balance sheet selected');
		return;
	}
	if (!this.sheet.player)
		return
	}
	this.sheet.players[player].__balance += amount;
};

BalanceSheet.prototype.report = function (sheet) {
	if (!isValidSheet(sheet)) return false;

	return this.sheets[sheet].keep(['__balance']);

};

/**
 * ### BalanceSheet.clear
 *
 * Clears the trigger array
 *
 * Requires a boolean parameter to be passed for confirmation
 *
 * @param {boolean} clear TRUE, to confirm clearing
 * @return {boolean} TRUE, if clearing was successful
 */
BalanceSheet.prototype.clear = function (clear) {
	if (!clear) {
		node.log('Do you really want to clear the current BalanceSheet obj? Please use clear(true)', 'WARN');
		return false;
	}
	triggersArray = [];
	return clear;
};

// # Sheet

//Inheriting from NDDB
Sheet.prototype = JSUS.clone(NDDB.prototype);
Sheet.prototype.constructor = Sheet;

function Sheet(options, db, parent) {
	options = options || {};
	if (!options.log) options.log = node.log;
	NDDB.call(this, options, db, parent);

	this.name;
	this.pl;
}

/**
 * ### GameDB.add
 *
 * Creates a GameBit and adds it to the database
 *
 * @param {string} key An alphanumeric id for the entry
 * @param {mixed} value Optional. The value to store
 * @param {Player} player Optional. The player associated to the entry. Defaults, node.player
 * @param {GameState} player Optional. The state associated to the entry. Defaults, node.game.state
 *
 * @return {boolean} TRUE, if insertion was successful
 *
 * @see GameBit
 */
Sheet.prototype.add = function (player, value, state, key) {
	if (!key) return false;

	state = state || node.game.state;
	player = player || node.player;

	this.insert(new GameBit({
						player: player,
						key: key,
						value: value,
						state: state,
	}));

	return true;
};

Sheet.prototype.init = function(options) {
	NDDB.prototype.init.call(this, options);

	options = options || this.options;
};

// ## Closure
})(
	('undefined' !== typeof node) ? node : module.exports
  , ('undefined' !== typeof node) ? node : module.parent.exports
);


/**
 * # GroupManager
 *
 * Copyright(c) 2013 Stefano Balietti
 * MIT Licensed
 *
 * `nodeGame` group manager.
 * @experimental
 * ---
 */
(function(exports, node) {

    "use strict";

    // ## Global scope
    var J = node.JSUS;

    console.log(node)

    exports.GroupManager = GroupManager;
    exports.Group = Group;

    function GroupManager() {
        // TODO GroupManager
    }

    // Here follows previous implementation of GroupManager, called RMatcher
    // poorly commented.
    // RMatcher is not the same as a GroupManager,
    // but does something very useful:
    // It assigns elements to groups based on a set of preferences

    // elements: what you want in the group
    // pools: array of array. it is set of preferences (elements
    // from the first array will be used first

    // Groups.rowLimit determines how many unique elements per row

    exports.RMatcher = RMatcher;

    //J = require('nodegame-client').JSUS;

    function RMatcher (options) {
        this.groups = [];
        this.maxIteration = 10;
        this.doneCounter = 0;
    }

    RMatcher.prototype.init = function(elements, pools) {
        var i, g;
        for (i = 0; i < elements.length; i++) {
            g = new Group();
            g.init(elements[i], pools[i]);
            this.addGroup(g);
        }
        this.options = {
            elements: elements,
            pools: pools
        };
    };

    RMatcher.prototype.addGroup = function(group) {
        if (!group) return;
        this.groups.push(group);
    };

    RMatcher.prototype.match = function() {
        var i;
        // Do first match
        for (i = 0 ; i < this.groups.length ; i++) {
            this.groups[i].match();
            if (this.groups[i].matches.done) {
                this.doneCounter++;
            }
        }

        if (!this.allGroupsDone()) {
            this.assignLeftOvers();
        }

        if (!this.allGroupsDone()) {
            this.switchBetweenGroups();
        }

        return J.map(this.groups, function(g) { return g.matched; });
    };

    RMatcher.prototype.invertMatched = function() {

        var tmp, elements = [], inverted = [];
        J.each(this.groups, function(g) {
            elements = elements.concat(g.elements);
            tmp = g.invertMatched();
            for (var i = 0; i < tmp.length; i++) {
                inverted[i] = (inverted[i] || []).concat(tmp[i]);
            }
        });

        return {
            elements: elements,
            inverted: inverted
        };
    };

    RMatcher.prototype.allGroupsDone = function() {
        return this.doneCounter === this.groups.length;
    };

    RMatcher.prototype.tryOtherLeftOvers = function(g) {
        var group, groupId;
        var order = J.seq(0, (this.groups.length-1));
        order = J.shuffle(order);
        for (var i = 0 ; i < order.length ; i++) {
            groupId = order[i];
            if (groupId === g) continue;
            group = this.groups[groupId];
            leftOver = [];
            if (group.leftOver.length) {
                group.leftOver = this.groups[g].matchBatch(group.leftOver);

                if (this.groups[g].matches.done) {
                    this.doneCounter++;
                    return true;
                }
            }

        }
    };

    RMatcher.prototype.assignLeftOvers = function() {
        var g, i;
        for (i = 0 ; i < this.groups.length ; i++) {
            g = this.groups[i];
            // Group is full
            if (!g.matches.done) {
                this.tryOtherLeftOvers(i);
            }

        }
    };

    RMatcher.prototype.collectLeftOver = function() {
        return J.map(this.groups, function(g) { return g.leftOver; });
    };

    RMatcher.prototype.switchFromGroup = function(fromGroup, toGroup, fromRow,
                                                  leftOvers) {

        var toRow, j, n, x, h, switched;
        for (toRow = 0; toRow < fromGroup.elements.length; toRow++) {

            for (j = 0; j < leftOvers.length; j++) {
                for (n = 0; n < leftOvers[j].length; n++) {

                    x = leftOvers[j][n]; // leftover n from group j

                    if (fromGroup.canSwitchIn(x, toRow)) {
                        for (h = 0 ; h < fromGroup.matched[toRow].length; h++) {
                            switched = fromGroup.matched[toRow][h];

                            if (toGroup.canAdd(switched, fromRow)) {
                                fromGroup.matched[toRow][h] = x;
                                toGroup.addToRow(switched, fromRow);
                                leftOvers[j].splice(n,1);

                                if (toGroup.matches.done) {

                                    //  console.log('is done')
                                    //  console.log(toGroup);
                                    //  console.log('is done')

                                    this.doneCounter++;
                                }
                                return true;
                            }
                        }
                    }
                }
            }
        }
    };

    /**
     *
     * @param {integer} g Group index
     * @param {integer} row Row index
     */
    RMatcher.prototype.trySwitchingBetweenGroups = function(g, row) {
        var lo = this.collectLeftOver();
        var toGroup = this.groups[g];
        var i, fromGroup;
        // Tries with all, even with the same group, that is why is (g + 1)
        for (i = (g + 1) ; i < (this.groups.length + g + 1) ; i++) {
            fromGroup = this.groups[i % this.groups.length];

            if (this.switchFromGroup(fromGroup, toGroup, row, lo)) {
                if (toGroup.matches.done) return;
            }
        }

        return false;
    };

    RMatcher.prototype.switchBetweenGroups = function() {
        var i, g, j, h, diff;
        for ( i = 0; i < this.groups.length ; i++) {
            g = this.groups[i];
            // Group has free elements
            if (!g.matches.done) {
                for ( j = 0; j < g.elements.length; j++) {
                    diff = g.rowLimit - g.matched[j].length;
                    if (diff) {
                        for (h = 0 ; h < diff; h++) {
                            this.trySwitchingBetweenGroups(i, j);
                            if (this.allGroupsDone()) {
                                return true;
                            }
                        }
                    }
                }
            }
        }
        return false;
    };

    ////////////////// GROUP

    function Group() {

        this.elements = [];
        this.matched = [];

        this.leftOver = [];
        this.pointer = 0;

        this.matches = {};
        this.matches.total = 0;
        this.matches.requested = 0;
        this.matches.done = false;

        this.rowLimit = 3;

        this.noSelf = true;

        this.pool = [];

        this.shuffle = true;
        this.stretch = true;
    }

    Group.prototype.init = function(elements, pool) {
        this.elements = elements;
        this.pool = J.clone(pool);

        for (var i = 0; i < this.pool.length; i++) {
            if (this.stretch) {
                this.pool[i] = J.stretch(this.pool[i], this.rowLimit);
            }
            if (this.shuffle) {
                this.pool[i] = J.shuffle(this.pool[i]);
            }
        }

        if (!elements.length) {
            this.matches.done = true;
        }
        else {
            for (i = 0 ; i < elements.length ; i++) {
                this.matched[i] = [];
            }
        }

        this.matches.requested = this.elements.length * this.rowLimit;
    };

    /**
     * ## Group constructor
     *
     * Creates a group
     */
    function Group(options) {

        /**
         * ## Group.name
         *
         * The name of the group
         *
         * Must be unique amongst groups
         */
        this.name = null;

        /**
         * ## Group.elements
         *
         * The elements belonging to this group
         *
         * They can be matched with other elements contained in the _pool_.
         *
         * @see Group.pool
         * @see Group.matched
         */
        this.elements = [];

        /**
         * ## Group.pool
         *
         * Sets of elements that to match with the group members sequentially
         *
         * It is an array of arrays, and elements in ealier sets are more
         * likely to be matched than subsequent ones.
         *
         * @see Group.elements
         * @see Group.matched
         */
        this.pool = [];

        /**
         * ## Group.matched
         *
         * Array of arrays of matched elements
         *
         * Each index in the parent array corresponds to a group member,
         * and each array are the matched element for such a member.
         *
         * @see Group.elements
         * @see Group.pool
         */
        this.matched = [];

        /**
         * ## Group.leftOver
         *
         * Array of elements from the pool that could not be matched
         */
        this.leftOver = [];

        /**
         * ## Group.pointer
         *
         * Index of the row we are trying to complete currently
         */
        this.pointer = 0;

        /**
         * ## Group.matches
         *
         * Summary of matching results
         *
         */
        this.matches = {
            total: 0,
            requested: 0,
            done: false
        };

        /**
         * ## Group.rowLimit
         *
         * Number of elements necessary to a row
         *
         * Each group member will be matched with _rowLimit_ elements from
         * the _pool_ elements.
         */
        this.rowLimit = 1;

        /**
         * ## Group.noSelf
         *
         * If TRUE, a group member cannot be matched with himself.
         */
        this.noSelf = true;

        /**
         * ## Group.shuffle
         *
         * If TRUE, all elements of the pool will be randomly shuffled.
         */
        this.shuffle = true;

        /**
         * ## Group.stretch
         *
         * If TRUE,  each element in the pool will be replicated
         * as many times as the _rowLimit_ variable.
         */
        this.stretch = true;

        // Init user options.
        this.init(options);
    }

    /**
     * ## Group.init
     *
     * Mixes in default and user options
     *
     * @param {object} options User options
     */
    Group.prototype.init = function(options) {

        this.name = 'undefined' === typeof options.name ?
            this.name : options.name;

        this.noSelf = 'undefined' === typeof options.noSelf ?
            this.noSelf : options.noSelf;

        this.shuffle = 'undefined' === typeof options.shuffle ?
            this.shuffle : options.shuffle;

        this.stretch = 'undefined' === typeof options.stretch ?
            this.stretch : options.stretch;

        this.rowLimit = 'undefined' === typeof options.rowLimit ?
            this.rowLimit : options.rowLimit;

        if (options.elements) {
            this.setElements(options.elements);
        }

        if (options.pool) {
            this.setPool(options.pool);
        }
    };

   /**
     * ## Group.setElements
     *
     * Sets the elements of the group
     *
     * Updates the number of requested matches, and creates a new matched
     * array for each element.
     *
     * @param {array} elements The elements of the group
     */
    Group.prototype.setElements = function(elements) {
        var i;

        if (!J.isArray(elements)) {
            throw new TypeError('Group.setElements: elements must be array.');
        }

        this.elements = elements;

        if (!elements.length) {
            this.matches.done = true;
        }
        else {
            for (i = 0 ; i < elements.length ; i++) {
                this.matched[i] = [];
            }
        }

        this.matches.requested = this.elements.length * this.rowLimit;
    };
   /**
     * ## Group.setPool
     *
     * Sets the pool of the group
     *
     * A pool can contain external elements not included in the _elements_.
     *
     * If the _stretch_ option is on, each element in the pool will be copied
     * and added as many times as the _rowLimit_ variable.
     *
     * If the _shuffle_ option is on, all elements of the pool (also those
     * created by the _stretch_ options, will be randomly shuffled.
     *
     * Notice: the pool is cloned, cyclic references in the pool object
     * are not allowed.
     *
     * @param {array} pool The pool of the group
     *
     * @see Group.shuffle
     * @see Group.stretch
     */
    Group.prototype.setPool = function(pool) {
        var i;

        if (!J.isArray(pool)) {
            throw new TypeError('Group.setPool: pool must be array.');
        }

        this.pool = J.clone(pool);

        for (i = 0; i < this.pool.length; i++) {
            if (this.stretch) {
                this.pool[i] = J.stretch(this.pool[i], this.rowLimit);
            }
            if (this.shuffle) {
                this.pool[i] = J.shuffle(this.pool[i]);
            }
        }
    };

    /**
     * ## Group.getMembers
     *
     * Returns the members of the group
     *
     * @return {array} The elements of the group
     */
    Group.prototype.getMembers = function() {
        return this.elements;
    };

    /**
     * The same as canAdd, but does not consider row limit
     */
    Group.prototype.canSwitchIn = function(x, row) {
        // Element already matched
        if (J.in_array(x, this.matched[row])) return false;
        // No self
        if (this.noSelf && this.elements[row] === x) return false;

        return true;
    };

    Group.prototype.canAdd = function(x, row) {
        // Row limit reached
        if (this.matched[row].length >= this.rowLimit) return false;

        return this.canSwitchIn(x, row);
    };

    Group.prototype.shouldSwitch = function(x, fromRow) {
        if (!this.leftOver.length) return false;
        if (this.matched.length < 2) return false;
        //      var actualLeftOver = this.leftOver.length;
        return true;

    };

    // If there is a hole, not in the last position, the algorithm fails
    Group.prototype.switchIt = function() {

        for (var i = 0; i < this.elements.length ; i++) {
            if (this.matched[i].length < this.rowLimit) {
                this.completeRow(i);
            }
        }

    };

    Group.prototype.completeRow = function(row, leftOver) {
        leftOver = leftOver || this.leftOver;
        var clone = leftOver.slice(0);
        for (var i = 0 ; i < clone.length; i++) {
            for (var j = 0 ; j < this.elements.length; j++) {
                if (this.switchItInRow(clone[i], j, row)){
                    leftOver.splice(i,1);
                    return true;
                }
                this.updatePointer();
            }
        }
        return false;
    };

    Group.prototype.switchItInRow = function(x, toRow, fromRow) {
        if (!this.canSwitchIn(x, toRow)) {
            //console.log('cannot switch ' + x + ' ' + toRow)
            return false;
        }
        //console.log('can switch: ' + x + ' ' + toRow + ' from ' + fromRow)
        // Check if we can insert any of the element of the 'toRow'
        // inside the 'toRow'
        for (var i = 0 ; i < this.matched[toRow].length; i++) {
            var switched = this.matched[toRow][i];
            if (this.canAdd(switched, fromRow)) {
                this.matched[toRow][i] = x;
                this.addToRow(switched, fromRow);
                return true;
            }
        }

        return false;
    };

    Group.prototype.addToRow = function(x, row) {
        this.matched[row].push(x);
        this.matches.total++;
        if (this.matches.total === this.matches.requested) {
            this.matches.done = true;
        }
    };

    Group.prototype.addIt = function(x) {
        var counter = 0, added = false;
        while (counter < this.elements.length && !added) {
            if (this.canAdd(x, this.pointer)) {
                this.addToRow(x, this.pointer);
                added = true;
            }
            this.updatePointer();
            counter++;
        }
        return added;
    };

    Group.prototype.matchBatch = function(pool) {
        var leftOver = [];
        for (var i = 0 ; i < pool.length ; i++) {
            if (this.matches.done || !this.addIt(pool[i])) {
                // if we could not add it as a match, it becomes leftover
                leftOver.push(pool[i]);
            }
        }
        return leftOver;
    };

    Group.prototype.match = function(pool) {
        pool = pool || this.pool;
        //      console.log('matching pool');
        //      console.log(pool)
        if (!J.isArray(pool)) {
            pool = [pool];
        }
        // Loop through the pools: elements in lower
        // indexes-pools have more chances to be used
        var leftOver;
        for (var i = 0 ; i < pool.length ; i++) {
            leftOver = this.matchBatch(pool[i]);
            if (leftOver.length) {
                this.leftOver = this.leftOver.concat(leftOver);
            }
        }

        if (this.shouldSwitch()) {
            this.switchIt();
        }
    };

    Group.prototype.updatePointer = function() {
        this.pointer = (this.pointer + 1) % this.elements.length;
    };

    Group.prototype.summary = function() {
        console.log('elements: ', this.elements);
        console.log('pool: ', this.pool);
        console.log('left over: ', this.leftOver);
        console.log('hits: ' + this.matches.total + '/' +
                    this.matches.requested);
        console.log('matched: ', this.matched);
    };

    Group.prototype.invertMatched = function() {
        return J.transpose(this.matched);
    };

    // Testing functions

    var numbers = [1,2,3,4,5,6,7,8,9];

    function getElements() {

        var out = [],
        n = J.shuffle(numbers);
        out.push(n.splice(0, J.randomInt(0,n.length)));
        out.push(n.splice(0, J.randomInt(0,n.length)));
        out.push(n);

        return J.shuffle(out);
    }

    function getPools() {
        var n = J.shuffle(numbers);
        out = [];

        var A = n.splice(0, J.randomInt(0, (n.length / 2)));
        var B = n.splice(0, J.randomInt(0, (n.length / 2)));
        var C = n;

        var A_pub = A.splice(0, J.randomInt(0, A.length));
        A = J.shuffle([A_pub, A]);

        var B_pub = B.splice(0, J.randomInt(0, B.length));
        B = J.shuffle([B_pub, B]);

        var C_pub = C.splice(0, J.randomInt(0, C.length));
        C = J.shuffle([C_pub, C]);

        return J.shuffle([A,B,C]);
    }
    //console.log(getElements())
    //console.log(getPools())

    function simulateMatch(N) {

        for (var i = 0 ; i < N ; i++) {

            var rm = new RMatcher(),
            elements = getElements(),
            pools = getPools();

            //          console.log('NN ' , numbers);
            //          console.log(elements);
            //          console.log(pools)
            rm.init(elements, pools);

            var matched = rm.match();

            if (!rm.allGroupsDone()) {
                console.log('ERROR');
                console.log(rm.options.elements);
                console.log(rm.options.pools);
                console.log(matched);
            }

            for (var j = 0; j < rm.groups.length; j++) {
                var g = rm.groups[j];
                for (var h = 0; h < g.elements.length; h++) {
                    if (g.matched[h].length !== g.rowLimit) {
                        console.log('Wrong match: ' +  h);

                        console.log(rm.options.elements);
                        console.log(rm.options.pools);
                        console.log(matched);
                    }
                }
            }
        }

    }

    //simulateMatch(1000000000);

    //var myElements = [ [ 1, 5], [ 6, 9 ], [ 2, 3, 4, 7, 8 ] ];
    //var myPools = [ [ [ ], [ 1,  5, 6, 7] ], [ [4], [ 3, 9] ],
    //                [ [], [ 2, 8] ] ];

    //4.07A 25
    //4.77C 25
    //4.37B 25
    //5.13B 25 [08 R_16]
    //0.83A 25 [09 R_7]
    //3.93A 25 [09 R_23]
    //1.37A 25 [07 R_21]
    //3.30C 25
    //4.40B 25
    //
    //25
    //
    //389546331863136068
    //B
    //
    //// submissions in r 26
    //
    //3.73A 26 [05 R_25]
    //2.40C 26
    //undefinedC 26 [05 R_25]
    //4.37C 26 [06 R_19]
    //6.07A 26 [06 R_19]
    //undefinedB 26 [06 R_18]
    //4.33C 26 [05 R_25]
    //undefinedC 26 [08 R_19]
    //4.40B 26
    //
    //
    //26
    //
    //19868497151402574894
    //A
    //
    //27
    //
    //5688413461195617580
    //C
    //20961392604176231
    //B

    //20961392604176200 SUB     A       1351591619837
    //19868497151402600000      SUB     A       1351591620386
    //5688413461195620000       SUB     A       1351591652731
    //2019166870553500000       SUB     B       1351591653043
    //389546331863136000        SUB     B       1351591653803
    //1886985572967670000       SUB     C       1351591654603
    //762387587655923000        SUB     C       1351591654648
    //1757870795266120000       SUB     B       1351591655960
    //766044637969952000        SUB     A       1351591656253

    //var myElements = [ [ 3, 5 ], [ 8, 9, 1, 7, 6 ], [ 2, 4 ] ];
    //var myPools = [ [ [ 6 ], [ 9, 7 ] ], [ [], [ 8, 1, 5, 4 ] ],
    //                [ [], [ 2, 3 ] ] ];

    //var myElements = [ [ '13988427821680113598', '102698780807709949' ],
    //  [],
    //  [ '15501781841528279951' ] ]
    //
    //var myPools = [ [ [ '13988427821680113598', '102698780807709949' ] ],
    //  [ [] ],
    //   [ [ '15501781841528279951' ] ] ]
    //
    //
    //var myRM = new RMatcher();
    //myRM.init(myElements, myPools);
    //
    //var myMatch = myRM.match();
    //
    //
    //for (var j = 0; j < myRM.groups.length; j++) {
    //  var g = myRM.groups[j];
    //  for (var h = 0; h < g.elements.length; h++) {
    //          if (g.matched[h].length !== g.rowLimit) {
    //                  console.log('Wrong match: ' + j + '-' + h);
    //
    //                  console.log(myRM.options.elements);
    //                  console.log(myRM.options.pools);
    ////                        console.log(matched);
    //          }
    //  }
    //}

    //if (!myRM.allGroupsDone()) {
    //  console.log('ERROR')
    //  console.log(myElements);
    //  console.log(myPools);
    //  console.log(myMatch);
    //
    //  console.log('---')
    //  J.each(myRM.groups, function(g) {
    //          console.log(g.pool);
    //  });
    //}

    //console.log(myElements);
    //console.log(myPools);
    //console.log('match')
    //console.log(myMatch);

    //console.log(myRM.invertMatched());
    //console.log(J.transpose(myMatch));
    //
    //console.log(myRM.doneCounter);

    //var poolA = [ [1, 2], [3, 4], ];
    //var elementsA = [7, 1, 2, 4];
    //
    //var poolB = [ [5], [6], ];
    //var elementsB = [3 , 8];
    //
    //var poolC = [ [7, 8, 9] ];
    //var elementsC = [9, 5, 6, ];
    //
    //var A, B, C;
    //
    //A = new Group();
    //A.init(elementsA, poolA);
    //
    //B = new Group();
    //B.init(elementsB, poolB);
    //
    //C = new Group();
    //C.init(elementsC, poolC);
    //
    //
    //rm.addGroup(A);
    //rm.addGroup(B);
    //rm.addGroup(C);
    //
    //rm.match();
    //

    //  [ [ [ 2, 1, 4 ], [ 2, 3, 4 ], [ 1, 4, 3 ], [ 1, 2, 3 ] ],
    //  [ [ 5, 6, 9 ], [ 5, 6, 7 ] ],
    //  [ [ 8, 6, 5 ], [ 9, 8, 7 ], [ 9, 7, 8 ] ] ]

    //console.log(rm.allGroupsDone())

    //console.log(g.elements);
    //console.log(g.matched);

    // ## Closure
})(
    'undefined' != typeof node ? node : module.exports,
    'undefined' != typeof node ? node : module.parent.exports
);


/**
 * # SocketDirect
 * Copyright(c) 2015 Stefano Balietti
 * MIT Licensed
 *
 * Implementation of a direct socket communicating directly with the server
 * through a shared event-emitter object
 *
 * http://nodegame.org
 */
(function(exports, parent) {

    /**
     * ## SocketDirect constructor
     *
     * Creates a new instance of SocketDirect
     *
     * @param {NodeGameClient} node Reference to the node instance
     * @param {object} options Optional. Configuration options
     */
    function SocketDirect(node, options) {
        options = options || {};

        // ## Private properties

        /**
         * ### SocketDirect.node
         *
         * Reference to the node object.
         */
        this.node = node;

        /**
         * ## SocketDirect.socket
         *
         * The SocketDirect object shared with the server
         */
        this.socket = options.socket;

        /**
         * ## SocketDirect.connected
         *
         * TRUE, if a connection is established
         */
        this.connected = false;
    }

    /**
     * ### SocketDirect.connect
     *
     * Establishes a connection with the server
     *
     * Sets the on: 'connect', 'message', 'disconnect' event listeners.
     *
     * @param {string} url The address of the server channel
     * @param {object} options Optional. Configuration options
     *
     * @return {boolean} TRUE, on success
     */
    SocketDirect.prototype.connect = function(socket, options) {
        var node, res;
        node = this.node;

        if (socket) this.socket = socket;

        if (!this.socket) {
            throw new Error('SocketDirect.connect: empty socket.');
        }

        this.connected = true;

        // Call the SocketDirect method on the server.
        res = this.socket.connect(this, options);
        if (res) {
            node.info('socket.direct connection open');
            node.socket.onConnect();
        }
        return res;
    };

    /**
     * ### SocketDirect.isConnected
     *
     * Returns TRUE, if currently connected
     */
    SocketDirect.prototype.isConnected = function() {
        return this.connected;
    };

    /**
     * ### SocketDirect.message
     *
     * Delivers an incoming message to the node object
     *
     * @param {object} msg Message object, usually, a game message.
     *
     * @see GameMessage
     */
    SocketDirect.prototype.message = function(msg) {
        this.node.socket.onMessage(msg);
    };

    /**
     * ### SocketDirect.disconnect
     *
     * Triggers the disconnection from the server
     *
     * @param {boolean} If FALSE, Socket.onDisconnect will not be called.
     *   Default: undefined.
     *
     * @see Socket.onDisconnect
     */
    SocketDirect.prototype.disconnect = function(notifySocket) {
        this.connected = false;
        if (notifySocket !== false) this.node.socket.onDisconnect();
    };

    /**
     * ### SocketDirect.send
     *
     * Sends a message to the SocketDirect socket of the server
     *
     * @param {GameMsg} gameMsg The game mesage to send
     *
     * @see GameMessage
     */
    SocketDirect.prototype.send = function(gameMsg) {
        if (!this.connected || !this.socket) {
            this.node.err('SocketDirect.send: could not deliver message, ' +
                          'no open socket found. ');
            return;
        }
        this.socket.message(gameMsg);
    };

    parent.SocketFactory.register('SocketDirect', SocketDirect);

})(
    'undefined' != typeof node ? node : module.exports,
    'undefined' != typeof node ? node : module.parent.exports
);


// # nodegame-client game object

// ## Configuration options

// See the documentation for Game.js in the docs folder.

// ## Example object

// All configuration options are listed below with their default value.

function myGame() {

	this.solo_mode = false;

	this.auto_wait = false;

	this.auto_step = false;

	this.observer = false;

	this.minPlayers = 1;

	this.maxPlayers = 1000;
}


/**
 * # GameSession
 * Copyright(c) 2015 Stefano Balietti
 * MIT Licensed
 *
 * `nodeGame` session manager
 */
(function(exports, node) {

    "use strict";

    // ## Global scope

    var J = node.JSUS;

    // Exposing constructor.
    exports.GameSession = GameSession;
    exports.GameSession.SessionManager = SessionManager;

    GameSession.prototype = new SessionManager();
    GameSession.prototype.constructor = GameSession;

    /**
     * ## GameSession constructor
     *
     * Creates a new instance of GameSession
     *
     * @param {NodeGameClient} node A reference to the node object.
     */
    function GameSession(node) {
        SessionManager.call(this);

        /**
         * ### GameSession.node
         *
         * The reference to the node object.
         */
        this.node = node;

        // Register default variables in the session.
        this.register('player', {
            set: function(p) {
                node.createPlayer(p);
            },
            get: function() {
                return node.player;
            }
        });

        this.register('game.memory', {
            set: function(value) {
                node.game.memory.clear(true);
                node.game.memory.importDB(value);
            },
            get: function() {
                return (node.game.memory) ? node.game.memory.fetch() : null;
            }
        });

        this.register('events.history', {
            set: function(value) {
                node.events.history.history.clear(true);
                node.events.history.history.importDB(value);
            },
            get: function() {
                return node.events.history ?
                    node.events.history.history.fetch() : null;
            }
        });

        this.register('stage', {
            set: function() {
                // GameSession.restoreStage
            },
            get: function() {
                return node.player.stage;
            }
        });

        this.register('node.env');
    }

//    GameSession.prototype.restoreStage = function(stage) {
//
//        try {
//            // GOTO STATE
//            node.game.execStage(node.plot.getStep(stage));
//
//            var discard = ['LOG',
//                           'STATECHANGE',
//                           'WINDOW_LOADED',
//                           'BEFORE_LOADING',
//                           'LOADED',
//                           'in.say.STATE',
//                           'UPDATED_PLIST',
//                           'NODEGAME_READY',
//                           'out.say.STATE',
//                           'out.set.STATE',
//                           'in.say.PLIST',
//                           'STAGEDONE', // maybe not here
//                           'out.say.HI'
//                          ];
//
//            // RE-EMIT EVENTS
//            node.events.history.remit(node.game.getStateLevel(), discard);
//            node.info('game stage restored');
//            return true;
//        }
//        catch(e) {
//            node.err('could not restore game stage. ' +
//                     'An error has occurred: ' + e);
//            return false;
//        }
//
//    };

    /**
     * ## SessionManager constructor
     *
     * Creates a new session manager.
     */
    function SessionManager() {

        /**
         * ### SessionManager.session
         *
         * Container of all variables registered in the session.
         */
        this.session = {};
    }

    // ## SessionManager methods

    /**
     * ### SessionManager.getVariable (static)
     *
     * Default session getter.
     *
     * @param {string} p The path to a variable included in _node_
     * @return {mixed} The requested variable
     */
    SessionManager.getVariable = function(p) {
        return J.getNestedValue(p, node);
    };

    /**
     * ### SessionManager.setVariable (static)
     *
     * Default session setter.
     *
     * @param {string} p The path to the variable to set in _node_
     * @param {mixed} value The value to set
     */
    SessionManager.setVariable = function(p, value) {
        J.setNestedValue(p, value, node);
    };

    /**
     * ### SessionManager.register
     *
     * Register a new variable to the session
     *
     * Overwrites previously registered variables with the same name.
     *
     * Usage example:
     *
     * ```javascript
     * node.session.register('player', {
     *       set: function(p) {
     *           node.createPlayer(p);
     *       },
     *       get: function() {
     *           return node.player;
     *       }
     * });
     * ```
     *
     * @param {string} path A string containing a path to a variable
     * @param {object} conf Optional. Configuration object containing setters
     *   and getters
     */
    SessionManager.prototype.register = function(path, conf) {
        if ('string' !== typeof path) {
            throw new TypeError('SessionManager.register: path must be ' +
                                'string.');
        }
        if (conf && 'object' !== typeof conf) {
            throw new TypeError('SessionManager.register: conf must be ' +
                                'object or undefined.');
        }

        this.session[path] = {

            get: (conf && conf.get) ?
                conf.get : function() {
                    return J.getNestedValue(path, node);
                },

            set: (conf && conf.set) ?
                conf.set : function(value) {
                    J.setNestedValue(path, value, node);
                }
        };

        return this.session[path];
    };

    /**
     * ### SessionManager.unregister
     *
     * Unegister a variable from session
     *
     * @param {string} path A string containing a path to a variable previously
     *   registered.
     *
     * @see SessionManager.register
     */
    SessionManager.prototype.unregister = function(path) {
        if ('string' !== typeof path) {
            throw new TypeError('SessionManager.unregister: path must be ' +
                                'string.');
        }
        if (!this.session[path]) {
            node.warn('SessionManager.unregister: path is not registered ' +
                      'in the session: ' + path + '.');
            return false;
        }

        delete this.session[path];
        return true;
    };

    /**
     * ### SessionManager.clear
     *
     * Unegister all registered session variables
     *
     * @see SessionManager.unregister
     */
    SessionManager.prototype.clear = function() {
        this.session = {};
    };

    /**
     * ### SessionManager.get
     *
     * Returns the value/s of one/all registered session variable/s
     *
     * @param {string|undefined} path A previously registred variable or
     *   undefined to return all values
     *
     * @see SessionManager.register
     */
    SessionManager.prototype.get = function(path) {
        var session = {};
        // Returns one variable.
        if ('string' === typeof path) {
            return this.session[path] ? this.session[path].get() : undefined;
        }
        // Returns all registered variables.
        else if ('undefined' === typeof path) {
            for (path in this.session) {
                if (this.session.hasOwnProperty(path)) {
                    session[path] = this.session[path].get();
                }
            }
            return session;
        }
        else {
            throw new TypeError('SessionManager.get: path must be string or ' +
                                'undefined.');
        }
    };

    /**
     * ### SessionManager.isRegistered
     *
     * Returns TRUE, if a variable is registred
     *
     * @param {string} path A previously registred variable
     *
     * @return {boolean} TRUE, if the variable is registered
     *
     * @see SessionManager.register
     * @see SessionManager.unregister
     */
    SessionManager.prototype.isRegistered = function(path) {
        if ('string' !== typeof path) {
            throw new TypeError('SessionManager.isRegistered: path must be ' +
                                'string.');
        }
        return this.session.hasOwnProperty(path);
    };

    /**
     * ### SessionManager.serialize
     *
     * Returns an object containing that can be to restore the session
     *
     * The serialized session is an object containing _getter_, _setter_, and
     * current value of each of the registered session variables.
     *
     * @return {object} session The serialized session
     *
     * @see SessionManager.restore
     */
    SessionManager.prototype.serialize = function() {
        var session = {};
        for (var path in this.session) {
            if (this.session.hasOwnProperty(path)) {
                session[path] = {
                    value: this.session[path].get(),
                    get: this.session[path].get,
                    set: this.session[path].set
                };
            }
        }
        return session;
    };

    /**
     * ### SessionManager.restore
     *
     * Restore a previously serialized session object
     *
     * @param {object} session A serialized session object
     * @param {boolean} register Optional. If TRUE, every path is also
     *    registered before being restored.
     */
    SessionManager.prototype.restore = function(session, register) {
        var i;
        if ('object' !== typeof session) {
            throw new TypeError('SessionManager.restore: session must be ' +
                                'object.');
        }
        register = 'undefined' !== typeof register ? register : true;
        for (i in session) {
            if (session.hasOwnProperty(i)) {
                if (register) this.register(i, session[i]);
                session[i].set(session[i].value);
            }
        }
    };

//    SessionManager.prototype.store = function() {
//        //node.store(node.socket.id, this.get());
//    };

})(
    'undefined' != typeof node ? node : module.exports,
    'undefined' != typeof node ? node : module.parent.exports
);


var hogan = require('hogan.js');
var fs = require('fs');
var path = require('path');
var open = require('open');
var sass = require('node-sass');

var schemes = ['mono', 'complementary', 'triad', 'tetrad', 'analogic'];
var variants = ['--lightest', '--lighter', '', '--darker', '--darkest'];
var colors = ['primary', 'complementary', 'secondary-a', 'secondary-b'];
var variations = ['none', 'pastel', 'dark', 'light', 'hard', 'pale'];
var index;

// set seed color
if (process.argv[2]) {
    var re = /(?:.*'seed'\:)(.*)(?=,)/;
    var settings = fs.readFileSync(path.join(__dirname, 'defaults.scss')).toString();
    settings = settings.replace(settings.match(re)[1], process.argv[2]);
    fs.writeFileSync(path.join(__dirname, 'defaults.scss'), settings);
}

// compile sass
sass.render({
    file: path.join(__dirname, 'example.scss')
}, function(err, result) {
    if (err) throw err;
    fs.writeFile(path.join(__dirname, 'example.css'), result.css.toString(), function(err) {if (err) throw err;});
    generateDataArrays();
    compileIndex();
});

function generateDataArrays() {
    // fill arrays with values for hogan template
    schemes.forEach(function(value, index, arr) {arr[index] = {'scheme': value};});
    variants.forEach(function(value, index, arr) {arr[index] = {'variant': value};});
    colors.forEach(function(value, index, arr) {arr[index] = {'color': value};});
    variations.forEach(function(value, index, arr) {arr[index] = {'variation': value};});
}

// compile hogan template and open index.html
function compileIndex() {
    fs.readFile(path.join(__dirname, 'templates', 'index.hogan'), function(err, data) {
        if (err) throw err;
        // write rendered result to index.html
        fs.writeFile(path.join(__dirname, 'index.html'),
                    hogan.compile(data.toString()).render({
                        'schemes': schemes,
                        'variations': variations,
                        'colors': colors,
                        'variants': variants,
                        }),
                    function(err) {if (err) throw err});

        // open index.html in browser
        open(path.join(__dirname, 'index.html'));
    });
}


import urlRegExp from './url-regex';

function escapeStringRegExp(str) {
  return str.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&');
}

function Token(type, text, raw = text) {
  this.type = type;
  this.text = text;
  this.raw = raw;
}

/**
 * Sort users by username length. Longest usernames first.
 *
 * @param {Array.<Object>} users
 * @return {Array.<Object>}
 */

function sortMentions(mentions) {
  return mentions.slice().sort((a, b) => b.length - a.length);
}

/**
 * Create a regex that matches a specific username or group being mentioned.
 *
 * @param {string} mention Mentionable name.
 * @return {RegExp}
 */
function mentionRegExp(mention) {
  return new RegExp(`^${escapeStringRegExp(mention)}(?:\\b|\\s|\\W|$)`, 'i');
}

/**
 * Case-insensitively get the correct emoji name from the possible emoji for an
 * input string.
 *
 * @param {Array.<string>} names All possible emoji names.
 * @param {string} match The input string.
 * @return {string|null} The correct emoji name (including casing), or `null` if
 *    the requested emoji does not exist.
 */
function findEmoji(names, match) {
  const compare = match.toLowerCase();
  for (let i = 0; i < names.length; i += 1) {
    const name = names[i].toLowerCase();
    if (name === compare) {
      return names[i];
    }
  }

  return null;
}

function tokenize(text, opts) {
  let chunk;
  let i = 0;
  const mentions = sortMentions(opts.mentions || []);
  const tokens = [];
  // adds a token of type `type` if the current chunk starts with
  // a `delim`-delimited string
  const delimited = (start, endRx, type) => {
    if (chunk[0] === start && chunk[1] !== start) {
      const end = 1 + chunk.slice(1).search(endRx);
      if (end) {
        tokens.push(new Token(type, chunk.slice(1, end)));
        i += end + 1;
        return true;
      }
    }
    return false;
  };
  const emoji = (type, emojiNames) => {
    const match = /^:([A-Za-z0-9_+-]+):/.exec(chunk);
    if (match) {
      // if a whitelist of emoji names is given, only accept emoji from that
      // list.
      const emojiName = emojiNames ? findEmoji(emojiNames, match[1]) : match[1];
      if (emojiName) {
        tokens.push(new Token(type, emojiName, match[0]));
        i += match[0].length;
        return true;
      }
    }
    return false;
  };
  const mention = (start, type) => {
    if (chunk[0] === start) {
      const maybeMention = chunk.slice(1);
      for (let mi = 0, ml = mentions.length; mi < ml; mi += 1) {
        const candidate = mentions[mi];
        if (mentionRegExp(candidate).test(maybeMention)) {
          const end = candidate.length + 1;
          tokens.push(new Token(type, chunk.slice(1, end), chunk.slice(0, end)));
          i += end;
          return true;
        }
      }
    }
    return false;
  };
  const linkRx = new RegExp(`^${urlRegExp().source}`, 'i');
  const link = (type) => {
    const match = linkRx.exec(chunk);
    if (match) {
      tokens.push(new Token(type, chunk.slice(0, match[0].length)));
      i += match[0].length;
      return true;
    }
    return false;
  };
  // eat spaces
  const space = () => {
    // .slice again because `i` changed
    const m = /^\s+/.exec(text.slice(i));
    if (m) {
      tokens.push(new Token('word', m[0]));
      i += m[0].length;
    }
  };
  // tokenize text, just loop until it's done!
  chunk = text;
  while (chunk) {
    const found = emoji('emoji', opts.emojiNames)
      || delimited('_', /_(\W|$)/, 'italic')
      || delimited('*', /\*(\W|$)/, 'bold')
      || delimited('`', /`(\W|$)/, 'code')
      || delimited('~', /~(\W|$)/, 'strike')
      || mention('@', 'mention')
      || link('link');
    if (!found) {
      let end = chunk.indexOf(' ', 1) + /* eat space */ 1;
      if (end === 0) { // no match, = -1 + 1
        end = chunk.length;
      }
      // append to previous token if it was also a word
      if (tokens.length > 0 && tokens[tokens.length - 1].type === 'word') {
        tokens[tokens.length - 1].text += chunk.slice(0, end);
      } else {
        tokens.push(new Token('word', chunk.slice(0, end)));
      }
      i += end;
    }
    space();
    chunk = text.slice(i);
  }
  return tokens;
}

function httpify(text) {
  if (!/^[a-z]+:/.test(text)) {
    return `http://${text}`;
  }
  return text;
}

// Parses a chat message into a tree-ish structure.
// Options:
//  * mentions: Names that can be mentioned.
export default function parse(message, opts = {}) {
  if (typeof message !== 'string') {
    throw new TypeError('Expected a string');
  }

  return tokenize(message, opts).map((token) => {
    switch (token.type) {
      case 'italic':
        return { type: 'italic', content: parse(token.text, opts) };
      case 'bold':
        return { type: 'bold', content: parse(token.text, opts) };
      case 'code':
        return { type: 'code', content: [token.text] };
      case 'strike':
        return { type: 'strike', content: parse(token.text, opts) };
      case 'emoji':
        return { type: 'emoji', name: token.text };
      case 'mention':
        return { type: 'mention', mention: token.text.toLowerCase(), raw: token.text };
      case 'link':
        return { type: 'link', text: token.text, href: httpify(token.text) };
      default:
        return token.text;
    }
  });
}


/* Copyright (C) 2018 Canonical Ltd. */

'use strict';

const macaroonlib = require('macaroon');

// Define the bakery protocol version used by the GUI.
const PROTOCOL_VERSION = 1;
// Define the HTTP content type for JSON and encoded form requests.
const JSON_CONTENT_TYPE = 'application/json';
const WWW_FORM_CONTENT_TYPE = 'application/x-www-form-urlencoded';
// Define HTTP statuses.
const STATUS_UNAUTHORIZED = 401;
const STATUS_PROXY_AUTH_REQUIRED = 407;
// Define bakery specific errors.
const ERR_DISCHARGE_REQUIRED = 'macaroon discharge required';
const ERR_INTERACTION_REQUIRED = 'interaction required';

let TextDecoder;
if (typeof window === 'undefined') {
  // No window if it's node.js.
  const util = require('util');
  TextDecoder = util.TextDecoder;
} else {
  TextDecoder = window.TextDecoder;
}

/**
  Serialize the given macaroons.

  @param {Array} macaroons The macaroons to be serialized.
  @return {string} The resulting serialized string.
*/
const serialize = macaroons => {
  return btoa(JSON.stringify(macaroons));
};

/**
  De-serialize the given serialized macaroons.

  @param {string} serialized The serialized macaroons.
  @return {Array} The resulting macaroon slice.
*/
const deserialize = serialized => {
  return JSON.parse(atob(serialized));
};

/**
  A macaroon bakery implementation.

  The bakery implements the protocol used to acquire and discharge macaroons
  over HTTP.
*/
const Bakery = class Bakery {

  /**
    Initialize a macaroon bakery with the given parameters.

    @param {Object} params Optional parameters including:
      - onSuccess: a function to be called when the request completes
        properly. It defaults to a no-op function.
      - storage: the storage used to persist macaroons. It must implement the
        following interface:
        - get(key) -> value;
        - set(key, value, callback): the callback is called without arguments
          when the set operation has been performed.
        If not provided, it defaults to BakeryStorage using an in memory store.
      - visitPage: the function used to visit the identity provider page when
        required, defaulting to opening a pop up window. It receives an
        error object containing:
          - Info: an object containing relevant info for the visit handling:
            - WaitURL: the url to wait on for IdM discharge
            - VisitURL: the url to visit to authenticate with the IdM
          - jujugui: an optional value specifying a method to use against
            idm to authenticate. Used in non interactive authentication
            scenarios.
      - sendRequest: a function used to make XHR HTTP requests, with the
        following signature:
        func(path, method, headers, body, withCredentials, callback) -> xhr.
        By default an internal function is used. This is mostly for testing.
  */
  constructor(params={}) {
    this._onSuccess = params.onSuccess || (() => {});
    this._sendRequest = params.sendRequest || _request;
    this.storage = params.storage || new BakeryStorage(new InMemoryStore());
    this._visitPage = params.visitPage || (error => {
      window.open(error.Info.VisitURL, 'Login');
    });
    this._dischargeDisabled = false;
  }

  /**
    Send an HTTP request to the given URL with the given HTTP method, headers
    and body. The given callback receives an error and a response when the
    request is complete.

    @param {String} url The URL to which to send the request.
    @param {String} method The HTTP method, like "get" or "POST".
    @param {Object} headers Headers that must be included in the request.
      Note that bakery specific headers are automatically added internally.
    @param {String} body The request body if it applies, or null.
    @param {Function} callback A function called when the response is
      received from the remote URL. It receives a tuple (error, response).
      If the request succeeds the error is null.
    @return {Object} the XHR instance.
  */
  sendRequest(url, method, headers, body, callback) {
    method = method.toLowerCase();
    // Prepare the send method and wrap the provided callback.
    const wrappedCallback = this._wrapCallback(
      url, method, headers, body, callback);
    // Prepare the header. Include already stored macaroons in the header if
    // present for the current URL.
    const allHeaders = {'Bakery-Protocol-Version': PROTOCOL_VERSION};
    const macaroons = this.storage.get(url);
    if (macaroons) {
      allHeaders['Macaroons'] = macaroons;
    }
    Object.keys(headers || {}).forEach(key => {
      const value = headers[key];
      allHeaders[key] = value;
    });
    // Prepare the parameters for sending the HTTP request.
    // The only time we need the with credentials header is for cookie auth;
    // it's not pretty, but special casing here is the most direct solution.
    // Another option is to implement a factory method on bakery, e.g.
    // bakery.withCredentials(), which would return a bakery that sets the
    // withCredentials param to true rather than false.
    let withCredentials = false;
    if (method === 'put' && url.indexOf('/set-auth-cookie') !== -1) {
      withCredentials = true;
    }
    // Send the request.
    return this._sendRequest(
      url, method, allHeaders, body, withCredentials, wrappedCallback);
  }

  /**
    Send an HTTP GET request to the given URL with the given headers.
    The given callback receives an error and a response when the request is
    complete.

    See the "sendRequest" method above for a description of the parameters.
  */
  get(url, headers, callback) {
    return this.sendRequest(url, 'get', headers, null, callback);
  }

  /**
    Send an HTTP DELETE request to the given URL with the given headers and
    body. The given callback receives an error and a response when the
    request is complete.

    See the "sendRequest" method above for a description of the parameters.
  */
  delete(url, headers, body, callback) {
    return this.sendRequest(url, 'delete', headers, body, callback);
  }

  /**
    Send an HTTP POST request to the given URL with the given headers and
    body. The given callback receives an error and a response when the
    request is complete.

    See the "sendRequest" method above for a description of the parameters.
  */
  post(url, headers, body, callback) {
    return this.sendRequest(url, 'post', headers, body, callback);
  }

  /**
    Send an HTTP PUT request to the given URL with the given headers and
    body. The given callback receives an error and a response when the
    request is complete.

    See the "sendRequest" method above for a description of the parameters.
  */
  put(url, headers, body, callback) {
    return this.sendRequest(url, 'put', headers, body, callback);
  }

  /**
    Send an HTTP PATCH request to the given URL with the given headers and
    body. The given callback receives an error and a response when the
    request is complete.

    See the "sendRequest" method above for a description of the parameters.
  */
  patch(url, headers, body, callback) {
    return this.sendRequest(url, 'patch', headers, body, callback);
  }

  /**
    Discharge the given macaroon. Acquire any third party discharges.

    @param {Object} macaroon The decoded macaroon to be discharged.
    @param {Function} onSuccess The function to be called if the
      discharge succeeds. It receives the resulting macaroons array.
    @param {Function} onFailure The function to be called if the
      discharge fails. It receives an error message.
  */
  discharge(macaroon, onSuccess, onFailure) {
    try {
      macaroonlib.dischargeMacaroon(
        macaroonlib.importMacaroons(macaroon)[0],
        this._getThirdPartyDischarge.bind(this),
        discharges => {
          onSuccess(discharges.map(m => m._exportAsJSONObjectV1()));
        },
        onFailure
      );
    } catch (exc) {
      onFailure(`discharge failed: ${exc.message}`);
    }
  }

  /**
    Wrap the given callback function so that "discharge required" and
    "interaction required" errors in the response are internally handled.

    See the "sendRequest" method above for a description of the parameters.

    @return {Function} A callable accepting an HTTP response.
  */
  _wrapCallback(url, method, headers, body, callback) {
    return response => {
      // This is the bakery exit point when everything goes well there is
      // nothing to be done further.
      const exitSuccessfully = resp => {
        callback(null, resp);
        this._onSuccess();
      };
      const error = this._getError(response.target);
      if (!error) {
        // No discharge or interaction required.
        exitSuccessfully(response);
        return;
      }
      // At this point we must either discharge or make the user interact.
      let onSuccess;
      const onFailure = msg => {
        callback(msg, null);
      };
      switch (error.Code) {
        case ERR_INTERACTION_REQUIRED:
          onSuccess = resp => {
            // Once the interaction is done, store any resulting macaroons
            // and then exit successfully. From now on, the stored macaroons
            // will be reused and included in all requests to the same URL.
            const jsonResponse = JSON.parse(resp.target.responseText);
            const macaroons = serialize(jsonResponse.Macaroon);
            this.storage.set(url, macaroons, () => {
              if (jsonResponse.DischargeToken) {
                const token = serialize(jsonResponse.DischargeToken);
                this.storage.set(url, token, () => {
                  // Also set this token with a fixed "identity" key so that
                  // it is easy to inject the identity token from an external
                  // system, like OpenStack Horizon/Keystone.
                  this.storage.set('identity', token, () => {
                    exitSuccessfully(resp);
                  });
                });
                return;
              }
              exitSuccessfully(resp);
            });
          };
          this._interact(error, onSuccess, onFailure);
          break;
        case ERR_DISCHARGE_REQUIRED:
          // In the case discharge has been disabled in this bakery instance
          // (see the withoutDischarge method above) just return here.
          if (this._dischargeDisabled) {
            callback('discharge required but disabled', response);
            return;
          }
          onSuccess = macaroons => {
            // Once the discharge is acquired, store any resulting macaroons
            // and then retry the original requests again. This time the
            // resulting macaroons will be properly included in the request
            // header.
            this.storage.set(url, serialize(macaroons), () => {
              this.sendRequest(url, method, headers, body, callback);
            });
          };
          this.discharge(error.Info.Macaroon, onSuccess, onFailure);
          break;
        default:
          // An unexpected error has been encountered.
          callback(this._getErrorMessage(error), null);
          break;
      }
    };
  }

  /**
    Obtain a discharge macaroon for the given third party location.

    @param {String} location The origin location.
    @param {String} thirdPartyLocation The third party location where to
      discharge.
    @param {Uint8Array} condition The caveat to be discharged.
    @param {Function} onSuccess A function that will be called with the
      discharge macaroon when the acquisition is successfully completed.
    @param {Function} onFailure A function that will be called with an error
      message when the third party discharge fails.
  */
  _getThirdPartyDischarge(
    location, thirdPartyLocation, condition, onSuccess, onFailure) {
    const url = thirdPartyLocation + '/discharge';
    const headers = {'Content-Type': WWW_FORM_CONTENT_TYPE};
    // convert condition to ascii from uint8array
    const caveatString = new TextDecoder('utf-8').decode(condition);
    const encodedCondition = encodeURIComponent(caveatString);
    const encodedLocation = encodeURIComponent(location);
    const body = `id=${encodedCondition}&location=${encodedLocation}`;
    const callback = (err, response) => {
      if (err) {
        onFailure(err);
        return;
      }
      const jsonResponse = JSON.parse(response.target.responseText);
      const macaroons = macaroonlib.importMacaroons(jsonResponse.Macaroon)[0];
      onSuccess(macaroons);
    };
    // Use the bakery itself to get the third party discharge, so that we
    // support recursive discharge requests.
    this.post(url, headers, body, callback);
  }

  /**
    Interact to be able to acquire authentication macaroons.

    @param {String} visitURL The URL that must be visited to authenticate.
    @param {String} waitURL The URL where to wait for the authentication to
      be completed, and that will eventually provide the authentication
      macaroons and the discharge token.
    @param {Function} onSuccess The function that will be called with the
      macaroon when the acquisition succeeds.
    @param {Function} onFailure The function that will be called with an
      error string when the acquisition fails.
  */
  _interact(error, onSuccess, onFailure) {
    this._visitPage(error);
    const generateRequest = callback => {
      const headers = {'Content-Type': JSON_CONTENT_TYPE};
      const body = undefined;
      const withCredentials = false;
      return this._sendRequest(
        error.Info.WaitURL, 'get', headers, body, withCredentials, callback);
    };
    // When performing a "wait" request for the user logging into identity
    // it is possible that they take longer than the server timeout of
    // 1 minute: when this happens the server just closes the connection.
    let retryCounter = 0;
    const retryCallback = response => {
      const target = response.target;
      if (
        target.status === 0 &&
        target.response === '' &&
        target.responseText === ''
      ) {
        // Server closed the connection, retry and increment the counter.
        if (retryCounter < 5) {
          retryCounter += 1;
          generateRequest(retryCallback);
          return;
        }
        // We have retried 5 times so fall through to call handler.
      }
      const error = this._getError(target);
      if (error) {
        // The interaction failed.
        onFailure('cannot interact: ' + this._getErrorMessage(error));
        return;
      }
      // The interaction succeeded.
      onSuccess(response);
    };
    generateRequest(retryCallback);
  }

  /**
    Return any error present in the given response.

    @param {Object} target The XHR response.target.
    @return {Object or String} The error as found in the request.
  */
  _getError(target) {
    // Check bakery statuses.
    if (
      target.status !== STATUS_UNAUTHORIZED &&
      target.status !== STATUS_PROXY_AUTH_REQUIRED
    ) {
      return null;
    }
    // Bakery protocol errors always have JSON payloads.
    if (target.getResponseHeader('Content-Type') !== JSON_CONTENT_TYPE) {
      return null;
    }
    // At this point it should be possible to decode the error response.
    let error;
    try {
      error = JSON.parse(target.responseText);
    } catch(err) {
      return 'cannot parse error response';
    }
    return error;
  }

  /**
    Try to parse the given JSON decoded response in order to retrieve a
    human friendly error.

    @param {Object} jsonResponse The JSON decoded response text.
    @return {String} The error message.
  */
  _getErrorMessage(jsonResponse) {
    return (
      jsonResponse.Message ||
      jsonResponse.message ||
      jsonResponse.Error ||
      jsonResponse.error ||
      'unexpected error: ' + JSON.stringify(jsonResponse)
    );
  }

};

/**
  A storage for the macaroon bakery.

  The storage is used to persist macaroons.
*/
const BakeryStorage = class BakeryStorage {

  /**
    Initialize a bakery storage with the given underlaying store and params.

    @param {Object} store A store object implement the following interface:
      - getItem(key) -> value;
      - setItem(key, value);
      - clear().
    @param {Object} params Optional parameters including:
      - initial: a map of key/value pairs that must be initially included in
        the storage;
      - services: a map of service names (like "charmstore" or "terms") to
        the base URL of their corresponding API endpoints. This is used to
        simplify and reduce the URLs passed as keys to the storage;
      - charmstoreCookieSetter: a function that can be used to register
        macaroons to the charm store service. The function accepts a value
        and a callback, which receives an error and a response.
  */
  constructor(store, params={}) {
    this._store = store;
    this._services = params.services || {};
    this._charmstoreCookieSetter = params.charmstoreCookieSetter || null;
    const initial = params.initial || {};
    Object.keys(initial).forEach(key => {
      const value = initial[key];
      if (value) {
        this.set(key, value);
      }
    });
  }

  /**
    Retrieve and return the value for the provided key.

    @param {String} key The storage key, usually a URL.
    @return {String} The corresponding value, usually a serialized macaroon.
  */
  get(key) {
    key = this._getKey(key);
    return this._store.getItem(key);
  }

  /**
    Store the given value in the given storage key.

    Call the callback when done.

    @param {String} key The storage key, usually a URL.
    @param {String} value The value, usually a serialized macaroon.
    @param {Function} callback A function called without arguments when the
      value is properly stored.
  */
  set(key, value, callback) {
    key = this._getKey(key);
    this._store.setItem(key, value);
    if (key === 'charmstore' && this._charmstoreCookieSetter) {
      // Set the cookie so that images can be retrieved from the charm store.
      const macaroons = deserialize(value);
      this._charmstoreCookieSetter(macaroons, (err, resp) => {
        if (err) {
          console.error('cannot set charm store cookie:', err);
        }
        callback();
      });
      return;
    }
    callback();
  }

  /**
    Remove all key/value pairs from the storage.
  */
  clear() {
    this._store.clear();
  }

  /**
    Turn the given key (usually a URL) into a more friendly service name,
    when possible. This also means that different endpoints of the same
    service are reduced to the same service name key, which is ok given that
    all our services use the same macaroon root id for all endpoints.

    If the given key is not a URL, then return it untouched, so that it is
    still possible to set arbitrary keys in the storage. For instance, it is
    surely useful to be able to set or retrieve a service macaroon by using
    its corresponding service name (and not necessarily a URL).

    @param {String} key The original key.
    @return {String} A possibly simplified/reduced key.
  */
  _getKey(key) {
    // If this is one of the external services known by the GUI, such as
    // the charm store or terms, flat out the key to the service name.
    for (const service in this._services) {
      const baseURL = this._services[service];
      if (key.indexOf(baseURL) === 0) {
        return service;
      }
    }
    // If the endpoint ends with "/discharge", remove the suffix, which is
    // usally added back by bakery by convention.
    const suffix = '/discharge';
    if (key.slice(-suffix.length) === suffix) {
      return key.slice(0, -suffix.length);
    }
    return key;
  }

};

/**
  An in-memory store for the BakeryStorage.
*/
class InMemoryStore {
  constructor() {
    this._items = {};
  }
  getItem(key) {
    return this._items[key];
  }
  setItem(key, value) {
    this._items[key] = value;
  }
  clear() {
    this._items = {};
  }
}

/**
  Create, set up and send an asynchronous request to the given path/URL with
  the given method and parameters.

  @param {String} path The remote target path/URL.
  @param {String} method The request method (e.g. "GET" or "POST").
  @param {Object} headers Additional request headers as key/value pairs.
  @param {Object} body The data to send as a file object, a string or in
    general as an ArrayBufferView/Blob object.
  @param {Boolean} withCredentials Whether to include credentials.
  @param {Function} callback The load event callback.
  @return {Object} The xhr asynchronous request instance.
*/
function _request(path, method, headers, body, withCredentials, callback) {
  const xhr = new XMLHttpRequest({});
  // Set up the event handlers.
  const handler = evt => {
    if (callback) {
      callback(evt);
    }
    // The request has been completed: detach all the handlers.
    xhr.removeEventListener('error', handler);
    xhr.removeEventListener('load', handler);
  };
  xhr.addEventListener('error', handler, false);
  xhr.addEventListener('load', handler, false);
  // Set up the request.
  xhr.open(method, path, true);
  Object.keys(headers || {}).forEach(key => {
    xhr.setRequestHeader(key, headers[key]);
  });
  if (withCredentials) {
    xhr.withCredentials = withCredentials;
  }
  xhr.send(body || undefined);
  return xhr;
}

module.exports = {
  Bakery,
  BakeryStorage,
  InMemoryStore
};


// Copyright 2013 Joyent, Inc.  All rights reserved.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.

var stream = require('stream');
var util = require('util');

var assert = require('assert-plus');

var XDR = require('./xdr').XDR;
var utils = require('./utils');

///--- Globals

var sprintf = util.format;

///--- API

function RpcMessage(opts) {
    assert.object(opts, 'options');
    assert.bool(opts.incoming, 'options.incoming');
    assert.optionalNumber(opts.type, 'options.type');
    assert.optionalNumber(opts.xid, 'options.xid');

    stream.Transform.call(this, opts);

    this.type = opts.type;
    this.xid = opts.xid;

    this.incoming = opts.incoming;

    this._rpc_wrote_head = false;

    this._rpc_message = true; // MDB
}
util.inherits(RpcMessage, stream.Transform);

RpcMessage.prototype.send = function send() {
    this.writeHead();
    this.end();
};

RpcMessage.prototype.writeHead = function writeHead(size) {
    assert.optionalNumber(size, 'size');

    if (this._rpc_wrote_head)
        throw new Error('writeHead: already called');

    var xdr = this._serialize(size || 0);
    this.write(xdr.buffer());
    this._rpc_wrote_head = true;

    return (xdr);
};

RpcMessage.prototype.__serialize = function __serialize(len, extra) {
    assert.number(len, 'length');
    assert.optionalNumber(extra, 'extra');

    len = len + (extra ? extra : 0);
    var buf = new Buffer(12 + len);
    var xdr = new XDR(buf);

    xdr.writeInt(8 + len);
    // TODO - support fragments
    buf[0] = utils.set_bit(buf[0], 7);

    xdr.writeInt(this.xid);
    xdr.writeInt(this.type);

    return (xdr);
};

RpcMessage.prototype._transform = function _transform(chunk, encoding, cb) {
    if (!Buffer.isBuffer(chunk))
        chunk = new Buffer(chunk, encoding);

    this.push(chunk);

    cb();
};

RpcMessage.prototype._flush = function _flush(cb) {
    cb();
};

RpcMessage.prototype.toString = function toString() {
    return (sprintf('[object %s <xid=%d>]', this.constructor.name, this.xid));
};

///--- Exports

module.exports = {
    RpcMessage: RpcMessage
};