/*
	Constraint Rule Engine
	
	Author: Chuck Ingrum, Nathan Huebner
*/

/* 
All material herein (c) 2004 TechniCon Corporation              
All Rights Reserved.

The source code is owned by TechniCon Corporation and is protected by  
copyright laws and international copyright treaties, as well as other   
intellectual property laws and treaties. COPYRIGHT. The source code is  
licensed, not sold. All right, title and interest in the source code    
(including any images, "applets," photographs, animations, video, audio,
music, and text incorporated into the source code), accompanying printed
materials, and any copies you are permitted to make herein, are owned by
TechniCon Corporation, and the source code is protected by United States 
copyright laws and international treaty provisions.  Therefore, you must
treat the source code like any other copyrighted material.      
*/

//DomainMap constructor
function DomainMap() {
	this.debug_msg_function = null;
	this.cpcarray = new Array();
	this.domains = new Object();
	if (arguments.length > 0) {
		this.nopick = arguments[0];	//Placeholder string/character for domains without selections
	} else {
		this.nopick = '&middot;';
	}
	this.grammarfilter = new RegExp(/^[\-\(\)\,]$/); //Filter for grammar domains
}

/**
	Prototype fields that are shared by all instantiations of
	this object.
**/
DomainMap.prototype.oEvaluatedRules = '';	//Rules that have been evaluated - Assigned a new object for each configurator state evaluation
DomainMap.prototype.oFullfilledRules = '';	//Rules with all domains fulfilled or a domain selection that invalidates the rule - Assigned a new object for each configurator state evaluation

DomainMap.prototype.debug = function(indent, text) {
	if (this.debug_msg_function) {
		var sindent = "&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;";
		sindent = sindent.substring(0,Math.min(10,Math.max(0,indent-1))*6);
		sindent = sindent+sindent;
		this.debug_msg_function(sindent + text);
	}
}

DomainMap.prototype.addDomain = function (arglabel, argcode, argdid, arglid, argdtype, argcode2, argattval, argfunctype) {

	this.domains[argdid] = new Domain(arglabel, argcode, argdid, arglid, argdtype, argcode2, argattval, argfunctype);
	
	this.cpcarray[this.cpcarray.length] = this.domains[argdid];
	
	return this.domains[argdid];
}
DomainMap.prototype.setNoPick = function (sNoPick) {
	this.nopick = sNoPick;
}
DomainMap.prototype.setGrammarFilter = function (sGrammarFilter) {
	this.grammarfilter = new RegExp(sGrammarFilter);
}
/**
	This function can be replaced by the calling application
	to provide a partnumber in the desired format.
**/
DomainMap.prototype.getPartNumber = function () {
	var oDom;
	var aPNBlocks = new Array();
	for (var i=0; i<this.cpcarray.length; i++) {
		oDom = this.cpcarray[i];
		switch (oDom.dtype) {
		case Domain.Constant:
			aPNBlocks[i] = oDom.label;
			break;
		case Domain.List:
			if (oDom.state == Domain.NotSet) {
				aPNBlocks[i] = this.nopick;
			} else {
				aPNBlocks[i] = oDom.selectionString();
			}
			break;
		case Domain.Mixed:
			if (oDom.state == Domain.NotSet) {
				aPNBlocks[i] = this.nopick;
			} else {
				aPNBlocks[i] = oDom.selectionString();
			}
			break;
		case Domain.IntegerRange:
			if (oDom.state == Domain.NotSet) {
				aPNBlocks[i] = this.nopick;
			} else {
				aPNBlocks[i] = oDom.selectionString();
			}
			break;
		case Domain.DecimalRange:
			if (oDom.state == Domain.NotSet) {
				aPNBlocks[i] = this.nopick;
			} else {
				aPNBlocks[i] = oDom.selectionString();
			}
			break;
		default:
			alert('Domain ('+oDom.label+') is an unsupported domain type ('+oDom.dtype+')');
			break;
		}
	}
	return aPNBlocks;
}

DomainMap.prototype.getConstraintEngineState = function () {
	var oDom;
	var bInvalid = false;
	var nNotSet = 0;
	var sState = 'Complete';

	for (var domainid in this.domains) {
		if (this.domains.hasOwnProperty(domainid)) {
			oDom = this.domains[domainid];
			if (oDom.state == Domain.NotSet) {
				nNotSet++;
			} else if (oDom.contype == Rule.HardConstraint) {
				bInvalid = true;
				break;
			}
		}
	}
	if (bInvalid) {
		sState = 'Invalid';
	} else if (nNotSet > 0) {
		sState = 'Partial';
	}
	return sState;
}

