Point = function(x,y)
{
	this.x = x;
	this.y = y;	
}

Point.prototype = 
{
	equals:function(point)
	{		
		if ((this.x == point.x) && (this.y == point.y))
		{
			return true;
		}
		
		return false;
	}
}

Direction = function(letter, display, plural, xAdd, yAdd)
{
  this.letter = letter;
  this.display = display;
  this.plural = plural;
  this.xAdd = xAdd;
  this.yAdd = yAdd;
}

Direction.prototype = 
{
  nextDirection:function(directions)
  {
    for (var i = 0; i < directions.length; i++)
    {
      var dir = directions[i];
      if (dir == this.letter) {
        var nextLetter = directions[(i+1) % directions.length];
        return directionFromLetter(nextLetter);
      }
    }
  }
}

ACROSS = new Direction('A', 'across', 'acrosses', 1, 0);
DOWN = new Direction('D', 'down', 'downs', 0, 1);
RIGHT_TO_LEFT = new Direction('L', 'right_to_left', '', -1, 0);
DOWN_TO_UP = new Direction('U', 'down_to_up', '', 0, -1);
DOWN_RIGHT = new Direction('V', 'down_right', '', 1, 1);

DIRECTIONS = new Array(ACROSS, DOWN_RIGHT, DOWN, RIGHT_TO_LEFT, DOWN_TO_UP);
DEFAULT_DIRECTION_LETTERS = new Array('A', 'D');

function directionFromLetter(letter)
{
  for (var i = 0; i < DIRECTIONS.length; i++) {
    var dir = DIRECTIONS[i];
    if (letter == dir.letter) {
      return dir;
    }
  }

  return null;
}

function directionFromWord(word)
{
  for (var i = 0; i < DIRECTIONS.length; i++) {
    var dir = DIRECTIONS[i];
    if (word == dir.display) {
      return dir;
    }
  }

  return null;
}

Rules = function(hash)
{
	this.hash = hash
}

Rules.prototype =
{
	get:function(name, defult)
	{
		if (typeof(this.hash[name]) == "undefined")
		{
			return defult;
		}
		
		return this.hash[name];
	},
   
  set:function(name, value)
  {
    this.hash[name] = value;
  },
  
  setHash:function(h)
  {
    this.hash = h;
  }
}

//TODO: remove?
function showFirstLoginDialog()
{
	var html = "<h4>Welcome to The Endless Crossword</h4>";
	html += "<p>This is a <b>Beta</b>. Please let us know any feedback you have and excuse any problems</p>";
	html += "<p>The Endless Crossword is a huge crossword puzzle (thousands of clues) that you work on collaboratively (everyone on facebook is working on the same puzzle with you)</p>";
	html += "<h5>The Controls</h5>";
	html += "<ul>";
	html += "<li>Arrow keys - move around the puzzle</li>";
	html += "<li>Tab - switch direction (across/down)</li>";
	html += "<li>Drag mouse - move the puzzle around</li>";
	html += "</ul>";
	html += "<h5>What do the different color letters mean?</h5>";
	html += "<ul>";
	html += "<li>Black - correct letter</li>";
	html += "<li>Orange - your pending guess (may be right or wrong)</li>";
	html += "<li>Crossed out - wrong letter that you guessed</li>";
	html += "</ul>";
	html += "<p>Have fun and remember to floss.</p>";
	html += "<input style='margin: 0px 0px 10px 100px;' type='button' value='Start Puzzling' onclick='clueFiller.hideDialog();' class='inputbutton inputaux'/>";
	
	document.getElementById('dialog_content').innerHTML = html;
	
	clueFiller.showDialog();
}

function quoteJS(str)
{
	var s = "";
	for (var i = 0; i < str.length; i++)
	{
		var c = str[i];
		if (c == '\'') {
			s += '\\';
		}
		s += c;
	}
	
	return s;
}


function mkDelegate(obj, method) {
    function delegate() {
        return method.apply(obj, arguments);
    }    
    return delegate;
}


/* BSD3-style license for fixup code from tool-man.org
   we use capturing events where possible for maximum browser beatdown potential :)

Copyright (c) 2005 Tim Taylor Consulting <http://tool-man.org/>

Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included
in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
IN THE SOFTWARE.
*/

fixupEvent = function(event)
{
    if(!event) event = window.event;

    return event;
}

fixupAddListener = function(element, type, func)
{
    if (element.addEventListener) {
         element.addEventListener(type, func, true)
    } else if (element.attachEvent) {
        if (!element._listeners) element._listeners = new Array()
        if (!element._listeners[type]) element._listeners[type] = new Array()
        var workaroundFunc = function() {
            func.apply(element, new Array())
        }
        element._listeners[type][func] = workaroundFunc
        element.attachEvent('on' + type, workaroundFunc)
    }
}

fixupRemoveListener = function(element, type, func)
{
    if (element.removeEventListener) {
        element.removeEventListener(type, func, true)
    } else if (element.detachEvent) {
        if (element._listeners
            && element._listeners[type]
            && element._listeners[type][func])
        {
            element.detachEvent('on' + type, element._listeners[type][func])
        }
    }
}

DragHandler = function(id, handleFunction)
{
    this.handler = handleFunction;
    this.element = document.getElementById(id);
    
    this.mouseDownDelegate = mkDelegate(this, this.handleMouseDown);
    this.mouseMoveDelegate = mkDelegate(this, this.handleMouseMove);
    this.mouseUpDelegate = mkDelegate(this, this.handleMouseUp);
    
    this.element.onmousedown = this.mouseDownDelegate;
}

DragHandler.prototype = 
{
    dragThreshold:5,
    handler:undefined,
    element:undefined,

    isDragging:false,
    eventsRegistered:false,
    startX:0,
    startY:0,
    endX:0,
    endY:0,
    
    handleMouseDown:function(event)
    {
        event = fixupEvent(event);
        
        this.startX = event.clientX;
        this.startY = event.clientY;

        this.isDragging = false;

        if(!this.eventsRegistered)
        {
            fixupAddListener(document, 'mousemove', this.mouseMoveDelegate);
            fixupAddListener(document, 'mouseup', this.mouseUpDelegate);
            this.eventsRegistered = true;
        }
        
        return false;
    },
    
    handleMouseUp:function(event)
    {
        if(this.eventsRegistered)
        {
            fixupRemoveListener(document, 'mousemove', this.mouseMoveDelegate);
            fixupRemoveListener(document, 'mouseup', this.mouseUpDelegate);
            this.eventsRegistered = false;
        }

        event = fixupEvent(event);
        
        this.endX = event.clientX;
        this.endY = event.clientY;

        if(this.isDragging)
        {
            this.handler(this.startX, this.startY, this.endX, this.endY);            
        }

        this.isDragging = false;
        
        return false;
    },
    
    handleMouseMove:function(event)
    {
        event = fixupEvent(event);
        var newX = event.clientX;
        var newY = event.clientY;
        
        if (this.isDragging 
            || (Math.abs(newX - this.startX) + Math.abs(newY - this.startY) > this.dragThreshold))
        {
            this.handler(this.startX, this.startY, newX, newY);
            
            this.startX = newX;
            this.startY = newY;
            this.isDragging = true;
        }
        
        return false;
    },
    
    handleMouseOut:function(event)
    {
        //event = fixupEvent(event);
    }
}


KeyHandler = function()
{
	this.lastKeyTime = new Date();	
	this.lastLetterTime = new Date();
}

KeyHandler.prototype = 
{
    handleKeyDown:function(e)
    {
   		this.handleKey(e);
    },
    
    handleKeyPress:function(e)
    {
    	if (false)
    	{
    		this.handleKey(e);
    	}
    },
    
    handleKey:function(e)
    {	
    	var keynum;
		var keychar;
		var numcheck;

		keyHandler.lastKeyTime = new Date();

		if(window.event) // IE
		{
			keynum = window.event.keyCode
		}
		else if(e.which) // Netscape/Firefox/Opera
		{
			keynum = e.which
		}
		
		if (dialogs.showingVideo)
		{
			dialogs.hideDialog();
		}
		
		switch (keynum)
		{
			case 8: board.handleBackspace(); break;
			case 9: board.handleTab(); break;
			case 13: board.handleEnter(); break;
			case 63234:
			case 37: board.handleArrowPressed("left"); break;
			case 63232:
			case 38: board.handleArrowPressed("up"); break;
			case 63235:
			case 39: board.handleArrowPressed("right"); break;
			case 63233:
			case 40: board.handleArrowPressed("down"); break;
			case 46: board.handleDelete(); break;
			default:
				keychar = String.fromCharCode(keynum).toUpperCase();				
				
				if (keychar == " ")
				{
					board.handleSpace();
				}
				if ((keychar >= "A") && (keychar <= "Z"))
				{ 		
					board.handleLetterTyped(keychar);
				}
				keyHandler.lastLetterTime = new Date();
				break;			
		}
    },
    
    wasLastLetterPressSecsAgo:function(secs)
    {
    	var now = new Date();
    	
    	return (now.getTime() - this.lastLetterTime.getTime())/1000 > secs;
    }
}
keyHandler = new KeyHandler();


Board = function(id, puzzle, isPrint)
{
	//constants
	this.SQUARE_SIZE = 31;

	this.id = id;
	this.data = puzzle;
	this.xOffset = 0;
	this.yOffset = 0;
	this.positionX = 0;
	this.positionY = 0;
	this.absSelectedSpot = new Point(0, 0);
	this.currentDirection = ACROSS;
	this.allowDirSwitching = true;
  this.isPrint = isPrint;
     
  $(id).onselectstart = function () { return false; }
	
	this.width = $(id).getWidth() / this.SQUARE_SIZE | 0;
	this.height = $(id).getHeight() / this.SQUARE_SIZE | 0;
     
	this.dragMgr = new DragHandler(id, mkDelegate(this, this.handleDrag));

	this._reorientTimer = mkDelegate(this, this.reorientBoardTimer);
	setTimeout(this._reorientTimer, 100);
}

