var hash = require("../../utils/hash");
var sprintf = require("sprintf").sprintf;
var _ = require("underscore")._;
var multilabelutils = require('./multilabelutils');
// var fs = require('fs');

/**
 * BinaryRelevance - Multi-label classifier, based on a collection of binary classifiers.
 * Also known as: One-vs-All.
 *
 * @param opts
 *            binaryClassifierType (mandatory) - the type of the base binary classifier. There is one such classifier per label.
 */
var BinaryRelevance = function(opts) {
	if (!opts.binaryClassifierType) {
		console.dir(opts);
		throw new Error("opts.binaryClassifierType not found");
	}
	this.binaryClassifierType = opts.binaryClassifierType;
	this.debug = opts.debug || false
	this.mapClassnameToClassifier = {};
}

BinaryRelevance.prototype = {

	/**
	 * Tell the classifier that the given sample belongs to the given labels.
	 *
	 * @param sample
	 *            a document.
	 * @param labels
	 *            an object whose KEYS are labels, or an array whose VALUES are labels.
	 */
	trainOnline: function(sample, labels) {
		labels = multilabelutils.normalizeOutputLabels(labels);
		for (var l in labels) {
			var positiveLabel = labels[l];
			this.makeSureClassifierExists(positiveLabel);
			this.mapClassnameToClassifier[positiveLabel].trainOnline(sample, 1);
		}
		for (var negativeLabel in this.mapClassnameToClassifier) {
			if (labels.indexOf(negativeLabel)<0)
				this.mapClassnameToClassifier[negativeLabel].trainOnline(sample, 0);
		}
	},

	/**
	 * Train the classifier with all the given documents.
	 *
	 * @param dataset
	 *            an array with objects of the format:
	 *            {input: sample1, output: [class11, class12...]}
	 */
	trainBatch : function(dataset) {
		// this variable will hold a dataset for each binary classifier:
		var mapClassnameToDataset = {};

		// create positive samples for each class:
		for (var d in dataset) {
			var sample = dataset[d].input;
			dataset[d].output = multilabelutils.normalizeOutputLabels(dataset[d].output);
			var labels = dataset[d].output;

			for (var l in labels) {
				var positiveLabel  = labels[l];
				this.makeSureClassifierExists(positiveLabel);
				if (!(positiveLabel in mapClassnameToDataset)) // make sure dataset for this class exists
					mapClassnameToDataset[positiveLabel] = [];
				mapClassnameToDataset[positiveLabel].push({
					input : sample,
					output : 1
				})
			}
		}

		// create negative samples for each class (after all labels are in the array):
		for (var d in dataset) {
			var sample = dataset[d].input;
			var labels = dataset[d].output;
			for (var negativeLabel in this.mapClassnameToClassifier) {
				if (!(negativeLabel in mapClassnameToDataset)) // make sure dataset for this class exists
					mapClassnameToDataset[negativeLabel] = [];
				if (labels.indexOf(negativeLabel)<0)
					mapClassnameToDataset[negativeLabel].push({
						input : sample,
						output : 0
					});
			}
		}

		// train all classifiers:
		for (var label in mapClassnameToDataset) {
			if (this.debug) console.dir("TRAIN class="+label);
			this.mapClassnameToClassifier[label]
					.trainBatch(mapClassnameToDataset[label]);
		}
	},

	/**
	 * Use the model trained so far to classify a new sample.
	 *
	 * @param sample a document.
	 * @param explain - int - if positive, an "explanation" field, with the given length, will be added to the result.
	 * @param withScores - boolean - if true, return an array of [class,score], ordered by decreasing order of score.
	 *
	 * @return an array whose VALUES are the labels.
	 * @output
	 * scores [hash] - the scores of each binary classifier in the class
	 * explanations [hash] positive - features of the classifier with positive labels
	 *					   negative - features of classifiers with negative labels
	 * classes [list] the list of given labels
	 */

	classify: function(sample, explain, withScores) {
		var labels = []
		var scores = []
		var explanations = [];
		var positive_explanations = {};
		var negative_explanations = []

		for (var label in this.mapClassnameToClassifier) {
			var classifier = this.mapClassnameToClassifier[label];

			if (this.debug) console.dir("Classify for class="+label)

			// fs.writeFileSync('/tmp/labels/'+label, JSON.stringify(classifier.getFeatures(), null, 4), 'utf8');

			var scoreWithExplain = classifier.classify(sample, explain, withScores);
			if (this.debug) console.log(JSON.stringify(scoreWithExplain, null, 4))

			var score = scoreWithExplain.explanation?  scoreWithExplain.classification: scoreWithExplain;
			if (this.debug) console.dir("score="+score)

			explanations_string = scoreWithExplain.explanation

			// if (score>0.5)
			if (score>0)
				{
				labels.push([label, score])
				if (explanations_string) positive_explanations[label]=explanations_string;
				}
			else
				{
				if (explanations_string) negative_explanations.push([label, score, explanations_string])
				}

			scores.push([label,score])
		}

		if (this.debug) console.dir(scores)

		if (explain>0)
		{
			scores = _.sortBy(scores, function(num){ return num[1] }).reverse()
			var scores_hash = _.object(scores)

			negative_explanations = _.sortBy(negative_explanations, function(num){ return num[1] }).reverse()
			negative_explanations = _.map(negative_explanations, function(num){ return [num[0],num[2]] });

			var negative_explanations_hash = _.object(negative_explanations)
		}

		labels = _.sortBy(labels, function(num){ return num[1] });
		labels = _.map(labels.reverse(), function(num){ return num[0] });

		return (explain>0?
			{
				classes: labels,
				scores: scores_hash,
				explanation: {
					positive: positive_explanations,
					negative: negative_explanations_hash,
				}
			}:
			labels);
	},

	classifyBatch: function(testSet) {
		var labels = []
		var results = {}
		var output = []

		for (var label in this.mapClassnameToClassifier) {
			var classifier = this.mapClassnameToClassifier[label]
			var scoreWithExplain = classifier.classifyBatch(testSet)
			results[label] = scoreWithExplain
		}

		_.each(testSet, function(value, key, list){
			testSet[key]['output'] = []
			_.each(results, function(ar, label, list){
				if (ar[key]!=0)
					testSet[key]['output'].push(label)
			}, this)
		}, this)

		return testSet
	},

	getAllClasses: function() {
		return Object.keys(this.mapClassnameToClassifier);
	},

	/**
	 * Link to a FeatureLookupTable from a higher level in the hierarchy (typically from an EnhancedClassifier), used ONLY for generating meaningful explanations.
	 */
	setFeatureLookupTable: function(featureLookupTable) {
		//console.log("BR setFeatureLookupTable "+featureLookupTable);
		this.featureLookupTable = featureLookupTable;
		for (var label in this.mapClassnameToClassifier)
			if (featureLookupTable && this.mapClassnameToClassifier[label].setFeatureLookupTable)
				this.mapClassnameToClassifier[label].setFeatureLookupTable(featureLookupTable);
	},

	toJSON : function() {
		var result = {};
		for (var label in this.mapClassnameToClassifier) {
			var binaryClassifier = this.mapClassnameToClassifier[label];
			if (!binaryClassifier.toJSON) {
				console.dir(binaryClassifier);
				console.log("prototype: ");
				console.dir(binaryClassifier.__proto__);
				throw new Error("this binary classifier does not have a toJSON function");
			}
			result[label] = binaryClassifier.toJSON();
		}
		return result;
	},

	fromJSON : function(json) {
		for (var label in json) {
			this.mapClassnameToClassifier[label] = new this.binaryClassifierType();
			this.mapClassnameToClassifier[label].fromJSON(json[label]);
		}
	},

	// private function:
	makeSureClassifierExists: function(label) {
		if (!this.mapClassnameToClassifier[label]) { // make sure classifier exists
			this.mapClassnameToClassifier[label] = new this.binaryClassifierType();
			if (this.featureLookupTable && this.mapClassnameToClassifier[label].setFeatureLookupTable)
				this.mapClassnameToClassifier[label].setFeatureLookupTable(this.featureLookupTable);

		}
	},
}

module.exports = BinaryRelevance;


var hash = require("../../utils/hash");
var sprintf = require("sprintf").sprintf;
var multilabelutils = require('./multilabelutils');
var _ = require("underscore")._;
var PrecisionRecall = require('../../utils/PrecisionRecall');
var partitions = require('../../utils/partitions');
var ulist = require('../../utils/list');

/* ThresholdClassifier - classifier that converts multi-class classifier to multi-label classifier by finding
 * the best appropriate threshold.
 * @param opts
 *            numOfFoldsForThresholdCalculation - =1 the threshold is approximated on validation set of size 10% of training set
 												  >1 n - fold cross - validation is applied to approximate the threshold
 *            evaluateMeasureToMaximize (['Accuracy','F1']) - string of the measure that operate the improvement of threshold
 *			  multiclassClassifier - multi-class classifier used for classification.
 * @author Vasily Konovalov
 */

var ThresholdClassifier = function(opts) {

	opts = opts || {};

	if (!('multiclassClassifierType' in opts)) {
		console.dir(opts);
		throw new Error("opts must contain multiclassClassifierType");
	}
	if (!opts.multiclassClassifierType) {
		console.dir(opts);
		throw new Error("opts.multiclassClassifierType is null");
	}

	if (!('evaluateMeasureToMaximize' in opts)) {
		console.dir(opts);
		throw new Error("opts must contain evaluateMeasureToMaximize");
	}
	if (!opts.evaluateMeasureToMaximize) {
		console.dir(opts);
		throw new Error("opts.evaluateMeasureToMaximize is null");
	}
	if (!opts.numOfFoldsForThresholdCalculation) {
		console.dir(opts);
		throw new Error("opts.numOfFoldsForThresholdCalculation is null");
	}

	this.multiclassClassifier = new opts.multiclassClassifierType();

	// [F1, Accuracy]
	this.evaluateMeasureToMaximize = opts.evaluateMeasureToMaximize;

	// constant size of validation set
	this.devsetsize = 0.1

	// > 1, n - fold cross - validation, otherwise validation set
	this.numOfFoldsForThresholdCalculation = opts.numOfFoldsForThresholdCalculation
}

