/*
2010-09-18T08:23:41-08:00 - Changed alert for bad elements in getElemsFromIds 
                    to reduce number of alert boxes.
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;
        var badList = [];
        
        for( iCount = 0; iCount< idNames.length; iCount++ ){
        
            elem = document.getElementById( idNames[ iCount ] );
            
            if( elem === null ){
            
                badList.push( idNames[ iCount ] );
            
            } else {
            
                // put them in the global id name array
                this.elemList[ idNames[ iCount ] ] = elem;
            }
        }
        
        if( badList.length > 0 ){
        
            elem = "";
            
            for( iCount = 0; iCount< badList.length; iCount++ ){
                elem += badList[ iCount ] + ", ";
            }

            iCount = elem.lastIndexOf( "," );
            elem = elem.slice( 0, iCount );
            elem = "Problem with element list in 'getElemsFromIds'. Not found: " + elem;
            alert( elem );
            throw new Error( 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;
})();
/***********************************************************************/