Board.prototype = 
{
    getHTML:function()
	{
		this.data.grabData(this.xOffset, this.yOffset, this.xOffset + this.width, this.yOffset + this.height);
		
		//TODO: has dupe below
		var extras = this._calculateExtras();

		var pixelWidth = (this.width + 2) * this.SQUARE_SIZE;
		var pixelHeight = (this.height + 2) * this.SQUARE_SIZE;
		
		var html = "<div id='grid_table' style='position: relative; left: " + extras.x + "px; top: " + extras.y + "px; width: " + pixelWidth + "px; height: " + pixelHeight + "px;'>"; 
		html += "<table cellpadding=0 cellspacing=0>";
	
		var currentClue = this.data.getClue(this.absSelectedSpot, this.currentDirection);
		for (var i = this.yOffset - 1; i < this.yOffset + this.height + 2; i++)
		{
			html += "<tr>";
			
			for (var j = this.xOffset - 1; j < this.xOffset + this.width + 1; j++)
			{
				var absSquare = new Point(j, i);
				
				if (this.data.isUnknown(absSquare))
				{
					html += this._getUnknownSquareHTML(absSquare);
				}
				else if (this.data.isSolid(absSquare))
				{
					html += this._getBlockSquareHTML();
				}
				else
				{				
					html += this._getSquareHTML(currentClue, absSquare);
				}
			}
			
			html += "</tr>";
		}
		
		html += "</table></div>";
		
		return html;
	},
	
	_getUnknownSquareHTML:function(absSquare)
	{
		var html = "<td class='square' class='" + this._getSquareClass(null, absSquare) + "'>";
		html += "<div class='letter'>?</div>";
		html += "</td>";
		
		return html;
	},
	
	_getSquareHTML:function(currentClue, absSquare)
	{		 
		var html = "<td id='" + this._getID(absSquare) + "' class='" + this._getSquareClass(currentClue, absSquare) + "'";
        if (!this.isPrint) html += " onclick='board.handleClick(this, event)'";
        html += ">";

		if (this.data.isClueStart(absSquare))
		{
			html += "<div class='number'>" + this.data.getClueStartNumber(absSquare) + "</div>";
		}	
				
		html += this._getSquareLetterHTML(absSquare);				
		
		html += "</td>";		
		
		return html;
	},
	
	_getSquareLetterHTML:function(absSquare)
	{					
		var guess = this.isPrint ? null : guesses.getGuess(absSquare);
		var clas = "letter";
		var letter = "?";
		var extra = "";
		if (!guess)
		{
			letter = this.data.getLetter(absSquare); 
		}
		else
		{
			letter = guess.letter;
			if (!guess.correct)
			{
				extra = "<div class='wrongx'><img src='/images/wrong.gif'></div>";
				clas += " wrong";
			}
			else
			{
				letter = guess.letter;
				clas += " guess";
			} 
		}
		
		return "<div style='height: 30px; overflow: hidden;'><div class='" + clas + "'>" + letter + "</div>" + extra + "</div>";
	},
	
	_getSquareClass:function(currentClue, absP)
	{
		var guessClass = "";
		if ((!this.isPrint) && (guesses.getGuess(absP)))
		{
			guessClass = " guess";
		}
		
        if (!this.isPrint)
        {
            if (absP.equals(this.absSelectedSpot))
            {
                return "square selected" + guessClass;
            }
		
            if ((currentClue) && (this.data.isWithinClue(currentClue, absP)))
            {
                return "square currentClue" + guessClass;
            }
        }
        
		if (this.data.isWithinThemeClue(absP))
		{
			return "square theme" + guessClass;
		}
		
		if (!this.data.isEnabled(absP))
		{
			return "square disabled";
		}
		
		return "square" + guessClass;
	},
	
	_getBlockSquareHTML:function()
	{
		return "<td class='square solid'>&nbsp;</td>";
	},
	
	_getID:function(absP)
	{
		return absP.x + "x" + absP.y;
	},
	
	handleClick:function(el, event)
	{
		var idparts = el.id.split('x');
		
		var point = new Point(parseInt(idparts[0]), parseInt(idparts[1])); 
		board.handleSquareClicked(point);
	},

	_swapCurrentDirection:function()
	{
		if (!this.allowDirSwitching)
		{
			return;
		}

    var newDir = this.currentDirection;    
    do {
      var newDir = this._oppositeDirection(newDir);      
      if (newDir == this.currentDirection) {        
        return;
      }
    } while (!this.canPlaceCursor(this.absSelectedSpot, newDir));
                      
    this.currentDirection = newDir;		
	},
	
  _oppositeDirection:function(dir)
  {
    //TODO: needs to handle more directions
    return dir.nextDirection(rules.get('directions', DEFAULT_DIRECTION_LETTERS));
  },
   
	handleSquareClicked:function(absP)
	{
		if (this.absSelectedSpot.equals(absP))
		{			
			this._swapCurrentDirection();
		}
		
		if (!this.canPlaceCursor(absP))
		{
			var opp = this._oppositeDirection(this.currentDirection);
			if (!this.canPlaceCursor(absP, opp))
			{
				return;			
			}
			this.currentDirection = opp;
		}
		
		this.absSelectedSpot = absP;
		
		this.forceSelectedCluesVisible();

		this.reDraw();
	},
	
	getClueHTML:function()
	{
		var currentClue = this.data.getClue(this.absSelectedSpot, this.currentDirection);
		if (!currentClue)
		{
			return "";
		}

    this.data.scrollToClue(currentClue); //TODO: somewhere else?
		
		return this.data.getClueHTML(currentClue);
	},

	reDrawClue:function()
	{
		var el = $('clue');
  		if (el)
  		{  		
  			el.innerHTML = this.getClueHTML();
  		}
	},
		
	reDraw:function()
	{
       
		var el = $('grid');
		if (el)
		{  		
  			el.innerHTML = this.getHTML();
		}
  		
  		this.reDrawClue();
  		this.setFocus();
	},
	
	setFocus:function()
	{
		//TODO: move inputbox to middle of puzzle???
		
  	    var el = $('inputBox');
  		if (el)
  		{
  			el.focus();
  		}
  		setTimeout("var el = document.getElementById('inputBox'); if (el) { el.focus(); }",1);				
	},
	
	handleDrag:function(x1, y1, x2, y2)
	{
	    if(this.width >= this.data.width
	        && this.height >= this.data.height)
	    {
	        // no scrolling if the puzzle is too small
	        return;
	    }
	    
		var moveX = (x1 - x2);
		var moveY = (y1 - y2);
		this.positionX += moveX;
		this.positionY += moveY;
		 
		var gridMoveX = Math.round(this.positionX/this.SQUARE_SIZE) - this.xOffset;
		var gridMoveY = Math.round(this.positionY/this.SQUARE_SIZE) - this.yOffset;

		var extras = this._calculateExtras();		
		
		if ((gridMoveX == 0) && (gridMoveY == 0))
		{			
			document.getElementById('grid_table').style.left = extras.x + "px";
			document.getElementById('grid_table').style.top = extras.y + "px";
			return;
		}				
		
		this.xOffset += gridMoveX;
		this.yOffset += gridMoveY;		
				
//		if (this.xOffset < 0)
//		{
//			this.xOffset = 0;
//		}
//		if (this.xOffset > this.data.getWidth() - this.width)
//		{
//			this.xOffset = this.data.getWidth() - this.width;
//		}
//		
//		if (this.yOffset < 0)
//		{
//			this.yOffset = 0;
//		}
//		if (this.yOffset > this.data.getHeight() - this.height)
//		{
//			this.yOffset = this.data.getHeight() - this.height;
//		}
		
		board.reDraw();
	},
	
	_calculateExtras:function()
	{
		var extraX = this.xOffset * this.SQUARE_SIZE - this.positionX - this.SQUARE_SIZE;
		var extraY = this.yOffset * this.SQUARE_SIZE - this.positionY - this.SQUARE_SIZE;
		
		return new Point(extraX, extraY);
	},
	
	handleLetterTyped:function(letter)
	{
		if ((!this.data.isSquareFilled(this.absSelectedSpot)) && (!rules.get('read_only', false)) && (guesses.can_add()))
		{
			guesses.add(this.absSelectedSpot, letter);
		}		
		     
    this.advancePosition(this.currentDirection.xAdd, this.currentDirection.yAdd, false);
		
		board.reDraw();				
	},
	
	handleBackspace:function()
	{
		guesses.removeAt(this.absSelectedSpot);
		
    this.advancePosition(-1 * this.currentDirection.xAdd, -1 * this.currentDirection.yAdd, false);
     
		this.reDraw();		
	},
	
	handleDelete:function()
	{
		guesses.removeAt(this.absSelectedSpot);
		
		this.reDraw();
	},
	
	handleSpace:function()
	{
		guesses.removeAt(this.absSelectedSpot);
		
    this.advancePosition(this.currentDirection.xAdd, this.currentDirection.yAdd, false);
		
		this.reDraw();
	},
	
	handleArrowPressed:function(direction)
	{
		if (direction == "left")
		{
			this.advancePosition(-1, 0, true);
		}
		else if (direction == "up")
		{
			this.advancePosition(0, -1, true);
		}			
		else if (direction == "right")
		{
			this.advancePosition(1, 0, true);
		}
		else if (direction == "down")
		{
			this.advancePosition(0, 1, true);
		}
		
		this.reDraw();
	},
	
	handleTab:function()
	{
		this._swapCurrentDirection();
		this.reDraw();
	},
	
	handleEnter:function()
	{
		var currentClue = this.data.getClue(this.absSelectedSpot, this.currentDirection);
		if (currentClue.video)
		{
			alert('show video');
		}
		else
		{
			guesses.submitGuesses(true);
		}
	},
	
	advancePosition:function(xAdd, yAdd, skipBlocks)
	{
		var newAbsPosition = new Point(this.absSelectedSpot.x + xAdd, this.absSelectedSpot.y + yAdd);
		if (!this.data.isValidPosition(newAbsPosition))
		{
			return;
		}
		
		if (this.data.isSolid(newAbsPosition))
		{
			if (!skipBlocks)
			{
				return;
			}
			
			if (xAdd > 0) xAdd++;
			if (xAdd < 0) xAdd--;
			if (yAdd > 0) yAdd++;
			if (yAdd < 0) yAdd--;

			this.advancePosition(xAdd, yAdd, skipBlocks);
						
			return;
		}
	
		if (!this.canPlaceCursor(newAbsPosition))
		{
			return;
		}
		
		this.absSelectedSpot = newAbsPosition;				
		
		if (!this.isVisible(newAbsPosition))
		{
			this._changeOffset(xAdd, yAdd);
		}

		this.forceSelectedCluesVisible();				
	},
	
	canPlaceCursor:function(pos, dir)
	{
		if (false)
		{
			return true;
		}
		
		if (!dir)
		{
			dir = this.currentDirection;
		}
         
        var clue = this.data.getClue(pos, dir);
        if (!clue) {
          return false;
        }

		return this.data.isEnabledWithDirection(pos, dir);
		
		if ((pos.x == 0) && (pos.y == 0))
		{
			return true;
		}
		
		if ((this.hasLetterOrGuess(pos.x - 1, pos.y)) ||
		    (this.hasLetterOrGuess(pos.x + 1, pos.y)) ||
		    (this.hasLetterOrGuess(pos.x, pos.y - 1)) ||
		    (this.hasLetterOrGuess(pos.x, pos.y + 1)))
		{
			return true;
		}
		
		return false;
	},
	
	hasLetterOrGuess:function(x,y)
	{
		if ((this.data.isSquareFilledWithLetter(x, y)) ||
		    (guesses.hasGuess(new Point(x,y))))
		{
			return true;
		}
		
		return false;
	},
	
	forceSelectedCluesVisible:function()
	{
		this.forceSelectedClueVisible(false);
		this.forceSelectedClueVisible(true);
        this.scrollIntoView(1);
	},
	
	forceSelectedClueVisible:function(direction)
	{
		var clue = this.data.getClue(this.absSelectedSpot, direction);
		if (clue)
		{
			if (!clue.down)
			{
				while (!this.isVisibleX(clue.point))
				{
					if (!this._changeOffset(-1, 0))
					{
						break;
					}
				}
				
				while (!this.isVisibleX(new Point(clue.point.x + clue.length, clue.point.y)))
				{
					if (!this._changeOffset(1, 0))
					{
						break;
					}					
				}
			}
			else
			{
				while (!this.isVisibleY(clue.point))
				{
					if (!this._changeOffset(0, -1))
					{
						break;
					}
				}
				
				while (!this.isVisibleY(new Point(clue.point.x, clue.point.y + clue.length)))
				{
					if (!this._changeOffset(0, 1))
					{
						break;
					}
				}
			}
		}
	},
	
	isVisible:function(absP)
	{
		if (!this.isVisibleX(absP))
		{
			return false;
		}
		
		return this.isVisibleY(absP);
	},
	
	isVisibleX:function(absP)
	{
		if (absP.x < this.xOffset + 1) return false;
		if (absP.x > this.xOffset + this.width - 1) return false;
		
		return true;
	},
	
	isVisibleY:function(absP)
	{
		if (absP.y < this.yOffset + 1) return false;
		if (absP.y > this.yOffset + this.height -1) return false;
		
		return true;
	},
	
	_changeOffset:function(changeX, changeY)
	{
		this.xOffset += changeX;
		this.positionX += (changeX * this.SQUARE_SIZE);

		this.yOffset += changeY;
		this.positionY += (changeY * this.SQUARE_SIZE);

		if ((this.xOffset < 0) || (this.xOffset > this.data.getWidth()))
		{
			return false;
		}
					
		if ((this.yOffset < 0) || (this.yOffset > this.data.getHeight()))
		{
			return false;
		}				
		
		return true;	
	},

	validateSelectedPosition:function()
	{
		var selectedX = this.absSelectedSpot.x;
		var selectedY = this.absSelectedSpot.y;
		while ((selectedX < this.data.getWidth()) && (this.data.isSolid(new Point(selectedX, selectedY))))
		{
			selectedX++;
		}
		
		this.absSelectedSpot = new Point(selectedX, selectedY);
	},

	centerGrid:function()
	{
		var offx = Math.round(this.data.getWidth() / 2 - this.width/2);
	    var offy = Math.round(this.data.getHeight() / 2 - this.height/2);

	    this.setGridOffset(offx, offy);
	},
	
	setGridOffset:function(x, y)
	{
		this.xOffset = x;
		this.yOffset = y;
		this.positionX = x * this.SQUARE_SIZE - 15;
		this.positionY = y * this.SQUARE_SIZE - 15;
        this.scrollIntoView(1);
		
		var selectedX = x + Math.round(this.width/2);
		var selectedY = y + Math.round(this.height/2);
		while ((selectedX < this.data.getWidth()) && (this.data.isSolid(new Point(selectedX, selectedY))))
		{
			selectedX++;
		}
		
		this.absSelectedSpot = new Point(selectedX, selectedY);		        
	},
	
	//TODO: combine with above
	jumpTo:function(x, y, direction, dontscroll)
	{
		var selectedX = x;
		var selectedY = y;
		
		this.absSelectedSpot = new Point(selectedX, selectedY);		

		if (direction)
		{
			this.currentDirection = directionFromWord(direction);
		}
	
		if (!dontscroll)
		{	
			this.xOffset = x - Math.round(this.width/2);
			this.yOffset = y - Math.round(this.height/2);
			this.positionX = this.xOffset * this.SQUARE_SIZE;
			this.positionY = this.yOffset * this.SQUARE_SIZE;                        
      this.scrollIntoView(1.0, true);
		}
        		
		this.reDraw();
	},
	
	disallowSwitchDirections:function()
	{
		this.allowDirSwitching = false;
	},
     
    scrollIntoView:function(factor, skipRedraw)
    {
      var boardWidth = $(this.id).getWidth();
      var boardHeight = $(this.id).getHeight();
      var puzzleWidth = this.data.getWidth() * this.SQUARE_SIZE;
      var puzzleHeight = this.data.getHeight() * this.SQUARE_SIZE;
      
      var paddingX = (boardWidth - puzzleWidth)/2;      
      if (paddingX < 10) { paddingX = 15; }
      
      var paddingY = (boardHeight - puzzleHeight)/2;      
      if (paddingY < 10) { paddingY = 15; }
       
      var changed = false;

      if (this.positionX < -1 * paddingX - 1)
      {
        this.positionX -= (this.positionX + paddingX)/factor;
        changed = true;
      }
            
      if (this.positionX + boardWidth > puzzleWidth + paddingX + 1)
      {
        this.positionX -= (this.positionX + boardWidth - (puzzleWidth + paddingX))/factor;
        changed = true;
      }
      
      if (this.positionY < -1 * paddingY - 1)
      {
        this.positionY -= (this.positionY + paddingY)/factor;        
        changed = true;
      }
      
      if (this.positionY + boardHeight > puzzleHeight + paddingY + 1)
      {
        this.positionY -= (this.positionY + boardHeight - (puzzleHeight + paddingY))/factor;
        changed = true;
      }

      if (changed) {
        this.xOffset = this.positionX / this.SQUARE_SIZE | 0;
        this.yOffset = this.positionY / this.SQUARE_SIZE | 0;
        if (!skipRedraw) {
          this.reDraw();
        }
      }
      
      return changed;
    },
     
    reorientBoardTimer:function()
	{
	    var timer = 150; // check every 150-ish ms
	    
	    if(!this.dragMgr.isDragging)
	    {
	        if(this.scrollIntoView(2, false))
	            timer = 30; // faster update while scrolling
	    }
	    
	    setTimeout(this._reorientTimer, timer);
	}

		
}