ThresholdClassifier.prototype = {

	trainOnline: function(sample, labels) {
		throw new Error("ThresholdClassifier does not support online training");
	},

	/**
	 * Train the classifier with all the given documents and identify the best possible threshold
	 * simply by running over all relevant scores and determining the value of feedback function
	 * (F1 by default)
	 *
	 * @param dataset
	 *            an array with objects of the format:
	 *            {input: sample1, output: [class11, class12...]}
	 * @author Vasily Konovalov
	 *
	 */
	trainBatch : function(dataset) {

		_.times(3, function(n) {dataset = _.shuffle(dataset)})

		if (this.numOfFoldsForThresholdCalculation > 1) {
			thresholds=[]
			best_performances=[]
			average_performances = []
			median_performances = []
			partitions.partitions_consistent(dataset, this.numOfFoldsForThresholdCalculation, (function(trainSet, testSet, index) {
				this.multiclassClassifier.trainBatch(trainSet);
				result = this.receiveScores(testSet)
				performance = this.CalculatePerformance(result[0], testSet, result[1])
				best_performances.push(performance)
			}).bind(this))

			this.stats = best_performances

			threshold_average = ulist.average(_.pluck(best_performances, 'Threshold'))
			threshold_median = ulist.median(_.pluck(best_performances, 'Threshold'))

			Threshold = threshold_median
		}
		else
		{
			dataset = partitions.partition(dataset, 1, Math.round(dataset.length*this.devsetsize))
			trainSet = dataset['train']
			testSet = dataset['test']
			this.multiclassClassifier.trainBatch(trainSet);
			result = this.receiveScores(testSet)
			performance = this.CalculatePerformance(result[0], testSet, result[1])
			Threshold = performance['Threshold']
		}

		this.multiclassClassifier.threshold = Threshold
	},
	/*
	* Classify dataset and return the scored result in sorted list
	*/
	receiveScores: function(dataset) {
		list_of_scores = [];
		FN=0
		for (var i=0; i<dataset.length; ++i)
		{
 			var scoresVector = this.multiclassClassifier.classify(dataset[i].input, false, true);

 			for (score in scoresVector)
 			{
 				if (dataset[i].output.indexOf(scoresVector[score][0])>-1)
 				{
 					scoresVector[score].push("+")
 					FN+=1
 				}
 				else {scoresVector[score].push("-")}

 				scoresVector[score].push(i)
			}

 			list_of_scores = list_of_scores.concat(scoresVector)
  		}

  		// list_of_scores = [['d',4],['b',2],['a',1],['c',3]]

		list_of_scores.sort((function(index){
		    return function(a, b){
	        return (a[index] === b[index] ? 0 : (a[index] < b[index] ? 1 : -1));
		    };
		})(1))

		return [list_of_scores, FN]
	},

	/*
	Calculate the bst threshold with the highest evaluateMeasureToMaximize
	@param  list_of_scores list of scores
	@param  testSet test set
	@param FN false negative
	*/
	CalculatePerformance: function(list_of_scores, testSet, FN){

		current_set=[]

		TRUE = 0
		FP = 0
		TP = 0

		result = []

		for (var th=0; th<list_of_scores.length; ++th) {

			if (list_of_scores[th][2]=="+") {TP+=1; FN-=1}
			if (list_of_scores[th][2]=="-") {FP+=1;}

			// console.log(list_of_scores[th])
			// console.log("TP "+TP+" FP "+FP+" FN "+FN)

			index_in_testSet = list_of_scores[th][3]

			if (_.isEqual(current_set[index_in_testSet], testSet[index_in_testSet]['output']))
			{TRUE-=1}

			if (!current_set[index_in_testSet])
			{current_set[index_in_testSet] = [list_of_scores[th][0]]}
			else
			{current_set[index_in_testSet].push(list_of_scores[th][0])}

 			if (_.isEqual(current_set[index_in_testSet], testSet[index_in_testSet]['output']))
			{TRUE+=1 }

 			PRF = calculate_PRF(TP, FP, FN)
 			PRF['Accuracy'] = TRUE/testSet.length
 			PRF['Threshold'] = list_of_scores[th][1]

 			result[list_of_scores[th][1]] = PRF
 			}

			optial_measure=0
			index=Object.keys(result)[0]
			for (i in result)
			{
				if (result[i][this.evaluateMeasureToMaximize] >= optial_measure)
				{
					index = i
					optial_measure = result[i][this.evaluateMeasureToMaximize]
				}
			}

 			return result[index]

	},

	classify: function(sample, explain) {
		return this.multiclassClassifier.classify(sample, explain, /*withScores=*/false);
	},

	getAllClasses: function() {
		return this.multiclassClassifier.getAllClasses();
	},

	toJSON : function() {
		return this.multiclassClassifier.toJSON();
	},

	fromJSON : function(json) {
		this.multiclassClassifier.fromJSON(json);
	},

	setFeatureLookupTable: function(featureLookupTable) {
		if (this.multiclassClassifier.setFeatureLookupTable)
			this.multiclassClassifier.setFeatureLookupTable(featureLookupTable);
	},
}

function calculate_PRF(TP, FP, FN)
	{
	stats = {}
	stats['TP']=TP
	stats['FP']=FP
	stats['FN']=FN
	stats['Precision'] = (TP + FP == 0? 0: TP/(TP+FP))
	stats['Recall'] = (TP + FN == 0? 0: TP/(TP+FN))
	stats['F1'] = (stats['Precision'] + stats['Recall'] == 0? 0: 2*stats['Precision']*stats['Recall']/(stats['Precision'] + stats['Recall']))
	return stats
	}

module.exports = ThresholdClassifier;


var hash = require("../../utils/hash");
var FeaturesUnit = require("../../features");
var sprintf = require("sprintf").sprintf;
var _ = require("underscore")._;

/**
 * MulticlassSegmentation - Multi-label text classifier, based on a segmentation scheme using a base multi-class classifier.
 *
 * Inspired by:
 *
 * Morbini Fabrizio, Sagae Kenji. Joint Identification and Segmentation of Domain-Specific Dialogue Acts for Conversational Dialogue Systems. ACL-HLT 2011
 * http://www.citeulike.org/user/erelsegal-halevi/article/10259046
 *
 * @author Erel Segal-haLevi
 *
 * @param opts
 *            binaryClassifierType (mandatory) - the type of the base binary classifier.
 *            featureExtractor (optional) - a single feature-extractor (see the "features" folder), or an array of extractors, for extracting features from the text segments during classification.
 */
var MulticlassSegmentation = function(opts) {
	if (!opts.multiclassClassifierType) {
		console.dir(opts);
		throw new Error("opts.multiclassClassifierType not found");
	}
	this.multiclassClassifierType = opts.multiclassClassifierType;
	this.featureExtractor = FeaturesUnit.normalize(opts.featureExtractor);

	this.multiclassClassifier = new this.multiclassClassifierType();
}

MulticlassSegmentation.prototype = {

	/**
	 * Tell the classifier that the given sample belongs to the given classes.
	 *
	 * @param sample
	 *            a document.
	 * @param classes
	 *            an object whose KEYS are classes, or an array whose VALUES are classes.
	 */
	trainOnline: function(sample, classes) {
		sample = this.sampleToFeatures(sample, this.featureExtractor);
		var category = (Array.isArray(classes)? classes[0]: classes);
		this.multiclassClassifier.trainOnline(sample, category);
		/*for (var positiveClass in classes) {
			this.makeSureClassifierExists(positiveClass);
			this.mapClassnameToClassifier[positiveClass].trainOnline(sample, 1);
		}
		for (var negativeClass in this.mapClassnameToClassifier) {
			if (!classes[negativeClass])
				this.mapClassnameToClassifier[negativeClass].trainOnline(sample, 0);
		}*/
	},

	/**
	 * Train the classifier with all the given documents.
	 *
	 * @param dataset
	 *            an array with objects of the format:
	 *            {input: sample1, output: [class11, class12...]}
	 */
	trainBatch : function(dataset) {
		for ( var i = 0; i < dataset.length; ++i) {
			dataset[i] = {
				input: this.sampleToFeatures(dataset[i].input, this.featureExtractor),
				output: (Array.isArray(dataset[i].output)? dataset[i].output[0]: dataset[i].output)
			};
		}

		this.multiclassClassifier.trainBatch(dataset);
	},

	/**
	 * Internal function - use the model trained so far to classify a single segment of a sentence.
	 *
	 * @param segment a part of a text sentence.
	 * @param explain - int - if positive, an "explanation" field, with the given length, will be added to the result.
	 *
	 * @return {category: xxx, probability: yyy}
	 */
	classifySegment: function(segment, explain) {
		var sample = this.sampleToFeatures(segment, this.featureExtractor);
		return this.multiclassClassifier.classify(sample, explain);
	},

	/**
	 * @param segment a part of a text sentence.
	 * @return {category: best_category_of_segment, probability: its_probability}
	 */
	bestClassOfSegment: function(segment) {
		var sample = this.sampleToFeatures(segment, this.featureExtractor);
		var classification = this.multiclassClassifier.classify(sample, 1);
		//console.log(segment+": ");	console.dir(classification)
		return classification;
	},

	/**
	 * protected function:
	 * Strategy of finding the cheapest segmentation (- most probable segmentation), using a dynamic programming algorithm.
	 * Based on:
	 * Morbini Fabrizio, Sagae Kenji. Joint Identification and Segmentation of Domain-Specific Dialogue Acts for Conversational Dialogue Systems. ACL-HLT 2011
 	 * http://www.citeulike.org/user/erelsegal-halevi/article/10259046
	 */
	cheapestSegmentSplitStrategy: function(words, accumulatedClasses, explain, explanations) {

		// Calculate the cost of classification of the segment from i to j.
		// (Cost = - log probability).
		var segmentClassificationCosts = [];  // best cost to classify segment [i,j]
		for (var start=0; start<=words.length; ++start) {
			segmentClassificationCosts[start] = [];
			for (var end=0; end<start; ++end)
				segmentClassificationCosts[start][end]=Infinity;
			segmentClassificationCosts[start][start]=0;
			for (var end=start+1; end<=words.length; ++end) {
				var segment = words.slice(start,end).join(" ");
				var classification = this.bestClassOfSegment(segment);
				segmentClassificationCosts[start][end] = -Math.log(classification.probability);
			}
		}
		//console.log(words+":  ");		console.log("segmentClassificationCosts");		console.dir(segmentClassificationCosts);
		var cheapest_paths = require("graph-paths").cheapest_paths;
		cheapestSegmentClassificationCosts = cheapest_paths(segmentClassificationCosts, 0);
		cheapestSentenceClassificationCost = cheapestSegmentClassificationCosts[words.length];
		if (!cheapestSentenceClassificationCost)
			throw new Error("cheapestSegmentClassificationCosts["+words.length+"] is empty");
		//console.log("cheapestSentenceClassificationCost");		console.dir(cheapestSentenceClassificationCost);

		var cheapestClassificationPath = cheapestSentenceClassificationCost.path;
		explanations.push(cheapestSentenceClassificationCost);
		for (var i=0; i<cheapestClassificationPath.length-1; ++i) {
			var segment = words.slice(cheapestClassificationPath[i],cheapestClassificationPath[i+1]).join(" ");
			//console.log(segment+":  ");
			var segmentCategoryWithExplain = this.classifySegment(segment, explain);
			//console.dir(segmentCategoryWithExplain);
			var segmentCategory = (segmentCategoryWithExplain.category? segmentCategoryWithExplain.category: segmentCategoryWithExplain);
			accumulatedClasses[segmentCategory]=true;
			if (explain>0) {
				explanations.push(segment);
				explanations.push(segmentCategoryWithExplain.explanation);
			};
		}
	},

	/**
	 * Use the model trained so far to classify a new sample.
	 *
	 * @param segment a part of a text sentence.
	 * @param explain - int - if positive, an "explanation" field, with the given length, will be added to the result.
	 *
	 * @return an array whose VALUES are classes.
	 */
	classify: function(sentence, explain) {
		var minWordsToSplit = 2;
		var words = sentence.split(/ /);
		if (words.length>=minWordsToSplit) {
			var accumulatedClasses = {};
			var explanations = [];
			this.cheapestSegmentSplitStrategy(words, accumulatedClasses, explain, explanations);

			var classes = Object.keys(accumulatedClasses);
			return (explain>0?	{
				classes: classes,
				explanation: explanations
			}:
			classes);
		} else {
			return this.classifySegment(sentence, explain);
		}
	},

	toJSON : function() {
		return this.multiclassClassifier.toJSON();
	},

	fromJSON : function(json) {
		this.multiclassClassifier.fromJSON(json);
	},

	// private function:
	sampleToFeatures: function(sample, featureExtractor) {
		var features = sample;
		if (featureExtractor) {
			try {
				features = {};
				featureExtractor(sample, features);
			} catch (err) {
				throw new Error("Cannot extract features from '"+JSON.stringify(sample)+"': "+JSON.stringify(err));
			}
		}
		return features;
	},
}

module.exports = MulticlassSegmentation;


/*
 * grunt-easy-rpm
 * https://github.com/panitw/easy-rpm
 *
 * Copyright (c) 2013 Panit Wechasil
 * Licensed under the MIT license.
 */

'use strict';

var fs = require('fs'),
    path = require('path'),
    shortid = require('shortid'),
    chalk = require('chalk'),
    _ = require('lodash'),
    SpecFile = require('./lib/spec'),
    specValidator = require('./lib/spec-validator'),
    specWriter = require('./lib/spec-writer');

// Loads the package definition (if it exists) and picks specific properties
// to expose.  These properties are later merged into the options object as
// default values.
function loadPackageProperties(grunt) {
    var packageFile = 'package.json';

    if (grunt.file.exists(packageFile)) {
        return _.pick(grunt.file.readJSON(packageFile), [
            'name',
            'version',
            'description'
        ]);
    }

    return {};
}

// Sets the `prop` property on options to the concatenation of that property
// from both options and data, if both exist.  Otherwise, if either exist
// exclusively, that array will be set to the options.  When neither exist,
// nothing is done to the options object.
function concatOptionDataArrays(options, data, prop) {
    if (!_.has(options, prop) && !_.has(data, prop)) {
        return;
    }

    var combined = [];
    if (_.isArray(options[prop])) {
        combined = combined.concat(options[prop]);
    }
    if (_.isArray(data[prop])) {
        combined = combined.concat(data[prop]);
    }
    options[prop] = combined;
}

