// ******************************************************************
// AttributeManager.js
//
// Version 3.0
// Copyright (c) 2007 Catalog Data Solutions. All Rights Reserved.
//
// ******************************************************************

cds.namespace("configurator");

// ---------------------------------------------------------
// AttributeManager class
// ---------------------------------------------------------

cds.configurator.AttributeManager = function(renderer) {
	this.attributes = new Array();
	this.attributeMap = new Object();	// this is for fast retrieval of attributes
	this.valueMap = new Object();		// this is for fast retrieval of values, format: attribute:value
	this.renderer = renderer;
	this.nestingManager = null;
	this.allowEvents = true;			// set this to false to disable events temporarily
	this.currentUnit = "metric";
    this.managerObjects = new Array();  // use this to contain objects to destroy

	this.handleEvent = function(object, type, args) {
		if (!this.allowEvents) return;
		this.doPreEvent(object, type, args);

		// object.handleEvent should return null if no changes were made
		var result = object.handleEvent(type);
		if (result != null) {
			this.renderer.display(object, type);
			if (object.rules != null) {
				for (var i = 0; i < object.rules.length; i++) {
					// enforce should return an array of objects to display or null if none
					var changed = object.rules[i].enforce(object, result, type);
					if (changed != null) {
						for (var j = 0; j < changed.length; j++) {
							this.renderer.display(changed[j], type);
						}
					}
				}
			}
		}

		// if we have a nesting manager, enforce it's rules
		if (this.nestingManager != null && type == "click") {
			this.nestingManager.enforce();
		}

		this.doPostEvent(object, type, args);
	};

	// Override these event handler to execute arbitrary code on configurator
	// events pre and post event handling.
	this.doPreEvent = function(object, type) {}
	this.doPostEvent = function(object, type) {}

	this.getValue = function() {
		var obj = new Object();
		for (var i = 0; i < this.attributes.length; i++) {
			var v = this.attributes[i].getValue();
			if (v != null) {
				obj[this.attributes[i].id] = v;
			}
		}
		return obj;
	};

	this.getValueAsString = function() {
		var s = "";
		for (var i = 0; i < this.attributes.length; i++) {
			var v = this.attributes[i].getValue();
			if (v != null) {
				if (s.length > 0) s += ";";
				s += this.attributes[i].id + "=";
				if (typeOf(v) == "array") {
					for (var j = 0; j < v.length; j++) {
						if (j > 0) s += ",";
						s += v[j].id;
					}
				} else {
					s += v;
				}
			}
		}
		return s;
	};

	this.setValue = function(value) {
		// first reset, then set new values if any exist
		for (var i = 0; i < this.attributes.length; i++) {
			this.attributes[i].setValue(null);
		}
		if (value != null) {
			var attrs = value.split(";");
			for (var i = 0; i < attrs.length; i++) {
				var s = attrs[i].split("=");
				var a = this.getAttribute(s[0]);
				if (a != null) {
					a.setValue(s[1]);
				}
			}
		}
	};

    // just set the values without checking any constraints
    this.setValueWithoutChecks = function(value) {
        // first reset, then set new values if any exist
        for (var i = 0; i < this.attributes.length; i++) {
            this.attributes[i].setValueWithoutChecks(null);
        }
        if (value != null) {
            var attrs = value.split(";");
            for (var i = 0; i < attrs.length; i++) {
                var s = attrs[i].split("=");
                var a = this.getAttribute(s[0]);
                if (a != null) {
                    a.setValueWithoutChecks(s[1]);
                }
            }
        }
    };

	this.load = function() {
	};

	this.save = function() {
	};

	// renders all attributes
	this.render = function() {
		for (var i = 0; i < this.attributes.length; i++) {
			this.renderer.render(this.attributes[i]);
		}
	};

	this.display = function() {
		for (var i = 0; i < this.attributes.length; i++) {
			this.renderer.display(this.attributes[i]);
		}
	};

	// returns true if all required attributes have values
	this.isComplete = function() {
		for (var i = 0; i < this.attributes.length; i++) {
			if (!this.attributes[i].isComplete()) {
				return false;
			}
		}
		return true;
	};

	// returns all attributes which have data in them
	this.getConfiguredAttributes = function() {
		var a = null;
		for (var i = 0; i < this.attributes.length; i++) {
			if (this.attributes[i].getValue() != null) {
				if (a == null) a = new Array();
				a[a.length] = this.attributes[i];
			}
		}
		return a;
	};

	// returns the attribute with the given id
	this.getAttribute = function(attributeId) {
		var a = this.attributeMap[attributeId];
		return (a !== undefined) ? a : null;
	};

	// returns a set attribute value with the given id
	this.getAttributeValue = function(valueId) {
		var a = this.valueMap[valueId];
		return (a !== undefined) ? a : null;
	};

	this.addAttribute = function(attribute) {
		attribute.manager = this;
		this.attributes[this.attributes.length] = attribute;
		this.attributeMap[attribute.id] = attribute;
		return attribute;
	};

    this.getLocalizedLabel = function(obj) {
        var l;
        if (typeof getLocaleResource === "function") {
            l = getLocaleResource(obj.id);
        }
        if (!l) {
            l = obj.label;
        }
        return l;
    };

    this.addDestroyableObject = function(obj) {
        this.managerObjects.push(obj);
        return obj;
    };

	this.destroy = function() {
		for (var i = 0; i < this.attributes.length; i++) {
			if (typeof this.attributes[i].destroy == "function") this.attributes[i].destroy();
		}
		this.attributes = null;
		this.attributeMap = null;
		this.valueMap = null;
		this.renderer = null;
		this.nestingManager = null;
        for (var i = 0; i < this.managerObjects.length; i++) {
            if (typeof this.managerObjects[i].destroy == "function") this.managerObjects[i].destroy();
        }
	};

	this.toString = function() {
		return "cds.configurator.AttributeManager(" + this.renderer + ")";
	};
}