//TODO: just make data a global

Dialogs = function()
{
	this.showingVideo = false;
	this.weekdays = new Array("Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday");	
}

Dialogs.prototype = 
{
	showDialog:function()
	{		
    var dialog = $('dynamic_dialog');
    
    //if can find grid put in the middle of it
    var grid = $('grid');
    var clist = $('theme_clue_list');

    if (grid) {
      dialog.style.left = (grid.offsetLeft + 25) + "px";
      dialog.style.top = (grid.offsetTop + 50) + "px";
    } else if (clist) {
      dialog.style.left = (clist.offsetLeft + 75) + "px";
      dialog.style.top = (clist.offsetTop + 75) + "px";
    } else {
      dialog.style.top = "150px";      
    }
    
    dialog.style.display = "block";
	},

	isDialogVisible:function()
	{
		return document.getElementById('dynamic_dialog').style.display == "block";
	},
	
	hideDialog:function()
	{
		$('dynamic_dialog').style.display = "none";
		if ((typeof(board) != "undefined") && (board)) {
			board.setFocus();
		}
		this.showingVideo = false;
	},
    
    //
    // Dialogs
    //
    
    fixDifficulty:function(x, y, dir)
	{
		this._showDifficultyDialog("dialogs.updatePuzzleClueDifficulty("+ x + "," + y + ",'" + dir + "')");
	},

	editDifficulty:function(clue_id)
	{
		this._showDifficultyDialog("dialogs.updateClueDifficulty("+ clue_id + ")");
	},
	
	_showDifficultyDialog:function(updateFunc)
	{
		var html = "<h3>What is the proper difficulty of this clue?</h3>";
		
		html += "<select id='difficulty'>";
		for (var i = 0; i < this.weekdays.length; i++)
		{			
			html += "<option value='" + this.weekdays[i] + "'>" + this.weekdays[i] + "</option>";
		}
		html += "</select>";
		html += "<input type='button' class='inputsubmit' onClick=\"" + updateFunc + "\" value='Fix'>";
		html += "<input type='button' class='inputsubmit' value='Close' onclick='dialogs.hideDialog();'>";		
		
		
		$('dialog_content').innerHTML = html;
				
		dialogs.showDialog();
	},
	
 	updatePuzzleClueDifficulty:function(x, y, dir)
	{
		var difficulty = document.getElementById('difficulty').value;
	
		API.updatePuzzleClueDifficulty(challengeID, x, y, dir, difficulty, this.onUpdatedDifficulty);
		if (data)
		{
			clue = this.getClue(new Point(x, y), dir != "across");
			clue.difficulty = difficulty;
			board.reDrawClue();	 
		}
		
		//urchinTracker('/endlesscrossword/ajax/updateDifficulty');
	},
	
	updateClueDifficulty:function(id)
	{
		var difficulty = $('difficulty').value;
	
		API.updateClueDifficulty(id, difficulty, this.onUpdatedDifficulty);
	},
	
    onUpdatedDifficulty:function()
	{
		dialogs.hideDialog();
	},
	
	editPuzzleClueCategory:function(x, y, dir)
	{
		var clue = puzzle.getClue(new Point(x,y), dir == "down");				
		var word = puzzle.getClueSolution(clue);

		this._showCategorizerDialog(word, clue.text, "dialogs.updatePuzzleClueCategory( "+ x + "," + y + ",'" + dir + "'");
	},

	editClueCategory:function(word, text, clue_id)
	{
		this._showCatagorizerDialog(word, text, "dialogs.updateClueCategory("+ clue_id + ")");
	},
	    
	_showCatagorizerDialog:function(word, text, updateFunc)
	{
		html = "<div style='padding: 0px 10px 10px'><h4>Categorize a Clue</h4>";		
		html += "<p>Can you put this clue in a category? ";
		html += "<a onClick=\"document.getElementById('why').style.display = 'block'\">Why?</a>";
		html += "<span id='why' style='display:none'>We are attempting to categorize clues so that more customized puzzles can be made (puzzle with no french word clues, puzzle that has a lot of geography clues, etc.)</span>";
		html += "</p>";
		html += "<div style='padding-left: 30px'><div style='padding: 15px 0px; font-size: 14px;'>" + text + ": " + word + "</div>";
		html += "<div style='float: left'><div>";
		html += "<select id='category'>";
		for (var i = 0; i < CLUE_CATEGORIES.length; i++)
		{
			var cat = CLUE_CATEGORIES[i];
			html += "<option value='" + cat + "'>" + cat + "</option>";
		}
		html += "</select></div>";
		html += "<div style='padding: 5px 0px;'><input type='button' class='inputsubmit' value='Categorize Clue' onClick=\"" + updateFunc + "\"></div></div>";
		html += "<div style='float: right'>";
		html += "<INPUT TYPE=CHECKBOX name='nomore' id='nomore'>Don't ask again<br>";
		html += "<input type='button' class='inputsubmit' value='Close' onclick='dialogs.hideDialog();'></div></div>";		

		$('dialog_content').innerHTML = html;
				
		dialogs.showDialog();
	},
	
	updateClueCategory:function(id)
	{
		var category = $('category').value;
	
		API.updateClueCategory(id, category, this.onUpdatedDifficulty);
	},
	
    onUpdatedCategory:function()
	{
		dialogs.hideDialog();
	},

	editClueType:function(word, text, clue_id)
	{
		html = "<div style='padding: 0px 10px 10px'><h4>Categorize a Clue</h4>";		
		html += "<p>Please enter the type for this clue? ";
		html += "</p>";
		html += "<div style='padding-left: 30px'><div style='padding: 15px 0px; font-size: 14px;'>" + text + ": " + word + "</div>";
		html += "<div style='float: left'><div>";
		html += "<select id='type'>";
		for (var i = 0; i < CLUE_TYPES.length; i++)
		{
			var t = CLUE_TYPES[i];
			html += "<option value='" + t + "'>" + t + "</option>";
		}
		html += "</select></div>";
		html += "<div style='padding: 5px 0px;'><input type='button' class='inputsubmit' value='Typize Clue' onClick=\"dialogs.updateClueType("+ clue_id + ")\"></div></div>";
		html += "<div style='float: right'>";
		html += "<input type='button' class='inputsubmit' value='Close' onclick='dialogs.hideDialog();'></div></div>";		

		$('dialog_content').innerHTML = html;
				
		dialogs.showDialog();
	},

	updateClueType:function(id)
	{
		var type = $('type').value;
	
		API.updateClueType(id, type, this.onUpdatedDifficulty);
	},
	
    onUpdatedType:function()
	{
		dialogs.hideDialog();
	},

  showPuzzleComplete:function(congrats)
	{
    if (!congrats) {
      congrats = "Congratulations! You completed the puzzle!"; 
    }
		var html = "<div style='padding: 0px 10px 10px'><h4>Puzzle Complete</h4>";		
		html += "<p>" + congrats + "</p>";
    
//    if (show_input_name) {
//      html += "<p>Please enter your name for the high scores page</p>";
//      html += "<p>First name: <input type='text' name='first_name'><p>";
//    }
    
		html += "<div style='float: right'>";
		html += "<input type='button' class='inputsubmit' value='Close' onclick='dialogs.hideDialog();'></div></div>";		

		$('dialog_content').innerHTML = html;
		
		dialogs.showDialog();
	},
	
	editClueText:function(oldText, clue_id)
	{
		var html = "<h3>Enter a better clue</h3>";
		
		html += "<div><input type='text' value=\"" + oldText + "\" id='clue_text'></div>";
		html += "<input type='button' class='inputsubmit' value='Update' onClick=\"dialogs.updateText(" + clue_id + ")\">";
		html += "<input type='button' class='inputsubmit' value='Close' onclick='dialogs.hideDialog();'>";		
		
		$('dialog_content').innerHTML = html;
				
		dialogs.showDialog();
	},
	
	updateText:function(clue_id)
	{
		var newText = $('clue_text').value;
	
		API.updateClueTextByClueID(clue_id, newText, this.onUpdatedText);
	},
	
    onUpdatedText:function()
	{
		dialogs.hideDialog();
	},
	
	showVideoClue:function(video_id, text)
	{
		var html = "<div style='padding: 0px 10px 10px'><h4>Video Clue</h4>";		
		html += "<div style='float: left';>";
        html += "<img id='video_spinner' style='position:relative;left: 100px; top: 100px;z-index: 2;' src='/images/video_spinner.gif'/>";
		html += "<div id='videospot' style='z-index: 3'></div>";
		html += "<p>" + text + "</p>";
		html += "<div style='float: right'>";
		html += "<input type='button' class='inputsubmit' value='Close' onclick='dialogs.hideDialog();'></div></div>";		
		html += "</div>";
		
		$('dialog_content').innerHTML = html;
		$('dynamic_dialog').style.left = "65px";
		$('dynamic_dialog').style.top = "50px";
		$('dynamic_dialog').style.width = "550px";
		
		dialogs.showDialog();
		this.showingVideo = true;
		
        setTimeout(dialogs.hideSpinner, 50);	
         
		var flashVars = {clip:video_id, autoPlay:true, format:"f4v",logo:"http://endlesscrossword.com/images/1x1.png"};
      	var params =  {allowScriptAccess:"always", allowFullScreen:true ,base:"http://static.thoughtequity.com/video/etc/"};
      	var attributes = {};
      	swfobject.embedSWF("http://static.thoughtequity.com/video/etc/TePlayer.swf", "videospot", 480, 270, "9.0.0", null, flashVars, params, attributes);
	},
   
  showPhotoClue:function(img_url, text, attribution)
	{
		var html = "<div style='padding: 0px 10px 10px'><h4>Photo Clue</h4>";		
		html += "<div style='float: left';>";
		html += "<div><img src='" + img_url+"'></div>";
    html += "<div style='padding: 5px 0px;'>";
		html += "  <p style='float:left'>" + text + "</p>";
    if (attribution) {
      html += "  <div style='float:right;font-size: 9px;'>" + attribution + "</div>";
    }
    html += "  <div class='clear'></div>";
    html += "</div>";
		html += "<div style='float: right'>";
		html += "<input type='button' class='inputsubmit' value='Close' onclick='dialogs.hideDialog();'></div></div>";		
		html += "</div>";
		
		$('dialog_content').innerHTML = html;
		$('dynamic_dialog').style.left = "65px";
		$('dynamic_dialog').style.top = "50px";
		$('dynamic_dialog').style.width = "550px";
		
		dialogs.showDialog();
		this.showingVideo = true;         
	},
     
    hideSpinner:function()
    {
      var el = $('videospot');
      
      if ((el) && (el.tagName == 'OBJECT')) {
          $('video_spinner').style.display = 'none';
      } else {
          setTimeout(dialogs.hideSpinner, 50);
      }
    },
	
	showAddClueDialog:function(button_text, word, clue, media_link, attribution, extra_params, dest)
	{
    if (!word) { word = ''; }
    if (!clue) { clue = ''; }
    if (!extra_params) { extra_params = ''; }
    if (!media_link) { media_link = ''; }
    if (!attribution) { attribution = ''; }
    if (!dest) { dest = 'updater_div'; }
    
    //media check
    var none_checked = '';
    var picture_checked = '';
    var video_checked = '';
    if (media_link.match("thoughtequity://")) {
      video_checked = "checked='true'";
    } else if (media_link.match("flickr.com")) {
      picture_checked = "checked='true'";
    } else {
      none_checked = "checked='true'";
    }
    
    var is_edit = true;
    var word_disabled = "disabled";
    if (extra_params == '') { 
      is_edit = false; 
      word_disabled = '';
    }
    
		var html = "";    
    html += "<form id='new_clue_form' method='get'>";    
    html += "<input type='hidden' id='dialog_media' value='" + media_link + "'>";    
    html += "<input type='hidden' id='dialog_attribution' value=''>";
    html += "<input type='hidden' id='extra_params' value='" + extra_params + "'>";
    
    html += "<a onclick='dialogs.hideDialog();' id='dialog_cancel'>&nbsp;</a>";
    if (is_edit) {
      html += "<h4>Edit Clue</h4>";
    } else {      
      html += "<h4>Submit a New Clue</h4>";
    }
    		
    html += "<div id='dialog_buttons'>";    
		html += "<input class='button enabled' type='button' onclick='dialogs.addClue(" + dest.inspect(true) + ")' value='" + button_text + "' name='add' id='add_clue' class='inputsubmit'/>";						
    html += "</div>";
    html += "<div class='clear'></div>";
    
    html += "<div id='dialog_body'>"; 
		html += "  <div class='dialog_line'>";
		html += "    <div class='dialog_label'>Word:</div>";
    html += "	   <div class='dialog_input'><input id='dialog_word' class='inputtext' type='text' style='width:100px' value='" + word + "' "+word_disabled+"></input></div>";
		html += "    <div class='clear'></div>";
    html += "  </div>";
		
    html += "  <div class='dialog_line'>";
		html += "	   <div class='dialog_label'>Clue:</div>";
		html += "    <div class='dialog_input'><input id='dialog_clue' class='inputtext' type='text' style='width:307px' value='" + clue + "'></div>";
    html += "    <div class='clear'></div>";
		html += "  </div>";
     
		html += "  <div class='dialog_line'>";         
		html += "	   <div class='dialog_label'>Media:</div>";
    html += "    <div class='dialog_input'>";
    html += "      <input id='media_radio_none' type='radio' name='media' onchange='dialogs.mediaChange();' value='none' "+none_checked+"/> None";
    html += "      <input id='media_radio_picture' type='radio' name='media' onchange='dialogs.mediaChange();' value='picture' "+picture_checked+"/> Picture";
//    html += "      <input id='media_radio_video' type='radio' name='media' onchange='dialogs.mediaChange();' value='video' "+video_checked+"/> Video";
    html += "    </div>";
    html += "    <div class='clear'></div>";
    html += "  </div>"
    
    //preview of media
    html += "  <div id='image_display' style='display:none'>";
    html += "    <div id='current_image'></div>";
    html += "    <div id='change_image'><input class='button' type='button' value='Change' onclick=\"$('dialog_media').value=''; dialogs.mediaChange();\"></div>";
    html += "      <div class='clear'></div>";
    html += "  </div>";
    
    //image search
    html += "  <div id='image_search' style='display:none'>";
    html += "    <div clas='dialog_line'>";
    html += "      <div class='dialog_label'>Keyword:</div>";
    html += "      <div class='dialog_input' id='media_search_tag'><input class='inputtext' type='text' id='tag' value='" + word + "'></div>";
    html += "      <input class='button' type='button' onclick='dialogs.mediaSearch()' value='Search'/>";				    
    html += "      <div class='clear'></div>";
		html += "    </div>";
    html += "    <div id='media_results'></div>";
    html += "  </div>"
		html += "</div>";
		html += "</form>";
    
    $('dynamic_dialog').style.width = "360px";
		$('dialog_content').innerHTML = html;

		dialogs.showDialog();
    
    $('dialog_attribution').value = attribution; //couldn't get quoting right
    dialogs.mediaChange();
	},
   
  mediaChange:function()
  {
    var r = $('media_radio_picture');
    if (r.checked) {
      var val = $('dialog_media').value;
      if (val != '') {
        $('current_image').innerHTML = "<img width='200' src='" + val + "'>";
        $('image_display').style.display = 'block';
        $('image_search').style.display = 'none';
      } else {
        $('image_display').style.display = 'none';
        $('image_search').style.display = 'block';
      }
    } else {
      $('dialog_media').value = '';
      $('dialog_attribution').value = '';
      $('image_display').style.display = 'none';      
      $('image_search').style.display = 'none';
    }
  },
   
   mediaSearch:function()
   {
     var tags = $('tag').value;    	
     var params = "tags=" + tags;
     
     $('media_results').innerHTML = "<img src='/images/loader.gif'/>"
     
     new Ajax.Updater('media_results', '/admin/media_search?' + params, {asynchronous:true, evalScripts:true});
   },
   
   mediaSet:function(url, attribution)
   {
     $('dialog_media').value = url;
     $('dialog_attribution').value = attribution;
     $('media_results').innerHTML = '';
     dialogs.mediaChange();
   },
	
	addClue:function(dest)
	{
    	var word = $('dialog_word').value;
    	var clue = $('dialog_clue').value;
    	var media = $('dialog_media').value;
      var attribution = $('dialog_attribution').value;
    	var extra_params = $('extra_params').value;
       
      var params = "word=" + word;
      params += "&clue=" + escape(clue);
      params += "&media=" + media;
      params += "&attribution=" + escape(attribution);
      params += "&" + extra_params;
         
      $('dialog_buttons').innerHTML = "<img src='/images/loader.gif'>";
         
      new Ajax.Updater(dest, '/build/add_clue?' + params, {asynchronous:true, evalScripts:true});
	},
	
	recentCluePicker:function()
	{
		var html = "<h4>Your Recently Added Clues</h4>";
		
		recentClues.each(function(clue) { 
			html += "<div>";
			html += "<a href=\"javascript:dialogs.addThemeClue('" + clue.word + "','" + quoteJS(clue.clue) + "')\">" + clue.word + " - " + clue.clue + "</a>";
			html += "</div>";
		});
		
		html += "<div class='dialog_buttonline clearfix'>";
		html += "<input type='button' value='Cancel' name='close' id='close' onclick='dialogs.hideDialog();' class='inputbutton inputaux'/></div>";
		html += "</div>";
		html += "</form>";
		
		$('dialog_content').innerHTML = html;

		dialogs.showDialog();
	},
	
	addThemeClue:function(word,clue)
	{
		var c = new Object;
		c.word = word;
		c.clue = clue;
		themeWords.push(c);
    drawThemeWords();
		
		dialogs.hideDialog();    
	},
	
    onAddedClue:function(newClue)
    {
    	themeWords.push(newClue);
    	drawThemeWords();
		
		dialogs.hideDialog();    
    }
	
}
dialogs = new Dialogs();