//DomainToRuleMap creator
/*		DomainToRuleMap
					DomainName - Array of Rules containing references to this domain
							+------Rule_n 
							|			+--Domain_n
							|					+--Choice Array
							|			+--Domain_N
							|					+--Choice Array
							|------Rule_N
*/
DomainMap.prototype.createDomainToRuleMap = function (aR) {
	var theMap = new Object();
	
	//Make sure each domain is present in the map whether it is 
	//referenced by any rules or not.
	for (var domainid in this.domains) {
		if (this.domains.hasOwnProperty(domainid)) {
			theMap[domainid] = new Array();
		}
	}
	
	for (var i=0; i<aR.length; i++) {
	//alert('i='+i+' aR[i]]='+aR[i]+' aR.length='+aR.length);
		for (var domainid in aR[i]['members']) {
			if (aR[i]['members'].hasOwnProperty(domainid)) {
				theMap[domainid].push(aR[i]);
			}
		}
	}
	//alert('done');
	return theMap;
}
DomainMap.prototype.evaluateRules = function (aR, oDRMap, nTriggerDomainID) {
	//aR     - array of Exclusion Rules -> aR is not referenced, all rules are referenced through oDRMap
	//oDRMap - domain to rule map
	//nTriggerDomainID  - is the domain that triggered
	//					  the calling of this routine. It is supplied
	//					  so that it may recieve special conflicts or 
	//					  special domain restriction.
	
	/**
		I have considered eliminating the use of the DomainToRule map,
		which links Domains to the Rules that reference them, and going
		directly to the evaluateRuleArray method.
		
		As the constraint engine is currently implemented the two approaches
		are basically equivalent so why go to the extra trouble of creating and
		navigating the DomainToRule map?
		
		One interesting thing about the DomainToRule map is that it would
		allow the state evaluation to be performed only on those rules that
		reference domains that have changed. Thus in a user interface where
		the configuration state is evaluated after each domain change we could
		limit the evaluation to just the rules referencing the modified domain.
		
		So I am going to leave it in even though the process described above has 
		not yet been implemented and it means we are doing a little more work than
		is necessary at the present.
	**/
	var i, cnt;
	var domainid, theDomain, aRules;
	var member, oDom;
	var aSDRules;
					
	this.debug(1,"evaluateRules: init trigger domain="+nTriggerDomainID);
	this.initialize();
	
	//Identify Domains with selected choices
	for (var domainid in this.domains) {
		//Eliminate prototype properties (functions)
		if (this.domains.hasOwnProperty(domainid)) {
			theDomain = this.domains[domainid];
			aRules    = oDRMap[domainid];
			if (theDomain.hasSelection()) {
				if (aRules.length > 0) {
					//TODO: Set domain filter
					this.evaluateRuleArray(aRules);
				}
			} else {
				//Cycle through rules - execute any unexecuted rules with only 1 domain.
				aSDRules = new Array();
				for (i=0; i<aRules.length; i++) {
					if (aRules[i].getMemberCount() == 1) {
						aSDRules.push(aRules[i]);
					}
				}
				
				if (aSDRules.length > 0) {
					this.evaluateRuleArray(aRules, this.grammarfilter);
				}
			}
		}
	}
	
	//Examine each domain looking for the following conditions
	//	Domain restrictions leave only one valid choice.
	//		Set the remaining value and mark as autoset
	//	All domain choices are restricted. 
	//		Mark the trigger domain (the one modified by the user)
	//		as conflicted and generate error message.
	//
	//If any domains are autoset then run through the rule evaluator for
	//rules referencing the autoset domains.
	var autoDomMap;
	var domainModified = true;
	while (domainModified) {
		autoDomMap = new Object();
		
		domainModified = this.evaluateDomains(nTriggerDomainID, autoDomMap);
		
		if (domainModified) {
			this.clearEvaluatedRules();  //Is causing problems with rules intended to enforce dash appearance in part number. See note in clearEvaluatedRules function
			for (domainid in autoDomMap) {
				if (autoDomMap.hasOwnProperty(domainid)) {
					theDomain = autoDomMap[domainid];
					aRules = oDRMap[domainid];
					if (aRules.length > 0) {
						this.evaluateRuleArray(aRules, this.grammarfilter);
					}
				}
			}
		}
	}
	
	//Evaluate rules on part number grammar blocks
	//	What is a part number grammar rule? 
	//		Any rule whose purpose is to cause a part number block containing a non configuration separator to be set, such as a -
	//		Any rule containing a part number separator domain should be treated as a PN grammar rule rather than a configuration rule. 	
	if (this.grammarfilter != null) {
		var bEval = false;
		for (domainid in this.domains) {
			//Eliminate prototype properties (functions)
			if (this.domains.hasOwnProperty(domainid)) {
				theDomain = this.domains[domainid];
				if (theDomain.dtype == Domain.List 
						&& !theDomain.hasSelection()
						&& this.grammarfilter.test(theDomain.label)) {
					aRules    = oDRMap[domainid];
					this.evaluateRuleArray(aRules, null);
					bEval = true;
				}
			}
		}
		if (bEval) {
			autoDomMap = new Object();
			this.evaluateDomains(nTriggerDomainID, autoDomMap);
			//No further restriction of domain choices is allowed
			//after processing grammar rules.
		}
	}
}

