/*
2009-03-26T13:21:20PST - Minor updates, ready for V1.0
2009-01-21T11:36:14PST - Code move to SudokuPG.js
*/

var com = com || {};
com.YourFriendPaul = com.YourFriendPaul || {}; 

(function( $ ){
   $.pbSudoku = {};
   
   // no longer need to do this, but since that's the 
   //  way the code was written, just leave it.
   var pbSudoku = {};
   // the full Sudoku table lives here for all others to use
   pbSudoku.solutions = null;

   // get your elements here by id name. elems not in array return "undefined"
   pbSudoku.elemList = [];

   /*******************************************************************/
   /*******************************************************************/
   /*
     Entry point
   */
   /*******************************************************************/
   /*******************************************************************/
   $.pbSudoku.init = function(){
   
      // parse all the element ids
      pbSudoku.getElemsFromIds();
      
      // save the size of the body text so it can be restored after the print view
      pbSudoku.printView.saveMainContainerMargin();
      
      // set up ui interactions
      pbSudoku.setEventHandlers();

      // shouldn't need the if because we are in init; make a table only once, for all to use
      if( pbSudoku.solutions === null ) { pbSudoku.solutions = new pbSudoku.CSolutions(); }

      // always reset the default for a page reload
      //this.elemList[ "fNumDigits" ].value = 2;
      pbSudoku.elemList.fNumDigits.value = 2;
      pbSudoku.elemList.fSumDigits.value = 17;
      pbSudoku.inputChanged();	// make the first answer show up
   };
   
   // this makes sure all the elements exist, mostly to avoid programming errors
   // Elements can now be returned by name from the global array
   pbSudoku.getElemsFromIds = function(){
   
      // all the element name that this program uses
      var idNames = [ "fNumDigits", "fSumDigits", "printModeButton", 
         "mainText", "footerContainer", "rhContainer", "mainContainer", 
         "fResults", "tableDisplayDiv", "fNumMust", "fNumNot", "inputMsg" ];

      var iCount = 0;
      var elem = null;
      
      for( iCount = 0; iCount< idNames.length; iCount++ ){
      
         elem = document.getElementById( idNames[ iCount ] );
         if( elem === null ){
         
            alert( "Problem with element list" );
         }
         
         // put them in the global id name array
         this.elemList[ idNames[ iCount ] ] = elem;
      }
      //alert( pbSudoku.elemList[ "fNumDigits" ].value );
   };
   
   /*******************************************************************/
   /*******************************************************************/
   /*******************************************************************/
   pbSudoku.printView = {
   
      // false if in print view, true for normal view
      currentPrintView: false,
      mainContainerMarginRight: "",	// this will probably be 180px
      msgPrintFriendly: "Print friendly view",
      msgNormal: "Normal view",

      // this is called by the print button
      change: function( ){
   
         var textNode;
         pbSudoku.elemList.mainText.style.display = this.currentPrintView ? "block" : "none";
         pbSudoku.elemList.footerContainer.style.display = this.currentPrintView ? "block" : "none";
         pbSudoku.elemList.rhContainer.style.display = this.currentPrintView ? "block" : "none";
         pbSudoku.elemList.mainContainer.style.marginRight = this.currentPrintView ? this.mainContainerMarginRight : "0";
         textNode = document.createTextNode( this.currentPrintView ? this.msgPrintFriendly : this.msgNormal );
         pbSudoku.pbNodeOps.deleteChildNodes( pbSudoku.elemList.printModeButton );
         pbSudoku.elemList.printModeButton.appendChild( textNode );
         this.currentPrintView = !this.currentPrintView; 
      },
   
      // need to save the right margin in this type of page layout to be
      // able to restore it after doing a print view
      saveMainContainerMargin : function(){
      
         var elem = pbSudoku.elemList.mainContainer;
         var curMargin;
         if( elem.currentStyle ){	// ie
            curMargin = elem.currentStyle.marginRight;
         } else if ( window.getComputedStyle ){ // firefox
            curMargin = window.getComputedStyle( elem, null ).marginRight;
         }
         this.mainContainerMarginRight = curMargin; // save the original margin
         //alert( curMargin );
      }
   };
   
   
   /*******************************************************************/
   /*******************************************************************/
   /*******************************************************************/
   pbSudoku.eventInfo = function( e ){

      var ei = {
         event: e || window.event,
         target: null,
         keycode: null
      };
      
      ei.keycode = ei.event.keyCode || ei.event.which;
      ei.target = ei.event.target || ei.event.srcElement;
      if( ei.target.nodeType == 3 ){ // defeat Safari bug
     	 	ei.target = ei.target.parentNode;
      }
      
      return ei;
   };

   pbSudoku.uiKeydown = function( e ){
      var ei = pbSudoku.eventInfo( e );
      if ( ei.keycode == 13 ) { pbSudoku.inputChanged(); }
      return(e);
   };

   // use onkeypress for characters, ie doesn't send ret here, so use keydown for it
   pbSudoku.uiKeypress = function( e ){
   
      var ei = pbSudoku.eventInfo( e );
      var lowChar = 48;	// don't allow 0 as input
      lowChar = ( ei.target == pbSudoku.elemList.fSumDigits ) ? 47 : lowChar; // allow 0 for sum

      if ( ( ei.keycode > lowChar && ei.keycode < 58 ) || ei.keycode < 20 ){ 
      
         ei.event.returnValue=true;
         return true;
      }
      else{
      
         if ( typeof ei.event.preventDefault!= "undefined" ) { 
            ei.event.preventDefault(); // W3C 
         } else { 
            ei.event.returnValue = false; // IE 
         }
         return false;
      }  
   };
   
   pbSudoku.setEventHandlers = function(){
      
      // shouldn't need all the function statements, but can't get it
      // to work otherwise.  this is passed from onchange and
      // refers to the element that made the call
      var elem = this.elemList.fNumDigits;
      elem.onchange=function(){ pbSudoku.inputChanged( this ); };
      this.addKeydownListener( elem, pbSudoku.uiKeydown );
      this.addKeypressListener( elem, pbSudoku.uiKeypress );
      
      elem = this.elemList.fSumDigits;
      elem.onchange=function(){ pbSudoku.inputChanged( this ); };
      this.addKeydownListener( elem, pbSudoku.uiKeydown );
      this.addKeypressListener( elem, pbSudoku.uiKeypress );

      elem = this.elemList.fNumMust;
      elem.onchange=function(){ pbSudoku.inputChanged( this ); };
      this.addKeydownListener( elem, pbSudoku.uiKeydown );
      this.addKeypressListener( elem, pbSudoku.uiKeypress );

      elem = this.elemList.fNumNot;
      elem.onchange=function(){ pbSudoku.inputChanged( this ); };
      this.addKeydownListener( elem, pbSudoku.uiKeydown );
      this.addKeypressListener( elem, pbSudoku.uiKeypress );

      elem = this.elemList.printModeButton;
      elem.onclick=function(){ pbSudoku.printView.change(this); };
      
      elem = this.elemList.tableDisplayDiv;
      elem.onclick=function(){ pbSudoku.makeFullTable(this); };
      
   };
   

   pbSudoku.addKeydownListener = function( elem, funcName ){
   
      var el = 0;
      if( typeof document.addEventListener != "undefined" ){
         elem.addEventListener( "keydown", funcName, false);
         el=1;
      }
      else if( typeof document.attachEvent != "undefined" ){
         elem.attachEvent( "onkeydown", funcName);
         el=2;
      } else {
         if( elem.onkeydown !== null ){
            var oldOnkeydown = elem.onkeydown;
            elem.onkeydown = function(e){ oldOnkeydown(e); funcName(e); };
            el=3;
         } else {
            elem.onkeydown = funcName;
            el=4;
         }
      }
   };
   
   pbSudoku.addKeypressListener = function( elem, funcName ){
   
      var el = 0;
      if( typeof document.addEventListener != "undefined" ){
         elem.addEventListener( "keypress", funcName, false );
         el=1;
      }
      else if( typeof document.attachEvent != "undefined" ){
         elem.attachEvent( "onkeypress" , funcName );
         el=2;
      } else {
         if( elem.onkeydown !== null ){
            var oldOnkey = elem.onkeypress;
            elem.onkeypress = function(e){ oldOnkey(e); funcName(e); };
            el=3;
         } else {
            elem.onkeypress = funcName;
            el=4;
         }
      }
   };
   

   // display text concerning user input
   pbSudoku.replaceInputMessage = function( inStr ){
   
      var outElem = this.elemList.inputMsg; // where to put displays
      pbSudoku.pbNodeOps.deleteChildNodes( outElem );
      var textNode = document.createTextNode( inStr );
      outElem.appendChild( textNode );
   };
   
   /****************************************************************/
   /****************************************************************/
   /****************************************************************/
   // This handles all input changes to the specific solutions area,
   // no matter which value changes.
   pbSudoku.inputChanged = function(){
   
      // No global object created if first button isn't pressed; this fixes that case
      if( this.solutions === null ) { this.solutions = new this.CSolutions(); }
      
      var numDigElem = this.elemList.fNumDigits;
      var sumDigElem = this.elemList.fSumDigits;
      
      /* do nothing on an empty field */
      if( numDigElem.value === "" || sumDigElem.value === "" ) { return; }
      
      var digits = parseInt( numDigElem.value, 10 );
      var total = parseInt( sumDigElem.value, 10 );

      // get an array of the results, each is a string
      var result = this.solutions.getResultsArray( digits, total );
      // at this point we have all solutions, now filter it

      if( result !== null ){
         result = this.filterResult( result );
      }

      var iCount = 0;
      var outText = "";
      
      if( result === null || result === "" ){
         outText = "No solution";
      } else { 
         for( iCount=0; iCount<result.length; iCount++){
      
            outText += result[ iCount ];
            if( iCount < result.length-1 ){ outText += ", "; }
         }
      }

      var text = document.createTextNode( outText );
      
      var resElem = this.elemList.fResults;
      pbSudoku.pbNodeOps.deleteChildNodes( resElem );
      resElem.appendChild( text );
   };
   
   // resList is an array of the results, each is a string
   pbSudoku.filterResult = function( rawList ){
   
      var iCount = 0;
      var jCount = 0;
      var searchFor = "";
      var valueList = ""; // typeing shortcut
      var resList = [];
      
      if( this.elemList.fNumNot.value === "" && this.elemList.fNumMust.value === "" ){
      
         return rawList;	// nothing to do
      }
      
      // don't want to modify the original array
      resList = rawList.slice( 0 );	// copies all elements
      
      valueList = this.elemList.fNumNot.value;
      
      // There is a not list, elminate items that contain the value
      if( valueList.length !== 0 ){	

         // check each digit in the not list
         for( iCount=0; iCount<valueList.length; iCount++ ){
         
            // get the digit to seach for
            searchFor = valueList.substring( iCount, iCount+1 );
            
            jCount=0;

            while( jCount < resList.length ){
            
               if( resList[ jCount ].indexOf( searchFor ) >=0 ){	// found, so delete it from the array
               
                  resList.splice( jCount, 1 );	// deleted, so don't incr. jCount

               } else {
                  jCount++;
               }
            }
         }
      }

      valueList = this.elemList.fNumMust.value;
      
      if( valueList.length !== 0 ){	// There is a must list
      
         // check each digit in the not list
         for( iCount=0; iCount<valueList.length; iCount++ ){
         
            // check each digit in the must list
            searchFor = valueList.substring( iCount, iCount+1 );
            
            jCount=0;

            while( jCount < resList.length ){
            
               if( resList[ jCount ].indexOf( searchFor ) == -1 ){	// not found, so delete it from the array
               
                  resList.splice( jCount, 1 );	// deleted, so don't incr. jCount

               } else {
                  jCount++;
               }
            }
         }
      }

      return resList;
   };

   // make the full table display
   pbSudoku.makeFullTable = function(){
   
      var outElem = this.elemList.tableDisplayDiv; // where to put displays
      pbSudoku.pbNodeOps.deleteChildNodes( outElem );

      // No global object created if first button isn't pressed; this fixes that case
      /* Put it in a global object that the form can use */
      if( this.solutions === null ) { this.solutions = new this.CSolutions(); }
      /* here is how to just use the basic function */
      // var solutions = new pbSudoku.CSolutions;
      this.solutions.showList( outElem );
   };
   

   /*******************************************************************/
   /*******************************************************************/
   /*******************************************************************/
   // the CSolutions class
   // this is the main interface
   pbSudoku.CSolutions = function(){
      this.twoDigit = null;	// will contain a CSolutionList object
      this.threeDigit = null;
      this.fourDigit = null;
      this.fiveDigit = null;
      this.sixDigit = null;
      this.sevenDigit = null;
      this.eightDigit = null;

      this.buildList();	// build the list on object creation
   };

   pbSudoku.CSolutions.prototype.showList = function( parentNode ){ 
      if( this.initialized() !== true ){
      
         alert( "Table not built. Need to run pbSudoku.buildList()" );
         return;
      }

      this.twoDigit.showCompressed( parentNode );
      this.threeDigit.showCompressed( parentNode );
      this.fourDigit.showCompressed( parentNode );
      this.fiveDigit.showCompressed( parentNode );
      this.sixDigit.showCompressed( parentNode );
      this.sevenDigit.showCompressed( parentNode );
   };

   // called when looking for a single solution
   pbSudoku.CSolutions.prototype.getResultsArray = function( numDigits, digitSum ){

      if( this.initialized() !== true ){
      
         alert( "Table not built. Need to run pbSudoku.buildList()" );
         return null;
      }

      /* check that values are correct, otherwise NaN can sneak through */
      if( !( numDigits >= 2 && numDigits <= 8 ) ){
      
         pbSudoku.replaceInputMessage( "All solutions need to have between 2 and 8 digits" );
         return null;
      }
      
      var numDigitsSolution = null;
      switch( numDigits ){
      
         case 2:
            numDigitsSolution = this.twoDigit;
            break;
            
         case 3:
            numDigitsSolution = this.threeDigit;
            break;
            
         case 4:
            numDigitsSolution = this.fourDigit;
            break;
            
         case 5:
            numDigitsSolution = this.fiveDigit;
            break;
            
         case 6:
            numDigitsSolution = this.sixDigit;
            break;
            
         case 7:
            numDigitsSolution = this.sevenDigit;
            break;
            
         case 8:
            numDigitsSolution = this.eightDigit;
            break;
            
         default:
            alert( "getResultsArray has bad switch: " + numDigits );
            break;
            
      }
      
      if( digitSum < numDigitsSolution.getMinSum() || digitSum > numDigitsSolution.getMaxSum() ){
      
         pbSudoku.replaceInputMessage( "" + numDigits + "-digit solutions must be between " + 
                  numDigitsSolution.getMinSum() + " and " + numDigitsSolution.getMaxSum() );
         return null;
      } else { pbSudoku.replaceInputMessage( "" ); }
      
      
      // finally, get an answer, there will be one text solution in each element of the arry.
      var resArray = numDigitsSolution.getNum( digitSum );
      //alert( resArray.toString() );
      return resArray;
   };

   /* internal functions ************************************/
   // is the object completely built?
   pbSudoku.CSolutions.prototype.initialized = function(){ 
      if( this.twoDigit === null || this.threeDigit === null || 
         this.fourDigit === null || this.fiveDigit === null ||
         this.sixDigit === null || this.sevenDigit === null ||
         this.eightDigit === null ) { 
         return false;
      }
      return true; 
   };
   

   pbSudoku.CSolutions.prototype.buildList = function(){ 
   
      // returns a CSolutionList object
      // the lists must be built in increasing order
      this.twoDigit = this.createGeneric( 2, null );
      this.threeDigit = this.createGeneric( 3, this.twoDigit );
      this.fourDigit = this.createGeneric( 4, this.threeDigit );
      this.fiveDigit = this.createGeneric( 5, this.fourDigit );
      this.sixDigit = this.createGeneric( 6, this.fiveDigit );
      this.sevenDigit = this.createGeneric( 7, this.sixDigit );
      this.eightDigit = this.createGeneric( 8, this.sevenDigit );
   };

   // returns a CSolutionList object
   pbSudoku.CSolutions.prototype.createGeneric = function( buildNum, prevSol ){
   
      var fDig = 1;
      var sum = 0;
      var remSum = 0;
      var list = [];
      var sol;
      var childCount = 0;
      var solutionList = new pbSudoku.CSolutionList( buildNum );
      
      // limit overkill (because no duplicate numbers): num of digits * 1 for low and * 9 for high
      for( sum = buildNum; sum <= buildNum * 9; sum++ ){
      
         for( fDig = 1; fDig <=9; fDig++ ){
            
            if( fDig > sum ) { break; }	// don't check numbers bigger than the total
            
            remSum = sum - fDig;	// total other than fDig
            
            if( remSum <= fDig || remSum > ( buildNum - 1 ) * 9 ){  continue; }
            
            if( null !== prevSol ){
            
               // get the results from the previous (buildNum-1) computation
               // that add up to remSum.
               // look in this list for items and reject items that have duplicate digits
               list = prevSol.getNum( remSum );
         
               for( childCount=0; childCount<list.length; childCount++ ){
               
                  if( fDig >= parseInt( list[ childCount ].substring(0,1), 10 ) ){
                     continue;	// number is already used
                  }
   
                  /* what's left is a three number solution */
                  sol = new pbSudoku.CSingleSolution( sum, "" + fDig + "" + list[ childCount ] );
                  solutionList.add( sol );
               }
            } else {	// two digit solution, no previous results
            
               /* what's left is a two number solution */
               sol = new pbSudoku.CSingleSolution( sum, "" + fDig + "" + remSum );
               /* put the result in the CSolutionList object */
               solutionList.add( sol );
            }
         }
      }

      return solutionList;
   };



   /*******************************************************************/
   /*******************************************************************/
   /*******************************************************************/
   /*******************************************************************/
   /*
      This object holds a single solution of digits that add up 
      to the value in sum. 
      The digitList is a string of comma separate values. 
      
      This is add done as text values, so formatting for printing is,
      unfortunately, done here as well.
   */
   pbSudoku.CSingleSolution = function( num, components ){
      this.sum = num;
      this.digitList = components; 
   };
   
   
   /*******************************************************************/
   /*******************************************************************/
   /*******************************************************************/
   /*
      This class holds an array of CSingleSolution objects
      The array is added to by the add() function
      These will be the results for a single "number of digits" solution
   */
   pbSudoku.CSolutionList = function( value ){
      this.list = []; // will hold CSingleSolution objects
      this.numDigits = "";
      
      /* a way to display what type of solution this object is */
      if( typeof value != "undefined" ){
         this.numDigits += value;
      }
   };
   
   // adds a single entry to the list of solutions
   pbSudoku.CSolutionList.prototype.add = function( totRes ){

      this.list.push( totRes );
   };
   
   pbSudoku.CSolutionList.prototype.showCompressed = function( parentNode ){
      
      var iCount = 0;
      var iSol = 0;
      var iMin = this.getMinSum();
      var iMax = this.getMaxSum();
      var text = "";
      var sumText = "";
      
      // create a text heading
      if( this.numDigits === "" ){
         text = "CSolutionList.showCompressed(" + this.numDigits + ")";
      }
      else {
         text = this.numDigits + "-digit Solutions";
      }
      
      // make a heading
      pbSudoku.pbNodeOps.appendToNode( parentNode, text, "h3" );
      
      // go through the possible values for this digit (number of digits)
      for( iCount = iMin; iCount <= iMax; iCount++ ){
      
         sumText = "" + (iCount < 10 ? " " : "") + iCount;
         text = "sum: " + sumText + ", solutions: ";
         
         // get an array of the results for this total
         var sol = this.getNum( iCount );
         for( iSol = 0; iSol < sol.length; iSol++ ){
         
            text += sol[ iSol ];
            if( iSol < sol.length - 1 ) { text += ", "; }
         }
         
         // wrap the resulting text in a div element, and add to parent node
         pbSudoku.pbNodeOps.appendToNode( parentNode, text, "div" );
      }
   };
   
   // Also used when looking for a single solution
   // returns array of all solutions that add up to sumDig
   pbSudoku.CSolutionList.prototype.getNum = function( sumDig ){
      
      if( sumDig === null ) { alert ( "CSolutionList.getNum type error" ); }
      var res = [];
      var iCount = 0;

      for( iCount = 0; iCount < this.list.length; iCount++ ){
      
         if( this.list[ iCount ].sum == sumDig ){
         
            // add the CSingleSolution object to the result array
            res.push( this.list[ iCount ].digitList );
            //res[ res.length ] = this.list[ iCount ].digitList;
         }
      }
      
      return res;
   };
   
   // internal function *************************************
   
   // These two functions are almost unnecessary: it is always the case
   // that the first entry is the min and the last is the max
   
   // 0 can never be a valid sum, so it means there are no entries in the list
   pbSudoku.CSolutionList.prototype.getMinSum = function( ){
   
      // get min value stored in list, always the first one
      var iMin = this.list.length ? this.list[ 0 ].sum : 0;
      return iMin;
   };

   // 0 can never be a valid sum, so it means there are no entries in the list
   pbSudoku.CSolutionList.prototype.getMaxSum = function( ){
   
      // get max value stored in list, always the last one
      var iMax = this.list.length ? this.list[ this.list.length-1 ].sum : 0;
      return iMax;
   };

   /*******************************************************************/

   pbSudoku.pbNodeOps = {
   
      deleteChildNodes : function( inobj ){

         var obj = inobj || {};
         
         while ( obj.hasChildNodes() ){
            obj.removeChild( obj.firstChild );
         }
      },
      
      // adds text with an optional elem wrapper
      appendToNode : function( startNode, text, elemWrapper ){
      
         //var debugDiv = document.getElementById( "debugDisplay" );
         if( startNode === null ){
            alert( "Invaid start node in appendToNode" );
            return;
         }

         var newNode = document.createTextNode( text || "" );
         var wrapperElem = null;
         var validElemList = [ "p", "div", "h1", "h2", "h3" ];
         var iCount; 
         
         if( typeof( elemWrapper ) != 'undefined' ){
            for( iCount=0; iCount< validElemList.length; iCount++ ){
               if( elemWrapper == validElemList[ iCount ] ){
               
                  wrapperElem = document.createElement( elemWrapper );
                  startNode.appendChild( wrapperElem );
                  wrapperElem.appendChild( newNode );
                  break;
               }
            }
         } else {
         
            startNode.appendChild( newNode );
         }
      }
   };

})( com.YourFriendPaul );

/***********************************************************************/
/***********************************************************************/
/***********************************************************************/
// anonamous function
// Function not used here
(function(){
   
   // find the text in removeName in the element's class, then delete it
   function removeClass( elem, removeName ){
   
      var classList = elem.className || "";
      var findRx = new RegExp( "\\b" + removeName + "\\b", "g" );
      var res = classList.replace(findRx, " ");
      elem.className = res;
      alert( "\"" + classList + "\" || \"" + res + "\" || \"" + elem.className + "\"");
      
   }
   
   // this is the namespace where we want this available
   var ns = com.YourFriendPaul.pbSudoku;
   
   // these are the functions to be made public
   ns.removeClass = removeClass;
})();
/***********************************************************************/