ClueFiller = function()
{
	this.offset = 0;
	this.list = new Array();
}

ClueFiller.prototype = 
{
    refresh:function()
    {
        if (!$('clueless_words')) {
          return;
        }
        var params = "";
        if (sessionID) { params += "&s_id=" + sessionID; }
        
        new Ajax.Updater('puzzle_data', '/puzzle/some_words?' + params, {asynchronous:true, evalScripts:true});

//		API.getWordsNeedingClues(this.onReceivedWordList);    	
    },
    
    next:function()
    {
    	this.offset++;
    	if (this.offset >= this.list.length)
    	{
    		this.refresh();
    		return;
    	}
    	
    	this.showWord();
    },
    
    onReceivedWordList:function(list)
    {
    	clueFiller.offset = 0;
    	clueFiller.list = list;
    	clueFiller.showWord();
    },
	
	showFillerDialog:function(heading)
	{
		if (!heading)
		{
			heading = "Please enter a clue for the given word";
		}
		
		var html = "<h4>Submit a New Clue</h4>";
		html += "<p>" + heading + ":</p>";
		html += "<form id='rsvp_form' name='rsvp_form' action='events.php' method='get''>";
		html += "<div class='dialog_line clearfix'>";
		html += "   <div class='dialog_label'>Word:</div>";
		html += "	<div class='dialog_input'><input class='inputtext' type='text' id='cluefiller_word' value=''></input></div>";
		//html += "	<span style='padding-left: 50px;'><a href='javascript:clueFiller.next()'>more</a></div></span>";
        html += "<div class='clear'></div>";
		html += "</div>";
		html += "<div class='dialog_line clearfix'>";
		html += "	<div class='dialog_label'>Clue:</div>";
		html += "   <div class='dialog_input'><input class='inputtext' type='text' id='clue_textbox'></div>";
		html += "</div>";
		html += "<div class='dialog_buttonline clearfix'>";
		html += "<input type='button' onclick='clueFiller.submitClue()' value='Add' name='add' id='add_clue' class='inputsubmit'/>";				
		html += "<input type='button' value='Close' name='close' id='close' onclick='dialogs.hideDialog();' class='inputsubmit'/></div>";
		html += "</div>";
		html += "</form>";
		
		document.getElementById('dialog_content').innerHTML = html;

		this.showWord();		
		dialogs.showDialog();
	},
	
    showDialogWithWord:function(word)
    {
    	this.showFillerDialog('Please enter a better clue for this word.');
    	document.getElementById('cluefiller_word').value = word;
    },
    
    showWord:function()
    {
    	var html = "<div>Do you have a clue for " + this.list[this.offset] + "?</div>";
    	html += "<div style='float: right'><span><a href=\"javascript:clueFiller.showFillerDialog()\">Yes</a></span>";
    	html += "&middot;<span><a href='javascript:clueFiller.next()'>No</a></span></div><div style='clear: both;'></div>";
    	
    	document.getElementById('clueless_words').innerHTML = html;
    	
    	var word = document.getElementById('cluefiller_word');
    	if (word)
    	{
    		word.value = this.list[this.offset];
    	}
    	var textbox = document.getElementById('clue_textbox');
    	if (textbox)
    	{
    		textbox.value = "";
    	}
    },
    
    submitClue:function()
    {
    	var word = document.getElementById('cluefiller_word').value;
    	var clue = document.getElementById('clue_textbox').value;
    	
        var params = "?word=" + word + "&clue=" + clue;
         
        new Ajax.Updater('puzzle_data', '/puzzle/new_clue' + params, {asynchronous:true, evalScripts:true});
    	//API.submitNewClue(challengeID, word, clue, this.onSubmitted);
    	
    	//this.list.splice(this.offset, 1);
    	
    	this.next();
    },
    
    onSubmitted:function()
    {
    	//TODO: say thank you
    }
}
clueFiller = new ClueFiller();