DomainMap.prototype.evaluateDomains = function (nTriggerDomainID,autoDomMap) {
	var oDom, nUnrestricted, nLastChoiceID;
	var bDomainModified=false;
	for (var domainid in this.domains) {
		if (this.domains.hasOwnProperty(domainid)) {
			oDom = this.domains[domainid];
			if (!oDom.hasSelection()  
				&& oDom.did != nTriggerDomainID) {
				nUnrestricted = 0;
				nLastChoiceID = 0;
				for (var choiceid in oDom.members) {
					if (oDom.members.hasOwnProperty(choiceid)) {
						if (oDom.members[choiceid].state == Domain.ChoiceAllowed) {
							nUnrestricted++;
							if (nUnrestricted > 1) {
								break;
							}
							nLastChoiceID = choiceid;
						}
					}
				}
				if (nUnrestricted == 1 && oDom.dtype == Domain.List) {
					//AutoSet List Domains
					//oDom.dsv = nLastChoiceID;
					//oDom.state = Domain.AutoSet;
					oDom.setChoice(nLastChoiceID, Domain.AutoSet);
					autoDomMap[domainid] = 1; //Value doesn't matter. Just membership in map
					bDomainModified = true;
				} else if (nUnrestricted == 0 && nTriggerDomainID != 0) {
					//Closed Domain - conflict
					var msgid;
					var myDom=this.domains[nTriggerDomainID];
					myDom.contype = Rule.HardConstraint;
					msgid = myDom.did + '_' + oDom.did;
					//Generate Reason Text for this case
					myDom.addGeneratedMessage(msgid, 'The current selection of '+myDom.label+' excludes all choices of '+oDom.label+'.');
				}
			}
		}
	}
	return bDomainModified;
}