function preserveCopy(grunt, srcpath, destpath, options) {
    grunt.file.copy(srcpath, destpath, options);
    try {
        fs.chmodSync(destpath, fs.statSync(srcpath).mode);
    } catch (e) {
        throw grunt.util.error('Error setting permissions of "' +
            destpath + '" file.', e);
    }
}

function _checkNotifyPackageInherit(grunt, pkg, options, propName) {
    if (_.has(pkg, propName) && !_.has(options, propName)) {
        grunt.log.writelns(chalk.gray('[Notice] Property inheritance from ' +
            'package.json: "' + propName +
            '" will be inherited by the ' +
            'options with value "' + pkg[propName] + '".'));
    }
}

function _defaultOptionNotice(grunt, propName, defaultVal) {
    grunt.log.writelns(chalk.gray('[Notice] Default property set: ' +
        '`' + propName + '` will be set by default to "' +
        defaultVal + '".'));
}

// Applies properties from the options object to the spec object.  This is
// done explicitly to mitigate pollution from the options object and allow for
// notification of default assignments.
function applySpecSettings(grunt, options, spec) {
    spec.tags.name = options.name || spec.tags.name;

    if (!_.has(options, 'version')) {
        _defaultOptionNotice(grunt, 'version', '0.0.0');
    }
    spec.tags.version = options.version || '0.0.0';

    if (!_.has(options, 'release')) {
        _defaultOptionNotice(grunt, 'release', '1');
    }
    spec.tags.release = options.release || '1';

    if (!_.has(options, 'buildArch')) {
        _defaultOptionNotice(grunt, 'buildArch', 'noarch');
    }
    spec.tags.buildArch = options.buildArch || 'noarch';

    if (!_.has(options, 'description')) {
        _defaultOptionNotice(grunt, 'description', 'No Description');
    }
    spec.tags.description = options.description || 'No Description';

    if (!_.has(options, 'summary')) {
        _defaultOptionNotice(grunt, 'summary', 'No Summary');
    }
    spec.tags.summary = options.summary || 'No Summary';

    if (!_.has(options, 'license')) {
        _defaultOptionNotice(grunt, 'license', 'MIT');
    }
    spec.tags.license = options.license || 'MIT';

    spec.tags.epoch = options.epoch || spec.tags.epoch;
    spec.tags.distribution = options.distribution || spec.tags.distribution;

    if (!_.has(options, 'vendor')) {
        _defaultOptionNotice(grunt, 'vendor', 'Vendor');
    }
    spec.tags.vendor = options.vendor || 'Vendor';

    spec.tags.url = options.url || spec.tags.url;

    if (!_.has(options, 'group')) {
        _defaultOptionNotice(grunt, 'group', 'Development/Tools');
    }
    spec.tags.group = options.group || 'Development/Tools';

    spec.tags.packager = options.packager || spec.tags.packager;

    if (_.has(options, 'defines')) {
        spec.addDefines.apply(spec, options.defines);
    }

    // To maintain backwards compatability with the older API, the arrays
    // `dependencies` and `requires` are synonymous.
    if (_.has(options, 'dependencies')) {
        // TODO deprecate post 1.5.0
        grunt.log.writelns(chalk.gray('[Notice] Deprecation warning: ' +
            'the use of "dependencies" is deprecated in favour of ' +
            'the RPM "requires" and "conflicts" tags.'));
        spec.addRequirements.apply(spec, options.dependencies);
    }
    if (_.has(options, 'requires')) {
        spec.addRequirements.apply(spec, options.requires);
    }
    if (_.has(options, 'buildRequires')) {
        spec.addBuildRequirements.apply(spec, options.buildRequires);
    }
    if (_.has(options, 'provides')) {
        spec.addProvides.apply(spec, options.provides);
    }

    if (options.autoReq === false) {
        spec.tags.autoReq = options.autoReq;
    }
    if (options.autoProv === false) {
        spec.tags.autoProv = options.autoProv;
    }

    if (options.hasOwnProperty('excludeArchs')) {
        spec.addExcludeArchs.apply(spec, options.excludeArchs);
    }
    if (options.hasOwnProperty('exclusiveArchs')) {
        spec.addExclusiveArchs.apply(spec, options.exclusiveArchs);
    }

    if (options.hasOwnProperty('excludeOS')) {
        spec.addExcludeOS.apply(spec, options.excludeOS);
    }
    if (options.hasOwnProperty('exclusiveOS')) {
        spec.addExclusiveOS.apply(spec, options.exclusiveOS);
    }

    spec.tags.prefix = options.prefix || spec.tags.prefix;
    spec.tags.buildRoot = options.buildRoot || spec.tags.buildRoot;

    if (options.hasOwnProperty('sources')) {
        spec.addSources.apply(spec, options.sources);
    }
    if (options.hasOwnProperty('noSources')) {
        spec.addNoSources.apply(spec, options.noSources);
    }

    if (options.hasOwnProperty('patches')) {
        spec.addPatches.apply(spec, options.patches);
    }
    if (options.hasOwnProperty('noPatches')) {
        spec.addNoPatches.apply(spec, options.noPatches);
    }

    // Add scripts from options.
    if (options.hasOwnProperty('prepScript')) {
        spec.addPrepScripts.apply(spec, options.prepScript);
    }
    if (options.hasOwnProperty('buildScript')) {
        spec.addBuildScripts.apply(spec, options.buildScript);
    }
    if (options.hasOwnProperty('checkScript')) {
        spec.addCheckScripts.apply(spec, options.checkScript);
    }
    if (options.hasOwnProperty('cleanScript')) {
        spec.addCleanScripts.apply(spec, options.cleanScript);
    }

    if (options.hasOwnProperty('installScript')) {
        spec.addInstallScripts.apply(spec, options.installScript);
    }
    if (options.hasOwnProperty('preInstallScript')) {
        spec.addPreInstallScripts.apply(spec, options.preInstallScript);
    }
    if (options.hasOwnProperty('postInstallScript')) {
        spec.addPostInstallScripts.apply(spec, options.postInstallScript);
    }
    if (options.hasOwnProperty('preUninstallScript')) {
        spec.addPreUninstallScripts.apply(spec, options.preUninstallScript);
    }
    if (options.hasOwnProperty('postUninstallScript')) {
        spec.addPostUninstallScripts.apply(spec, options.postUninstallScript);
    }

    if (options.hasOwnProperty('verifyScript')) {
        spec.addVerifyScripts.apply(spec, options.verifyScript);
    }

    // Add the default file attributes from options.
    if (options.hasOwnProperty('defaultAttributes')) {
        spec.setDefaultAttributes(options.defaultAttributes);
    }

    // Add the changelogs.
    if (options.hasOwnProperty('changelog')) {
        var changelog;
        if (_.isFunction(options.changelog)) {
            changelog = options.changelog();
        } else if (_.isArray(options.changelog)) {
            changelog = options.changelog;
        }
        spec.addChangelogs.apply(spec, changelog);
    }
}

module.exports = function(grunt) {
    grunt.registerMultiTask('easy_rpm', 'Easily create RPM packages.', function() {
        var pkg = loadPackageProperties(grunt),
            defaults = {
                tempDir: "tmp-" + shortid.generate(),
                rpmDestination: ".",
                keepTemp: false,
                quoteFilePaths: true
            },
            rpmStructure = [
                "BUILD", "BUILDROOT", "RPMS", "SOURCES", "SPECS", "SRPMS"
            ],
            spec = new SpecFile(),
            done = this.async(),
            options, tmpDir, buildRoot, i;

        // Check the loaded package properties and issue notices if they are
        // not already specified in the options - they will be inherited by
        // the options object in that case.
        _checkNotifyPackageInherit(grunt, pkg, this.options(), 'name');
        _checkNotifyPackageInherit(grunt, pkg, this.options(), 'version');
        _checkNotifyPackageInherit(grunt, pkg, this.options(), 'description');

        // Apply options and defaults in the following order of precedence:
        //    gruntInit > package > defaults
        // Options on the left take precedence over those on the right.
        options = this.options(_.defaults(pkg, defaults));

        // Allow defines to be set in both the options and target configs.
        concatOptionDataArrays(options, this.data, 'defines');

        // Setup paths for storing the RPM directory structure and source
        // file destination (BUILDROOT).
        tmpDir = path.resolve(options.tempDir);
        buildRoot = path.resolve(tmpDir + "/BUILDROOT/");

        // Assign the options to the spec file object.
        applySpecSettings(grunt, options, spec);

        // If the tmpDir exists (probably from previous build), delete it
        // first.
        if (grunt.file.exists(tmpDir)) {
            grunt.log.writeln(chalk.blue('Deleting old temporary directory.'));
            grunt.file.delete(tmpDir);
        }

        // Create RPM build directory structure.
        grunt.log.writelns(
            chalk.blue('Creating RPM directory structure at: ') + tmpDir);
        for (i = 0; i < rpmStructure.length; i++) {
            grunt.file.mkdir(tmpDir + '/' + rpmStructure[i]);
        }

        // Expand the files to exclude.
        var filesToExclude = [];
        if (this.data.excludeFiles) {
            filesToExclude = grunt.file.expand(this.data.excludeFiles)
                .map(function(fileName) {
                    return path.normalize(fileName);
                });
        }

        // Copy sources to the BUILDROOT directory (dest).
        grunt.log.writeln(
            chalk.blue('Copying files into RPM directory structure.'));
        this.files.forEach(function(file) {
            // All files must specify both 'src' and 'dest'.
            if (!file.hasOwnProperty('src') || !file.hasOwnProperty('dest')) {
                grunt.log.error('All file entries must have both \'src\' ' +
                    'and \'dest\' property.');
                done(false);
            }

            file.src.filter(function(srcPath) {
                var actualSrcPath = srcPath,
                    copyTargetPath,
                    fileSpec;

                // Check whether to ignore this file.
                if (filesToExclude.indexOf(actualSrcPath) >= 0) {
                    return false;
                }

                // If the CWD option is specified, look for each file from
                // CWD path.
                if (file.cwd) {
                    actualSrcPath = path.join(file.cwd, srcPath);
                }

                copyTargetPath = path.join(buildRoot, file.dest, srcPath);
                fileSpec = {
                    path: path.join(file.dest, srcPath),
                    doc: file.doc || false,
                    config: file.config || false,
                    noreplace: file.noreplace || false,
                    dir: file.dir || false,
                    mode: file.mode || null,
                    user: file.user || file.owner || null,
                    group: file.group || null
                };

                // If this is a file, copy it to the BUILDROOT directory.
                // If it is a directory, mark it as a %dir before adding it
                // to the spec.
                if (!grunt.file.isDir(actualSrcPath)) {
                    grunt.verbose.writeln("Copying: " + actualSrcPath);
                    preserveCopy(grunt, actualSrcPath, copyTargetPath);
                } else {
                    grunt.verbose.writeln("Creating: " + actualSrcPath);
                    grunt.file.mkdir(copyTargetPath);
                    fileSpec.dir = true;
                }

                // Add the file or directory to the spec.
                spec.addFiles(fileSpec);
            });
        });

        // Validate the state of the spec file.  If there are errors, exit
        // the task unsuccessfully.  Print all warnings and errors.
        grunt.log.writeln(chalk.blue('Validating spec.'));
        var validationResults = specValidator(spec);
        if (validationResults.warnings.length > 0) {
            for (i = 0; i < validationResults.warnings.length; i++) {
                grunt.log.writeln(chalk.yellow('Warning: ' +
                    validationResults.warnings[i]));
            }
        }
        if (validationResults.errors.length > 0) {
            for (i = 0; i < validationResults.errors.length; i++) {
                grunt.log.writeln(chalk.red('Error: ' +
                    validationResults.errors[i]));
            }
        }
        if (validationResults.valid === false) {
            done(false);
            return;
        }

        // Generate and write out the spec file.
        var pkgName = spec.tags.name +
            '-' + spec.tags.version +
            '-' + spec.tags.release +
            '.' + spec.tags.buildArch,
            specFilepath = path.join(options.tempDir, "SPECS",
                pkgName + ".spec");

        grunt.log.writeln(chalk.blue('Generating RPM spec file.'));
        specWriter(spec, function(out, err) {
            grunt.file.write(specFilepath, out);
        });

        // Build the RPM package.
        grunt.log.writeln(chalk.blue('Building the RPM package.'));

        // Spawn rpmbuild tool.
        var buildCmd = 'rpmbuild';

        var buildArgs = [
            '-bb',

            // In order to build out the RPM into our specified directory, we
            // must append a --define directive to rpmbuild that indicates where
            // the top level of the build is.
            '--define',
            '_topdir ' + tmpDir,
            '-vv',
            '--buildroot',
            buildRoot,
            specFilepath
        ];

        grunt.util.spawn({
            cmd: buildCmd,
            args: buildArgs
        }, function(error, result, code) {
            if (error || code) {
                grunt.log.writeln(chalk.red(buildCmd + ' failed, errors:'));
                grunt.log.writeln(chalk.red(String(result)));
                done(false);
                return;
            }

            // Copy the build output to the current directory.
            var outputFilename = spec.tags.name +
                '-' + spec.tags.version +
                '-' + spec.tags.release +
                '.' + spec.tags.buildArch + '.rpm',
                outputFilepath = path.join(tmpDir, 'RPMS',
                    spec.tags.buildArch, outputFilename),
                rpmDestination = path.resolve(options.rpmDestination);

            grunt.log.writelns(chalk.blue('Copying RPM package to: ') +
                rpmDestination);
            grunt.file.copy(outputFilepath, path.join(rpmDestination,
                outputFilename));

            // Execute the postPackageCreate callback.
            if (options.hasOwnProperty('postPackageCreate')) {
                var rpmPath = path.join(tmpDir, 'RPMS', spec.tags.buildArch);

                if (_.isString(options.postPackageCreate)) {
                    // TODO Deprecate post 1.5.0
                    grunt.log.writeln(chalk.gray(
                        'Deprecation warning: use the rpmDestination ' +
                        'option instead of supplying a string to ' +
                        'postPackageCreate.'
                    ));

                    if (grunt.file.isDir(options.postPackageCreate)) {
                        var destinationFile = path.join(
                            options.postPackageCreate,
                            outputFilename);
                        // FIXME Why is the rpm filename being joined to itself?
                        grunt.file.copy(
                            path.join(outputFilename, outputFilename),
                            destinationFile);
                        grunt.log.writelns(
                            'Copied output RPM package to: ' + destinationFile);
                    } else {
                        grunt.fail.warn('Destination path is not a directory');
                    }
                } else if (_.isFunction(options.postPackageCreate)) {
                    options.postPackageCreate(rpmPath, outputFilename);
                }
            }

            // Delete temporary directory.
            if (!options.keepTemp) {
                grunt.log.writelns(chalk.blue(
                    'Deleting RPM directory structure at: ') + tmpDir);
                grunt.file.delete(tmpDir);
            }

            done();
        });
    });
};


