/*
PURPOSE = Classes that provide the majority of the basic Su Doku workings

-------------------------
--- TABLE OF CONTENTS ---
-------------------------
1. GLOBALS
2. BOARD CLASS
3. DIFFICULTY CLASS
4. CELL CLASS
5. MOVE CLASS
6. GROUPING CLASS
7. MESSAGES CLASS
8. PROGRESS BAR CLASS
*/




/*----------------
--- 1. GLOBALS ---
----------------*/
//NOTE:  These are global variables and constants that are only needed by the code in this file.  
//NOTE:  The code in this file may depend on globals are not here because they are common to other files.
//-- General
var CHAR_PLUS="+", CHAR_SPACE=" ", CHAR_PERCENT="%";

//-- CELL 
var STRING_DIGITLOCK="digitlock", STRING_DIGITACTIVE="digitactive", STRING_DIGIT="digit";
var lock_flag=0x00020000;		// cell locked

//-- MESSAGES
var TIMEOUT_CLEARMESSAGE = "g_oBoard.Messages.ClearActive()";
var STRING_ENABLED = "MsgNavEnabled", STRING_DISABLED = "MsgNavDisabled";
var TOOLTIP_MESSAGES = " messages";




/*--------------------
--- 2. BOARD CLASS ---
--------------------*/
	function Board(rarrBoardValues){
	//PURPOSE = Represent the actions and data for the puzzle board
		var GROUPSIZE=9; BOARDSIZE = GROUPSIZE*GROUPSIZE;
		var arrCells = new Array(BOARDSIZE);
		
		//-- Intitialize
		var arrBoardValues = (rarrBoardValues) ?  rarrBoardValues : new Array(BOARDSIZE);
		for (var i=0; i<BOARDSIZE; i++)
			arrCells[i] = new Cell(i, arrBoardValues);
		var oDifficulty = new Difficulty(this);
		var oMessages = new Messages(this);
		
		//-- Properties
		this.ActiveCellIndex = null;
		this.BoardValues = arrBoardValues;
		this.BoardSize = BOARDSIZE;
		this.GroupSize = GROUPSIZE;
		this.Cells = arrCells;
		this.Difficulty = oDifficulty;
		this.Messages = oMessages;
		
		//-- Methods:  Non-Mutating
		this.Export = fnBoard_Export;					
		this.HasActiveCell = fnBoard_HasActiveCell;
		this.GetActiveCell = fnBoard_GetActiveCell;
		this.SetInitialActiveCell = fnBoard_SetInitialActiveCell;
		this.SetCellToActiveState = fnBoard_SetCellToActiveState;
		this.GetMoves = fnBoard_GetMoves;
		this.BlankCells = fnBoard_BlankCells;
		this.RowBlankCount = fnBoard_RowBlankCount;
		this.ColumnBlankCount = fnBoard_ColumnBlankCount;
		this.RegionBlankCount = fnBoard_RegionBlankCount;
		this.LockedCells = fnBoard_LockedCells;
		//this.Clone = fnBoard_Clone;
		
		//-- Methods:  Mutating
		this.Clear = fnBoard_Clear;
		this.Lock = fnBoard_Lock;
		this.Unlock = fnBoard_Unlock;
		this.Reset = fnBoard_Reset;
		this.ResetCell = fnBoard_ResetCell;
		this.Import = fnBoard_Import;
	}
	
	
	//-- Methods:  Non-Mutating
	function fnBoard_Export(){
	//PURPOSE = Create export string
	//RETURN = String
		return this.BoardValues.join("|");
	}
	
	function fnBoard_HasActiveCell(){
	//PURPOSE = Indicate if board has an active cell
	//RETURN = Boolean
		return (this.ActiveCellIndex != null);
	}
	
	function fnBoard_GetActiveCell(){
	//PURPOSE = Get a reference to the active cell
	//RETURN = Cell
		if (this.HasActiveCell)
			return this.Cells[this.ActiveCellIndex];
		else return null;
	}
	
    function fnBoard_SetInitialActiveCell(){
    //PURPOSE = Set the top left non-locked cell as active
    //RETURN = Reference to Board
        //NOTE:  de-active active cell if present
        if (this.ActiveCellIndex) this.GetActiveCell().SetStyleInactive();
        
        //NOTE:  active new cursor cell
        var iCellIndex=0;
		while(this.Cells[iCellIndex].IsLocked())
			if ((iCellIndex+=1) > this.BoardSize-1) iCellIndex-= this.BoardSize;        
        this.ActiveCellIndex = iCellIndex;
        
		this.GetActiveCell().SetStyleActive().GetInputControl().focus();
		return this;
    }
	
	function fnBoard_SetCellToActiveState(viCellIndex){
	//PURPOSE = Move cursor cell to specified location
    //RETURN = Reference to Board
        //NOTE:  Return current cell to normal style
        if (this.HasActiveCell)	this.GetActiveCell().SetStyleInactive();
        
		//NOTE:  Activate cell as cursor
		this.ActiveCellIndex = viCellIndex;
		this.GetActiveCell().SetStyleActive();
		this.GetActiveCell().GetInputControl().focus();
		
		fnUpdateNumberButtonStyle();
		return this;
	}
	
	function fnBoard_GetMoves(){
	//PURPOSE = Build an array of immediately available logical moves
		var oGrouping = new Grouping();

		var arrBoxMoves = fnIdentifyGroupCertainties(this, g_arrBoxIndex, oGrouping.RegionValue);
		var arrRowMoves = fnIdentifyGroupCertainties(this, g_arrRowIndex, oGrouping.RowValue);
		var arrColMoves = fnIdentifyGroupCertainties(this, g_arrColumnIndex, oGrouping.ColumnValue);
		var arrCellMoves = fnIdentifyCellCertainties(this, oGrouping.CellValue);
		//TODO:  Find moves using Fifth line of logic
		//			May need a new "grouping" enum value to represent the logic
		var arrMoves = arrBoxMoves.concat(arrRowMoves, arrColMoves, arrCellMoves);
		
		//NOTE:  Remove duplicates - merging grouping info
		for (var i=0; i<arrMoves.length; i++)
			for (var j=i+1; j<arrMoves.length; j++)
				if (arrMoves[i].CellIndex == arrMoves[j].CellIndex){
					arrMoves[i].CausalGrouping.Concat(arrMoves[j].CausalGrouping);
					arrMoves.splice(j--,1);
				}
		
		//NOTE:  Sort by grouping's set count - desc
		var iSwaps = 0;
		do{
			iSwaps = 0;
			for (var i=0,iL=arrMoves.length-1; i<iL; i++)
				for (var j=i+1, oSwap, jL=arrMoves.length; j<jL; j++)
					if (arrMoves[i].CausalGrouping.CountSet() < arrMoves[j].CausalGrouping.CountSet()){
						oSwap = arrMoves[i];
						arrMoves[i] = arrMoves[j];
						arrMoves[j] = oSwap;
						iSwaps++;
					}
		}while(iSwaps!=0);
		
		return arrMoves;
	}
	
	function fnBoard_BlankCells(){
	//PURPOSE = Collect array of blank cell references
	//RETURN = Array
//TODO:  Maintain this array as a property on the board... all digit sets must update this array
//TODO:  Have GetMoves work off of a blank cells array... more efficient
		var arrBlanks = new Array();
		for (var i=0,iL=this.BoardSize; i<iL; i++)
			if (!this.Cells[i].HasDigit())
				arrBlanks.push(this.Cells[i]);	
		return arrBlanks;
	}
	
	function fnBoard_RowBlankCount(viCellIndex){
	//PURPOSE = Indicate if row is filled with digits - no blanks
	//RETURN = Number, 0-9
//TODO:  Update to work off of a specific group array that has had locked digits removed, and maybe placed digits too...
		return fnCountBlankCellsInArray(g_arrRowIndex[this.Cells[viCellIndex].GetRowNumber()], this);
	}

	function fnBoard_ColumnBlankCount(viCellIndex){
	//PURPOSE = Indicate if column is filled with digits - no blanks
	//RETURN = Number, 0-9
//TODO:  Update to work off of a specific group array that has had locked digits removed, and maybe placed digits too...	
		return fnCountBlankCellsInArray(g_arrColumnIndex[this.Cells[viCellIndex].GetColumnNumber()], this);
	}

	function fnBoard_RegionBlankCount(viCellIndex){
	//PURPOSE = Indicate if region is filled with digits - no blanks
	//RETURN = Number, 0-9
//TODO:  Update to work off of a specific group array that has had locked digits removed, and maybe placed digits too...	
		return fnCountBlankCellsInArray(g_arrBoxIndex[this.Cells[viCellIndex].GetRegionNumber()], this);
	}
	
	function fnBoard_LockedCells(){
	//PURPOSE = Collect array of blank cell references
	//RETURN = Array
//TODO:  Maintain this array as a property on the board... all digit sets must update this array
		var arrLocked = new Array();
		for (var i=0,iL=this.BoardSize; i<iL; i++)
			if (this.Cells[i].IsLocked())
				arrLocked.push(this.Cells[i]);
		return arrLocked;	    
	}
	
	/*function fnBoard_Clone(){
	//PURPOSE = Provide a clone of this board
		var arrCloneValues = new Array(81);
		for (var i=0,iL=this.BoardSize; i<iL; i++) 
			arrCloneValues[i] = this.BoardValues[i];
		
		var oCloneBoard = new Board(arrCloneValues);
		return oCloneBoard;
	}*/
	
	
	//-- Methods:  Mutating
	
	function fnBoard_Clear(){
	//PURPOSE = Clear all the digits from the board and reset to empty state
	//RETURN = Reference to Board 			
		for (var i=0,iL=this.BoardSize; i<iL; i++)
			this.Cells[i].Clear();
		return this;
	}
	
	function fnBoard_Lock(){
	//PURPOSE = Lock all the digits on the board
	//RETURN = Reference to Board
		for (var i=0,iL=this.BoardSize; i<iL; i++)
			this.Cells[i].Lock();
		return this;
	}

	function fnBoard_Unlock(){
	//PURPOSE = Unlock all the digits on the board
	//RETURN = Reference to Board				
		for (var i=0,iL=this.BoardSize; i<iL; i++)
			this.Cells[i].Unlock();
		return this;
	}
	
	function fnBoard_Reset(){
	//PURPOSE = Clear all the digits from the board and reset to empty state
	//RETURN = Reference to Board
		for (var i=0,iL=this.BoardSize; i<iL; i++)
			this.Cells[i].Reset();
		return this;
	}
	
	function fnBoard_ResetCell(viCellIndex){
	//PURPOSE = Reset this cell 
		this.Cells[viCellIndex].Reset();
	}

	function fnBoard_Import(vsExportString){
	//PURPOSE = Import board from an export string
	//RETURN = Reference to Board
		var arrBoardValues = vsExportString.split("|");
		
		//NOTE:  Load board values from array
		if (arrBoardValues.length >= this.BoardSize) {
			for (var i=0,iL=this.BoardSize; i<iL; i++) {
				this.Cells[i].UpdateRawValue(parseInt(arrBoardValues[i],10));
				this.Cells[i].SetDigit(this.BoardValues[i] & digit_mask).ShowDigit().SetStyleInactive();
			}
		}
		
		return this;
	}		
	
	
	//-- Functions
    function fnIdentifyCellCertainties(roBoard, viGroupingValue){
    //PURPOSE = Identify cells with a single legal digit
    //RETURN = Integer, count of certainties found
		var arrMoves = Array();
		
		for (var iCell=0,iL=roBoard.BoardSize; iCell<iL; iCell++){
			var oCell = roBoard.Cells[iCell];
			//NOTE:  If the cell is not locked and empty
			if ((!oCell.IsLocked()) && (!oCell.HasDigit())){
				//NOTE:  Count the number of legal digits
				var bFoundSingleLegal = false, iLastLegalDigit=null;
				for (var iDigit=0; iDigit<9; iDigit++){
					if (oCell.IsDigitLegal(iDigit)){ 
						//NOTE:  If finding the second legal digit
						if (bFoundSingleLegal){
							bFoundSingleLegal = false;
							iLastLegalDigit = null;
							break;
						}else{
							bFoundSingleLegal = true;
							iLastLegalDigit = iDigit;
						}
					}
				}
				
				//NOTE:  If only one digit is legal then add cell to array
				if (bFoundSingleLegal)
					arrMoves.push(new Move(iCell, iLastLegalDigit, viGroupingValue));
			}
		}
		
		return arrMoves;
    }
    
    function fnIdentifyGroupCertainties(roBoard, arrCellIndexGroup, viGroupingValue){
    //PURPOSE = Identify digits that can only be legal for a single cell in a group (box, row, column)
    //RETURN = Integer, count of certainties found
		var arrMoves = Array(), iGroupSize = roBoard.GroupSize;
		
		for (var iGroup=0; iGroup<iGroupSize; iGroup++){
			for (var iDigit=0; iDigit<iGroupSize; iDigit++){
				var bFoundSingleLegal=false, iLastLegalCellIndex=null;
				//NOTE:  Check the digit in every cell of the group
				for (var iCell=0; iCell<iGroupSize; iCell++){
					//NOTE:  If cell is not locked, empty, and digit is legal
					var oCell = roBoard.Cells[arrCellIndexGroup[iGroup][iCell]];
					if ((!oCell.IsLocked()) && (!oCell.HasDigit()) && (oCell.IsDigitLegal(iDigit))){
						//NOTE:  If this is the second legal cell found for the digit
						if (bFoundSingleLegal){
							bFoundSingleLegal=false;
							iLastLegalCellIndex=null;
							break;
						}else{
							bFoundSingleLegal=true;
							iLastLegalCellIndex = oCell.Index;
						}
					}
				}
				
				//NOTE:  If the digit was only legal in one cell of the group
				if (bFoundSingleLegal)
					arrMoves.push(new Move(iLastLegalCellIndex, iDigit, viGroupingValue));
			}
		}
		
		return arrMoves;
    }
    
    
    

	/*-------------------------
	--- 3. DIFFICULTY CLASS ---
	-------------------------*/
	function Difficulty(roBoard, viValue){
		var iValue = (viValue) ? viValue : 0;
		
		//-- Constants - gleaned from statistical analysis of testing runs
		this.MinValue = 1.66;
		this.MaxValue = 3.13;
		this.Mean = 2.30;
		this.StndDev = 0.23;
		
		//-- Properties
		this.Board = roBoard;
		this.DifficultyTimeOut = null;
		this.RequiresGuess = false;
		this.Value = 0;                     //NOTE:  Set to zero by Clear and upated by GetValue
		this.ProgressBar = new ProgressBar(document.getElementById("DifficultyBar"), document.getElementById("DifficultyBarAmount"), document.getElementById("DifficultyBarRemainder"), this.MinValue, this.MaxValue);
		
		//-- Methods
		this.AsynchSetValue = fnDifficulty_AsynchSetValue;
		this.Clear = fnDifficulty_Clear;
		this.GetRating = fnDifficulty_GetRating;
		this.GetValue = fnDifficulty_GetValue;
		this.SetValue = fnDifficulty_SetValue;
		this.ZScore = fnDifficulty_ZScore;
		
		//-- Initialization
		this.ProgressBar.SetBarWidth("20em");
	}
    
    
    //-- Methods
    
    function fnDifficulty_AsynchSetValue(){
    //PURPOSE = Start difficulty update in 1/4 of a second
    //RETURN = Reference to difficulty
		this.Clear();
		this.ProgressBar.SetRemainderText("working...".italics());
		this.DifficultyTimeOut = window.setTimeout("g_oBoard.Difficulty.SetValue();", 100);
		return this;
    }
	
	function fnDifficulty_Clear(){
	//PURPOSE = Set difficulty back to zero and everything else with it
	//RETURN = Reference to difficulty
		window.clearTimeout(this.DifficultyTimeOut);
		this.DifficultyTimeOut = null;
		this.RequiresGuess = false;
		this.Value = 0;
		this.ProgressBar.BarControl.title = EMPTYSTRING;
		this.ProgressBar.Reset();
		return this;
	}
	
	function fnDifficulty_GetValue(bUseFullBoard){
    //PURPOSE = Figure difficulty of puzzle in a numeric fashion              
    //PARAM.1.in = Boolean, setting to true will use the full board (locked and unlocked) to figure difficutly
    //					Default is false, where determination starts with locked digits only
		//NOTE:  Build working board
//TODO:  Why doesn't this clone work????? the reset call effects the main board in a weird way...
        //var oWorkingBoard = g_oBoard.Clone();
        //if (!bUseFullBoard) oWorkingBoard.Reset();                    
        
		//NOTE:  Build working board
		var arrWorkingBoard = new Array(81);
		if (bUseFullBoard)
			for (var i=0; i<81; i++) arrWorkingBoard[i] = this.Board.BoardValues[i];
		else{
			var iBlankValue = allow_mask | solve_mask | digit_mask;
			for (var i=0; i<81; i++) 
				arrWorkingBoard[i] = iBlankValue;
			var arrLocked = this.Board.LockedCells();
			for (var i=0,iL=arrLocked.length; i<iL; i++) 
				arrWorkingBoard[arrLocked[i].Index] = arrLocked[i].GetRawValue();
		}
		
        var oWorkingBoard = new Board(arrWorkingBoard);                    
        
        //NOTE:  Solve working board in sets
		var arrSetAverage = new Array(), iSetDifficulty=0;
		var arrMoves = oWorkingBoard.GetMoves();
		while(arrMoves.length > 0){
			//NOTE:  Figure difficulty for set of moves
			iSetDifficulty = 0;
			for (var i=0,iL=arrMoves.length; i<iL; i++)
				iSetDifficulty+= arrMoves[i].Difficulty(oWorkingBoard);
			
			//NOTE:  Make moves on working board
			for (var i=0,iL=arrMoves.length; i<iL; i++)
				oWorkingBoard.Cells[arrMoves[i].CellIndex].SetDigit(arrMoves[i].DigitIndex);
			
			//NOTE:  Get average move difficulty for the set
			arrSetAverage.push(iSetDifficulty/arrMoves.length);
			
			arrMoves = oWorkingBoard.GetMoves();
        }
        
        //NOTE:  Figure overall puzzle difficulty by averaging sets
        var iDifficulty = 0;
        for (var i=0, iL=arrSetAverage.length; i<iL; i++)		//NOTE:  Ignoring final set since they are all relatively identical
			iDifficulty+=arrSetAverage[i];
		if (iDifficulty){
			iDifficulty/= (arrSetAverage.length);
			iDifficulty = Math.round(iDifficulty*100)/100;
		}
		
		//NOTE:  Determine if a guess is required
		if (oWorkingBoard.BlankCells().length!=0) 
			this.RequiresGuess=true;
        
        return (this.Value = iDifficulty);				
	}
	
	function fnDifficulty_GetRating(){
	//PURPOSE =  Determine a 0 to five rating for the difficulty
	//RETURN = Number
        if (this.Value)	return Math.round((this.ProgressBar.GetPercentage()/20) * 10) / 10;
		else return 0;
	}
	
    function fnDifficulty_SetValue(){
    //PURPOSE = Figure difficulty of puzzle in a numeric fashion and show visually
    //RETURN = Reference to difficulty
        if (!this.Value) this.GetValue();
        if (this.Value){
			this.ProgressBar.ClearRemainderText();
			this.ProgressBar.SetValue(this.Value);
			
			//NOTE:  Set text and tooltip for a "n out of 5" type rating
            var iRating = this.GetRating();
            this.ProgressBar.SetValueText(iRating);
            this.ProgressBar.BarControl.title = iRating + " out of 5";
			
			//NOTE:  Indicate if guess is required to solve
			if (this.RequiresGuess) this.ProgressBar.SetValueText("Guess Required");
        }else{
			this.ProgressBar.BarControl.title = EMPTYSTRING;
			this.ProgressBar.Reset();
		}
        return this;
    }
	
	function fnDifficulty_ZScore(){
	//PURPOSE = Figure the difficulty z score
	//RETURN = Number
		if (!this.Value) this.GetValue();
		return ((this.Value - this.Mean) / this.StndDev);
	}

	
	
	
	/*-------------------
	--- 4. CELL CLASS ---
	-------------------*/
	function Cell(viCellIndex, rarrBoardValues){
	//PURPOSE = Represent the actions and data for a puzzle cell
		//-- Properties
		this.Index = viCellIndex;
		this.BoardValues = rarrBoardValues;
		this.PointsAwarded = 0;
		
		//-- Methods:  Non-Mutating
		this.GetRawValue = fnCell_GetRawValue;
		this.GetInputControl = fnCell_GetInputControl;
		this.GetRowNumber = fnCell_GetRowNumber;
		this.GetColumnNumber = fnCell_GetColumnNumber
		this.GetRegionNumber = fnCell_GetRegionNumber
		this.IsLocked = fnCell_IsLocked;
		this.IsDigitLegal = fnCell_IsDigitLegal;
		this.HasDigit = fnCell_HasDigit;
		this.GetDigit = fnCell_GetDigit;
		this.GetSolution = fnCell_GetSolution;
		this.HasWrongDigit = fnCell_HasWrongDigit;
		
		//-- Methods:  Mutating
		this.UpdateRawValue = fnCell_UpdateRawValue;
		this.Clear = fnCell_Clear;
		this.Reset = fnCell_Reset;
		this.Lock = fnCell_Lock;
		this.Unlock = fnCell_Unlock;
		this.SetDigit = fnCell_SetDigit;
		this.ShowDigit = fnCell_ShowDigit;
		this.SetStyleInactive = fnCell_SetStyleInactive;
		this.SetStyleActive = fnCell_SetStyleActive;
	}
	
	
	//-- Methods:  Non-Mutating
	
	function fnCell_GetRawValue(){
	//PURPOSE = get the raw value for the cell from the board value array
	//RETURN = Integer
		return this.BoardValues[this.Index];
	}
	
	function fnCell_GetInputControl(){
	//PURPOSE = Get a reference to the cell's input control
	//RETURN = HTML control reference
		return document.getElementById("c" + this.Index);
	}
	
	function fnCell_GetRowNumber(){
	//PURPOSE = Determine the cell's row number
	//RETURN = Interger
		return(Math.floor(this.Index/9));				
	}

	function fnCell_GetColumnNumber(){
	//PURPOSE = Determine the cell's column number
	//RETURN = Interger
		return(Math.floor(this.Index%9));
	}
	
	function fnCell_GetRegionNumber(){
	//PURPOSE = Determine the cell's box number
	//RETURN = Interger
		var iRow = this.GetRowNumber();
		var iCol = this.GetColumnNumber();
		return(Math.floor(3*Math.floor(iRow/3) + Math.floor(iCol/3)));
	}
	
	function fnCell_IsLocked(){
	//PURPOSE = Indicate if the cell is locked.  
	//RETURN = Boolean
		return ((this.GetRawValue() & lock_flag) != 0);  //NOTE:  Non-zero (value of the lock flag) indicates the cell is locked and zero idicates it is unlocked.
	}
	
    function fnCell_IsDigitLegal(viDigitIndex, roGrouping){
	//PURPOSE = Determine if digit is legal in cell for board, ignoring the current cells contents
	//RETURN = Boolean
	//NOTE:  This is not saying the digit is correct, just that it is legal
	//NOTE:  Space is always legal
        var oGrouping = (roGrouping) ? roGrouping : new Grouping();

	    if (viDigitIndex != 15){
	    //NOTE:  Non-Space Digits
            var arrValues = this.BoardValues;
            
		    //NOTE:  Check Row
		    var row = this.GetRowNumber();
		    for (var i=0; i<9; i++)
			    if ((this.Index!=g_arrRowIndex[row][i]) && ((arrValues[g_arrRowIndex[row][i]] & digit_mask) == viDigitIndex))
				    oGrouping.SetRow();
            
		    //NOTE:  Check Column
		    var col = this.GetColumnNumber();
		    for (var i=0; i<9; i++)
			    if ((this.Index!=g_arrColumnIndex[col][i]) && ((arrValues[g_arrColumnIndex[col][i]] & digit_mask) == viDigitIndex))
				    oGrouping.SetColumn();
            
		    //NOTE:  Check Region
		    var box = this.GetRegionNumber();
		    for (var i=0; i<9; i++)
			    if ((this.Index!=g_arrBoxIndex[box][i]) && ((arrValues[g_arrBoxIndex[box][i]] & digit_mask) == viDigitIndex))
				    oGrouping.SetRegion();
		}
		
		return (oGrouping.Value == 0);
    }
	
	function fnCell_HasDigit(){
	//PURPOSE = Indicate if the cell has a digit value
	//RETURN = Boolean
		return ((this.GetRawValue() & digit_mask) < 9);
	}
	
	function fnCell_GetDigit(){
	//PURPOSE = Return the digit index value for the cell
	//RETURN = Integer
		return (this.GetRawValue() & digit_mask);
	}
	
	function fnCell_GetSolution(){
	//PURPOSE = provide solution digit index value
		return ((this.GetRawValue() & solve_mask) >> 4)
	}
	
	function fnCell_HasWrongDigit(){
	//PURPOSE = Indicate if the cell has a wrong answer in it
	//RETURN = Boolean
		return ((this.HasDigit()) && (!this.IsLocked()) && (this.GetDigit() != this.GetSolution()));
	}
	
	
	//-- Methods:  Mutating
	
	function fnCell_UpdateRawValue(viNewRawValue){
	//PURPOSE = Update the raw value for the cell as well as for the global board variable
		this.BoardValues[this.Index] = this.RawValue = viNewRawValue;
	}
	
	function fnCell_Clear(){
	//PURPOSE = Clear the cell completely
	//RETURN = Cell reference
		with(this.GetInputControl()){
			className = STRING_DIGIT;
			value = CHAR_SPACE;
		}
		
		this.UpdateRawValue(allow_mask | solve_mask | digit_mask);
		
		return this;
	}
	
	function fnCell_Reset(){
	//PURPOSE = Reset the cell's value to empty and reflect in raw value if its not locked.  If locked do nothing
	//RETURN = Cell reference
		if (!this.IsLocked()){
			this.GetInputControl().value = CHAR_SPACE;
			this.UpdateRawValue(this.GetRawValue() | digit_mask);
		}
		return this;
	}
	
	function fnCell_Lock(){
	//PURPOSE = Lock the cell if it holds a digit				
	//RETURN = Cell reference
		if (this.HasDigit()) {
			this.UpdateRawValue(this.GetRawValue() | lock_flag);
			this.SetStyleInactive();
		}
		return this;
	}

	function fnCell_Unlock(){
	//PURPOSE = Unlock the cell if it holds a digit				
	//RETURN = Cell reference
		if (this.HasDigit()) {
			this.UpdateRawValue(this.GetRawValue() & ~lock_flag);
			this.SetStyleInactive();
		}
		return this;
	}
	
	function fnCell_SetDigit(viDigitIndex){
	//PURPOSE = Set a digit into cell
	//RETURN = Cell reference
		this.UpdateRawValue((this.GetRawValue() & ~digit_mask) | viDigitIndex);
		return this;
	}

	function fnCell_ShowDigit(){
	//PURPOSE = Show the digit in raw value by assigning to input control
	//RETURN = Cell reference
		this.GetInputControl().value = g_arrDigit[this.GetDigit()];			
		return this;
	}
	
	function fnCell_SetStyleInactive(){
	//PURPOSE = Set cell to the active\focus cell style 
	//RETURN = Cell reference
		this.GetInputControl().className = (this.IsLocked()) ? STRING_DIGITLOCK: STRING_DIGIT;
		return this;
	}
	
	function fnCell_SetStyleActive(){
	//PURPOSE = Set cell to the active\focus cell style 
	//RETURN = Cell reference				
		this.GetInputControl().className = (this.IsLocked()) ? STRING_DIGITLOCK: STRING_DIGITACTIVE;
		return this;
	}
	
	
	
	
	/*-----------------
	-- 5. MOVE CLASS --
	-----------------*/
	function Move(viCellIndex, viDigitIndex, viGroupingValue){
		var iGroupingValue = (viGroupingValue) ? viGroupingValue : 0;
		var oGrouping = new Grouping(iGroupingValue);
		
		//-- Properties
		this.DateTime = new Date();
		this.CellIndex = viCellIndex;
		this.DigitIndex = viDigitIndex;
		this.PrevDigitIndex = null;
		this.CausalGrouping = oGrouping;
		
		//-- Methods
		this.Difficulty = fnMove_Difficulty;
	}
	
	
	//-- Methods
	
	function fnMove_Difficulty(roBoard){
	//PURPOSE = Count the number of blank cells in the move's causal groupings
	//RETURN = Number, 0-8
		var oCell = roBoard.Cells[this.CellIndex], oGrouping=this.CausalGrouping;
		var iGrpVal=oGrouping.Value;
		var iRow=0, iCol=0, iRegion=0, iCell=0;
		
		//NOTE:  Count blank cells for groupings, ignoring move's cellindex
		if (iGrpVal & oGrouping.RowValue)
			iRow = fnCountBlankCellsInArray(g_arrRowIndex[oCell.GetRowNumber()], roBoard) - 1;
		
		if (iGrpVal & oGrouping.ColumnValue)
			iCol = fnCountBlankCellsInArray(g_arrColumnIndex[oCell.GetColumnNumber()], roBoard) - 1;					
		
		if (iGrpVal & oGrouping.RegionValue)
			iRegion = fnCountBlankCellsInArray(g_arrBoxIndex[oCell.GetRegionNumber()], roBoard) - 1;
		
		var iPreComposite = iRow + iCol + iRegion;
		
		//TODO:  Figure out better way to calculate this value	
		//			- This type move may be more difficult than those above				
		if (iGrpVal & oGrouping.CellValue){
			if (!iRow) iRow = fnCountBlankCellsInArray(g_arrRowIndex[oCell.GetRowNumber()], roBoard) - 1;
			if (!iCol) iCol = fnCountBlankCellsInArray(g_arrColumnIndex[oCell.GetColumnNumber()], roBoard) - 1;
			if (!iRegion) iRegion = fnCountBlankCellsInArray(g_arrBoxIndex[oCell.GetRegionNumber()], roBoard) - 1;
			iCell = ((iRow + iCol + iRegion) / 3);
		}
		
		//TODO:  Figure difficulty for 5th line of logic
		//			- this move involves more cells than just those along the solutions' axis'
		//			- a modifier may work fine but it would be better to count the blank cells actually involved
		
		var iComposite = iPreComposite + iCell;
		return (iComposite / oGrouping.CountSet());
	}
	
	
	//-- Functions
	
	function fnCountBlankCellsInArray(rarrCells, roBoard){
	//PURPOSE = Count blank cells in cell array
		var iBlankCellCount=0;
		for (var i=0; i<rarrCells.length; i++){
			if (!roBoard.Cells[rarrCells[i]].HasDigit()) iBlankCellCount++;
		}
		return iBlankCellCount;
	}
	
	
	
	
	/*---------------------
	-- 6. GROUPING CLASS --
	---------------------*/	
	function Grouping(viValue){
		var iValue = (viValue) ? viValue: 0;
		
		//-- Properties
        this.Value = iValue;
		this.CellValue = 1;
		this.RowValue = 2;
		this.ColumnValue = 4;
		this.RegionValue = 8;
		
		//-- Methods
		this.Concat = fnGrouping_Concat;
		this.SetCell = fnGrouping_SetCell;
		this.UnsetCell = fnGrouping_UnsetCell;
		this.SetRow = fnGrouping_SetRow;
		this.UnsetRow = fnGrouping_UnsetRow;
		this.SetColumn = fnGrouping_SetColumn;
		this.UnsetColumn = fnGrouping_UnsetColumn;
		this.SetRegion = fnGrouping_SetRegion;
		this.UnsetRegion = fnGrouping_UnsetRegion;
		
		this.CountSet = fnGrouping_CountSet;
		this.CountUnSet = fnGrouping_CountUnSet;
		this.ToString = fnGrouping_ToString;
	}
    
    function fnGrouping_Concat(roGrouping){
    //PURPOSE = Combine the the paramater's value with this one's
    //RETURN = reference to Grouping
        this.Value|=roGrouping.Value;
        return this;
    }
    
    function fnGrouping_SetCell(){
    //PURPOSE = Set value to indicate cell 
    //RETURN = reference to Grouping
        this.Value|= this.CellValue;
        return this;
    }

    function fnGrouping_UnsetCell(){ 
        this.Value^= this.CellValue;
        return this;
    }

    function fnGrouping_SetRow(){
    //PURPOSE = Set value to indicate row
    //RETURN = reference to Grouping
        this.Value|= this.RowValue;
        return this;
    }

    function fnGrouping_UnsetRow(){
        this.Value^= this.RowValue;
        return this;
    }
    
    function fnGrouping_SetColumn(){
    //PURPOSE = Set value to indicate column
    //RETURN = reference to Grouping
        this.Value|= this.ColumnValue;
        return this;
    }

    function fnGrouping_UnsetColumn(){
        this.Value^= this.ColumnValue;
        return this;
    }

    function fnGrouping_SetRegion(){
    //PURPOSE = Set value to indicate region
    //RETURN = reference to Grouping
        this.Value|= this.RegionValue;
        return this;
    }

    function fnGrouping_UnsetRegion(){
        this.Value^= this.RegionValue;
        return this;
    }

    function fnGrouping_CountSet(){
    //PURPOSE = Provide count of groupings set
        var iCnt=0;
		if (this.Value & this.CellValue) iCnt++;
		if (this.Value & this.RowValue) iCnt++;
		if (this.Value & this.ColumnValue) iCnt++;
		if (this.Value & this.RegionValue) iCnt++;
		return iCnt;
    }
    
    function fnGrouping_CountUnSet(){
    //PURPOSE = Provide count of groupings not set
        return 4-this.CountSet();
    }
    
	function fnGrouping_ToString(){
	//PURPOSE = Create string description of grouping value
	//RETURN = String
		var arrGrouping = new Array();
		
		if (this.Value & 1) arrGrouping.push("cell");
		if (this.Value & 2) arrGrouping.push("row");
		if (this.Value & 4) arrGrouping.push("column");
		if (this.Value & 8) arrGrouping.push("region");

		if (arrGrouping.length == 1)
			return arrGrouping[0];
		else if (arrGrouping.length == 2)
			return arrGrouping[0] + " and " + arrGrouping[1];
		else {
            var sAndValue = " and " + arrGrouping.pop();
            return arrGrouping.join(", ") + sAndValue;
		}
	}
	
	
	
	
	/*-----------------------
    --- 7. MESSAGES CLASS ---
    -----------------------*/
    function Messages(roBoard){
		var arrMessageItems = new Array();
		
		//-- Properties
		this.Board = roBoard;
		this.ActiveMessageItem = null;
		this.IsShowing = false;
		this.Items = arrMessageItems;
		this.MessageTimeOut = null;
		
		//-- Methods
		this.Clear = fnMessage_Clear;
		this.ClearActive = fnMessage_ClearActive;
		this.GetMsgTxtControl = fnMessage_GetMsgTxtControl;
		this.GetMsgNavNextControl = fnMessage_GetMsgNavNextControl;
		this.GetMsgNavPrevControl = fnMessage_GetMsgNavPrevControl;
		this.ShowNew = fnMessage_ShowNew;
		this.ShowNewLockedMessage = new Function('viCellIndex', 'this.ShowNew("Cell number " + (viCellIndex+1) + " is locked.", 5);');
		this.ShowNext = fnMessage_ShowNext;
		this.ShowPrev = fnMessage_ShowPrev;
		this.UpdateNavigation = fnMessage_UpdateNavigation;
    }
    
    function MessageItem(viMessageIndex, vsText, viTime, roRelatedMove){
    //PURPOSE = Provide message item data class 
		//NOTE:  Default optional construction parameters
		var iTime = ((viTime) ? viTime: 0), oRelatedMove=null;
		if (roRelatedMove) oRelatedMove = roRelatedMove;
		
		//-- Properties
		this.Index = viMessageIndex;
		this.Text = vsText;
		this.Time = iTime;					//NOTE:  Seconds
		this.RelatedMove = oRelatedMove;
    }
    
    
    //-- Methods
    
    function fnMessage_Clear(){
    //PURPOSE = Clear active and historic messages
		this.ClearActive();
		this.MessageTimeOut = null;
		this.Items = new Array();
		this.UpdateNavigation();
    }
    
    function fnMessage_ClearActive(viClearTimerSeconds){
    //PURPOSE = Clear the active message from display
    //Param.1.in = Optional, Number of seconds to leave message in display area.  If not present or zero area will clear immediately.
    //RETURN = Reference to Message object
		window.clearTimeout(this.MessageTimeOut);
   		
        if (viClearTimerSeconds)
            this.MessageTimeOut = window.setTimeout(TIMEOUT_CLEARMESSAGE, viClearTimerSeconds*1000);
        else{    
            this.ActiveMessageItem = null;
            this.GetMsgTxtControl().innerHTML = EMPTYSTRING;
            this.IsShowing = false;
            this.UpdateNavigation();
        }
        
        return this;
    }
    
    function fnMessage_GetMsgTxtControl(){
    //RETURN = Reference to messsage text display control
		return document.getElementById("MessageText");
    }
    
    function fnMessage_GetMsgNavNextControl(){
    //RETURN = Reference to messsage navigation display control
		return document.getElementById("MsgNavNext");
    }  

    function fnMessage_GetMsgNavPrevControl(){
    //RETURN = Reference to messsage navigation display control
		return document.getElementById("MsgNavPrev");
    }
    
    function fnMessage_ShowNew(vsMessage, viClearTimerSeconds, roRelatedMove){
	//PURPOSE = Show new message
	//PARAM.1.IN = String, Message text
	//PARAM.2.IN = Optional, Number, number of seconds to show the message (constructor will default to zero)
	//PARAM.3.IN = Optional, Object, move object reference related to the message (constructor will default to null)
	//RETURN = Reference to Message object
        window.clearTimeout(this.MessageTimeOut);
        
        this.GetMsgTxtControl().innerHTML = vsMessage;
        this.ActiveMessageItem = new MessageItem(this.Items.length, vsMessage, viClearTimerSeconds, roRelatedMove)
        this.Items.push(this.ActiveMessageItem);
        this.IsShowing = true;
        this.UpdateNavigation();
        
        if (viClearTimerSeconds) this.ClearActive(viClearTimerSeconds);
        
        return this;
    }
    
    function fnMessage_ShowNext(){
    //PURPOSE = Show the message that came after the active one
    //RETURN = Reference to Message object
		if (this.IsShowing){
			window.clearTimeout(this.MessageTimeOut);
			
			//NOTE:  Get message index to show
			var iMsgIndex = this.ActiveMessageItem.Index+1; 
			if (this.ActiveMessageItem.Index == this.Items.length-1)
				iMsgIndex = this.Items.length-1;
			
			//NOTE:  Show Message with no timer
			this.ActiveMessageItem = this.Items[iMsgIndex];
			this.GetMsgTxtControl().innerHTML = this.ActiveMessageItem.Text;
			this.IsShowing = true;
			
			//NOTE:  Make related move's cell active
			if (this.ActiveMessageItem.RelatedMove != null){
				this.Board.SetCellToActiveState(this.ActiveMessageItem.RelatedMove.CellIndex);
			}
			
			this.UpdateNavigation();			
		}
		
		return this;
    }
    
    function fnMessage_ShowPrev(){
    //PURPOSE = Show the message that came before the active one
    //RETURN = Reference to Message object
		if (this.Items.length){
			window.clearTimeout(this.MessageTimeOut);
			
			//NOTE:  Get message index to show
			var iMsgIndex = this.Items.length-1;	//NOTE:  Default to last shown\showing message
			if (this.IsShowing){
				if (this.ActiveMessageItem.Index > 0) iMsgIndex = this.ActiveMessageItem.Index-1;
				else iMsgIndex = 0;
			}
			
			//NOTE:  Show Message with no timer
			this.ActiveMessageItem = this.Items[iMsgIndex];
			this.GetMsgTxtControl().innerHTML = this.ActiveMessageItem.Text;
			this.IsShowing = true;
			
			//NOTE:  Make related move's cell active
			if (this.ActiveMessageItem.RelatedMove != null){
				this.Board.SetCellToActiveState(this.ActiveMessageItem.RelatedMove.CellIndex);
			}			
			
			this.UpdateNavigation();
		}
		
		return this;
    }
    
    function fnMessage_UpdateNavigation(){
    //PURPOSE = Update navigation links appropriately
    //RETURN = Reference to Message object
		var sNxtClass = STRING_DISABLED, sPrvClass = sNxtClass;	//NOTE:  Default both to disabled
		
		//NOTE:  Set to enabled when appropriate
		if (this.IsShowing){
			if (this.ActiveMessageItem.Index > 0) sPrvClass = STRING_ENABLED;
			if (this.ActiveMessageItem.Index < (this.Items.length-1)) sNxtClass = STRING_ENABLED;
		}else
			if (this.Items.length > 0) sPrvClass=STRING_ENABLED; 
		
		//NOTE:  Assign appropriate class names
		this.GetMsgNavNextControl().className = sNxtClass;
		this.GetMsgNavPrevControl().className = sPrvClass;
		
		//NOTE:  Update tooltips
		if ((sNxtClass==STRING_ENABLED) && (this.IsShowing)) 
			this.GetMsgNavNextControl().title = (this.Items.length - 1 - this.ActiveMessageItem.Index) + TOOLTIP_MESSAGES;
		else this.GetMsgNavNextControl().title = EMPTYSTRING;
		
		if (sPrvClass==STRING_ENABLED){
			if (this.IsShowing) this.GetMsgNavPrevControl().title = this.ActiveMessageItem.Index + TOOLTIP_MESSAGES;
			else this.GetMsgNavPrevControl().title = this.Items.length + TOOLTIP_MESSAGES;
		}else this.GetMsgNavPrevControl().title = EMPTYSTRING;
		
		return this;
    }
    
    
    
    
    /*---------------------------
    --- 8. PROGRESS BAR CLASS ---
    ---------------------------*/
	function ProgressBar(roProgressBarControl, roValueControl, roRemainderControl, viMinValue, viMaxValue){
	//PURPOSE = Manage a progress bar
	//PARAM.1.IN = Span for outer progress bar
		//-- Properties
		this.MinValue = viMinValue;
		this.MaxValue = viMaxValue;
		this.BarControl = roProgressBarControl;
		this.ValueControl = roValueControl;
		this.RemainderControl = roRemainderControl;
		
		//-- Methods
		this.ClearRemainderText = new Function('this.RemainderControl.innerHTML=""; return this;');
		this.ClearValueText = new Function('(this.ValueControl).innerHTML=""; return this;');
		this.GetPercentage = fnProgressBar_GetPercentage;
		this.Hide = new Function('this.BarControl.style.display="none"; return this;');
		this.Reset = new Function('this.ClearRemainderText(); this.ClearValueText(); this.SetPercentage(0); return this;');
		this.SetBarWidth = new Function('vsBarWidth', 'this.BarControl.style.width = vsBarWidth; return this;');
		this.SetRemainderText = new Function('sRemainderText', 'this.RemainderControl.innerHTML=sRemainderText; return this;');
		this.SetPercentage = fnProgressBar_SetPercentage;
		this.SetValue = fnProgressBar_SetValue;
		this.SetValueText = new Function('sValueText', 'this.ValueControl.innerHTML=sValueText; return this;');		
		this.Show = new Function('this.BarControl.style.display="inline"; return this;');
	}   
	
	
	//-- METHODS
	
	function fnProgressBar_GetPercentage(){
	//PURPOSE = Provide the percentage value amount of the progress bar
	//RETURN = Number
		return parseFloat(this.ValueControl.style.width.replace(CHAR_PERCENT, EMPTYSTRING), 10);
	}
	
	function fnProgressBar_SetPercentage(viPercentage){
	//PURPOSE = Set the value width by percentage 
	//PARAM.1.IN = Number, value's percentage of the whole
	//RETURN = Reference to Progress Bar object	
		this.ValueControl.style.width = viPercentage + CHAR_PERCENT;
		return this;
	}
	
	function fnProgressBar_SetValue(viValue){
	//PURPOSE = Figure value's percentage within min and max boundries 
	//PARAM.1.IN = Number, value for amount portion of bar to represent
	//RETURN = Reference to Progress Bar object	
		var iPercentage=0;
		if (viValue > this.MinValue){
			if (viValue > this.MaxValue) iPercentage=100;
			else{
				var iRange = this.MaxValue - this.MinValue;
				iPercentage = ((viValue - this.MinValue) / iRange) * 100;
			}
		}
			
		this.SetPercentage(iPercentage);
		return this;
	}	
	