Guesses = function()
{
	this.list = new Array();
	
	this.conversations = new Array();
	this.completions = new Array();
	
	this.GUESSES_TIME = 15 * 1000;	
	this.SUBMIT_AFTER_TIME = 15 * 1000;

  setTimeout(this.submitGuessesTimer, this.GUESSES_TIME);	
  //API.getRecentClueCompletions(challengeID, this.onReceivedRecentClueCompletions);	
}

Guesses.prototype = 
{
	add:function(p, letter)
	{
		this.removeAt(p);
		
		var guess = new Object;
		guess.x = p.x;
		guess.y = p.y;
		guess.letter = letter;		
		guess.submitted = false;
		guess.correct = true; //TODO: not really but board needs
		guess.enteredTime = new Date();
		
		this.list.push(guess);
    
    this._displayGuesses();		
    
    if (rules.get('submit_instantly', false)) {
      guesses.submitGuesses(true);								
    }
	},
	
	removeAt:function(absP)
	{
		//TODO: combine with other?
		for (var i = 0; i < this.list.length; i++)
		{
			var guess = this.list[i];
			
			if ((guess.x == absP.x) && (guess.y == absP.y))
			{
				this.list.splice(i, 1);
				this._displayGuesses();
				return;
			}
		}		
	},
   
  can_add:function()
  {
    if (rules.get('moves_per_turn', 0) <= 0)
    {
      return true;
    }
    
    var left = rules.get('moves_per_turn') - this._countPending();
    return (left > 0);
  },
	
	_displayGuesses:function()
	{
		var el = $('pending_guesses');
		if (el)
		{
			var cnt = this._countPending();
			var html = cnt + " letter";
			if (cnt != 1) html += "s";
       
      if (rules.get('moves_per_turn', 0) != 0)
      {
        var left = rules.get('moves_per_turn') - this._countPending();
        html += "<br/><em>" + left + " guesses left</em>";
      }
			
			el.innerHTML = html;
		}
	},
	
	_countPending:function()
	{
		return this._getPendingList().length;
	},
	
	_getPendingList:function()
	{
		var pending = new Array();
		for (var i = 0; i < this.list.length; i++)
		{
			if (this.list[i].submitted == false)
			{
				pending.push(this.list[i]);
			}
		}
		
		return pending;
	},
	
	_getToSubmitList:function()
	{
		var toSubmit = new Array();
		var now = new Date();
		
		for (var i = 0; i < this.list.length; i++)
		{
			var guess = this.list[i];
			var timeDiff = now.getTime() - guess.enteredTime.getTime();
			
			if ((guess.submitted == false) && (timeDiff > this.SUBMIT_AFTER_TIME))
			{
				toSubmit.push(this.list[i]);
			}
		}
		
		return toSubmit;
	},
	
	getGuess:function(absP)
	{
		for (var i = 0; i < this.list.length; i++)
		{	
			var guess = this.list[i];
			if ((guess.x == absP.x) && (guess.y == absP.y))			
			{				
				return guess;
			}
		}
		
		return null;
	},

	hasGuess:function(p)
	{
		return (this.getGuess(p) != null);
	},
	
	submitGuessesTimer:function()
	{
		var auto = $('autosubmit');
		if ((auto) && (auto.checked))
		{
			guesses.submitGuesses(false);
		}
    else
    {
        guesses.keepAlive();
    }

//		if ((keyHandler.wasLastLetterPressSecsAgo(15)) && (!dialogs.isDialogVisible()) && (canAsk))
//		{
//			if ((guesses.lastSubmittedGuesses) && (!isFramed))
//			{
//				data.showClueTagger();
//			}
//		}
	
		guesses.reDrawCompletions();
		
		setTimeout(guesses.submitGuessesTimer, guesses.GUESSES_TIME);
	},
	
	submitGuesses:function(force)
	{
		var toSubmitList;
		if (force)
		{
			toSubmitList = this._getPendingList();
		}
		else
		{
			toSubmitList = this._getToSubmitList();
		}
		
		if (toSubmitList.length > 0)
		{
        var gs = '';
        for (var i = 0; i < toSubmitList.length; i++) {
          var g = toSubmitList[i];
          if (i > 0) { 
            gs += '-';
          }
          gs += g.x + "," + g.y + "," + g.letter;
        }
        var params = 'challenge_id=' + challengeID;
        if (sessionID) { params += "&s_id=" + sessionID; }
        params += '&guesses=' + gs;

        new Ajax.Updater('puzzle_data', '/puzzle/guess?' + params, {asynchronous:true, evalScripts:true});
        
			this._markAllSubmitted(toSubmitList);
			
      var submit_btn = $('submit_guesses_button');
      if (submit_btn) {
			  submit_btn.innerHTML = "<img src='/images/loader.gif'>";
      }
			
			//urchinTracker('/endlesscrossword/ajax/submitGuesses');
		}
    else
    {
        this.keepAlive();
    }
	},
    
    keepAlive:function()
    {
      if (puzzleID == 1032) {
        return;
      }
        var params = "challenge_id=" + challengeID;
        if(sessionID) { params += "&s_id=" + sessionID; }
        new Ajax.Updater('keepalive', '/puzzle/keepalive?' + params, {asynchronous:true, evalScripts:true});
    },

	getHint:function()
	{
		var pos = board.absSelectedSpot;
		API.getHint(challengeID, pos.x, pos.y, board.currentDirection, this.onGotGuessesResponses);			
	},
		
	_markAllSubmitted:function(list)
	{
		for (var i = 0; i < list.length; i++)
		{
			list[i].submitted = true;
		}
	},
	
	//TODO: pass object to ourselves to DWR so don't get messy naming crap
	onGotGuessesResponses:function(results)
	{
    var submit_btn = $('submit_guesses_button');
    if (submit_btn) {
		  submit_btn.innerHTML = "<input type='button' class='inputsubmit' onClick='guesses.submitGuesses(true)' value='Submit'>";
    }
		
		for (var i = 0; i < results.guesses.length; i++)
		{
			var guess = results.guesses[i];			
			if (guess.correct)
			{
				board.data.setLetter(new Point(guess.x, guess.y), guess.letter);
				guesses.removeAt(new Point(guess.x, guess.y));
			}
			else
			{				
				var g = guesses.getGuess(new Point(guess.x, guess.y));
				g.correct = false;
			}
		}
		
		guesses._displayGuesses();
		board.reDraw();
		
    if (results.scores) {
      puzzle.showScores(results.scores);
    } else {
      puzzle.showScore(results.score);
    }
    
		guesses.lastSubmittedGuesses = new Date();
	},
	
	onReceivedRecentClueCompletions:function(completions)
	{
		guesses.completions = completions;
		guesses.reDrawCompletions();
	},
	
	reDrawCompletions:function()
	{
		var html = guesses.getCompletionsHTML(this.completions);
		
		var el = document.getElementById('recent_completions')
		if (el)
		{
			el.innerHTML = html;
		}
	},
	
	reDrawConversations:function()
	{
		var html = guesses.getCompletionsHTML(this.conversations);
		
		document.getElementById('conversations_box').innerHTML = html;
	},
	
	onReverseAjaxReceiveCompletion:function(completion)
	{
		if (completion.puzzleID != puzzleID)
		{
			return;
		}
		guesses.addCompletion(completion);
		guesses.reDrawCompletions();
		
		board.reDraw();
	},
	
	onReverseAjaxReceiveShout:function(shout)
	{
		guesses.updatePuzzleCompletePercent(shout.puzzlePercentage);
		
		guesses.addCompletion(shout);
		guesses.reDrawCompletions();
		
		//guesses.conversations.unshift(shout);		
		//guesses.reDrawConversations();
	},
	
	onReverseAjaxReceiveLogin:function(login)
	{
		guesses.addCompletion(login);
		guesses.reDrawCompletions();
	},
	
	onReverseAjaxReceiveNewClue:function(newClue)
	{
		guesses.addCompletion(newClue);
		guesses.reDrawCompletions();
	},
	
	submitShout:function()
	{
		var el = document.getElementById('shout_text');
		var text = el.value;
		API.say(challengeID, board.absSelectedSpot.x, board.absSelectedSpot.y, board.currentDirection, text, this.onSubmittedShout);
		el.value = "";
	},
	
	onSubmittedShout:function()
	{
		//nothing to do
	},

	addCompletion:function(completion)
	{
		this.updatePuzzleCompletePercent(completion.puzzlePercentage);
		
		guesses.completions.unshift(completion);
		if (completion.type == "completion")
		{
			puzzle.updateClueSolver(completion.clue, completion.who);
			puzzle.updateClueSolution(completion.clue, completion.word);
		}		
	},
	
	updatePuzzleCompletePercent:function(perc)
	{
		var complete = $('percent_complete');
		if (complete)
		{
			complete.innerHTML = perc;
			
			if ((!complete.done) && (perc == "100"))
			{
				complete.done = true;
				dialogs.showPuzzleComplete();
			}
		}
	},
		
	getCompletionsHTML:function(completions)
	{
		if (completions.length == 0)
		{
			return "No clues have been solved recently.";
		}
		
		var len = completions.length;
		if (len > 7) //TODO: define somewhere
		{
			len = 7;
		}
		
		var html = "";		
		for (var i = 0; i < len; i++)
		{
			var obj = completions[i];
			
			var clas = "completion";
			if (i % 2 == 1)
			{
				clas += " alt";
			}
			
			html += "<div class='" + clas + "'>";			

			if (obj.type == "newclue")
			{
				html += this.getNewClueHTML(obj);
			}
			else if (obj.type == "completion")
			{
				html += this.getCompletionHTML(obj);
			}
			else if (obj.type == "completionbyother")
			{
				if (obj.who.id != myUserID)
				{
					html += this.getCompletionHTML(obj);
				}
			}
			else if (obj.type == "shout")
			{
				html += this.getShoutHTML(obj);
			}
			else if (obj.type == "login")
			{
				html += this.getLoginHTML(obj);
			}
			else if (obj.type == "puzzleCompleted")
			{
				html += this.getPuzzleCompletedHTML(obj);
			}
			
			html += "</div>";
		}
		
		return html;
	},
	
	getNewClueHTML:function(newClue)
	{
		var html = "<div>" + this.getUserHTML(newClue.who); 
		html += " added the clue <i>" + newClue.clue + "</i> for the word <strong>" + newClue.word + "</strong></div>";
		html += "<div style='float:right'>" + this.prettyTimeAgo(newClue.when) + "</div><div style='clear:both'></div>";
		
		return html;
	},
	
	getCompletionHTML:function(comp)
	{
		var html = "<div>" + this.getUserHTML(comp.who); 
		html += " solved ";
		if (comp.type == "completion")
		{
			html += this.getClueLinkHTML(comp.clue, "<strong>" + comp.word + "</strong>");
		}
		else
		{
			html += this.getClueLinkHTML(comp.clue);
		}
		html += "</div>";
		html += "<div style='float:right'>" + this.prettyTimeAgo(comp.when) + "</div><div style='clear:both'></div>";
		
		return html;
	},
	
	getClueDisplay:function(clue)
	{
		var dir = "across"; //TODO: can use something else here?
		if (clue.down)
		{
			dir + "down";
		}

		return clue.number + " " + dir;		
	},
	
	getShoutHTML:function(shout)
	{
		var dir = "across"; //TODO: can use something else here?
		if (shout.clue.down)
		{
			dir + "down";
		}
		
		var html = "<div>" + this.getUserHTML(shout.who); 
		html += " said \"" + shout.text + "\"";
		html += " from " + this.getClueLinkHTML(shout.clue) + "</div>";
		html += "<div style='float:right'>" + this.prettyTimeAgo(shout.when) + "</div><div style='clear:both'></div>";
		
		return html;
	},
	
	getLoginHTML:function(login)
	{
		var html = "<div>" + this.getUserHTML(login.who);
		html += " is now working on <a href='Puzzle.action?id=" + login.puzzleID + "'>" + login.puzzleName + "</a>";
		html += "</div>";
		html += "<div style='float:right'>" + this.prettyTimeAgo(login.when) + "</div><div style='clear:both'></div>";
		
		return html;
	},
	
	getPuzzleCompletedHTML:function(comp)
	{
		var html = "<div>" + this.getUserHTML(comp.who);
		html += " finished <a href='Puzzle.action?id=" + comp.puzzleID + "'>" + comp.puzzleName + "</a> with " + comp.score + " points.";
		html += "</div>";
		html += "<div style='float:right'>" + this.prettyTimeAgo(comp.when) + "</div><div style='clear:both'></div>";
		
		return html;
	},

	getUserHTML:function(who)
	{
		return "<a href=\"Person.action?userID=" + who.id + "\">" + who.name + "</a>";
	},
	
	getClueLinkHTML:function(clue, text)
	{
		var clueDir = "across";
		if (clue.down)
		{
			clueDir = "updown";
		}
		var isCurPuzzle = clue.puzzleID == puzzleID;

		if (!text)
		{
			if (isCurPuzzle)
			{
				text = "<strong>" + this.getClueDisplay(clue) + "</strong>";
			}
			else
			{
				text = "<strong>" + clue.puzzleName + "</strong>";
			}
		}

		if ((typeof(board) != "undefined") && (isCurPuzzle))
		{
			return "<a href=\"javascript:board.jumpTo(" + clue.x + "," + clue.y + ",'" + clueDir + "')\">" + text + "</a>";
		}
		else
		{
			return "<a href='Puzzle.action?id=" + clue.puzzleID + "&initialX=" + clue.x + "&initialY=" + clue.y + "'>" + text + "</a>";
		}
	},
	
	//TODO: move to utils class
	shortenString:function(str, max)
	{
		var len = str.length;
		
		if (len < max)
		{
			return str;
		}
		
		return str.substring(0, max - 2) + "...";
	},
	
	prettyTimeAgo:function(d)
	{
		var now = new Date();
        var diff = ((now.getTime() - d.getTime()) / (1000)) | 0; 
		
		if (diff < 1)
		{
			return "just now";
		}
		
		if (diff < 60)
		{
			if (diff == 1)
			{
				return "1 sec ago";
			}
			return diff + " secs ago";
		}
		
		if (diff < 60 * 60)
		{
			var mins = (diff/60)|0;
			if (mins == 1)
			{
				return "1 min ago";
			}			
			return mins + " mins ago";
		}
		
		var hrs = (diff/60/60)|0;
		
		if (hrs == 1)
		{
			return "1 hr ago";
		}
		return hrs + " hrs ago";
	},
	
	hideClueTagger:function()
	{
		var checked = document.getElementById('nomore').checked;
		if (checked)
		{
			API.setNoMoreHelp(challengeID);
			canAsk = false;
		}
		
		this.lastSubmittedGuesses = null;
		dialogs.hideDialog();
	}
		
}