// Couch database
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

var defaultable = require('defaultable');

defaultable(module,
  { 'uuid_batch_size': 100
  }, function(module, exports, DEFS, require) {

//
// I know maintenance will eventually have to be performed, like purging old documents.
// This is a place where it can go. Maybe in the future you can have dedicated maintenance
// nodes that don't interfere with the main API.
//
// Also, hopefully this file can be ported to browser Javascript and the same code can
// run client-side.

var lib = require('./lib')
  , util = require('util')
  , events = require('events')
  , assert = require('assert')
  , follow = require('follow')
  , debug = require('debug')
  , querystring = require('querystring')

//
// Constants
//

var KNOWN_COUCHES = {};
var UUIDS         = {}; // Map couch URLs to UUID pools.

//
// API
//

function Couch (opts) {
  var self = this;

  self.url     = (typeof opts.url === 'string') ? opts.url : null;
  self.userCtx = opts.userCtx || null;
  self.time_C  = opts.time_C  || null;
  self.known_dbs = null;

  self.log = debug('cqs:couch:' + self.url);
}

Couch.prototype.request = function(opts, callback) {
  var self = this;
  assert.ok(self.url);
  assert.ok(callback);

  if(typeof opts === 'string')
    opts = {'uri':opts};
  opts.uri = self.url + '/' + opts.uri;
  opts.time_C = opts.time_C || self.time_C;

  self.confirmed(function(er) {
    if(er) return callback(er);

    var method = opts.method || 'GET';
    self.log(method + ' ' + opts.uri);
    return lib.req_json(opts, callback);
  })
}

Couch.prototype.uuid = function get_uuid(count, callback) {
  var self = this;
  if(typeof count === 'function') {
    callback = count;
    count = 1;
  }

  var uuids = uuids_for(self);
  return uuids.get(count, callback);
}

Couch.prototype.confirmed = function confirm_couch(callback) {
  var self = this;
  assert.ok(callback);
  assert.ok(self.url);

  if(self.userCtx && self.known_dbs)
    return callback();

  self.url = self.url.replace(/\/+$/, '')
  var confirmer = KNOWN_COUCHES[self.url];
  if(!confirmer)
    confirmer = KNOWN_COUCHES[self.url] = new lib.Once;

  confirmer.on_done(function(er, userCtx, known_dbs) {
    if(er)
      return callback(er);

    self.userCtx = userCtx;
    self.known_dbs = known_dbs;
    return callback();
  })

  // Don't use self.request because that calls confirmed().
  function req(uri, cb) {
    return lib.req_json({'uri':uri, 'time_C':self.time_C}, cb);
  }

  confirmer.job(function(done) {
    self.log('Confirming Couch: ' + self.url);

    req(self.url, function(er, resp, body) {
      if(er) return done(er);

      if(body.couchdb !== 'Welcome')
        return done(new Error('Bad CouchDB response from ' + self.url));

      self.log('Confirming session');
      req(self.url + '/_session', function(er, resp, session) {
        if(er) return done(er);

        if(!session.userCtx)
          return done(new Error('Bad CouchDB response: ' + self.url + '/_session'));

        self.log('Couch confirmed: ' + self.url + ': ' + lib.JS(session.userCtx));
        var known_dbs = {};
        return done(null, session.userCtx, known_dbs);
      })
    })
  })
}

function Database (opts) {
  var self = this;
  opts = defaultable.merge(opts || {}, DEFS);

  if(typeof opts.couch !== 'string')
    throw new Error('Required "couch" option with URL of CouchDB');

  opts.db = opts.db || "";
  if(typeof opts.db !== 'string')
    throw new Error('Optional "db" option must be string');

  self.name   = opts.db;
  self.couch  = new Couch({'url':opts.couch, time_C:opts.time_C});
  self.secObj = null;
  self.known_queues = null;

  self.log = debug('cqs:db:' + self.name);
}

Database.prototype.request = function(opts, callback) {
  var self = this;

  if(typeof opts === 'string')
    opts = {'uri':opts};
  opts.uri = self.name + '/' + opts.uri;

  self.confirmed(function(er) {
    if(er) return callback(er);

    self.couch.request(opts, callback);
  })
}

Database.prototype.changes = function(opts) {
  var self = this

  if(typeof opts != 'object')
    opts = {}

  opts.db = self.couch.url + '/' + self.name
  return new follow.Feed(opts)
}

Database.prototype.confirmed = function(callback) {
  var self = this;
  assert.ok(callback);
  assert.ok(self.couch);

  self.couch.confirmed(function() {
    var confirmer = self.couch.known_dbs[self.name];
    if(!confirmer)
      confirmer = self.couch.known_dbs[self.name] = new lib.Once;

    confirmer.on_done(function(er, secObj, known_queues) {
      if(er)
        return callback(er);

      self.secObj = secObj;
      self.known_queues = known_queues;
      callback();
    })

    confirmer.job(confirm_db);
  })

  function confirm_db(done) {
    self.log('Confirming DB: ' + self.name);
    self.couch.request(self.name, function(er, resp, db) {
      if(er) return done(er);

      if(db.db_name !== self.name)
        return done(new Error('Expected DB name "'+self.name+'": ' + db.db_name));

      self.log('Checking _security: ' + self.name);
      self.couch.request(self.name+'/_security', function(er, resp, secObj) {
        if(er) return done(er);

        if(!secObj)
          return done(new Error('Bad _security response from ' + self.name + ': ' + lib.JS(secObj)));

        self.log('Confirmed DB: ' + self.name + ': ' + lib.JS(secObj));
        var known_queues = {};
        return done(null, secObj, known_queues);
      })
    })
  }
}

module.exports = { "Database" : Database
                 };

//
// Utilities
//

function uuids_for(couch) {
  UUIDS[couch.url] = UUIDS[couch.url] || new UUIDGetter(couch);
  return UUIDS[couch.url];
}

function UUIDGetter (couch) {
  var self = this;

  self.couch = couch;
  self.waiting = [];
  self.pool = [];
  self.fetching = false;
}

UUIDGetter.prototype.get = function(count, callback) {
  var self = this;

  self.waiting.push({'count':count, 'callback':callback});
  self.try_to_send();
}

UUIDGetter.prototype.try_to_send = function() {
  var self = this;

  if(self.fetching)
    return; // When the fetch is done, this will be re-run.

  var waiter = self.waiting[0];
  if(!waiter)
    return; // No more waiters for UUIDs.

  // If enough UUIDs are in the pool, just send them now.
  if(waiter.count <= self.pool.length) {
    self.respond(self.waiting.shift());
    return self.try_to_send();
  }

  // Otherwise, fetch some more.
  self.fetch();
}

UUIDGetter.prototype.fetch = function() {
  var self = this;

  assert.equal(self.fetching, false, 'Fetch called twice in a row: ' + self.couch.url);
  self.fetching = true;

  self.couch.request('_uuids?count=' + DEFS.uuid_batch_size, function(er, resp, result) {
    self.fetching = false;
    if(er)
      self.waiting.forEach(function(waiter) {
        waiter.callback(er)
      })

    else if(!result.uuids || result.uuids.length !== DEFS.uuid_batch_size) {
      er = new Error('Unknown _uuids result: ' + lib.JS(result));
      self.waiting.forEach(function(waiter) {
        waiter.callback(er)
      })
    }

    else {
      self.pool = self.pool.concat(result.uuids);
      self.try_to_send();
    }
  })
}

UUIDGetter.prototype.respond = function(waiter) {
  var self = this
    , count = waiter.count
    , callback = waiter.callback

  var response = self.pool.slice(0, count);
  self.pool    = self.pool.slice(count + 1);

  if(response.length === 1)
    response = response[0];

  callback(null, response);
}

}, require) // defaultable


// Queue design document
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