DomainMap.prototype.evaluateRuleArray = function (aR, reFilter) {
	//aR is an array of rules each of which is gauranteed 
	//to have at least one domain with a selection
	var bEval;
	var member, oR;
	this.debug(2,"evaluateRuleArray: aR.length="+aR.length);
	for (var i=0; i<aR.length; i++) {
		if (!this.oEvaluatedRules.hasOwnProperty(aR[i].ruleid)) {
			//Block part number grammar rules here when specified in filter argument.
			bEval = true;
			oR = aR[i];
			if (reFilter != null) {
				//bEval = reFilter.test
				for (member in oR.members) {
					if (oR.members.hasOwnProperty(member)) {
						oDom = this.domains[member];
						bEval = bEval && !reFilter.test(oDom.label);
					}
				}
			}
			if (bEval) {			
				this.evaluateSingleRule(oR);
				this.oEvaluatedRules[oR.ruleid] = oR; //Doesn't really matter what we set it to.
			}
		}
	}
}
DomainMap.prototype.evaluateSingleRule = function (oR) {
	/**
		State A - All rule members are satisfied. Each domain
				  included in rule is in conflict with the others.
				  Put domain in conflict state. Add reason id if not
				  already present.
				  
		State B - At least one rule member is violated (contains a choice
				  not prohibited for the domain). Rule does not apply.
				  
		State C - No rule members are violated but one or more domains are
				  not satisfied. If only one domain is not satisfied
				  then it is restricted to future choices from the rule.
				  
		Originally the isValid variable was use to note if a domain had a selection
		that was not specified in the rule. It was thought that this could be used to implement
		early termination of a rule check. It turns out that the rule must be completely checked
		because restrictions may still need to be placed on choices in the domain so that the 
		user will see the correct choices if they want to change an already set domain selection.
	**/
	this.debug(3,"evaluateSingleRule: ruleid="+oR.ruleid);
	var nUnsatisfied = 0;
	var i, nCount, nUnsatisfiedDomID;
	var choice, savChoice, member;
	var oDom, oUnsatisfiedDom;
	//alert('start analysis');
	for (member in oR.members) {
		if (oR.members.hasOwnProperty(member)) {
			/**
				member is the domainid
				get the domain. if it is selected.
				test it's value against the rule.
			**/
			
			oDom = this.domains[member];
			if (oDom.hasSelection()) {
				if (!oDom.evaluateRuleDomain(oR.members[member])) {
					nUnsatisfied++;
					nUnsatisfiedDomID = member;
					this.debug(4,"Unsatisfied DomID: member="+member);
				}
			} else {	
				nUnsatisfied++;
				nUnsatisfiedDomID = member;
				this.debug(4,"Unsatisfied DomID: member="+member);
			}
			
			if (nUnsatisfied>1) {
				break;
			}
		}
	}
	//alert('end analysis');
	/**
		if all values are satisfied then selections are in conflict. All 
		domains in the rule are restricted to choices not in the rule.
		
		if one domain is unsatisfied, its future choices are restricted
		to domain members not in the rule
		
		if more than one domain is unsatisfied - no impact.
	**/
	if (nUnsatisfied == 0) {
		//all domains are in conflict
		this.debug(4,"All domains are in conflict - rule violated!");
		for (member in oR.members) {
			if (oR.members.hasOwnProperty(member)) {
				/**
					member is the domainid
					get the domain. Mark it with
					correct conflict marking, and get
					the reason text id.
				**/
				oDom = this.domains[member];			
				oDom.setConstraintType(oR.type);
				oDom.addViolatedRule(oR);
				
				if (oR.members[member].getDomainType() != Rule.CONTAINS && oR.type == Rule.HardConstraint) {
					//All rule domain choices are restriced
					for (choice in oR.members[member].oChoices) {
						if (oR.members[member].oChoices.hasOwnProperty(choice)) {
							oDom.members[choice].state = Domain.ChoiceRestricted;
						}
					}
				}
			}
		}
		//Action completed on this rule. Do not consider
		//again until external agent modifies a domain.
		this.oFullfilledRules[oR.ruleid] = 1; 
	} else if (nUnsatisfied == 1) {
		//unsatisfied domain is restricted
		if (oR.type == Rule.HardConstraint) {
			oDom = this.domains[nUnsatisfiedDomID];
			switch (oR.members[nUnsatisfiedDomID].getDomainType()) {
			case Rule.ANY:
				for (choice in oR.members[nUnsatisfiedDomID].oChoices) {
					if (oR.members[nUnsatisfiedDomID].oChoices.hasOwnProperty(choice)) {
						oDom.members[choice].state = Domain.ChoiceRestricted;
					}
				}
				break;
			case Rule.ALL:
				//Loop through selections in domain and see how many are in rule domain.
				//	Create a temp object that has all rule domain choices in it.
				//  Loop through domain selections, if selection is in temp object delete it.
				//  When done looping if the temp object only has one choice in it then restrict it.
				var oTempChoices = new Object();
				for (choice in oR.members[nUnsatisfiedDomID].oChoices) {
					if (oR.members[nUnsatisfiedDomID].oChoices.hasOwnProperty(choice)) {
						oTempChoices[choice] = true;
					}
				}
				for (i=0; i<oDom.aDSV.length; i++) {
					//delete fails silently if property not present.
					delete oTempChoices[oDom.aDSV[i]];
				}
				nCount = 0;
				for (choice in oTempChoices) {
					if (oTempChoices.hasOwnProperty(choice)) {
						nCount++;
						savChoice = choice;
						if (nCount > 1) {
							break;
						}
					}
				}
				if (nCount == 1) {
					oDom.members[savChoice].state = Domain.ChoiceRestricted;
				}
				break;
			case Rule.CONTAINS:
				//Domain members that are not in the rule should be set to a state indicating that a selection must
				//be made from one or more of those choices.
				break;
			default:
				alert('Unknown Rule domain type ('+oR.members[nUnsatisfiedDomID].getDomainType()+') while processing choice restrictions.');
				break;
			}
		}
	}
}

/**
	Note: Clearing evaluated rules between each cycle in the inspection was meant to ensure that each rule was evaluated in the cycle.
	 	  The reason for this was that autoselected choices could impose further restrictions so we needed to make sure that eash rule
	 	  was evaluated during the cycle. The problem this creates is that when a auto-dash domain get set it can then lead to restrictions
	 	  on choices that are visible to the user. But there is no apparent choice made by the user and this can be confusing. In fact it leads to
	 	  choices being made that do not cause conflicts. 
	 	  
	 	  The solution is probably to separate configuration from part number generation. This of course has its own implications.
*/
DomainMap.prototype.clearEvaluatedRules = function () {
	DomainMap.prototype.oEvaluatedRules = new Object();
}

