/**
 * @include "../Manhattan.js"
 */

/**
 * Construtor da classe Range
 * 
 * @param {mixed} firstElement
 * @param {mixed} lastElement
 * @param {Function} getValueFunction [Util.valueOf]
 * @constructor
 * @return {Range}
 */
function Range(firstElement, lastElement, getValueFunction) {

    /**
	 * @type mixed
	 */
	this.firstElement = Util.isUndefined(firstElement) ? -Infinity : firstElement;
	
	/**
	 * @type mixed
	 */
	this.lastElement = Util.isUndefined(lastElement) ? Infinity : lastElement;

	/**
	 * @type Function
	 */	
	this.getValueFunction = Util.isFunction(getValueFunction) ? getValueFunction : Util.valueOf;
}

/**
 * @param {mixed} value
 * @return {undefined}
 */
Range.prototype.setFirstElement = function(value) {
	this.firstElement = value;
}

/**
 * @return {mixed}
 */
Range.prototype.getFirstElement = function() {
	return this.firstElement;
}

/**
 * @param {mixed} value
 * @return {undefined}
 */
Range.prototype.setLastElement = function(value) {
	this.lastElement = value;
}

/**
 * @return {mixed}
 */
Range.prototype.getLastElement = function() {
	return this.lastElement;
}

/**
 * @param {Function} value
 * @return {undefined}
 */
Range.prototype.setGetValueFunction = function(value) {
	this.getValueFunction = value;
}

/**
 * @return {Function}
 */
Range.prototype.getGetValueFunction = function() {
	return this.getValueFunction;
}

/**
 * Retorna o valor do primeiro elemento
 * 
 * @return {Number}
 */
Range.prototype.getFirstValue = function() {
	return (
        Util.isInfinity(this.firstElement)
		? this.firstElement
		: this.getValueFunction(this.firstElement)
	);
}

/**
 * Retorna o valor do último elemento
 * 
 * @return {Number}
 */
Range.prototype.getLastValue = function() {
	return (
		Util.isInfinity(this.lastElement)
		? this.firstElement
		: this.getValueFunction(this.lastElement)
	);
}

/**
 * Retorna o elemento que possui o maior valor
 * em caso de igualdade, retorna null
 * 
 * @return {mixed}
 */
Range.prototype.getMaxElement = function() {
    var firstValue = this.getFirstValue(),
        lastValue = this.getLastValue();
    
    switch (true) {
        case (firstValue > lastValue)   : return this.firstElement;
        case (firstValue < lastValue)   : return this.lastElement;
        default                         : return null;
    }
}

/**
 * Retorna o elemento que possui o menor valor
 * em caso de igualdade, retorna null
 * 
 * @return {mixed}
 */
Range.prototype.getMinElement = function() {
    var firstValue = this.getFirstValue(),
        lastValue = this.getLastValue();
    
    switch (true) {
        case (firstValue < lastValue)   : return this.firstElement;
        case (firstValue > lastValue)   : return this.lastElement;
        default                         : return null;
    }    
}

/**
 * Retorna o valor máximo do campo
 * 
 * @return {Number}
 */
Range.prototype.getMinValue = function() {
	var firstValue = this.getFirstValue();
	var lastValue = this.getLastValue();

	return (
		firstValue > lastValue
		? lastValue
		: firstValue
	);	
}

/**
 * Retorna o valor mínimo do campo
 * 
 * @return {Number}
 */
Range.prototype.getMaxValue = function() {
	var firstValue = this.getFirstValue();
	var lastValue = this.getLastValue();

	return (
		firstValue > lastValue
		? firstValue
		: lastValue
	);	
}

/**
 * Retorna a diferença entre os dois valores
 * 
 * @return {Number}
 */
Range.prototype.diff = function() {
	return this.getFirstValue().diff(this.getLastValue());
}

/**
 * Verifica se existe intersecção entre dois campos
 * 
 * @param {Range} range
 * @return {Boolean}
 */
Range.prototype.intersects = function(range) {
	var maxR1 = this.getMaxValue();
	var minR1 = this.getMinValue();
	var maxR2 = range.getMaxValue();
	var minR2 = range.getMinValue();
	
	return (
		maxR1.between(maxR2, minR2) ||
		minR1.between(maxR2, minR2) ||
		maxR2.between(maxR1, minR1) ||
		minR2.between(maxR1, minR1)
	);
}

/** 
 * Verifica o valor de intersecção entre dois campos
 * 
 * @param {Range} range
 * @return {Number}
 */
Range.prototype.intersection = function(range) {
	var intersectionRange = this.intersectionRange(range);
	return intersectionRange !== null ? intersectionRange.diff() : null;
}

/** 
 * Retorna o campo (somente valores) de intersecção entre dois campos
 * 
 * @param {Range} range
 * @return {Number}
 */
Range.prototype.intersectionRange = function(range) {
    var maxR1 = this.getMaxValue(),
        minR1 = this.getMinValue(),
        maxR2 = range.getMaxValue(),
        minR2 = range.getMinValue();    
    
	if (!this.intersects(range)) {
		return null;
	}
	if (this.contains(range)) {
        return new Range(minR2, maxR2);
	} else if (this.within(range)) {
        return new Range(minR1, maxR1);
	}

	var lowestMax = Math.min(maxR1, maxR2)
	var highestMin = Math.max(minR1, minR2);
	
	if (this.contains(minR2)) {
		return new Range(minR2, lowestMax);
	}
	
	if (this.contains(maxR2)) {
		return new Range(maxR2, highestMin);
	}
	
	if (range.contains(minR1)) {
		return new Range(minR1, lowestMax);
	}
	
	if (range.contains(maxR1)) {
		return new Range(maxR1, highestMin);
	}
	
	return null;
}

/**
 * Verifica se o valor está contido neste campo
 * 
 * @param {mixed} value
 * @return {Boolean}
 */
Range.prototype.contains = function(value) {
	if (Util.isNumber(value) || value instanceof this.firstElement.constructor) {
		return (Util.isNumber(value) ? value : this.getValueFunction(value)).between(this.getFirstValue(), this.getLastValue());
	} else if (value instanceof Range) {
		return value.getMinValue() >= this.getMinValue() && value.getMaxValue() <= this.getMaxValue(); 
	} else {
        throw new TypeError("Range#contains: invalid value");
    }
}

/**
 * Verifica se o campo está contido em um outro campo
 * 
 * @param {Range} range
 * @return {Boolean}
 */
Range.prototype.within = function(range) {
	return range.contains(this);
}

/**
 * Retorna um array contendo o menor e o maior valor do range
 * 
 * @return {Number[]}
 */
Range.prototype.toArrayValue = function() {
	return [this.getMinValue(), this.getMaxValue()];
}

/**
 * Retorna um array contendo os elementos do range
 * 
 * @return {mixed[]}
 */
Range.prototype.toArray = function() {
	return [this.firstElement, this.lastElement];
}

/**
 * Soma a diferenca dos ranges de um array
 * 
 * @param {Range[]} ranges
 * @return {Number}
 */
Range.sumDiffRanges = function(ranges) {
	var sum = 0;
	ranges.each(function(range) { sum += range.diff(); });
	return sum;
}

/**
 * Retorna um clone do range
 * 
 * @param {Range} clone [undefined]
 * @return {Range}
 */
Range.prototype.clone = function(clone) {
    clone = clone || new Range();
    
    clone.firstElement = this.firstElement;
    clone.lastElement = this.lastElement;
    clone.getValueFunction = this.getValueFunction;
    
    return clone;
}