// ---------------------------------------------------------
// Constraint classes
// ---------------------------------------------------------

cds.configurator.Constraint = function(id, value, isMouseEvent) {
	this.id = id;						// the unique identifier of the rule which created this constraint
	this.value = value;					// a value specific to the type of constraint (used by range)
	this.isMouseEvent = isMouseEvent;	// null or false if this constraint is not the result of a hover event

	this.toString = function() {
		return "cds.configurator.Constraint(" + this.id + ")";
	};
}

cds.configurator.ConstraintSet = function() {
	this.constraints = new Object();	// current list of constraints
	this.count = 0;						// number of constraints
	this.mouseEventCount = 0;			// number of mouse event constraints

	this.isConstrained = function() { return (this.count > 0); }
	this.isMouseEventConstrained = function() { return (this.mouseEventCount > 0); }

	// helper functions for range constraints which uses value = [min,max]
	this.getRangeMin = function() {
		var min = null;
		for (var i in this.constraints) {
			var v = this.constraints[i].value;
			if (v != null && v[0] != null) {
				if (min == null || v[0] > min) {
					min = v[0];
				}
			}
		}
		return min;
	};

	this.getRangeMax = function() {
		var max = null;
		for (var i in this.constraints) {
			var v = this.constraints[i].value;
			if (v != null && v[1] != null) {
				if (max == null || v[1] < max) {
					max = v[1];
				}
			}
		}
		return max;
	};

	// Adds a constraint to the collection if it doesn't already exist there.
	// Returns true if the constraint was successfully added.
	this.add = function(constraint)
	{
		if (this.constraints[constraint.id] == null) {
			this.constraints[constraint.id] = constraint;
			this.count++;
			if (constraint.isMouseEvent) this.mouseEventCount++;
			return true;
		}
		return false;
	};

	// Removes a constraint from the collection if the constraint exists.
	// Returns true if the constraint was successfully removed.
	this.remove = function(id)
	{
		if (this.constraints[id] != null) {
			if (this.constraints[id].isMouseEvent) this.mouseEventCount--;
			this.count--;
			delete this.constraints[id];
			return true;
		}
		return false;
	};

	this.destroy = function() {
		this.constraints = null;
	};

	this.toString = function() {
		return "cds.configurator.ConstraintSet[" + this.count + "]";
	};
}

// ---------------------------------------------------------
// Set Attribute
// ---------------------------------------------------------