DomainMap.prototype.initialize = function () {
	//Prepare domain object for rule evaluation
	//
	// Clear states, conflicts etc.

	this.clearEvaluatedRules(); 
	DomainMap.prototype.oFullfilledRules = new Object();
	
	//Identify Domains with selected choices
	for (var domainid in this.domains) {
		if (this.domains.hasOwnProperty(domainid)) {
			//Eliminate prototype properties (functions)
			var theDomain = this.domains[domainid];
			
			//theDomain.reasons = new Object(); I think this is not used.
			theDomain.contype = Rule.NoConstraint;
			for (var choice in theDomain.members) {
				if (theDomain.members.hasOwnProperty(choice)) {
					theDomain.members[choice].clearState();
					/*
					if (theDomain.members[choice].state != Domain.ChoiceExternalRestriction) {
						theDomain.members[choice].state = Domain.ChoiceAllowed;
					}
					*/
				}
			}
			
			if (theDomain.state != Domain.UserSet) {
				theDomain.clearDomainChoices();
			}
			
			//Clear out array
			try {
				theDomain.RulesViolated.splice(0, theDomain.RulesViolated.length);
			} catch (e) {
				alert('JS Debug->Error Name='+e.name+' Message='+e.message+'\ntypeof theDomain.RulesViolated is '+typeof (theDomain.RulesViolated));
				theDomain.RulesViolated = new Array();
			}
			theDomain.generatedMessages = new Object();
		}
	}
}

//domainval argument is optional
DomainMap.prototype.setDomain = function (ndomid, choiceid, domainstate, domainval) {
	switch (arguments.length) {
	case 3:
		this.domains[ndomid].setChoice(choiceid, domainstate);
		break;
	case 4:
		this.domains[ndomid].setChoice(choiceid, domainstate, domainval);
		break;
	default:
		alert("setDomain method called with the wrong number of arguments.");
		break;
	}
}

DomainMap.prototype.getDomainObject = function (ndomid) {
	return this.domains[ndomid];
}
var configRootCode = "?"; // can be set to the root
var configLineCode = "?"; // can be set to the line
DomainMap.prototype.configStateToXML = function () {
	var newStr = '';
	var domStr = '';
	var oDom;
	for (var i=0; i<this.cpcarray.length; i++) {
		oDom = this.cpcarray[i];
		if (oDom.dtype != Domain.Constant) {
			domStr = domStr +  '<domain type="'+ oDom.dtype +'" code="' + oDom.code + '"';
			for (var key in oDom.extraAttributes) {
				domStr = domStr + " " + key + '="' + oDom.extraAttributes[key] + '"';
			}
			domStr += '>'
			for (j=0; j<oDom.aDSV.length; j++) {
				domStr += '<option';
				if (oDom.dtype != Domain.List) {
					domStr += ' value="'+oDom.inputval+'"';
				}
				domStr += '>' + oDom.members[oDom.aDSV[j]].code + '</option>';
			}
			domStr += '</domain>';
		} else {
			domStr = domStr +  '<domain type="'+ oDom.dtype +'" code="' + oDom.label + '"';
			domStr = domStr + '></domain>';
		}
	}
	newStr = '<configuration partNumber="' + this.getPartNumber().join() + '"';
	newStr = newStr + ' code="' + configRootCode + '"';
	newStr = newStr + ' line="' + configLineCode + '"';
	newStr = newStr + ' state="' + oDomains.getConstraintEngineState() + '">'; 
	newStr = newStr + domStr + '</configuration>';
	return newStr;
}  

//Domain constructor
function Domain(arglabel, argcode, argdid, arglid, argdtype, argcode2, argnattval, argFuncType) {
	this.label = arglabel;
	this.code = argcode;	//Domain code. If constant used in product number construction
	this.ncode = argcode2;	//code of the spec node
	this.attval = argnattval;//attribute value of the spec node
	this.did = argdid		//Domain ID (node id of option node in CC/CS
	this.lid = arglid;		//Domain Choice ID (link id of link from node in CC/CS
	this.contype = 0; 		//0=none,1=hard,2=soft
	this.dtype = argdtype;	//domain type (D_CONSTANT, D_LIST, ...)
	this.aDSV = new Array();//domain selected/specified value
	if (argdtype == Domain.Constant) {
		this.state = Domain.UserSet;//0=unset,1=autoset,2=userset
		this.aDSV.push(0);				
	} else {
		this.state = Domain.NotSet;		//0=unset,1=autoset,2=userset
		//this.dsv = -1;				//domain selected/specified value
	}
	this.domainFuncType = argFuncType; 	//OPTION or BUILDER
	this.inputval = null;				//Field to hold values input by user (text input fields). Calling application will assign values.
	this.members = new Object();		//Domain Choices
	this.RulesViolated = new Array();		//Rules that have been violated, rule id is the key
	this.generatedMessages = new Object();	//Generated Rules (non rule constraints - all choices restricted in a domain)
	this.multivalueSeparator = "";
	this.extraAttributes = new Object(); // if assigned will be put into XML
}