Puzzle = function(width, height)
{
	this.width = width;
	this.height = height;
	this.data = null;
	this.clues = null;
	this.minX = 0;
	this.minY = 0;
	this.maxX = 0;
	this.maxY = 0;	

	this.loadingPuzzleData = false;
	this.isClueLinksOpen = false;
	this.score = 0;
	this.isSample = false;
	this.isFirstLoad = true;
	this.lastShownClue = null;
  this.viewportHeight = null;
	
	this.PRELOAD = 10;
}

Puzzle.prototype = 
{
	grabData:function(minx, miny, maxx, maxy)
	{
		if (minx < 0) minx = 0;
		if (maxx > this.width) maxx = this.width;
		if (miny < 0) miny = 0;
		if (maxy > this.height) maxy = this.height;
		
		if (this._isWithin(minx, miny, maxx, maxy))
		{
			return;
		}
		
		if (this.loadingPuzzleData == true)
		{
			return;
		}
		
     //TODO: use library to put these together
        var params = "challenge_id=" + challengeID;
        if (sessionID) { params += "&s_id=" + sessionID; }
        params += "&x1=" + (minx - this.PRELOAD);
        params += "&y1=" + (miny - this.PRELOAD);
        params += "&x2=" + (maxx + this.PRELOAD);
        params += "&y2=" + (maxy + this.PRELOAD);
        params += "&show_complete=" + showComplete;
        new Ajax.Updater('puzzle_data', '/puzzle/section?' + params, {asynchronous:true, evalScripts:true});
        
		//API.getPuzzleSection(challengeID, minx - this.PRELOAD, miny - this.PRELOAD, maxx + this.PRELOAD, maxy + this.PRELOAD, showComplete, this.onReceivedPuzzle);
		this.loadingPuzzleData = true;
	},
	
	_isWithin:function(minx, miny, maxx, maxy)
	{
		if (minx < this.minX) return false;
		if (maxx > this.maxX) return false;
		if (miny < this.minY) return false;
		if (maxy > this.maxY) return false;
		
		return true;		
	},

	setData:function(pattern, clue)
	{
		this.minX = 0;
	    this.minY = 0;
	    this.maxX = pattern.length;
	    this.maxY = 1;
	    this.isSample = true;
	    
		this.data = new Array();		
		
		for (var i = 0; i < pattern.length; i++)
		{
			this.data[i] = new Array();
			this.data[i][0] = pattern[i];
		}
		
		this.clues = new Array();
		this.clueList = new Array();
	    var clueObj = new Object;
	    clueObj.point = new Point(0,0);
	    clueObj.down = false;
	    clueObj.number = 1;
	    clueObj.length = pattern.length;
	    clueObj.text = clue;      

	    this._hashClue(clueObj);
	},
	
	onReceivedPuzzle:function(puz)
	{
     puzzle.setDataFromServer(puz);
  },
  
  setDataFromServer:function(puz)
  {
		this.minX = puz.minX;
		this.minY = puz.minY;
		this.maxX = puz.maxX;
		this.maxY = puz.maxY;
		
		this.data = puz.grid;
		this.clues = new Array();
		this.clueList = new Array();
	
		puz.clues.sort(this._compareClueNumbers);	
		for (var i = 0; i < puz.clues.length; i++)
		{
			var clue = puz.clues[i];
			clue.point = new Point(clue.x, clue.y);
      clue.direction = directionFromLetter(clue.dir);
			this._hashClue(clue);			
		}
		
		this.loadingPuzzleData = false;

		if (this.isFirstLoad)
		{
			board.validateSelectedPosition();
		}
		         
    //for simple puzzles where might not have clue in cur dir
    board._swapCurrentDirection();
    board._swapCurrentDirection();        

		board.reDraw();        
		if (guesses) { guesses._displayGuesses(); }
		
		this.showScore(puz.score);
	},
	
	_showClueList:function(clues, dir)
	{
    var d = $(dir.plural);
		if (!d) return;
		
		var html = "";
		for (var i = 0; i < clues.length; i++)
		{
			var clue = clues[i];
			
			if (clue.direction == dir)
			{
				var text = clue.text;
				if (text.split(':').length == 3) text = text.split(':')[2]; //TODO: super-hackey
				
				html += "<div id='" + this._getKeyForClue(clue) + "' class='listclue' onclick=\"board.jumpTo(" + clue.x + "," + clue.y + ",'" + dir.display + "', false)\">";
				html += "<div class='listclue_num'>" + clue.number + ".</div>";
				html += "<div class='listclue_text'>" + text + "</div>";
				html += "<div class='clear'></div></div>";
			}
		}
		
		d.innerHTML = html;
	},
	
	scrollToClue:function(clue)
	{
		var d = $(this._getKeyForClue(clue));
		if (d) {
			if (this.viewPortHeight() > 550) {
				d.scrollIntoView(false);
			}
			$$('div.selectedclue').each(function(el){ el.removeClassName('selectedclue'); });
			
			d.addClassName('selectedclue');
		}
	},
   
  viewPortHeight:function()
  {
    if (this.viewportHeight) { return this.viewportHeight }
    this.viewportHeight = document.viewport.getHeight();
    return this.viewportHeight;
  },
	
	_compareClueNumbers:function(a, b)
	{
		return a.number - b.number;
	},
	
	showScore:function(user)
	{
		if ((!user) || (!$('score')))
		{
			return;
		}
		
		this.score = user.score;
		
		var html = this._getScoreLineHTML("Score", user.score);
		// html += this._getScoreLineHTML("Words finished", user.words_completed);
		html += this._getScoreLineHTML("Mistakes", user.letters_wrong);
		html += this._getScoreLineHTML("% Complete", user.percent_complete_str);
         
		$('score').innerHTML = html;
	},
   
  showScores:function(scores)
  {    
    var html = "";
    for (var i = 0; i < scores.length; i++) {
      var s = scores[i];
      html += this._getScoreLineHTML(s.name, s.score);
    }
    
    $('scores').innerHTML = html;
  },
	
	_getScoreLineHTML:function(title, value)
	{
		return "<div class='item_left'>" + title + ":</div><div class='item_right'>" + value + "</div><div class='clear'></div>";
	},
	
	_hashClue:function(clue)
	{       
    var x = clue.point.x;
    var y = clue.point.y;
    
		for (var i = 0; i < clue.length; i++)
		{
			var key = this._getClueKey(x, y, clue.direction);
		  
      x += clue.direction.xAdd;
      y += clue.direction.yAdd;
			this.clues[key] = clue;
		}		
		
		this.clueList.push(clue)
	},
	
	_getClueKey:function(x, y, dir)
	{
		var key = x + "x" + y + dir.letter;
		
		return key;
	},
	
	_getKeyForClue:function(clue)
	{
		return this._getClueKey(clue.point.x, clue.point.y, clue.direction);
	},
	
	isUnknown:function(p)
	{
		if (!this.isValidPosition(p))
		{
			return false;
		}

		//TODO: this is duplicated
		if (p.x < this.minX) return true;
		if (p.x >= this.maxX) return true;
		if (p.y < this.minY) return true;
		if (p.y >= this.maxY) return true;

		if (!this.data)
		{
			return true;
		}
		
		return false;
	},
	
	isSolid:function(p)
	{
		if (!this.isValidPosition(p))
		{
			return true;
		}
		
		if (!this.data)
		{
			return false;
		}
		
		return this.getLetter(p) == '*';
	},
	
	getWidth:function()
	{
		return this.width;		
	},
	
	getHeight:function()
	{
		return this.height;
	},

	getLetter:function(p)
	{
		if ((this.isUnknown(p)) || (!this.data))
		{
			return "?";
		}
		
		var letter = this.data[p.x - this.minX][p.y - this.minY];
		
		if ((letter == ' ') || (letter == '?'))
		{
			return "&nbsp;";
		}
		
		return letter;
	},
	
	isSquareFilled:function(p)
	{
		var let = this.getLetter(p);
		
		if (let == "&nbsp;")
		{
			return false;
		}
		
		return true;
	},
	
	isSquareFilledWithLetter:function(x,y)
	{
		var p = new Point(x,y);
		if (!this.isValidPosition(p))
		{
			return false;
		}
		var let = this.getLetter(p);
		
		if ((let == "&nbsp;") || (let == '*'))
		{
			return false;
		}
		
		return true;
	},
	
	isEnabled:function(p)
	{
		return true; //TODO: check rule
		
		if (this.isEnabledWithDirection(p, false))
		{
			return true;
		}
		
		return this.isEnabledWithDirection(p, true);
	},
	
	isEnabledWithDirection:function(p, dir)
	{
		return true; //TODO: check rule
		
		var clue = this.getClue(p, dir);
		if (clue.number == 1)
		{
			return true;
		}
		var word = this.getClueSolution(clue, true);
		if (!this._isAllSpaces(word))
		{
			return true;
		}
		
		return false;
	},
	
	//TODO: move somewhere better
	_isAllSpaces:function(str)
	{
		for (var i = 0; i < str.length; i++)
		{
			if (str[i] != ' ')
			{
				return false;
			}
		}
		
		return true;
	},
	
	getClue:function(p, dir)
	{
		if (!this.clues)
		{
			return null;
		}
		
		return this.clues[this._getClueKey(p.x, p.y, dir)];
	},
     
    isClueComplete:function(clue)
    {
        var x = clue.point.x;
        var y = clue.point.y;
        
        for (var i = 0; i < clue.length; i++) {
          if (!this.isSquareFilled(new Point(x,y))) {
            return false;
          }
          x += clue.direction.xAdd;
          y += clue.direction.yAdd;
        }
        
        return true;
    },
	
	getClueStartNumber:function(p)
	{
    for (var i = 0; i < DIRECTIONS.length; i++) {
      var dir = DIRECTIONS[i];
      
      var clue = this.getClue(p, dir);
      if ((clue) && (clue.point.equals(p))) {
        return clue.number;
      }
    }
		
		return -1;
	},
	
	isClueStart:function(p)
	{
		return (this.getClueStartNumber(p) != -1)
	},
	
	isWithinClue:function(clue, p)
	{
		if (!clue)
		{
			return false;
		}
	
    var x = clue.point.x;
    var y = clue.point.y;
    
    for (var i = 0; i < clue.length; i++) {
      if ((p.x == x) && (p.y == y)) {
        return true;
      }
      x += clue.direction.xAdd;
      y += clue.direction.yAdd;
    }
    
    return false;
	},
	
	isWithinThemeClue:function(p)
	{
     for (var i = 0; i < DIRECTIONS.length; i++) { //todo: only directions puzzle uses
        var dir = DIRECTIONS[i];
        var clue = this.getClue(p, dir);
		    if ((clue) && (clue.theme))
		    {
			    return true;
        }
     }
     
     return false;
	},
	
	isValidPosition:function(p)
	{		
		if ((p.x < 0) || (p.x >= this.getWidth()))
		{
			return false;
		}
		
		if ((p.y < 0) || (p.y >= this.getHeight()))
		{
			return false;
		}
		
		return true;
	},
	
	getClueHTML:function(clue)
	{
		var clueLinks = "";
		
		if (this.isSample)
		{		
			//no opening			
		}
		else if (!this.isClueLinksOpen)
		{
			clueLinks += "<span class='clue_action'><a href=\"javascript:puzzle.openClueLinks();\">more</a></span>";
		}
		else
		{
			clueLinks += "<span class='clue_action'><a href=\"javascript:puzzle.closeClueLinks();\">less</a></span>";
		}
		
    var text = '';
    if (!clue.media) {
      text = clue.text
		} else if (clue.media.match("thoughtequity://")) {
			var video_id = clue.media.slice(16);
			if ((this.lastShownClue) && (clue != this.lastShownClue)) {
			    dialogs.showVideoClue(video_id, clue.text);
			}
			text = clue.text + " <a href='javascript:dialogs.showVideoClue(" + video_id.inspect(true) + "," + clue.text.inspect(true) + ")'>Play video again</a>";
		}
    else if (clue.media.match("flickr.com")) {      
			if ((this.lastShownClue) && (clue != this.lastShownClue)) {
			    dialogs.showPhotoClue(clue.media, clue.text, clue.attribution);
			}
			text = clue.text + " <a href='javascript:puzzle.showPhotoClue(" + this._getKeyForClue(clue).inspect(true) + ")'>Show photo</a>";
    } else {
      text = clue.text;
    }
		this.lastShownClue = clue;
		
    var clueComplete = this.isClueComplete(clue);
		var html = "<span id='clue_display'><span id='clue_number'>";
    if (clueComplete) { html += "<s>"; }
    html += clue.number + " " + this.getClueDir(clue) + ":";
    if (clueComplete) { html += "</s>"; }
    html += "</span> " + text + clueLinks + "</span>";
		
		html += "<div style='clear: both;'></div>";
		
		if (this.isClueLinksOpen)
		{
			html += this.getClueLinksHTML(clue);
		}
		
		return html;
	},
   
  showPhotoClue:function(hash)
  {
    var clue = this.clues[hash];    
    dialogs.showPhotoClue(clue.media, clue.text, clue.attribution);
  },
  
	getClueLinksHTML:function(clue)
	{
	    var html = "";
	    
      if (isAdmin)
		  {
			  html += this.getClueLinkHTML('puzzle.editClue','edit', clue);
			  html += ", " + this.getClueLinkHTML('puzzle.replaceClue','replace', clue);
        html += ", " + this.getClueLinkHTML('puzzle.deleteClue', 'delete', clue);
		  }
       
	    html += "<div id='clue_links'>";
	    if (clue.solver)
		{
			html += "<div>Explanation: ";
			if (clue.explanation)
			{
				html += this._getExplanationHTML(clue) + " " + this.getClueLinkHTML('puzzle.addExplanation','edit', clue);
			}
			else
			{
				html += this._getExplanationHTML(clue) + " " + this.getClueLinkHTML('puzzle.addExplanation','add', clue);
			}
			html += "</div>";
		    html += "<div>Clue solved by <a href=\"Person.action?userID=" + clue.solver.id + "\">" + clue.solver.name + "</a></div>";

		    var word = this.getClueSolution(clue);
			
			html += "<div>";
			html += this.getClueLinkHTML('puzzle.flagClue', 'This clue is crap', clue);
			if (isAdmin)
			{
				html += " (" + this.getClueLinkHTML('puzzle.markToNotUse','mark to never use again', clue) + ")";
			}
			html += "</div>";
		}
	    else
		{
		    //html += "<div>" + this.getClueLinkHTML('data.askFriendAnswer','Get help from a friend', clue) + "</div>";
		    //html += "<div id='request_community_help'>" + this.getClueLinkHTML('data.askCommunityHelp','Get help from community', clue) + "</div>";
		    html += "<div id='buy_answer'>";
		    if (this.score >= 50)
		    {
		    	html += this.getClueLinkHTML('puzzle.buyAnswer','Buy answer (-50 points)', clue);
		    }
		    else
		    {
		    	html += "<span class='disabled'>Buy answer (-50 point)</span>";
		    }
		    html +=  "</div>";
		}

		if (clue.difficulty)
		{
			html += "<div>Difficulty: " + clue.difficulty;
			if (isAdmin)
			{
				html += " (" + this.getClueLinkHTML('dialogs.fixDifficulty','Fix', clue) + ")";
				
				html += "  Category: " + (clue.category ? clue.category : 'Not set');
				html += this.getClueLinkHTML('puzzle.showClueCatagorizerDialog','Set', clue);
			}  
			html += "</div>";
		}

	    if (clue.author)
		{
		    html += "<div>Written by <a href=\"Person.action?userID=" + clue.author.id + "\">" + clue.author.name + "</a></div>";
		}

	    html += "</div>";
	    return html;
	},
	
	_getExplanationHTML:function(clue)
	{
		ex = clue.explanation;
		if ((!ex) || (ex.length == 0))
		{
			return "<a href='http://en.wikipedia.org/wiki/" + this.getClueSolution(clue) + "' target='_blank'>wikipedia?</a>";
		}
		
		http = ex.indexOf("http://");
		if (http > -1)
		{
			prefix = ex.substr(0, http);
			link = ex.substr(ex);
			end = "";
			sp = ex.indexOf(" ", http);
			if (sp > -1)
			{
				link = ex.substr(http, sp - http);
				end = ex.substr(sp);
			}
			return prefix + "<a href='" + link + "' target='_blank'>link</a>" + end;
		}
		
		return ex;
	},
	
	getClueLinkHTML:function(func, text, clue)
	{
		return "<a href=\"javascript:" + func + "(" + clue.point.x + "," + clue.point.y + ",'" + this.getClueDir(clue) + "')\">" + text + "</a>";
	},
	
	getClueDir:function(clue)
	{
    return clue.direction.display;
	},
	
	openClueLinks:function()
	{
		this.isClueLinksOpen = true;
		board.reDrawClue();
	},
	
	closeClueLinks:function()
	{
		this.isClueLinksOpen = false;
		board.reDrawClue();
	},
	
	setLetter:function(p, letter)
	{
		this.data[p.x - this.minX][p.y - this.minY] = letter;
//		var str = this.data[p.x];
//		this.data[p.x] = str.substring(0, p.y) + letter + str.substring(p.y + 1, str.length);
	},
	
	setLetterIfVisible:function(x, y, letter)
	{
		if (x < this.minX) return;
		if (x >= this.maxX) return;
		if (y < this.minY) return;
		if (y >= this.maxY) return;
		
		this.setLetter(new Point(x, y), letter);
	},
	
	updateClueSolver:function(clue, solver)
	{
		var puzClue = this.getClue(new Point(clue.x, clue.y), clue.direction);
		
		if (puzClue)
		{
			puzClue.solver = solver;
		}
	},
	
	updateClueSolution:function(clue, word)
	{
		if (clue.down)
		{
			for (var i = 0; i < clue.length; i++)
			{
				if (word[i])
				{
					this.setLetterIfVisible(clue.x, clue.y + i, word[i]);
				}
			}
		}
		else
		{
			for (var i = 0; i < clue.length; i++)
			{
				if (word[i])
				{
					this.setLetterIfVisible(clue.x + i, clue.y, word[i]);
				}
			}
		}
	},
	
	flagClue:function(clue, word, x, y, dir)
	{    
		clue = this.getClue(new Point(x, y), directionFromLetter(dir));
		word = this.getClueSolution(clue);
			
		var reason=prompt('Why do you think the clue ' + clue + ' is bullshit?','');
		if (reason)
		{
			API.flagClue(challengeID, x, y, dir, reason, this.onClueFlagged);
			dialogs.showDialogWithWord(word);
		}
	},
	
	markToNotUse:function(x, y, dir)
	{
		API.markClueBad(challengeID, x, y, dir, this.onClueFlagged);
	},

	onClueFlagged:function()
	{
		//TODO: show thank you?
	},
	
	buyAnswer:function(x, y, dir)
	{
		document.getElementById('buy_answer').innerHTML = "Buying...";
		
		API.buySolution(challengeID, x, y, dir, this.onSolutionBought);
		
		//urchinTracker('/endlesscrossword/ajax/buyAnswer');
	},
	
	onSolutionBought:function(user)
	{
		puzzle.showScore(user);		
	},
	
	askCommunityHelp:function(x, y, dir)
	{
		document.getElementById('request_community_help').innerHTML = "Sending...";
		
		API.requestCommunityHelp(challengeID, x, y, dir, this.onRequestedHelp);
		
		//urchinTracker('/endlesscrossword/ajax/requestCommunityHelp');
	},
	
	onRequestedHelp:function()
	{
		document.getElementById('request_community_help').innerHTML = "Help requested. You will be notified when someone helps out.";		
	},
	
	askFriendAnswer:function(x, y, dir)
	{
		API.getFullFriendList(this.onGotFriendList);						
	},
	
	onGotFriendList:function(friends)
	{
		var html = "What smart friend can help you?";
		
		html += "<select>";
		for (var i = 0; i < friends.length; i++)
		{
			var friend = friends[i];
			html += "<option value='" + friend.facebookID + "'>" + friend.name + "</option>";
		}
		html += "</select>";
		
		document.getElementById('dialog_content').innerHTML = html;
				
		dialogs.showDialog();
	},
				
	_getLetterOrGuess:function(p, skipGuesses)
	{
		var letter = this.data[p.x - this.minX][p.y - this.minY];
		
		if ((letter == ' ') || (letter == '?'))
		{
			if (!skipGuesses)
			{
				var guess = guesses.getGuess(p);
				if (guess)
				{
					return guess.letter;
				}
			}
			return " ";
		}
		
		return letter;		
	},
	
	getClueSolution:function(clue, skipGuesses)
	{
		var word = "";
		var x = clue.x ? clue.x : clue.point.x;
		var y = clue.y ? clue.y : clue.point.y;
		
		if (clue.down)
		{
			for (var i = 0; i < clue.length; i++)
			{
				word += this._getLetterOrGuess(new Point(x, y + i), skipGuesses);
			}
		}
		else
		{
			for (var i = 0; i < clue.length; i++)
			{
				word += this._getLetterOrGuess(new Point(x + i, y), skipGuesses);
			}
		}
		
		return word;
	},
	
	haveFullClueSolution:function(clue)
	{
		var solution = this.getClueSolution(clue);
		
		return solution.indexOf(' ') == -1;
	},
	
	sendHelp:function()
	{
		var word = this.getClueSolution(this.getClue(new Point(0,0), false));
		
		if (word.indexOf(' ') != -1)
		{
			alert("Please fill in the word first.");
			return;
		}
		
		document.getElementById('submitWord').value = word;
		document.getElementById('submitForm').submit();
	},
		
  editClue:function(x, y, dir)
  {
    var clue = this.getClue(new Point(x,y), directionFromWord(dir));
    var params = "puz_id=" + puzzleID + "&x=" + x + "&y=" + y + "&dir=" + dir;
     
    dialogs.showAddClueDialog('Save', clue.word, clue.text, clue.media, clue.attribution, params);
  },
   
	deleteClue:function(x, y, dir)
	{
		var clue = this.getClue(new Point(x,y), directionFromWord(dir));
		
		var r = confirm('Never use this clue again?');
		if (r == true)
		{
        var params = "puz_id=" + puzzleID + "&x=" + x + "&y=" + y + "&dir=" + dir;
        new Ajax.Updater('puzzle_data', '/admin/delete_clue?' + params, {asynchronous:true, evalScripts:true});
		}
	},
   
  replaceClue:function(x, y, dir)
  {
    var clue = this.getClue(new Point(x,y), directionFromWord(dir));
    var params = "puz_id=" + puzzleID + "&x=" + x + "&y=" + y + "&dir=" + dir + "&replace=t";
     
    dialogs.showAddClueDialog('Replace', clue.word, '', '', '', params);
  },
   
//  replaceClue:function(x, y, dir)
//  {
//    var clue = this.getClue(new Point(x,y), directionFromWord(dir));
//		
//		var newText = prompt('Enter text for new clue', clue.text);
//		if (newText)
//		{
//        var params = "puz_id=" + puzzleID + "&x=" + x + "&y=" + y + "&dir=" + dir + "&text=" + newText;
//        new Ajax.Updater('puzzle_data', '/admin/replace_clue_text?' + params, {asynchronous:true, evalScripts:true});
//			  clue.text = newText;
//		}
//  },
	
	addExplanation:function(x,y,dir)
	{
		var clue = this.getClue(new Point(x,y), dir == "down");				
		var curExplanation = clue.explanation ? clue.explanation : "";
		
		html = "<div style='padding: 0px 10px 10px'><h4>Add an explanation</h4>";		
		html += "<p>This could be a be sentance or a link to wikipedia.</p>";
		html += "<div>" + clue.text + ": " + this.getClueSolution(clue) + "</div>";
		html += "<div><textarea rows=2 style='width: 320px' id='explanation'>" + curExplanation + "</textarea>";
		html += "<br><input type='button' class='inputsubmit' value='Add Explanation' onClick=\"puzzle.updateExplanation(" + x + "," + y + ",'" + dir + "')\">";
		html += "<input type='button' class='inputsubmit' value='Close' onclick='dialogs.hideDialog();'></div>";		

		document.getElementById('dialog_content').innerHTML = html;
				
		dialogs.showDialog();
	},
	
	updateExplanation:function(x,y,dir)
	{
		var explanation = document.getElementById('explanation').value;
	
		API.updateExplanation(challengeID, x, y, dir, explanation, this.onUpdatedExplanation);
		clue = this.getClue(new Point(x, y), dir != "across");
		clue.explanation = explanation;
		board.reDrawClue();
		
		//urchinTracker('/endlesscrossword/ajax/addExplanation');
	},
	
	onUpdatedExplanation:function()
	{
		dialogs.hideDialog();
	},

	getUncategorizedClue:function()
	{
		var len = this.clueList.length;
		var offset = Math.floor(Math.random()*len);
		for (var i = 0; i < this.clueList.length; i++)
		{
			var clue = this.clueList[(i + offset) % len];
			if ((clue.category == null) && (this.haveFullClueSolution(clue)))
			{
				return clue;
			}
		}
		
		return null;
	},

	showClueTagger:function()
	{
		var clue = this.getUncategorizedClue();
		if (clue == null)
		{
			return;
		}
		
		this.showClueCatagorizerDialog(clue.point.x, clue.point.y, this.getClueDir(clue));
	},
	
	showClueCatagorizerDialog:function(x, y, dir)
	{
		var clue = this.getClue(new Point(x,y), dir == "down");				
		var word = this.getClueSolution(clue);
		
		html = "<div style='padding: 0px 10px 10px'><h4>Categorize a Clue</h4>";		
		html += "<p>Can you put this clue in a category? ";
		html += "<a onClick=\"document.getElementById('why').style.display = 'block'\">Why?</a>";
		html += "<span id='why' style='display:none'>We are attempting to categorize clues so that more customized puzzles can be made (puzzle with no french word clues, puzzle that has a lot of geography clues, etc.)</span>";
		html += "</p>";
		html += "<div style='padding-left: 30px'><div style='padding: 15px 0px; font-size: 14px;'>" + clue.text + ": " + word + "</div>";
		html += "<div style='float: left'><div>";
		html += "<select id='category'>";
		for (var i = 0; i < CLUE_CATEGORIES.length; i++)
		{
			var cat = CLUE_CATEGORIES[i];
			html += "<option value='" + cat + "'>" + cat + "</option>";
		}
		html += "</select></div>";
		html += "<div style='padding: 5px 0px;'><input type='button' class='inputsubmit' value='Categorize Clue' onClick=\"puzzle.setCategory(" + clue.point.x + "," + clue.point.y + ",'" + this.getClueDir(clue) + "')\"></div></div>";
		html += "<div style='float: right'>";
		html += "<INPUT TYPE=CHECKBOX name='nomore' id='nomore'>Don't ask again<br>";
		html += "<input type='button' class='inputsubmit' value='Close' onclick='guesses.hideClueTagger();'></div></div>";		

		$('dialog_content').innerHTML = html;
				
		dialogs.showDialog();
	},
	
	setCategory:function(x,y,dir)
	{
		var clue = this.getClue(new Point(x,y), dir == "down");				
		var category = document.getElementById('category').value;
	
		API.setCategory(challengeID, x, y, dir, category, this.onUpdatedCategory);
		clue.category = category;
		guesses.hideClueTagger();
	},
	
	onUpdatedCategory:function()
	{
		//nothing to do
	}
	
}