cds.configurator.SetAttribute = function(id, label, isRequired, isDiscrete, displayColumns) {
	this.classId = "cds.configurator.SetAttribute";
	this.manager = null;
	this.id = id;
	this.label = label;
	this.isRequired = isRequired;
	this.isDiscrete = isDiscrete;
	this.isUnselectable = true;		// if this is false, you can't have no value selected
									// ... currently only works if isDiscrete is true
									// ... NOT CURRENTLY IMPLEMENTED
	this.values = new Array();
	this.selectedValue = null;		// only used when isDiscrete is true
	this.displayColumns = (displayColumns == null) ? 1 : displayColumns;	// might be used by renderer to change number of columns
	this.isReadOnly = false;		// no user events managed
	this.subtextHtml = null;		// arbitrary html to display beneath attribute label
	this.isHiddenFromNesting = false;	// true if this has been hidden by a NestingManager
    this.renderManually = false;    // do not generate HTML for this element

	// handles events, should return null if no changes were made
	this.handleEvent = function(type) {
		return null;
	};

	// returns true if any values are selected
	this.hasSelection = function() {
		if (this.isDiscrete) return this.selectedValue != null;
		for (var i = 0; i < this.values.length; i++) {
			if (this.values[i].isSelected) return true;
		}
		return false;
	};

	this.isComplete = function() {
		if (!this.isHiddenFromNesting && this.isRequired) {
			return this.hasSelection();
		}
		return true;
	};

	// returns an array of currently selected values
	this.getValue = function() {
		var svs = null;
		if (this.isDiscrete) {
			if (this.selectedValue != null) {
				svs = new Array();
				svs[0] = this.selectedValue;
			}
		} else {
			for (var i = 0; i < this.values.length; i++) {
				if (this.values[i].isSelected) {
					if (svs == null) svs = new Array();
					svs[svs.length] = this.values[i];
				}
			}
		}
		return svs;
	};

    // returns a value given a product number fragment
    this.getValueByProductNumber = function(pn) {
        for (var i = 0; i < this.values.length; i++) {
            if (this.values[i].productNumber === pn) {
                return this.values[i];
            }
        }
        return null;
    };

	// returns label as a localized string
	this.getLocalizedLabel = function() {
        return this.manager.getLocalizedLabel(this);
	};

	// returns the current value(s) as a localized string
	this.getLocalizedValueLabel = function() {
		var svs = this.getValue();
		var s = "";
		if (svs != null) {
			for (var i = 0; i < svs.length; i++) {
				if (i > 0) s += ", ";
                s += this.manager.getLocalizedLabel(svs[i]);
			}
		} else {
			return null;
		}
		return s;
	};

	this.setValue = function(value) {
		if (value == null) {
			for (var i = 0; i < this.values.length; i++) {
				if (this.values[i].isSelected) {
					this.manager.handleEvent(this.values[i], "click", "interim_event");
				}
			}
		} else {
			var vals = value.split(",");
			for (var i = 0; i < vals.length; i++) {
				var v = this.manager.getAttributeValue(vals[i]);
				if (v != null && v.attribute == this) {
					this.manager.handleEvent(v, "click", "interim_event");
				}
			}
		}
	};

    this.setValueWithoutChecks = function(value) {
        if (value == null) {
            for (var i = 0; i < this.values.length; i++) {
                if (this.values[i].isSelected) {
                    this.values[i].selected = false;
                }
                this.selectedValue = null;
            }
        } else {
            var vals = value.split(",");
            for (var i = 0; i < vals.length; i++) {
                var v = this.manager.getAttributeValue(vals[i]);
                if (v != null && v.attribute == this) {
                    v.isSelected = true;
                    if (this.isDiscrete) {
                        this.selectedValue = v;
                    }
                }
            }
        }
    };

	this.addValue = function(value) {
		value.attribute = this;
		this.values[this.values.length] = value;
		this.manager.valueMap[value.id] = value;
		return value;
	};

	this.getProductNumber = function() {
		if (this.isDiscrete) {
			if (this.selectedValue != null) {
				return this.selectedValue.productNumber;
			}
			return null;
		} else {
			var s = null;
			for (var i = 0; i < this.values.length; i++) {
				if (this.values[i].isSelected) {
					if (s == null) s = this.values[i].productNumber;
					else s += this.values[i].productNumber;
				}
			}
			return s;
		}
	};

    // NOTE: this currently only works for discrete attributes
    this.setProductNumber = function(pn) {
        if (pn == null) {
            for (var i = 0; i < this.values.length; i++) {
                if (this.values[i].isSelected) {
                    this.manager.handleEvent(this.values[i], "click", "interim_event");
                }
            }
        } else {
            for (var i = 0; i < this.values.length; i++) {
                if (this.values[i].productNumber === pn) {
                    this.manager.handleEvent(this.values[i], "click", "interim_event");
                    break;
                }
            }
        }
    };

	this.destroy = function() {
		this.manager = null;
		this.selectedValue = null;
		for (var i = 0; i < this.values.length; i++) {
			if (typeof this.values[i].destroy == "function") this.values[i].destroy();
		}
		this.values = null;
	};

	this.toString = function() {
		return "cds.configurator.SetAttribute[" + this.id + "]";
	};
}