//Class constants
Domain.ChoiceAllowed = 0;
Domain.ChoiceRestricted = 1;

Domain.NotSet = 0;
Domain.AutoSet = 1;
Domain.UserSet = 2;

Domain.IntegerRange = 'D_INTEGER_RANGE';
Domain.DecimalRange = 'D_DECIMAL_RANGE';
Domain.Mixed = 'MIXED';
Domain.List = 'D_LIST';
//Domain.MultiList = 'D_MULTILIST';
Domain.Constant = 'D_CONSTANT';

//Domain prototype members
Domain.prototype.addMember = function(id, code, value, image) {
	this.members[id] = new DomainMember(id, code, value, image);
};

Domain.prototype.getMember = function(id) {
	return this.members[id];
};

Domain.prototype.addPrice = function(id, price, code, adj_code) {
	if (!this.members[id].hasOwnProperty('pricing')) {
		this.members[id].pricing = new Object();
	}
	this.members[id].pricing[code] = new Object();
	this.members[id].pricing[code].price = price;
	this.members[id].pricing[code].adj_code = adj_code;
};

Domain.prototype.hasSelection = function() {
	return this.state != Domain.NotSet;
};

Domain.prototype.getNumberOfDSVChoices = function() {
	return this.aDSV.length;
}

Domain.prototype.getDSVChoice = function(choiceidx) {
	return this.aDSV[choiceidx];
}

Domain.prototype.setChoice = function(choiceid, domainstate) {
	if (this.dtype == Domain.Constant) {
		return; //Constant domains cannot be set!
	}
	
	if (domainstate == Domain.NotSet) {
		this.clearDomainChoices();
	} else {
		//this.dsv = choiceid;
		
		// check to see if the choice is already selected
		var found = false;
		for (var i=0; i<this.aDSV.length; i++) {
			if (this.aDSV[i] == choiceid) {
				found = true;
				break;
			}
		}
		// only add the choice if not selected
	    if (!found) {
			this.aDSV.push(choiceid);
		}

		if (arguments.length > 2) {
			this.inputval = arguments[2];
		}
		this.state = domainstate;
	}
};

Domain.prototype.clearDomainChoices = function() {
	this.aDSV.splice(0, this.aDSV.length);
	this.inputval = null;
	this.state = Domain.NotSet;
}

Domain.prototype.selectionString = function() {
	var aCodes = new Array();
	for (i=0; i<this.aDSV.length; i++) {
		aCodes[i] = this.members[this.aDSV[i]].code;
	}
	return aCodes.join(this.multivalueSeparator);
}

Domain.prototype.validateValueRange = function(value) {
	var oDomMember;
	var sRange, aRange, aRangeSpec;
	var nMin, nMax, nInc;
	var bIsRange=false;

	for (var idDomMember in this.members) {
			if (this.members.hasOwnProperty(idDomMember)) {
			/*
				Make sure that it is numeric range instruction
					1. Starts & ends with [...]
					2. Contains a list of the form min-max-increment,additional formatting instructions
					3. min, max and increment must be numbers
				If these conditions are not met then deliver an error message unless the domaintype is MIXED.
				
				compare value to range and return domainmember if value is in range.
			*/
			oDomMember = this.members[idDomMember];
			sRange = oDomMember.value; 
			if (sRange.charAt(0) == '['
				&& sRange.charAt(sRange.length-1) == ']') {
				aRange = sRange.slice(1,-1).split(',',1);
				if (aRange.length == 1) {
					aRangeSpec = aRange[0].split('-',3);
					if (aRangeSpec.length >= 2) {
						nMin = parseFloat(aRangeSpec[0]);
						nMax = parseFloat(aRangeSpec[1]);
						if (!isNaN(nMin) && !isNaN(nMax)) {
							bIsRange = true;
							if (nMin<=value && nMax>=value) {
								var bRangeSatisfied=true;
								if (aRangeSpec.length == 3) {
									/*
										Due to roundoff errors this incremental check test really only works for
										integer ranges. We need to find a way to handle roundoff in the calculations
										to make this useful for decimal ranges
									*/
									nInc = parseFloat(aRangeSpec[2]);
									if (isNaN(nInc) || (Math.abs((value-nMin) % nInc)) > nInc/1000000.0) {
										//alert('value rejected by increment boundary test ');
										bRangeSatisfied = false;
									}
								}		
								return (bRangeSatisfied) ? oDomMember:null;
							}
						}
					}
				}
			}
			if (!bIsRange && this.dtype != Domain.Mixed) {
				alert('Domain '+this.label+' member '+oDomMember.value+' cannot be parsed for range parameters.');
			}
		}
	}
	return null;
}