require('defaultable')(module,
  {
  }, function(module, exports, DEFS, require) {

var fs = require('fs')
  , lib = require('./lib')
  , path = require('path')
  , util = require('util')
  , debug = require('debug')
  , assert = require('assert')
  , browserify = require('browserify')
  ;

module.exports = { "DDoc" : DDoc

                 // For importing and unit testing.
                 , 'validate_doc_update': validate_doc_update
                 , 'visible_at'         : visible_at
                 , 'enqueue_message'    : enqueue_message
                 }

//
// Constants
//

var TEMPLATE =
  // _id
  // _rev
{ 'queues': {}

, updates: {"queue": enqueue_message}
, views: { "visible_at": { map: visible_at
                         , reduce: '_count'
                         }
         }

, "validate_doc_update": validate_doc_update
}

function enqueue_message(doc, req) {
  if(req.method != 'POST')
    return resp(400, {'error':'Only POST is allowed'})

  if(doc)
    return resp(400, {'error':'Only creating new messages is allowed'})

  try {
    var body = JSON.parse(req.body)
  } catch (er) {
    return resp(400, {'error':'Bad JSON body'})
  }

  // Figure out the queue to update, from .../_update/this_function/queue_name
  var i = req.path.indexOf('_update')
  var queue = req.path[i+2]

  var now = new Date
  doc =
    { _id: 'CQS/' + queue + '/' + req.uuid
    , ApproximateFirstReceiveTimestamp: null
    , ApproximateReceiveCount         : 0
    , Body                            : body
    , SenderId                        : req.userCtx.name
    , SentTimestamp                   : now
    , visible_at                      : now
    }

  //log('Create CQS document: ' + JSON.stringify(doc))
  return resp(201, doc)

  function resp(code, body) {
    if(code != 201)
      doc = null

    return [doc,
      { 'code'   : code
      , 'headers': {'content-type':'application/json'}
      , 'body'   : JSON.stringify(body) + "\n"
      }
    ]
  }
}

function validate_doc_update(newDoc, oldDoc, userCtx, secObj) {
  var ddoc = this
  var msg_id_re = /^CQS\/(.+?)\/([0-9a-f]{32})($|\/(.*)$)/

  var match = newDoc._id.match(msg_id_re)
  if(!match && ddoc.allow_foreign_docs)
    return // Ignore this non-CQS document.
  if(!match)
    throw {'forbidden': 'This database is for CQS only; bad message ID: ' + newDoc._id}

  var queue_id = match[1]
    , msg_id   = match[2]

  if(! (queue_id in ddoc.queues))
    throw {'forbidden':'Queue does not exist: '+queue_id}

  var IS_DB_ADMIN = false;

  secObj.admins = secObj.admins || {};
  secObj.admins.names = secObj.admins.names || [];
  secObj.admins.roles = secObj.admins.roles || [];

  if(userCtx.roles.indexOf('_admin') !== -1)
    IS_DB_ADMIN = true;
  if(secObj.admins.names.indexOf(userCtx.name) !== -1)
    IS_DB_ADMIN = true;
  for(i = 0; i < userCtx.roles; i++)
    if(secObj.admins.roles.indexOf(userCtx.roles[i]) !== -1)
      IS_DB_ADMIN = true;

  var good_keys = [ "_id", "_rev", "_revisions", "_deleted"
                  , 'SenderId'
                  , 'SentTimestamp'
                  , 'visible_at'
                  , 'ApproximateReceiveCount'
                  , 'ApproximateFirstReceiveTimestamp'
                  , 'MD5OfMessageBody'
                  , 'Body'

                  // Some extensions
                  , 'ReceiverId'
                  ];

  var key;
  for (key in newDoc)
    if(good_keys.indexOf(key) === -1)
      throw({forbidden: "Invalid field: " + key});

  if(! newDoc._deleted) {
    if(newDoc.Body === null || newDoc.Body === undefined)
      throw {forbidden: 'Invalid .Body: ' + JSON.stringify(newDoc.Body)};
  } else {
    if(!oldDoc || oldDoc.ReceiverId !== userCtx.name) {
      if(IS_DB_ADMIN)
        log('Allowing db admin "'+userCtx.name+'" to delete: ' + newDoc._id);
      else
        throw {forbidden: 'You may not delete this document'};
    }

    return;
  }

  if(!newDoc.visible_at)
    throw {forbidden: 'Must set visible_at'};

  if(oldDoc) {
    // Checkout ("receive")
    if(newDoc.ReceiverId !== userCtx.name && !IS_DB_ADMIN)
      throw({forbidden: 'Must set ReceiverId to your name: ' + JSON.stringify(userCtx.name)});

  } else {
    // Message send
    if(newDoc.SenderId !== userCtx.name && !IS_DB_ADMIN)
      throw({forbidden: 'Must set SenderId to your name: ' + JSON.stringify(userCtx.name)});
  }
}

function visible_at(doc) {
  var msg_id_re = /^CQS\/(.+?)\/([0-9a-f]{32})($|\/(.*)$)/

  var match = doc._id.match(msg_id_re)
  if(!match || !doc.visible_at)
    return

  var queue_id = match[1]
    , msg_id   = match[2]

  // The client must be able to check out ("receive") the message using this view data,
  // which means MVCC stuff and anything else necessary.
  var val = {"_id":doc._id, "_rev":doc._rev};
  for(var a in doc)
    if(a.match(/^[A-Z]/))
      val[a] = doc[a]

  var key = [queue_id, doc.visible_at]
  emit(key, val)
}

//
// API
//

function DDoc () {
  var self = this;

  self.copy_template();

  self._id = "_design/cqs"
}

// One common logger for them all, just so it won't get stored in couch.
DDoc.prototype.log = debug('cqs:ddoc');

DDoc.prototype.copy_template = function() {
  var self = this;

//  if(self.name.length < 1 || self.name.length > 80)
//    throw new Error("Queue name exceeds length max of 80: " + self.name.length)

  var ddoc = templated_ddoc()
  lib.copy(ddoc, self);
}

// Attach the web browser port.
DDoc.prototype.add_browser = function(callback) {
  if(!browserify){
    return callback(); //we're running in a browser (skip)
  }

  var counter = 1
    , self = this

  this._attachments = this._attachments || {}

  ;['showlist.js', 'index.html']
  .forEach(function(name) {
    counter++

    fs.readFile(__dirname + '/browser/' + name, function(err, data) {
      if(err) {
        handleError(err)
      } else {
        var content_type = name.substr(-3) === '.js' ? 'text/javascript' : 'text/html'
        addFile(name, data, content_type)
      }
    })
  })

  var file = ''

  browserify('./') // browserify the current module
    .require('./', {expose: 'cqs'})
    .bundle()
    .on('data', function(data){
      file += data
    })
    .on('error', handleError)
    .on('end', function(){
      addFile('index.js', new Buffer(file), 'text/javascript')
    })

  function handleError(err){
    counter = -1
    callback(err)
  }

  function addFile(name, data, content_type){
    self._attachments[name] = { content_type: content_type
                              , data        : data.toString('base64')
                              }

    if(!--counter)
      callback()
  }
}

//
// Utilities
//

function templated_ddoc(name) {
  return stringify_functions(TEMPLATE)
  function stringify_functions(obj) {
    var copy = {};

    if(Array.isArray(obj))
      return obj.map(stringify_functions)

    else if(typeof obj === 'object' && obj !== null) {
      Object.keys(obj).forEach(function(key) {
        copy[key] = stringify_functions(obj[key]);
      })
      return copy;
    }

    else if(typeof obj === 'function')
      return func_from_template(obj)

    else
      return lib.JDUP(obj);
  }
}

function func_from_template(func) {
  var src = func.toString();
  src = src.replace(/^function.*?\(/, 'function (');
  return src;
}

}, require) // defaultable


var DateTime = require('../DateTime')
var DateFormat = require('../DateFormat')

/**
 * For example:
 *   1 год
 *   2 года, 3 года, 4 года
 *   5 лет, 6 лет … 11 лет, 12 лет … 20 лет
 *   21 год, 31 год
 *   22 года, 32 года
 * @param {number} number
 * @param {Array} words
 * @return {string}
 */
function pluralize(number, words) {
  var magnitude = number % 100;
  var pluralWord = ''

  if ((magnitude > 10 && magnitude < 20) || (number === 0)) {
    pluralWord = words[2];
  } else {
    switch (Math.abs(number % 10)) {
      case 1:
        pluralWord = words[0];
        break
      case 2:
      case 3:
      case 4:
        pluralWord = words[1];
        break
      default:
        pluralWord = words[2];
        break
    }
  }
  return [number, pluralWord].join(' ')
}

module.exports = {
  id: 'RU',
  monthNames: ['Январь', 'Февраль', 'Март', 'Апрель', 'Май', 'Июнь', 'Июль', 'Август', 'Сентябрь', 'Октябрь', 'Ноябрь', 'Декабрь'],
  dayNames: ['Воскресенье', 'Понедельник', 'Вторник', 'Среда', 'Четверг', 'Пятница', 'Суббота'],
  shortDayNames: ['Вс', 'Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб'],
  yearsLabel: function (years) {return pluralize(years, ['Год', 'Года', 'Лет'])},
  monthsLabel: function (months) {return pluralize(months, ['Месяц', 'Месяца', 'Месяцев'])},
  daysLabel: function (days) {return pluralize(days, ['День', 'Дня', 'Дней'])},
  hoursLabel: function (hours, minutes) {
    var hoursAndMinutes = DateFormat.hoursAndMinutes(hours, minutes).replace('.', ',')
    /*
     * It's weird to say like this but correct pronounce is:
     * 1,2  = '1 целая две десятых часа'
     * 0,1  = '1 десятая часа'
     * 0,06 = '6 сотых часа'
     * 2,05 = '2 целых пять сотых часа'
     * 3,12 = '3 целых двенадцать сотых часа'
     * 4,29 = '4 целых 29 сотых часа'
     */
    return hoursAndMinutes + ' Часа'
  },
  learRangeLabel: 'Очистить диапазон',
  clearDateLabel: 'Очистить дату',
  shortDateFormat: 'j.n.Y',
  weekDateFormat: 'D j.n.Y',
  dateTimeFormat: 'D j.n.Y G:i',
  firstWeekday: DateTime.MONDAY,
  holidays: {}
}


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

require('defaultable')(module,
  {
  }, function(module, exports, DEFS, require) {

var lib = require('./lib')
  , util = require('util')
  , couch = require('./couch')
  , events = require('events')
  , assert = require('assert')
  , debug = require('debug')
  , querystring = require('querystring')
  ;

//
// Constants
//

//
// API
//

function Message (opts) {
  var self = this;
  events.EventEmitter.call(self);

  lib.copy(opts, self, 'uppercase');

  self.MessageId   = opts.MessageId   || opts._id  || null;
  self.Body        = opts.MessageBody || opts.Body || opts._str || null;
  self.MD5OfMessageBody = null;
  self.IdExtra     = opts.IdExtra     || null;
  self.queue       = opts.queue       || null;
  self.is_heartbeat= opts.is_heartbeat|| false;
  self.seq         = opts.seq         || null;

  self.log = debug('cqs:message:' + (self.MessageId || 'untitled'));
}
util.inherits(Message, events.EventEmitter);

Message.prototype.assert_received = function assert_received() {
  var self = this;
  assert.ok(self.queue        , 'Message not usable (no queue)');
  assert.ok(self.queue.db     , 'Message not usable (no queue db)');
  assert.ok(self.ReceiptHandle, 'Message not usable (no ReceiptHandle)');
}

Message.prototype.make_doc = function() {
  var self = this;

  var doc = lib.JDUP(self.mvcc || {});
  lib.copy(self, doc, 'uppercase');
  delete doc.MessageId; // _id is used instead
  delete doc.IdExtra;
  delete doc.VisibilityTimeout;
  delete doc.ReceiptHandle;

  return doc;
}

Message.prototype.send = function send_message(cb) {
  var self = this;
  assert.ok(cb);
  assert.ok(self.queue);
  assert.ok(self.queue.db);
  assert.ok(self.queue.db.couch);

  // Take advantage of the couch query to cache the userCtx a.k.a. pre-confirm before this query.
  var db = self.queue.db;
  db.couch.uuid(function(er, uuid) {
    if(er) return cb(er);

    var message_id = self.MessageId || uuid;
    if(typeof self.IdExtra === 'string')
      message_id += '/' + self.IdExtra;

    var doc_id = 'CQS/' + self.queue.name + '/' + message_id
      , sender_id = db.couch.userCtx.name
      , now = new Date
      ;

    // Unlike SQS, but like CouchDB, message bodies are structured (JSON), not just a string blob.
    var doc = { '_id'          : doc_id
              , 'SenderId'     : sender_id
              , 'SentTimestamp': now
              , 'visible_at'   : now
              , 'ApproximateReceiveCount': 0
              , 'ApproximateFirstReceiveTimestamp': null
              , 'Body'         : self.Body
              };

    //self.log = debug('PUT\n' + util.inspect(doc));
    db.request({method:'PUT',uri:lib.enc_id(doc._id), json:doc}, function(er, resp, result) {
      if(er) return cb(er);

      // The send was committed.
      self.MessageId = message_id;
      // TODO: MD5OfMessageBody

      cb(null, self);
    })
  })
}

Message.prototype.update = function update_message(callback) {
  var self = this;
  assert.ok(callback)
  assert.ok(callback);
  assert.ok(self.queue);
  assert.ok(self.queue.db);

  var doc_id = lib.enc_id('CQS/' + self.queue.name + '/' + self.MessageId);
  self.queue.db.request({'uri':doc_id, 'couch_errors':true}, function(er, resp, doc) {
    if(er)
      return callback(er);

    var key;
    if(resp.statusCode === 200)
      lib.copy(doc, self, 'uppercase');
    else {
      // This message was deleted;
      for (key in self)
        if(/^[A-Z]/.test(key))
          delete self[key];
      self.deleted = true;
    }

    // TODO: Detect if visible_at changed and update the alerts.
    return callback(null, self);
  })
}

Message.prototype.receive = function receive_message(callback) {
  var self = this;
  assert.ok(callback);
  assert.ok(self.queue);
  assert.ok(self.queue.db);
  assert.ok(self.queue.db.couch);

  assert.ok(self.mvcc);
  assert.ok(self.mvcc._id);
  assert.ok(new RegExp('/' + self.MessageId + '$').test(self.mvcc._id), lib.JS({_id:self.mvcc._id, ID:self.MessageId}));
  assert.ok('ApproximateReceiveCount'          in self, util.inspect(self));
  assert.ok('ApproximateFirstReceiveTimestamp' in self);

  self.queue.confirmed(function(er) {
    if(er) return callback(er);

    var doc = self.make_doc();
    doc.ReceiverId = self.queue.db.couch.userCtx.name;
    doc.ApproximateReceiveCount += 1;
    if(doc.ApproximateFirstReceiveTimestamp === null)
      doc.ApproximateFirstReceiveTimestamp = new Date;

    var timeout = self.VisibilityTimeout || self.queue.VisibilityTimeout;
    var visible_at = new Date;
    visible_at.setUTCMilliseconds(visible_at.getUTCMilliseconds() + (timeout * 1000));
    doc.visible_at = visible_at;

    var path = lib.enc_id(doc._id)
    self.queue.db.request({method:'PUT', uri:path, json:doc}, function(er, resp, result) {
      if(er) return callback(er);

      if(result.ok !== true)
        return callback(new Error('Bad doc update result: ' + lib.JS(result)));

      // Receive was a success.
      doc._rev = result.rev;
      self.reset(doc, visible_at);
      callback(null, self);
    })
  })
}

Message.prototype.reset = function(doc, new_visible_at) {
  var self = this;

  if(doc && new_visible_at) {
    delete self.mvcc;
    self.import_doc(doc);
    self.visible_at = new_visible_at;
    self.ReceiptHandle = {'_id':doc._id, '_rev':doc._rev};
  }

  self.receive_timers = self.receive_timers || [];
  self.receive_timers.forEach(function(timer) {
    clearTimeout(timer);
  })
  self.receive_timers = [];

  var reset_at = new Date
    , receipt_ms = new_visible_at - reset_at
    , warning_interval_ms, a, warner
    ;

  if(new_visible_at && self.is_heartbeat) {
    for(a = 1; a <= 12; a++) {
      warning_interval_ms = receipt_ms * (a / 12);
      warner = setTimeout(on_warn, warning_interval_ms);
      self.receive_timers.push(warner);
    }
  }

  function on_warn() {
    var interval_at = new Date
      , elapsed_ms = interval_at - reset_at
      , elapsed_pc = 100 * elapsed_ms / receipt_ms
      ;
    self.emit('heartbeat', elapsed_pc);
  }
}

Message.prototype.visibility =
Message.prototype.change_visibility = function (new_time, callback) {
  var self = this;

  assert.ok(callback);
  try        { self.assert_received() }
  catch (er) { return callback(er)    }

  var delta_ms, now = new Date;

  if(typeof new_time === 'number') {
    // Caller requests now + the given seconds.
    delta_ms = new_time * 1000;
    new_time = new Date;
    new_time.setUTCMilliseconds(new_time.getUTCMilliseconds() + delta_ms);
  } else {
    delta_ms = new_time - now;
    if(delta_ms < 0)
      return callback(new Error('Requested change ' + lib.JS(new_time) + ' is too late'));
    if(delta_ms < 2000)
      return callback(new Error('Requested change ' + lib.JS(new_time) + ' is less than 2s from now'));
  }

  self.mvcc = {'_id': self.ReceiptHandle._id, '_rev': self.ReceiptHandle._rev};
  var doc = self.make_doc();
  doc.visible_at = new_time;

  var path = lib.enc_id(doc._id)
  self.queue.db.request({method:'PUT', uri:path, json:doc}, function(er, resp, result) {
    if(er) return callback(er);

    if(result.ok !== true)
      return callback(new Error('Bad doc update result: ' + lib.JS(result)));

    // Update was a success.
    doc._rev = result.rev;
    self.reset(doc, new_time);
    callback(null, self);
  })
}

Message.prototype.del = function message_del(callback) {
  var self = this;

  assert.ok(callback);
  try        { self.assert_received() }
  catch (er) { return callback(er)    }

  var id = self.ReceiptHandle._id;
  var req = { method: 'DELETE'
            , uri   : lib.enc_id(id) + '?rev=' + self.ReceiptHandle._rev
            , headers: {'content-type': 'application/json'}
            }
  self.queue.db.request(req, function(er, resp, result) {
    // Note: delete always returns success.
    if(er)
      self.log = debug('Failed to delete ' + id + ': ' + er.message);

    if(!result || result.ok !== true)
      self.log = debug('Unknown response to delete' + lib.JS(result));

    Object.keys(self).forEach(function (key) {
      if(/^[A-Z]/.test(key))
        delete self[key];
    })

    self.reset();
    return callback(null, self);
  })
}

Message.prototype.import_doc = function(doc) {
  var self = this;

  lib.copy(doc, self, 'uppercase');
}

function change_visibility(opts, VisibilityTimeout, callback) {
  var message;
  if(lib.instanceof(opts, Message))
    message = opts;
  else if(opts.ReceiptHandle)
    return callback(new Error('ReceiptHandle is unsupported, use "Message" instead with message object'));
  else if(opts.Message || opts.message)
    message = opts.Message || opts.message;
  else
    return callback(new Error('Unknown options: ' + lib.JS(opts)));

  return message.change_visibility(VisibilityTimeout, callback);
}

function delete_message(msg, cb) {
  return msg.del(cb);
}

module.exports = { "Message" : Message
                 , "change_visibility"  : change_visibility
                 , "del"     : delete_message
                 };

//
// Utilities
//

}, require) // defaultable


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

var defaultable = require('defaultable');

defaultable(module,
  { 'visibility_timeout' : 30
  , 'browser_attachments': false
  , 'allow_foreign_docs' : false
  }, function(module, exports, DEFS, require) {

var lib = require('./lib')
  , txn = require('txn')
  , util = require('util')
  , debug = require('debug')
  , couch = require('./couch')
  , assert = require('assert')
  , message = require('./message')
  , queue_ddoc = require('./ddoc')
  , querystring = require('querystring')
  ;

//
// Constants
//

var DDOC_ID = '_design/cqs'

//
// API
//

function Queue (opts) {
  var self = this;

  if(typeof opts == 'string')
    opts = {'name':opts};

  opts = defaultable.merge(opts, DEFS);

  self.name = opts.name || opts.QueueName || opts._str || null;
  self.time_C = opts.time_C || null;
  self.db = new couch.Database({'couch':opts.couch, 'db':opts.db, time_C:self.time_C});

  self.VisibilityTimeout = opts.DefaultVisibilityTimeout || opts.VisibilityTimeout || DEFS.visibility_timeout;

  self.cache_confirmation = true;
  self.browser_attachments = !!(opts.browser_attachments);
  self.allow_foreign_docs = opts.allow_foreign_docs

  self.log = debug('cqs:queue:' + (self.name || 'untitled'));
}

Queue.prototype.confirm =
Queue.prototype.confirmed = function after_confirmed(opt, callback) {
  var self = this;

  if(!callback && typeof opt === 'function') {
    callback = opt;
    opt = '';
  }

  assert.ok(callback);
  assert.ok(self.db);

  self.db.confirmed(function(er) {
    if(er)
      return callback(er);

    var confirmer = self.db.known_queues[self.name];
    if(!confirmer || opt === '--force' || !self.cache_confirmation)
      confirmer = self.db.known_queues[self.name] = new lib.Once;

    confirmer.on_done(function(er, ddoc) {
      if(er)
        return callback(er);

      // Copy all attributes from the API.
      self.import_ddoc(ddoc);
      return callback(null, self);
    })

    confirmer.job(confirm_queue);
  })

  function confirm_queue(done) {
    self.log('Confirming queue: ' + self.name);
    self.db.request(lib.enc_id(DDOC_ID), function(er, res) {
      if(er)
        return done(er)

      if(! (self.name in res.body.queues)) {
        if(opt != '--allow-missing')
          return done(new Error('Queue does not exist: ' + self.name))

        self.log('Using non-existent queue: ' + self.name)
        res.body.queues[self.name] = {}
      }

      done(null, res.body)
    })
  }
}

Queue.prototype.create = function create_queue(callback) {
  var self = this;
  assert.ok(callback);

  self.db.confirmed(function(er) {
    if(er)
      return callback(er)
    txn({'couch':self.db.couch.url, 'db':encodeURIComponent(self.db.name), 'id':DDOC_ID, 'create':true}, add_queue, queue_added)
  })

  function add_queue(ddoc, to_txn) {
    //self.log('Add queue to ddoc: %j', ddoc)
    var now = new Date

    if(!ddoc._rev)
      ddoc = new queue_ddoc.DDoc

    ddoc.allow_foreign_docs = self.allow_foreign_docs

    ddoc.queues[self.name] = ddoc.queues[self.name] ||
      { ApproximateNumberOfMessages          : 0
      , ApproximateNumberOfMessagesNotVisible: 0
      , VisibilityTimeout                    : self.VisibilityTimeout || 30
      , CreatedTimestamp                     : now
      , LastModifiedTimestamp                : now
      , Policy                               : null
      , MaximumMessageSize                   : 8192
      , MessageRetentionPeriod               : 345600
      , QueueArn                             : null
      }

    if(!self.browser_attachments)
      return to_txn(null, ddoc)
    else
      queue_ddoc.DDoc.prototype.add_browser.call(ddoc, function(er) {
        return to_txn(er, ddoc)
      })
  }

  function queue_added(er, new_ddoc) {
    if(er)
      return callback(er)

    self.log('new_ddoc: %j', new_ddoc._id)
    self.log('Created queue: ' + self.name)

    // Consider myself confirmed as well.
    self.db.known_queues[self.name] = new lib.Once
    self.db.known_queues[self.name].job(function(done) { done(null, new_ddoc) })
    self.import_ddoc(new_ddoc)
    callback(null, self.name)
  }
}

Queue.prototype.import_ddoc = function(ddoc) {
  var self = this;
  self.ddoc_id = ddoc._id;
  self.ddoc_rev = ddoc._rev;
  lib.copy(ddoc.queues[self.name], self, 'uppercase');
}

Queue.prototype.send = function(opts, extra, callback) {
  var self = this;

  if(!callback && typeof extra === 'function') {
    callback = extra;
    extra = null;
  }

  self.confirmed(function(er) {
    if(er) return callback(er);

    opts = lib.opt_def(opts);
    var body = opts._str || opts;
    var msg = new message.Message({ 'couch'  : self.couch
                                  , 'db'     : self.db
                                  , 'queue'  : self
                                  , 'IdExtra': extra
                                  , 'Body'   : body
                                  });
    msg.send(callback);
  })
}

Queue.prototype.receive = function(opts, callback) {
  var self = this;

  if(typeof opts === 'function') {
    callback = opts;
    opts = 1;
  }

  if(typeof opts === 'number')
    opts = {'MaxNumberOfMessages': opts};

  var msg_count   = opts.MaxNumberOfMessages || 1;
  var vis_timeout = opts.VisibilityTimeout || self.VisibilityTimeout;
  var is_heartbeat = opts.is_heartbeat || false;

  assert.ok(msg_count > 0  , "Message count is too low");
  assert.ok(msg_count <= 10, "Message count is too high");
  assert.ok(vis_timeout >= 0, "Visibility timeout is too low");
  assert.ok(vis_timeout <= 43200, "Visibility timeout is too high");

  self.confirmed(function(er) {
    if(er) return callback(er);

    var startkey = lib.JS([ self.name, "" ]);
    var endkey   = lib.JS([ self.name, new Date ]); // Anything becoming visible up to now.
    var query = querystring.stringify({ reduce: false
                                      , limit : msg_count
                                      , startkey: startkey
                                      , endkey: endkey
                                      });
    var path = lib.enc_id(self.ddoc_id) + '/_view/visible_at?' + query;
    self.db.request(path, function(er, resp, view) {
      if(er) return callback(er);

      if(view.rows.length === 0)
        return callback(null, []);

      // Don't lose the order CouchDB set for the messages.
      var messages = [], count = 0;
      function on_receive(er, pos, msg) {
        if(er && er.statusCode != 409 && er.error != 'conflict') {
          self.log('Receive error: ' + er);
          return callback(er);
        }

        if(er && er.statusCode == 409 && er.error == 'conflict') {
          self.log('Missed message '+pos+': ' + msg.MessageId);
          msg = null;
        }

        messages[pos] = msg || null;

        count += 1;
        if(count === view.rows.length) {
          messages = messages.filter(function(x) { return !!x });
          callback(null, messages);
        }
      }

      view.rows.forEach(function(row, i) {
        var match = new RegExp('^CQS/' + self.name + '/' + '(.+)$').exec(row.value._id);
        assert.ok(match, "Bad view row: " + lib.JS(row)); // TODO: This does not belong in async code.

        var msg_opts = {};
        lib.copy(row.value, msg_opts, 'uppercase');
        msg_opts.MessageId = match[1];

        var msg = new message.Message(msg_opts);
        msg.queue = self;
        msg.VisibilityTimeout = vis_timeout;
        msg.is_heartbeat      = is_heartbeat;
        msg.mvcc = {'_id':row.value._id, '_rev':row.value._rev};
        msg.receive(function(er) { on_receive(er, i, msg) });
      })
    })
  })
}

Queue.prototype.changes = function(opts) {
  var self = this

  if(typeof opts != 'object')
    opts = {}

  var prefix = 'CQS/' + self.name + '/'
  var feed = self.db.changes(opts)
  var limit = opts.ConcurrencyLimit || 1e3
  var count = 0
  var vis_timeout = opts.VisibilityTimeout || self.VisibilityTimeout
  var is_heartbeat = opts.is_heartbeat || false

  assert.ok(vis_timeout >= 0, "Visibility timeout is too low")
  assert.ok(vis_timeout <= 43200, "Visibility timeout is too high")

  //TODO this is a rather ugly solution (as it consumes _all_ changes)
  //follow also allows `filter` to be based on a view, which is probably much better suited
  feed.filter = function(doc, req) {
    return doc._id.substr(0, prefix.length) === prefix
          && Date.parse(doc.visible_at) <= Date.now()
  }

  feed.on('change', function(change) {
    if(++count >= limit) feed.pause()

    var msg_opts = {}
    lib.copy(change.doc, msg_opts, 'uppercase')
    msg_opts.seq = change.seq;
    msg_opts.MessageId = change.id.substr(prefix.length)

    var msg = new message.Message(msg_opts)
    msg.queue = self
    msg.VisibilityTimeout = vis_timeout
    msg.is_heartbeat      = is_heartbeat
    msg.mvcc = { _id: change.doc._id, _rev: change.doc._rev }
    msg.receive(function(err) {
      if(err) {
        if(err.statusCode == 409 && err.error == 'conflict');
        else feed.emit('error', err)
        reduceCount()
      } else {
        feed.emit('message', msg, callback)
      }

      var called = false
      function callback() {
        if(!called){
          called = true
          reduceCount()
        }
      }
    })

    function reduceCount() {
      count -= 1
      if(feed.is_paused) feed.resume()
    }
  })

  feed.follow()

  return feed
}

Queue.prototype.scrub = function(opts, callback) {
  var self = this;

  if(typeof callback != 'function' || typeof opts != 'object')
    throw new Error('Need options object and callback function')
  if(typeof opts.match != 'function')
    throw new Error('Need options.match function to evaluate messages')

  self.confirmed(function(er) {
    if(er)
      return callback(er)

    var startkey = lib.JS([ self.name, "" ])
    var endkey   = lib.JS([ self.name, {} ]) // All messages

    if(opts.descending) {
      var _tmp = endkey
      endkey = startkey
      startkey = _tmp
    }

    var query = { 'reduce': false
                , 'startkey': startkey
                , 'endkey'  : endkey
                }

    if(opts.count)
      query.limit = opts.count

    query = querystring.stringify(query)

    var path = lib.enc_id(self.ddoc_id) + '/_view/visible_at?' + query
    self.db.request(path, function(er, resp, view) {
      if(er)
        return callback(er)

      var update = {'docs':[]}
      for(var i = 0; i < view.rows.length; i++) {
        var row = view.rows[i]
        var msg = row.value.Body

        try {
	  var is_match = !! opts.match(msg)
        } catch (match_er) {
          return callback(match_er)
        }

        // console.log('Row: %j', row)
        if(is_match)
          update.docs.push({'_id':row.id, '_rev':row.value._rev, '_deleted':true})
      }

      var found_rows = update.docs.length
      // console.log('Delete %d docs', found_rows)

      self.db.request({'method':'POST', 'uri':'_bulk_docs', 'json':update}, function(er, res) {
        if(er)
          return callback(er)
        if(res.statusCode != 201)
          return callback(new Error('Bad response '+res.statusCode+' to bulk update'))

        var deleted = 0
        var conflicts = 0

        for(var i = 0; i < res.body.length; i++) {
          var result = res.body[i]
          if(result.ok)
            deleted += 1
          else if(result.error == 'conflict')
            conflicts += 1
          else
            return callback(new Error('Unknown update: ' + JSON.stringify(result)))
        }

        return callback(null, found_rows, deleted, conflicts)
      })
    })
  }) // self.confirmed
}

Queue.prototype.set = function set_attrs(opts, callback) {
  var self = this;
  assert.ok(opts);
  assert.ok(callback);
  assert.equal(typeof opts, 'object');

  opts = lib.JDUP(opts)

  self.log('Set attributes: ' + self.name);
  self.confirmed('--force', function(er) {
    if(er)
      return callback(er)

    function go(er) {
      if(er) return callback(er);

      var req = { method: 'PUT'
                , uri   : lib.enc_id(ddoc._id)
                , json  : ddoc
                }
      self.db.request(req, function(er, resp, body) {
        if(er) return callback(er);
      })
    }
  })

  self.db.confirmed(function(er) {
    if(er)
      return callback(er)
    txn({'couch':self.db.couch.url, 'db':encodeURIComponent(self.db.name), 'id':DDOC_ID}, update_attrs, attrs_updated)
  })

  function update_attrs(ddoc, to_txn) {
    //self.log('Update queue on ddoc: %j', ddoc)
    var now = new Date
      , queue = ddoc.queues[self.name]

    lib.copy(opts, queue, 'uppercase');
    queue.LastModifiedTimestamp = now

    if(!self.browser_attachments)
      return to_txn()
    else
      ddoc.add_browser(to_txn)
  }

  function attrs_updated(er, new_ddoc) {
    if(er)
      return callback(er)

    // SetAttributes was committed, consider myself confirmed as well.
    self.db.known_queues[self.name] = new lib.Once;
    self.db.known_queues[self.name].job(function(done) { done(null, new_ddoc) });
    self.import_ddoc(new_ddoc);
    callback(null)
  }
}

Queue.prototype.GetAttributes = function get_attrs(attrs, callback, extra) {
  var self = this;

  if(!callback && typeof attrs === 'function') {
    callback = attrs;
    attrs = null;
  }

  var confirmer = function(cb) { return self.confirmed(cb) };
  if(attrs === '--force') {
    confirmer = function(cb) { return self.confirmed('--force', cb) };
    attrs = callback;
    callback = extra;
  }

  if(typeof attrs === 'string')
    attrs = [attrs];

  attrs = attrs || ['all'];
  assert.ok(Array.isArray(attrs));
  assert.ok(callback);

  self.log('Get attributes: ' + self.name);
  confirmer(function(er) {
    if(er) return callback(er);
    callback(null, self);
  })
}

function create_queue(opts, cb) {
  var queue = new Queue(opts);
  queue.create(function(er, name) {
    if(er) return cb(er);
    return cb(null, queue);
  })
}

function list_queues(opts, cb) {
  if(!cb && typeof opts == 'function') {
    cb = opts;
    opts = {};
  }

  if(typeof opts == 'string')
    opts = {'prefix':opts};
  opts = defaultable.merge(opts, DEFS);
  var prefix = opts.prefix || opts._str;

  if(!cb && opts._func)
    cb = opts._func;

  prefix = new RegExp('^' + (prefix || ""))

  var ddoc_url = opts.couch + '/' + encodeURIComponent(opts.db) + '/' + lib.enc_id(DDOC_ID)
  lib.req_json({'url':ddoc_url, time_C:opts.time_C}, function(er, res) {
    if(er)
      return cb(er)

    var queues = Object.keys(res.body.queues)
      .filter(function(name) { return !! name.match(prefix) })
      .map(get_queue)

    return cb(null, queues)

    function get_queue(name) {
      return new Queue({ couch: opts.couch
                       , db   : opts.db
                       , name : name
                       , time_C: opts.time_C
                       })
    }
  })
}

function set_attributes(queue, attrs, callback) {
  return queue.set(attrs, callback);
}

function get_attributes(opts, attrs, callback, extra) {
  var queue = (opts instanceof Queue) ? opts : new Queue(opts);
  return queue.GetAttributes(attrs, callback, extra);
}

function send_message(opts, message, extra, callback) {
  var queue;
  if(opts instanceof Queue)
    queue = opts;
  else if(opts.queue) {
    if(opts.queue instanceof Queue)
      queue = opts.queue
    else
      queue = new Queue({'QueueName':opts.queue, 'couch':opts.couch || DEFS.couch, 'db':opts.db || DEFS.db});
  }
  else
    queue = new Queue(opts);

  if(!callback && typeof extra === 'function') {
    callback = extra;
    extra = null;
  }

  return queue.send(message, extra, callback);
}

function receive_message(opts, callback, extra) {
  if(typeof opts == 'string')
    opts = {'queue':opts};

  var queue;
  if(opts instanceof Queue)
    queue = opts;
  else if(opts._str || opts.queue) {
    if(opts.queue instanceof Queue)
      queue = opts.queue;
    else
      queue = new Queue({'QueueName':opts._str || opts.queue, 'couch':opts.couch || DEFS.couch, 'db':opts.db || DEFS.db});
    delete opts.queue;
    delete opts._str;
  } else
    Queue = new Queue(opts);

  if(typeof callback === 'number' && typeof extra === 'function') {
    opts= { 'MaxNumberOfMessages': callback };
    callback = extra;
  }

  assert.ok(callback);
  return queue.receive(opts, callback);
}

module.exports = { "Queue" : Queue
                 , "create": create_queue
                 , "list"  : list_queues
                 , "send"  : send_message
                 , "receive": receive_message
                 , "set"   : set_attributes
                 , "get"   : get_attributes
                 };

//
// Utilities
//

}, require) // defaultable


'use strict'

var is = require('unist-util-is')

module.exports = remove

function remove(ast, opts, test) {
  var cascade

  if (!test) {
    test = opts
    opts = {}
  }

  cascade = opts.cascade
  cascade = cascade === null || cascade === undefined ? true : cascade

  return preorder(ast, null, null)

  // Check and remove nodes recursively in preorder.
  // For each composite node, modify its children array in-place.
  function preorder(node, nodeIndex, parent) {
    var children
    var length
    var index
    var position
    var child

    if (is(test, node, nodeIndex, parent)) {
      return null
    }

    children = node.children

    if (!children || children.length === 0) {
      return node
    }

    // Move all living children to the beginning of the children array.
    position = 0
    length = children.length
    index = -1

    while (++index < length) {
      child = preorder(children[index], index, node)

      if (child) {
        children[position++] = child
      }
    }

    // Cascade delete.
    if (cascade && position === 0) {
      return null
    }

    // Drop other nodes.
    children.length = position

    return node
  }
}


var debug = require('debug')('mdns-packet:lib:dns:dnsrecord');

var BufferConsumer = require('./bufferconsumer');
var errors = require('./errors');

/**
 * DNSRecord is a record inside a DNS packet; e.g. a QUESTION, or an ANSWER,
 * AUTHORITY, or ADDITIONAL record. Note that QUESTION records are special,
 * and do not have ttl or data.
 * @class
 * @param {string} name
 * @param {number} type
 * @param {number} cl - class
 * @param {number} [optTTL] - time to live in seconds
 */
var DNSRecord = module.exports = function (name, type, cl, optTTL) {
  var self = this;
  this.name = name;
  this.type = type;
  this.class = cl;

  if (type === 0 || typeof type === 'undefined') {
    throw new errors.MalformedPacket('Record.type is empty');
  }

  if (cl === 0) {
    throw new errors.MalformedPacket('Record.class is empty');
  }

  this.ttl = (typeof optTTL !== 'undefined') ? optTTL : DNSRecord.TTL;
  this.isQD = (arguments.length === 3);
  debug('new DNSRecord', this);

  this.__defineGetter__('typeName', function () {
    return DNSRecord.TypeName[self.type];
  });

  this.__defineGetter__('className', function () {
    return DNSRecord.ClassName[self.class & 0x7fff];
  });

  this.__defineGetter__('flag', function () {
    return (self.class & 0x8000) >> 15;
  });
};

/**
 * Enum for record type values
 * @readonly
 * @enum {number}
 */
DNSRecord.Type = {
  A: 0x01,        // 1
  NS: 0x02,       //2
  CNAME: 0x05,    //5
  SOA: 0x06,     //6
  PTR: 0x0c,      // 12
  MX: 0x0f,       //15
  TXT: 0x10,      // 16
  AAAA: 28,       // 0x16
  SRV: 0x21,       // 33
  OPT: 0x29,      //41 RFC6981 -needed for EDNS
  NSEC: 0x2f,    //47
  TLSA: 0x34,      //52 RFC6698 - associate TLS server certificate.
  ANY: 0xff
};

/**
* Enum for record class values
* @readonly
* @enum {number}
*/
DNSRecord.Class = {
  IN: 0x01,
  ANY: 0xff,
  FLUSH: 0x8000,
  IS_QM: 0x8000
};

DNSRecord.TTL = 3600; // one hour default TTL
DNSRecord.TypeName = {};
DNSRecord.ClassName = {};

var typekey;

for (typekey in DNSRecord.Type) {
  if (DNSRecord.Type.hasOwnProperty(typekey)) {
    DNSRecord.TypeName[DNSRecord.Type[typekey]] = typekey;
  }
}

for (typekey in DNSRecord.Class) {
  if (DNSRecord.Class.hasOwnProperty(typekey)) {
    DNSRecord.ClassName[DNSRecord.Class[typekey]] = typekey;
  }
}

DNSRecord.write = function (out, rec, withLength) {
  withLength = withLength || false;
  debug('#write() type: %s, flag:%d class:%s, withLength:%s',
    rec.typeName, rec.flag,
    rec.className, withLength);
  if (rec.type === 0 || rec.class === 0) {
    throw new Error('Bad record with empty type and/or class');
  }
  //TODO:if outer and any string in data or name
  //     can be found there. Do a ref instead.
  out.name(rec.name).short(rec.type).short(rec.class);
  if (rec.isQD) {
    return out;
  }

  out.long(rec.ttl);

  var startPos = out.tell();
  out.short(0xffff); //reserve some length

  switch (rec.type) {
    case DNSRecord.Type.A:
      writeA(out, rec.address);
      break;
    case DNSRecord.Type.NS:
    case DNSRecord.Type.CNAME:
    case DNSRecord.Type.PTR:
      out.name(rec.data);
      break;
    case DNSRecord.Type.MX:
      //asMx(consumer, rec);
      break;
    case DNSRecord.Type.TXT:
      for (var key in rec.data) {
        if (rec.data.hasOwnProperty(key)) {
          // properly encode this
          out.name(key + '=' + rec.data[key]);
          out.offset--;
        }
      }
      break;
    case DNSRecord.Type.AAAA:
      //asAAAA(consumer, rec);
      break;
    case DNSRecord.Type.SRV:
      out.short(rec.priority & 0xffff).short(rec.weight & 0xffff)
        .short(rec.port & 0xffff).name(rec.target);
      break;
    case DNSRecord.Type.SOA:
      out.name(rec.primary).name(rec.admin).long(rec.serial).long(rec.refresh)
        .long(rec.retry).long(rec.expiration).long(rec.minimum);
      break;
    default:
      debug('non implemented recordtype of ' + rec.type);
      throw new Error('Not implemented recordtype');
      //this.data = new BufferConsumer(consumer.slice(dataSize));
  }
  var endPos = out.tell();
  //update with correct size
  var correctSize = endPos - startPos - 2;
  debug('correct size=%s bytes', correctSize);
  out.seek(startPos).short(correctSize).seek(endPos);

  return out;
};

function writeA(out, ip) {
  var parts = ip.split('.');
  for (var i = 0; i < 4; i++) {
    out.byte(parts[i]);
  }
}

DNSRecord.parse = function (consumer) {
  if (consumer instanceof Buffer) {
    debug('making consumer out of buffer');
    consumer = new BufferConsumer(consumer);
    consumer.seek(0);
  }

  debug('#parse from %d', consumer.tell());
  var rec = new DNSRecord(
    consumer.name(),
    consumer.short(), // type
    consumer.short(), // class
    consumer.long() //ttlgf
  );

  debug('parsing from %d', consumer.tell());

  var dataSize = consumer.short();
  debug('going for type %s. start: %d, size: %d, end: %d, length: %d',
    rec.type,
    consumer.tell(),
    dataSize,
    consumer.tell() + dataSize,
    consumer.length
  );

  switch (rec.type) {
    case DNSRecord.Type.A:
      asA(consumer, rec);
      break;
    case DNSRecord.Type.NS:
    case DNSRecord.Type.CNAME:
    case DNSRecord.Type.PTR:
      rec.data = asName(consumer);
      break;
    case DNSRecord.Type.MX:
      asMx(consumer, rec);
      break;
    case DNSRecord.Type.TXT:
      rec.data = asTxt(consumer, consumer.tell() + dataSize);
      break;
    case DNSRecord.Type.AAAA:
      asAAAA(consumer, rec);
      break;
    case DNSRecord.Type.SRV:
      asSrv(consumer, rec);
      break;
    case DNSRecord.Type.SOA:
      asSoa(consumer, rec);
      break;
    case DNSRecord.Type.OPT:
      asOpt(consumer, rec);
      break;
    case DNSRecord.Type.TLSA:
      asTLSA(consumer, rec, dataSize);
      break;
    default:
      debug('non implemented recordtype of ' + rec.type);
      rec.data = new BufferConsumer(consumer.slice(dataSize));
  }
  debug('record done at %d', consumer.tell(), rec);
  return rec;
};

DNSRecord.parseQuestion = function (consumer) {
  if (consumer instanceof Buffer) {
    debug('making consumer out of buffer');
    consumer = new BufferConsumer(consumer);
  }
  debug('#parseQuestion from %d', consumer.tell());
  var r = new DNSRecord(
          consumer.name(),
          consumer.short(), // type
          consumer.short() // class
          );
  debug('record done at %d', consumer.tell(), r);
  return r;
};

function asName(consumer) {
  return consumer.name(true);
}

function asSrv(consumer, record) {
  debug('priority: %d', record.priority = consumer.short());
  debug('weight: %d', record.weight = consumer.short());
  debug('port: %d', record.port = consumer.short());
  record.target = consumer.name();
  // debug('priority:%d, weight: %d, port:%d, target:%s', record.priority,
  //   record.weight, record.port, record.target);

}

function asMx(consumer, record) {
  record.priority = consumer.short();
  record.exchange = asName(consumer);
}

function asTxt(consumer, endAt) {
  var items = consumer.name(false, endAt);
  debug('txt items', items);
  //note:disable to have same syntax as native-dns-packet
  // if (items.length === 1 && items[0].length > 0) {
  //   return items[0];
  // }
  return items;
}

function asA(consumer, record) {
  var data = '';
  for (var i = 0; i < 3; i++) {
    data += consumer.byte() + '.';
  }
  data += consumer.byte();
  record.address = data;
}

/*
 * Parse data into a IPV6 address string
 * @returns {string}
 */
function asAAAA(consumer, packet) {
  var data = '';
  for (var i = 0; i < 7; i++) {
    data += consumer.short().toString(16) + ':';
  }
  data += consumer.short().toString(16);
  packet.address = data;
}

function asSoa(consumer, packet) {
  packet.primary = consumer.name(true);
  packet.admin = consumer.name(true);
  packet.serial = consumer.long();
  packet.refresh = consumer.long();
  packet.retry = consumer.long();
  packet.expiration = consumer.long();
  packet.minimum = consumer.long();
}

function asOpt(consumer, packet) {
  //if at end of buffer there is no optional data.
  var opt = {
    code: 0,
    data: [],
    rcode: 0,
    version: 0,
    do: 0,
    z: 0
  };

  if (!consumer.isEOF()) {
    opt.code = consumer.short();
    opt.data = consumer.slice(consumer.short());
  }

  opt.rcode = (packet.ttl & 0xff000000) >> 24;
  opt.version = (packet.ttl & 0x00FF0000) >> 16;
  opt.do = (packet.ttl & 0x00008000) >> 15;
  opt.z = (packet.ttl & 0x00001FFF);

  debug('asOpt', opt);
  packet.opt = opt;
}

function asTLSA(consumer, packet, dataSize) {
  packet.usage = consumer.byte();
  packet.selector = consumer.byte();
  packet.matchingtype = consumer.byte();
  packet.buff = consumer.slice(dataSize - 3); //size - 3 because of 3 bytes above
}


'use strict';

Object.defineProperty(exports, "__esModule", {
  value: true
});

var _isArray = require('lodash/isArray');

var _isArray2 = _interopRequireDefault(_isArray);

var _util = require('util');

var _util2 = _interopRequireDefault(_util);

var _asset = require('../models/asset');

var _asset2 = _interopRequireDefault(_asset);

var _players = require('../models/players');

var _players2 = _interopRequireDefault(_players);

var _player = require('../models/player');

var _player2 = _interopRequireDefault(_player);

var _matches = require('../models/matches');

var _matches2 = _interopRequireDefault(_matches);

var _match = require('../models/match');

var _match2 = _interopRequireDefault(_match);

var _participant = require('../models/participant');

var _participant2 = _interopRequireDefault(_participant);

var _roster = require('../models/roster');

var _roster2 = _interopRequireDefault(_roster);

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

var map = {
  asset: _asset2.default,
  players: _players2.default,
  player: _player2.default,
  matches: _matches2.default,
  match: _match2.default,
  participant: _participant2.default,
  roster: _roster2.default
};

function getModel(entityType) {
  var model = map[entityType];
  if (!model) {
    throw new Error('Could not figure out class mapping');
  }

  return model;
}

exports.default = function (entity, data) {

  if (data === null) {
    return data;
  }

  var parentData = data.data;
  var includedData = data.included;
  var BaseModel = getModel(entity);
  var dataModel = new BaseModel(parentData);

  function checkForRelations(model, parent) {
    var clone = model;
    if (!model.relationships || !parent) {
      return clone;
    }

    if (model.relationships && model.relationships.length > 0) {
      model.relationships.forEach(function (item) {
        if ((0, _isArray2.default)(parent)) {
          clone[item.type] = parent.map(function (child) {
            var ChildModel = getModel(child.type);
            var childModel = new ChildModel(child);
            return checkForRelations(childModel, child);
          });
        } else {
          var relations = parent.relationships[item.type] ? parent.relationships[item.type].data : false;
          if (relations) {
            clone[item.type] = (0, _isArray2.default)(relations) ? relations.map(function (relation) {
              return filterRelations(relation);
            }) : filterRelations(relations);
          }
        }
      });
    }
    return clone;
  }

  // Filters models so they are formatted correctly.
  function filterRelations(relation) {
    var mappedData = includedData.find(function (inc) {
      return inc.id === relation.id;
    });
    var RelationModel = getModel(relation.type);
    var modeledData = new RelationModel(mappedData);

    return checkForRelations(modeledData, modeledData.data);
  }

  return checkForRelations(dataModel, parentData);
};