// ---------------------------------------------------------
// Set Attribute Value
// ---------------------------------------------------------

cds.configurator.SetAttributeValue = function(id, label, productNumber) {
	this.classId = "cds.configurator.SetAttributeValue";
	this.attribute = null;
	this.id = id;
	this.label = label;
	this.isSelected = false;
	this.isHovered = false;
	this.isConflicted = false;
	this.isDisabled = false;
	this.rules = new Array();
	this.constraints = new cds.configurator.ConstraintSet();
	this.isStandard = false;		// can be used to display different css and such
	this.isReadOnly = false;		// no user events managed
	this.suppressSelect = false;	// if the value should not be shown on selection ui
	this.productNumber = productNumber;	// productNumber representation of this value

	// handles events, should return null if no changes were made
	this.handleEvent = function(type) {
		if (this.constraints.isConstrained()) return;

		if (type == "click") {
			// if discrete, remove any current selection
			if (this.attribute.isDiscrete && this.attribute.selectedValue != null && this.attribute.selectedValue != this) {
				this.attribute.manager.handleEvent(this.attribute.selectedValue, "click", "interim_event");
			}
			this.isSelected = !this.isSelected;
			this.attribute.selectedValue = (this.isSelected) ? this : null;
			this.isHovered = false;
			return this.isSelected;

		} else {
			this.isHovered = (type == "mouseover");
			return true;
		}

		return null;
	};

	this.destroy = function() {
		this.attribute = null;
		this.constraints = null;
		for (var i = 0; i < this.rules.length; i++) {
			if (typeof this.rules[i].destroy == "function") this.rules[i].destroy();
		}
		this.rules = null;
	};

	this.toString = function() {
		return "cds.configurator.SetAttributeValue[" + this.id + "]";
	};
}

// ---------------------------------------------------------
// Range Attribute
// ---------------------------------------------------------