Domain.prototype.getConsolidatedValueRange = function() {
	
	//Cycle through the choices and fill in the minmax structure
	var oDomMember;
	var sRange, aRange, nMin, nMax;
	var bIsRange=false;
	var oMinMax = {minVal:Number.NaN, maxVal:Number.NaN};

	for (var idDomMember in this.members) {
			if (this.members.hasOwnProperty(idDomMember)) {
			/*
				Make sure that it is numeric range instruction
					1. Starts & ends with [...]
					2. Contains a list of the form min-max-increment,additional formatting instructions
					3. min and max must be numbers
				If these conditions are not met then deliver an error message unless the domaintype is MIXED.
				
				compare value to range and return domainmember if value is in range.
			*/
			oDomMember = this.members[idDomMember];
			if (oDomMember.state == Domain.ChoiceAllowed) {
				sRange = oDomMember.value; 
				if (sRange.charAt(0) == '['
					&& sRange.charAt(sRange.length-1) == ']') {
					aRange = sRange.slice(1,-1).split(',',1);
					if (aRange.length == 1) {
						aRange = aRange[0].split('-',2);
						if (aRange.length == 2) {
							nMin = parseFloat(aRange[0]);
							nMax = parseFloat(aRange[1]);
							if (!isNaN(nMin) && !isNaN(nMax)) {
								bIsRange = true;
								if (isNaN(oMinMax.minVal)) {
									oMinMax.minVal = nMin;
								} else {
									if (nMin < oMinMax.minVal) {
										oMinMax.minVal = nMin;
									}
								}
								if (isNaN(oMinMax.maxVal)) {
									oMinMax.maxVal = nMax;
								} else {
									if (nMax > oMinMax.maxVal) {
										oMinMax.maxVal = nMax;
									}
								}
								//if (nMin<=value && nMax>=value) {
									//return oDomMember;
								//}
							}
						}
					}
				}
				//if (!bIsRange && this.dtype != Domain.Mixed) {
				//	alert('Domain '+oDom.label+' member '+oDomMember.value+' cannot be parsed for range parameters.');
				//}
			}
		}
	}
	
	return oMinMax;
}

Domain.prototype.validateValueList = function(value) {
	var oDomMember;
	var valUC=value.toUpperCase();
	for (var idDomMember in this.members) {
		if (this.members.hasOwnProperty(idDomMember)) {
			oDomMember = this.members[idDomMember];
			if (valUC == oDomMember.value.toUpperCase()
				|| valUC == oDomMember.code.toUpperCase()) {
				return oDomMember;
			}
		}
	}
	return null;
}

Domain.prototype.setConstraintType = function(ctype) {
	switch (this.contype) {
	case Rule.NoConstraint:
		this.contype = ctype;
		break;
	case Rule.SoftConstraint:
		if (ctype == Rule.HardConstraint) {
			this.contype = ctype;
		}
		break;
	case Rule.HardConstraint:
		//No Action
		break;
	default:
		alert("Internal error: Illegal value for constraint type specified ("+ctype+")");
	}
};

Domain.prototype.getViolatedRule = function(nIndex) {
	return this.RulesViolated[nIndex];
}
Domain.prototype.addViolatedRule = function(oRule) {
		this.RulesViolated[this.RulesViolated.length] = oRule;
};
Domain.prototype.getNumberOfViolatedRules = function() {
	return this.RulesViolated.length;
}

Domain.prototype.addGeneratedMessage = function(msgid, sText) {
	this.generatedMessages[msgid] = sText;
};