cds.configurator.RangeAttribute = function(id, label, isRequired, precision, defaultMin, defaultMax) {
	this.classId = "cds.configurator.RangeAttribute";
	this.manager = null;
	this.id = id;
	this.label = label;
	this.isRequired = isRequired;
	this.precision = precision;		// number of decimal places to display in default units
	this.defaultMin = defaultMin;
	this.defaultMax = defaultMax;
	this.value = null;
	this.min = this.defaultMin;		// current min
	this.max = this.defaultMax;		// current max

	this.rules = new Array();
	this.constraints = new cds.configurator.ConstraintSet();
	this.isReadOnly = false;		// no user events managed
	this.subtextHtml = null;		// arbitrary html to display beneath attribute label
	this.isHiddenFromNesting = false;	// true if this has been hidden by a NestingManager

	// handles events, should return null if no changes were made
	this.handleEvent = function(type) {
		if (type == "change") {
			var element = document.getElementById(this.id + "_value");
			if (element == null) return null;

			var vs = element.value;
			var v = parseFloat(vs);
			if (vs == null || vs.length == 0) {
				this.value = null;
				return true;
			} else if (isNaN(v)) {
				alert("The value you entered is not a number. Please enter a valid number.");
				element.value = "";
				element.focus();
				return null;
			} else {
				// convert units if necessary
				// WARNING: big hack here on aNumberofGrooves and aTorque
				var tmin = this.min;
				var tmax = this.max;
				if (this.manager.currentUnit == "si" && this.id != "aNumberofGrooves" && this.id != "aRPM") {
					if (this.id == "aTorque") {
						v /= 8.85;
						tmin *= 8.85;
						tmax *= 8.85;
					} else {
						v *= 25.4;
						tmin /= 25.4;
						tmax /= 25.4;
					}
				}
				if (this.min != null && v < this.min) {
					alert("Please enter a value greater than or equal to " + tmin + ".");
					element.value = "";
					element.focus();
					return null;
				} else if (this.max != null && v > this.max) {
					alert("Please enter a value less than or equal to " + tmax + ".");
					element.value = "";
					element.focus();
					return null;
				} else {
					this.value = v;
					return true;
				}
			}
		}
		return null;
	};

	this.setMin = function() {
		var cmin = this.constraints.getRangeMin();
		this.min = (cmin != null && (cmin > this.defaultMin || this.defaultMin == null)) ? cmin : this.defaultMin;
	};

	this.setMax = function() {
		var cmax = this.constraints.getRangeMax();
		this.max = (cmax != null && (cmax < this.defaultMax || this.defaultMax == null)) ? cmax : this.defaultMax;
	};

	this.isComplete = function() {
		if (!this.isHiddenFromNesting && this.isRequired) {
			return (this.value != null);
		}
		return true;
	};

	this.getValue = function() {
		return this.value;
	};

	this.setValue = function(value) {
		if (this.value == value) return;
		var element = document.getElementById(this.id + "_value");
		if (element == null) return null;
		var v = (value == null) ? "" : value;
		element.value = v;
		this.manager.handleEvent(this, "change");
	};

	// returns label as a localized string
	this.getLocalizedLabel = function() {
        return this.manager.getLocalizedLabel(this);
	};

	// returns the current value(s) as a localized string
	this.getLocalizedValueLabel = function() {
		return this.value();
	};

	this.getProductNumber = function() {
		return this.getValue();
	};

    this.setProductNumber = function(pn) {
        this.setValue(pn);
    };

	this.destroy = function() {
		this.manager = null;
		this.constraints = null;
		for (var i = 0; i < this.rules.length; i++) {
			if (typeof this.rules[i].destroy == "function") this.rules[i].destroy();
		}
		this.rules = null;
	};

	this.toString = function() {
		return "cds.configurator.RangeAttribute[" + this.id + "]";
	};
}

// ---------------------------------------------------------
// Text Attribute
// ---------------------------------------------------------

cds.configurator.TextAttribute = function(id, label, isRequired, columns, rows) {
	this.classId = "cds.configurator.TextAttribute";
	this.manager = null;
	this.id = id;
	this.label = label;
	this.isRequired = isRequired;
	this.columns = columns;
	this.rows = rows;
	this.value = null;
	this.rules = new Array();
	this.constraints = new cds.configurator.ConstraintSet();
	this.isReadOnly = false;		// no user events managed
	this.subtextHtml = null;		// arbitrary html to display beneath attribute label
	this.isHiddenFromNesting = false;	// true if this has been hidden by a NestingManager

	// handles events, should return null if no changes were made
	this.handleEvent = function(type) {
		if (type == "change") {
			var element = document.getElementById(this.id + "_value");
			if (element == null) return null;
			this.value = element.value;
			return true;
		}
		return null;
	};

	this.isComplete = function() {
		if (!this.isHiddenFromNesting && this.isRequired) {
			return (this.value != null);
		}
		return true;
	};

	this.getValue = function() {
		return (this.value) ? escape(this.value) : null;
	};

	this.setValue = function(value) {
		var ueval = (value == null) ? null : unescape(value);
		if (this.value == ueval) return;
		var element = document.getElementById(this.id + "_value");
		var v = (ueval == null) ? "" : ueval;
        this.value = v;
        if (element) {
            element.value = v;
        }
		this.manager.handleEvent(this, "change");
	};

	// returns label as a localized string
	this.getLocalizedLabel = function() {
        return this.manager.getLocalizedLabel(this);
	};

	// returns the current value(s) as a localized string
	this.getLocalizedValueLabel = function() {
		return unescape(this.getValue());
	};

	this.getProductNumber = function() {
		return this.getValue();
	};

    this.setProductNumber = function(pn) {
        this.setValue(pn);
    };

	this.destroy = function() {
		this.manager = null;
		this.constraints = null;
		for (var i = 0; i < this.rules.length; i++) {
			if (typeof this.rules[i].destroy == "function") this.rules[i].destroy();
		}
		this.rules = null;
	};

	this.toString = function() {
		return "cds.configurator.TextAttribute[" + this.id + "]";
	};
}