Domain.prototype.evaluateRuleDomain = function(oRuleMember) {
	//This method should not be called if the domain does
	//not have a selection.
	var bRuleSatisfied = false;
	switch (oRuleMember.getDomainType()) {
	case Rule.ANY:
		bRuleSatisfied = this.evaluateRuleDomainAny(oRuleMember);
		break;
	case Rule.ALL:
		bRuleSatisfied = this.evaluateRuleDomainAll(oRuleMember);
		break;
	case Rule.CONTAINS:
		bRuleSatisfied = this.evaluateRuleDomainContains(oRuleMember);
		break;
	}
	return bRuleSatisfied;
}
Domain.prototype.evaluateRuleDomainAny = function(oRuleMember) {
	for (var i=0; i<this.aDSV.length; i++) {
		if (oRuleMember.hasChoice(this.aDSV[i])) {
			return true;
		}
	}
	return false;
}
Domain.prototype.evaluateRuleDomainAll = function(oRuleMember) {
	var nSatisfied = 0;
	for (var i=0; i<this.aDSV.length; i++) {
		if (oRuleMember.hasChoice(this.aDSV[i])) {
			nSatisfied++;
		}
	}
	return nSatisfied == oRuleMember.getNumberOfChoices();
}
Domain.prototype.evaluateRuleDomainContains = function(oRuleMember) {
	for (var i=0; i<this.aDSV.length; i++) {
		if (!oRuleMember.hasChoice(this.aDSV[i])) {
			return false;
		}
	}
	return true;
}

//DomainMember constructor
function DomainMember(id, code, value, image) {
	this.id = id;			//link id from CC/CS
	this.value = value;		//value displayed to users.
	this.code = code;		//code used in product numbers
	this.image = image;		//speclink image
	this.state = Domain.ChoiceAllowed;	//state=0/1 allowed/prohibited
}

DomainMember.prototype.clearState = function() {
	//if (this.state != Domain.ChoiceExternalRestriction) {
	this.state = Domain.ChoiceAllowed;
	//}
}

DomainMember.prototype.isAllowed = function() {
	return this.state == Domain.ChoiceAllowed;
}

DomainMember.prototype.isRestricted = function() {
	return this.state != Domain.ChoiceAllowed;
}


//Rule constructor
function Rule(ruleid, reasonid, constrainttype) {
	this.ruleid = ruleid;			//Is This Required for Processing?
	this.reasonid = reasonid; 		//Id of text
	this.type = constrainttype;		//hard or soft
	this.members = {}; 				//Object containing members
	this.nMembers = 0;				//Count of members - The length property cannot be used as it includes prototype members as well.
}

//Class constants

//Constraint types
Rule.NoConstraint = 0;
Rule.SoftConstraint = 1;
Rule.HardConstraint = 2;

//Rule Member Types
Rule.ANY = 1;		//Rule member is satisfied if any member choice is selected
Rule.ALL = 2;		//Rule member is satisfied if all member choices are selected
Rule.CONTAINS = 3; 	//Rule member is satisfied if only member choices are selected

//Rule prototype members
Rule.prototype.addMember = function(domainid, nDomainType /*, domain choices */) {
	this.members[domainid] = new RuleMember(arguments);
	this.nMembers++;
};
Rule.prototype.getMemberCount = function() {
	return this.nMembers;
};
Rule.prototype.hasMemberChoice = function(domainid, choiceid) {
	var bHasChoice = false;
	if (this.members.hasOwnProperty(domainid)) {
		//var oRuleMember = this.members[domainid];
		bHasChoice = this.members[domainid].oChoices.hasOwnProperty(choiceid);
	}	
	return bHasChoice;
};
var RuleViolationTemplate = 'The selected values in {0} are incompatible.';
Rule.prototype.createMessage = function(oDomMap) {
	var msg;
	var oDom;
	var aDomLabels=[];
	for (var domainid in this.members) {
		if (this.members.hasOwnProperty(domainid)) {
			oDom = oDomMap.domains[domainid];
			aDomLabels[aDomLabels.length] = oDom.label;
		}
	}
	
	if (aDomLabels.length == 1) {
		msg = aDomLabels[0];
	} else if (aDomLabels.length > 1) {
		msg = aDomLabels.join(', ');
	}
	msg = RuleViolationTemplate.replace(/\{0\}/,msg);
	return msg;
};
//RuleMember constructor: passed a arguments object from the Rule.addMember function.
function RuleMember(argarray) {
	//this.id = argarray[0]; not necessary
	this.nDomainType = argarray[1];
	this.oChoices = new Object();
	this.nNumberOfChoices = argarray.length - 2;
	for (var i=2; i<argarray.length; i++) {
		this.oChoices[argarray[i]] = 1; //The value is not important. Only that the domain choice id exist as a map key.
	}
}

RuleMember.prototype.getDomainType = function() { return this.nDomainType; }

RuleMember.prototype.hasChoice = function(key) { return this.oChoices.hasOwnProperty(key); }

RuleMember.prototype.getNumberOfChoices = function() { return this.nNumberOfChoices; }

