/*
   Copyright 2010 Hanov Solutions Inc. All Rights Reserved

   steve.hanov@gmail.com
 */

/** @constructor */
function TransformAction( root, nodes, xform )
{
    this.root = root;
    this.nodes = nodes;
    this.backups = [];
    this.xform = xform;
}

TransformAction.prototype = 
{
    execute: function()
    {
        this.backups.length = 0;
        for( var i = 0; i < this.nodes.length; i++ ) {
            this.backups.push( this.nodes[i].save() );
            //this.nodes[i].matrix = this.xform.multiply( this.nodes[i].matrix );
            this.nodes[i].transform( this.xform ); 
            this.nodes[i].format(gDrawApp.drawView.ctx);
        }

    },

    undo: function()
    {
        for( var i = 0; i < this.nodes.length; i++ ) {
            this.nodes[i].load( this.backups[i] );
            this.nodes[i].format(gDrawApp.drawView.ctx);
        }
    }

};

/** @constructor */
function NewNodeAction(view, root, node)
{
    this.view = view;
    this.root = root;
    this.node = node;
}

NewNodeAction.prototype = 
{
    execute: function()
    {
        this.view.clearSelection();
        this.root.addChild( this.node, 0 );
        this.view.setEditNode( null );
        this.view.addToSelection( this.node );
        this.view.doneSelecting();
    },

    undo: function()
    {
        this.root.removeChild( this.node );
        this.view.clearSelection();
        this.view.setEditNode( null );
    }
};

/** @constructor */
function ChangeOrderAction( doc, selection, type )
{
    this.doc = doc;
    this.selection = doc.collapseGroups( selection );
    this.oldPositions = [];
    this.type = type;
}

ChangeOrderAction.BRING_TO_FRONT = 0;
ChangeOrderAction.SEND_TO_BACK = 1;
ChangeOrderAction.MOVE_UP = 2;
ChangeOrderAction.MOVE_DOWN = 3;

ChangeOrderAction.prototype = 
{
    execute: function()
    {
        this.oldPositions.length = 0;

        for( var i = 0; i < this.selection.length; i++ ) {
            var node = this.selection[i];
            var parent = node.parent;
            var oldIndex = parent.removeChild( node );
            this.oldPositions.push( oldIndex );

            switch( this.type ) {
                case ChangeOrderAction.BRING_TO_FRONT:
                    parent.addChild( node, 0 );
                    break;
                case ChangeOrderAction.SEND_TO_BACK:
                    parent.addChild( node, parent.children.length );
                    break;
                case ChangeOrderAction.MOVE_UP:
                    if ( oldIndex > 0 ) {
                        parent.addChild( node, oldIndex - 1 );
                    } else {
                        parent.addChild( node, oldIndex );
                    }
                    break;
                case ChangeOrderAction.MOVE_DOWN:
                    if ( oldIndex < parent.children.length ) {
                        parent.addChild( node, oldIndex + 1 );
                    } else {
                        parent.addChild( node, oldIndex );
                    }
                    break;
            }
        }
    },

    undo: function()
    {
        for ( var i = 0; i < this.selection.length; i++ ) {
            var node = this.selection[i];
            var parent = node.parent;
            parent.removeChild( node );
            parent.addChild( node, this.oldPositions[i] );
        }
    }
};

/** @constructor */
function DeleteNodeAction( root, selection )
{
    this.root = root;
    this.selection = selection.concat();
    this.parents = []; 
    this.positions = [];
    for ( var i = 0; i < this.selection.length; i++ ) {
        this.parents.push( this.root.findParent( this.selection[i] ) );
    }
}

DeleteNodeAction.prototype = 
{
    execute: function()
    {
        // for each parent, delete the node at the given index.
        this.positions.length = 0;
        for ( var i = 0; i < this.selection.length; i++ ) {
            this.positions.push( this.parents[i].removeChild( this.selection[i]
                        ) );
        }
    },

    undo: function()
    {
        for ( var i = 0; i < this.selection.length; i++ ) {
            if ( this.positions[i] >= 0 ) {
                this.parents[i].children.splice( this.positions[i],
                        0, this.selection[i] );
            }
        }
    }
};

/** @constructor */
function DuplicateNodeAction( view, root, selection )
{
    this.root = root;
    this.nodes = [];
    this.view = view;
    this.view.sortNodes( selection );
    selection = this.view.doc.collapseGroups( selection );
    var xform = new TranslateMatrix( 10, 10 );

    for ( var i = 0; i < selection.length; i++ ) {
        this.nodes.push( selection[i].duplicate() );
        this.nodes[this.nodes.length-1].transform( xform );
        this.nodes[this.nodes.length-1].format( this.view.ctx );
    }
}

DuplicateNodeAction.prototype = 
{
    execute: function()
    {
        this.view.clearSelection();
        for ( var i = 0; i < this.nodes.length; i++ ) {
            this.root.addChild( this.nodes[i], 0 );
            this.view.addToSelection( this.nodes[i] );
        }

        this.view.doneSelecting();
    },

    undo: function()
    {
        this.view.clearSelection();
        for ( var i = 0; i < this.nodes.length; i++ ) {
            this.root.removeChild( this.nodes[i] );
        }
        this.view.doneSelecting();
    }
};


/** @constructor */
function MoveEditHandleAction( node, handle, fromX, fromY, toX, toY )
{
    this.node = node;
    this.handle = handle;
    this.fromX = fromX;
    this.fromY = fromY;
    this.toX = toX;
    this.toY = toY;
}

MoveEditHandleAction.prototype = 
{
    execute: function()
    {
        this.node.fixate();
        this.node.moveEditHandle( this.handle, this.toX, this.toY );
        this.node.format(gDrawApp.drawView.ctx);
    },

    undo: function()
    {
        this.node.fixate();
        this.node.moveEditHandle( this.handle, this.fromX, this.fromY );
        this.node.format(gDrawApp.drawView.ctx);
    }
};

/** @constructor */
function GroupAction( view )
{
    this.view = view;
    this.nodes = this.view.selection.concat();
    dbg.printf("Selection is %d nodes\n", this.nodes.length );
    this.view.sortNodes( this.nodes );
    this.oldParents = [];
    this.oldIndexes = [];
}

GroupAction.prototype = 
{
    execute: function()
    {
        dbg.printf("GroupAction::Execute()\n");
        // create a new group node.
        this.group = new BaseNode();

        this.oldIndexes.length = 0;
        this.oldParents.length = 0;
        // go through the selected nodes.
        for ( var i = 0; i < this.nodes.length; i++ ) {
            var node = this.nodes[i];
            if ( node.parent === null ) {
                continue;
            }

            // remember the old parent and position.
            this.oldParents.push( node.parent );
            this.oldIndexes.push( node.parent.removeChild( node ) );

            // add the node to the group.
            this.group.addChild( node,
                    this.group.children.length );
        }

        this.view.doc.root.addChild( this.group,
                this.view.doc.root.children.length );
    },

    undo: function()
    {
        this.view.doc.root.removeChild( this.group );
        for ( var i = this.nodes.length - 1; i >= 0; i-- ) {
            var node = this.nodes[i];
            if ( node.parent === null ) {
                continue;
            }
            this.oldParents[i].addChild( node, this.oldIndexes[i] );
        }
    }
};

/** @constructor */
function UngroupAction( view )
{
    this.view = view;
    this.nodes = this.view.selection.concat();
    this.view.sortNodes( this.nodes );
    this.groups = []; 
}

UngroupAction.prototype = 
{
    execute: function()
    {
        var nodesProcessed = {};

        // go through the selected nodes.
        for ( var i = 0; i < this.nodes.length; i++ ) {
            var node = this.nodes[i];

            // if the node is a member of a group, then get its parent.
            var parent = node.parent;

            if ( parent.parent === null ) {
                continue;
            }
            var grandparent = parent.parent;

            // if the parent has already been processed, skip it.
            if ( parent.id in nodesProcessed ) {
                continue;
            }

            //dbg.printf("Processing node %s with %s children\n",
            //        parent.id, parent.children.length);

            // add the parent to the processed list.
            nodesProcessed[parent.id] = true;

            // remember its children.
            var oldIndex = grandparent.removeChild( parent );
            var children = parent.children.concat();
            this.groups.push( {
                node: parent,
                grandparent: grandparent,
                index: oldIndex,
                children: children
            });

            // remove its children and add them to the root.
            for ( var j = 0; j < children.length; j++ ) {
                parent.removeChild( children[j] );
                grandparent.addChild( children[j], oldIndex + j );
            }
        }
    },

    undo: function()
    {
        for( var i = this.groups.length - 1; i >= 0; i-- ) {
            var group = this.groups[i];

            for ( var j = group.children.length - 1; j >= 0; j-- ) {
                //dbg.printf("Add back child id=%d\n", group.children[j].id); 
                group.grandparent.removeChild( group.children[j] );
                group.node.addChild( group.children[j], 0 );
            }

            group.grandparent.addChild( group.node, group.index );
        }
    }
};


/** @constructor */
function SelectBoxBehaviour(view, previousBehaviour, x, y)
{
    this.view = view;
    this.dx = 0;
    this.dy = 0;
    this.dragging = false;
    this.previousBehaviour = previousBehaviour;
    this.onMouseDown( x, y );
}

SelectBoxBehaviour.prototype = {

    onMouseDown: function( x, y ) 
    {
        this.dx = x;
        this.dy = y;
        this.dragging = true;
    },

    onMouseMove: function( x, y )
    {
        if ( this.dragging ) {
            var ctx = this.view.ctx;
            this.view.draw();
            ctx.save();
            ctx.strokeStyle = "#0050B7";
            ctx.lineWidth = 2;
            ctx.fillStyle = "rgba(0, 80, 183, 0.2 )";
            ctx.fillRect( this.dx+0.5, this.dy+0.5, x -
                    this.dx, y - this.dy );
            ctx.strokeRect( this.dx+0.5, this.dy+0.5, x -
                    this.dx, y - this.dy );

            ctx.restore();
        }
    },

    onMouseUp: function( x, y )
    {
        this.dragging = false;

        this.view.selectByRect( new Rectangle( this.dx, this.dy, x - this.dx, y -
                    this.dy ) ); 

        this.view.doneSelecting();            
            
        this.view.draw();
        this.view.behaviour = this.previousBehaviour;
    }
};

function ExtractMatrices( nodes )
{
    var result = [];
    for ( var i = 0; i < nodes.length; i++ ) {
        result.push( nodes[i].getMatrix() );
    }
    return result;
}

/** @constructor */
function TransformSelectionBehaviour(view, previousBehaviour, handle,
        toggleEditNode, x, y)
{
    this.view = view;
    this.handle = handle;
    this.previousBehaviour = previousBehaviour;
    this.toggleEditNode = toggleEditNode; // should we allow toggle to edit node if mouse didn't
    this.view.showRotationHandle( this.handle === this.view.ROT );

    //move?
    this.onMouseDown( x, y );
}

TransformSelectionBehaviour.prototype = {

    onMouseDown: function( x, y ) 
    {
        this.dx = x;
        this.dy = y;

        this.nodes = this.view.getSelectedNodes();
        this.matrices = ExtractMatrices( this.nodes );
        this.selRect = new Rectangle( this.view.selectionBounds.x,
                this.view.selectionBounds.y, 
                this.view.selectionBounds.width,
                this.view.selectionBounds.height );

        if ( this.handle === this.view.ROT ) {
            this.cx = this.selRect.x + this.selRect.width / 2;
            this.cy = this.selRect.y + this.selRect.height / 2;
            this.startAngle = Math.atan2( this.cy-y, x - this.cx );
        }
    },

    getTransform: function( x, y ) 
    {
        var offsetX = x - this.dx;
        var offsetY = y - this.dy;

        var xform;
        var sx;
        var sy;
        switch( this.handle ) {
            case this.view.NE:
                sx = (x - this.selRect.x) / this.selRect.width;
                sy = (this.selRect.y + this.selRect.height - y) / this.selRect.height;
                xform = new ScaleMatrix( sx, sy, this.selRect.x, 
                        this.selRect.y + this.selRect.height);
                break;
            case this.view.SE:
                sx = (x - this.selRect.x) / this.selRect.width;
                sy = (y - this.selRect.y) / this.selRect.height;
                xform = new ScaleMatrix( sx, sy, this.selRect.x, this.selRect.y );
                break;
            case this.view.SW:
                sx = (this.selRect.x + this.selRect.width - x) / this.selRect.width;
                sy = (y - this.selRect.y) / this.selRect.height;
                xform = new ScaleMatrix( sx, sy, this.selRect.x +
                        this.selRect.width, this.selRect.y );
                break;
            case this.view.NW:
                sx = (this.selRect.x + this.selRect.width - x) / this.selRect.width;
                sy = (this.selRect.y + this.selRect.height - y) / this.selRect.height;
                xform = new ScaleMatrix( sx, sy, 
                        this.selRect.x + this.selRect.width, 
                        this.selRect.y + this.selRect.height);
                break;
            case this.view.ROT:
                var angle = Math.atan2( this.cy-y, x-this.cx ) - this.startAngle;
                xform = new RotateMatrix( angle, this.cx, this.cy);
                break;
            default:
                xform = new TranslateMatrix( offsetX, offsetY );
        }
        return xform;

    },

    onMouseMove: function( x, y )
    {
        var xform = this.getTransform( x, y );

        for ( var i = 0; i < this.nodes.length; i++ ) {
            //this.nodes[i].matrix = xform.multiply( this.matrices[i] );
            this.nodes[i].setMatrix( xform.multiply( this.matrices[i] ) );
            this.nodes[i].format(gDrawApp.drawView.ctx);
        }
        this.view.transformSelectHandles( xform );
        if ( this.handle === this.view.ROT ) {

            this.view.rotHandleX = x;
            this.view.rotHandleY = y;
        }
        this.view.draw();
        if ( this.handle === this.view.ROT ) {
            var ctx = this.view.ctx;
            ctx.save();
            ctx.beginPath();
            ctx.strokeStyle = "#0050B7";
            ctx.lineWidth = 1;
            ctx.moveTo( this.cx, this.cy );
            ctx.lineTo( x, y );
            ctx.stroke();
            ctx.restore();
        }
    },

    onMouseUp: function( x, y )
    {
        var node;
        if ( x !== this.dx || y !== this.dy ) {
            var xform = this.getTransform( x, y );

            for ( var i = 0; i < this.nodes.length; i++ ) {
                //this.nodes[i].matrix = this.matrices[i];
                this.nodes[i].setMatrix( this.matrices[i] );
            }

            this.view.undoStack.execute( 
                    new TransformAction( this.view.doc.root,
                        this.nodes,
                        xform ) );
        } else if ( this.toggleEditNode ) {
            // didn't move!
            node = this.view.doc.root.hittest( x, y );
            if ( node && node.hasEditMode() ) {
                this.view.clearSelection();
                this.view.setEditNode( node );
            }
        }

        this.view.transformSelectHandles( new IdentityMatrix() );
        this.view.showRotationHandle( true );
        this.view.calcSelection();
        this.view.draw();
        this.view.behaviour = this.previousBehaviour;
    }
};

/** @constructor */
function MoveEditNodeBehaviour(view, previousBehaviour, node, handle, x, y)
{
    this.view = view;
    this.node = node;
    this.handle = handle;
    this.previousBehaviour = previousBehaviour;
    this.onMouseDown( x, y );
}

MoveEditNodeBehaviour.prototype = {

    onMouseDown: function( x, y ) 
    {
        this.dx = x;
        this.dy = y;

        this.node.fixate();
    },

    onMouseMove: function( x, y )
    {
        this.node.moveEditHandle( this.handle, x, y );
        this.node.format(this.view.ctx);
        this.view.draw();
    },

    onMouseUp: function( x, y )
    {
        if ( x !== this.dx || y !== this.dy ) {
            this.view.undoStack.execute( new MoveEditHandleAction( this.node,
                        this.handle, this.dx, this.dy, x, y ), true );
        } else {
            // didn't move!
        }

        this.view.draw();
        this.view.behaviour = this.previousBehaviour;
    }
};

/** @constructor */
function ResizeCanvasBehaviour(view, previousBehaviour, x, y, e)
{
    this.view = view;
    this.previousBehaviour = previousBehaviour;
    this.originalWidth = this.view.canvas.width;
    this.originalHeight = this.view.canvas.height;

    this.dx = e.pageX;
    this.dy = e.pageY;
    var self = this;
    this.movefunc = function( e ) {
        self.onRealMouseMove( e.pageX, e.pageY );
        e.stopPropagation();
    };
    this.upfunc = function( e ) {
        self.onRealMouseUp( e.pageX, e.pageY );
        e.stopPropagation();
    };

    $(document).bind( "mousemove", this.movefunc );
    $(document).bind( "mouseup", this.upfunc );
}

ResizeCanvasBehaviour.prototype = {

    onRealMouseMove: function( x, y )
    {
        this.view.canvas.width = this.originalWidth + x - this.dx;
        this.view.canvas.height = this.originalHeight + y - this.dy;
        this.view.draw();
    },

    onRealMouseUp: function( x, y )
    {
        $(document).unbind( "mousemove", this.movefunc );
        $(document).unbind( "mouseup", this.upfunc );
        this.view.behaviour = this.previousBehaviour;
    }
};

/** @constructor */
function DrawLinesBehaviour(view, previousBehaviour, isCurves, hasArrow)
{
    this.view = view;
    this.previousBehaviour = previousBehaviour;
    this.node = null;
    this.numPoints = 0;
    this.isCurves = isCurves;
    this.hasArrow = hasArrow;
    this.view.canvas.style.cursor = "crosshair";
    this.view.setHintText("Click to place first point of line");
    this.view.draw();
}

DrawLinesBehaviour.prototype = {

    onKeyDown: function( e )
    {
        var code = e.keyCode;

        if ( code === 27 ) {
            if ( this.node !== null ) {
                this.node.parent.removeChild( this.node );
            }
            this.exit();
        }

    },

    addPoint: function( x, y )
    {
        if ( this.isCurves ) {
            this.node.curveTo( x, y );
        } else {
            this.node.lineTo( x, y );
        }
    },

    onMouseClick: function( x, y )
    {
        if ( this.node === null ) {
            if ( this.hasArrow ) {
                this.node = new ArrowNode( x, y );
            } else {
                this.node = new PathNode( x, y );
            }
            this.addPoint( x, y );
            this.node.format(this.view.ctx);
            this.view.doc.root.addChild( this.node, 0 );
            this.index = 1;
            this.view.setHintText("Click to place another point, or double-click to end the line");
            this.view.draw();

            this.numPoints = 2;
        } else {
            this.addPoint( x,y );
            this.numPoints++;
            this.index += 1;
        }

    },

    onMouseMove: function( x, y )
    {
        if ( this.node ) {
            this.node.moveEditHandle( this.index, x, y );
            this.node.format(this.view.ctx);
            this.view.draw();
        }
    },

    onDoubleClick: function( x, y )
    {
        if ( this.node ) {
            var numPoints = this.node.getPointCount();
            this.node.removePoint( numPoints - 1 );
            this.node.removePoint( numPoints - 2 );
            this.node.autoclose();
            this.node.format();
            this.view.undoStack.execute( new NewNodeAction( this.view,
                        this.view.doc.root, this.node ), true );
            this.view.clearSelection();
            this.view.setEditNode( null );
            this.view.addToSelection( this.node );
            this.view.doneSelecting();
        }

        this.exit();
    },

    exit: function()
    {
        this.view.behaviour = this.previousBehaviour;
        this.view.canvas.style.cursor = "default";
        this.view.setHintText(null);
        this.view.draw();
    }
};

/** @constructor */
function FloodFillBehaviour(view, previousBehaviour)
{
    this.view = view;
    this.view.canvas.style.cursor = "crosshair";
    this.previousBehaviour = previousBehaviour;
}

FloodFillBehaviour.prototype = {

    onMouseDown: function( x, y, e )
    {
        this.view.canvas.style.cursor = "default";
        this.view.behaviour = this.previousBehaviour;
        Trace(this.view.ctx, x, y, this.view.canvas.width, 
            this.view.canvas.height);
    }
};

/** @constructor */
function DefaultBehaviour(view)
{
    this.view = view;
}


DefaultBehaviour.prototype = {
    onMouseMove: function( x, y )
    {

    },

    onMouseDown: function( x, y, e )
    {
        var node;
        var handle;

        // Check if we are resizing the entire display
        if ( this.view.pointInResizeBox( x, y ) ) {
            this.view.behaviour = new ResizeCanvasBehaviour( this.view, this, x, y, e );
            return;
        }

        // check if we clicked on a selection handle.
        var dir = this.view.getHandleUnderPoint( x, y );
        if ( dir ) {
            this.view.behaviour = new TransformSelectionBehaviour( this.view,
                    this, dir, false, x, y );
            return;
        }

        if ( this.view.selection.length && 
                this.view.selectionBounds.containsPoint( x, y ) ) 
        {
            this.view.behaviour = new TransformSelectionBehaviour( this.view, 
                    this, -1, true, x, y );

            return;
        }


        if ( !e.shiftKey ) {
            this.view.clearSelection();
        }

        // if there is an edit node,
        if ( this.view.editNode ) {
            // if we are over an edit node handle,
            node = this.view.editNode;
            handle = node.hitTestEditHandle( x, y );
            if ( handle !== null ) {
                // start moving it.
                this.view.behaviour = new MoveEditNodeBehaviour( this.view,
                        this, node, handle, x, y );
                return;
            } else if ( 0 && this.view.editNode.rect.containsPoint( x, y ) ) {
                // we are over an edit node but didn't click on an edit
                // handle. Switch out of node editing.
                this.view.setEditNode( null );
            }
        }

        // if we are over a node, 
        node = this.view.doc.root.hittest( x, y );
        if ( node ) {

            var wasEditNode = node === this.view.editNode;
            var wasSelected = this.view.isNodeSelected( node );
            // Did not click on the edit node.
            // if the node is not selected,
            if ( !wasSelected ) {
                // select the node.
                this.view.addToSelection( node );
                this.view.doneSelecting();
            }

            this.view.behaviour = new TransformSelectionBehaviour( 
                    this.view, this, -1, !wasEditNode && wasSelected, x, y );

        } else {
            // not over a node. Start drawing a selection box.
            this.view.setEditNode( null );
            this.view.behaviour = 
                new SelectBoxBehaviour( this.view, this, x, y );
        }

        this.view.draw();
    },

    onMouseUp: function( x, y )
    {

    },

    onKeyDown: function( e )
    {
        var code = e.keyCode;
        if ( code === 36 ) { // Home
            if ( this.view.selection.length ) {
                this.view.undoStack.execute( new ChangeOrderAction(
                            this.view.doc,
                            this.view.selection,
                            ChangeOrderAction.BRING_TO_FRONT) );
                this.view.update();
            }
        } else if ( code === 35 ) { // End
            // end
            if ( this.view.selection.length ) {
                this.view.undoStack.execute( new ChangeOrderAction(
                            this.view.doc,
                            this.view.selection,
                            ChangeOrderAction.SEND_TO_BACK) );
                this.view.update();
            }
        } else if ( code === 33 ) { // Page up
            // end
            if ( this.view.selection.length ) {
                this.view.undoStack.execute( new ChangeOrderAction(
                            this.view.doc,
                            this.view.selection,
                            ChangeOrderAction.MOVE_UP) );
                this.view.update();
            }
        } else if ( code === 34 ) { // Page down
            // end
            if ( this.view.selection.length ) {
                this.view.undoStack.execute( new ChangeOrderAction(
                            this.view.doc,
                            this.view.selection,
                            ChangeOrderAction.MOVE_DOWN) );
                this.view.update();
            }
        } else if ( code === 46 || code === 8 ) { // Del or backspace
            if ( this.view.selection.length ) {
                this.view.undoStack.execute( new DeleteNodeAction(
                            this.view.doc.root,
                            this.view.selection ) );
                this.view.clearSelection();
                this.view.update();

            }
        } else if ( code === 68 && e.ctrlKey === true ) {
            // CTRL-D
            if ( this.view.selection.length ) {
                this.view.undoStack.execute( new DuplicateNodeAction(
                            this.view,
                            this.view.doc.root,
                            this.view.selection ) );
                this.view.update();
            }
        } else if ( code === 67 ) { // C
            this.view.behaviour = new DrawLinesBehaviour( this.view, this,
                    true, false );
        } else if ( code === 76 ) { // L
            this.view.behaviour = new DrawLinesBehaviour( this.view, this,
                    false, false );
        } else if ( code === 71 && e.ctrlKey && e.shiftKey ) { // Ctrl+Shift+G
            this.view.undoStack.execute( new UngroupAction( this.view ) );
        } else if ( code === 71 && e.ctrlKey) { // Ctrl+G
            this.view.undoStack.execute( new GroupAction( this.view ) );
        } else if ( code === 85 && e.ctrlKey) { // Ctrl+U

        } else if ( (code === 107 ) && e.shiftKey || code === 187 &&
            !e.shiftKey) { // =
            // on firefox, + is 107 but on chrome it's 189
            this.view.scale = 1;
            this.view.translateX = 0;
            this.view.translateY = 0;
            this.view.draw();
        } else if ( code === 107 || code === 187 ) { // +
            this.view.scale = this.view.scale * 1.1;
            this.view.draw();
        } else if ( code === 109 || code === 189 ) { // -
            this.view.scale = this.view.scale / 1.1;
            this.view.draw();
        } else if ( code === 38 ) { // up
            this.view.translateY = Math.min( this.view.translateY + 16,
                    0 );
            this.view.draw();
        } else if ( code === 39 ) { // right
            this.view.translateX = Math.max( -this.view.canvas.width,
                    this.view.translateX - 16 );
            this.view.draw();
        } else if ( code === 40 ) { // down
            this.view.translateY = Math.max( -this.view.canvas.height,
                    this.view.translateY - 16 );
            this.view.draw();
        } else if ( code === 37 ) { // left
            this.view.translateX = Math.min( this.view.translateX + 16,
                    0 );
            this.view.draw();
        } else if ( code === 192 && e.shiftKey ) { // `
            this.view.canvas.height = this.view.canvas.height * 1.1;
            this.view.draw();
        } else if ( code === 192 ) { // `
            this.view.behaviour = new FloodFillBehaviour( this.view, this );
        }

        //dbg.printf("keyCode=%d which=%d charCode=%d\n", e.keyCode,  e.which, e.charCode);
    }
};

var gDrawView;

/** @constructor */
function DrawView(canvas, undoStack, panel )
{
    gDrawView = this;
    this.canvas = canvas;
    this.ctx = this.canvas.getContext("2d");
    this.panel = panel;
    panel.view = this;

    //this.tool = new DrawTool( this );

    this.doc = new DrawDoc();
    this.format();

    this.selection = [];
    this.selectionBounds = new Rectangle(0,0,0,0);

    this.editNode = null;

    this.undoStack = undoStack;

    this.scale = 1.0;
    this.translateX = 0.0;
    this.translateY = 0.0;

    // hint text. When set, the message will be displayed somewhere in the
    // window.
    this.hintText = null;
    this.hintImage = document.createElement("img");

    this.draw();

    this.NE = 1;
    this.SE = 2;
    this.SW = 3;
    this.NW = 4;
    this.ROT = 5;

    /** @const */
    this.HANDLE_RADIUS = 4;
    /** @const */
    this.RESIZE_RADIUS = 9;

    this.menu = null;
    this.selectHandleTransform = new IdentityMatrix();
    this.enableRotationHandle = true;

    // number used to identify nodes that are in the selection.
    this.selectGeneration = 1;

    this.behaviour = new DefaultBehaviour( this );

    var self = this;
    var border = parseInt(this.canvas.style.borderWidth, 10);
    this.border = border;

    $(this.canvas).mousemove( function(e) {
        if ( self.behaviour.onMouseMove ) {
            var offset = $(self.canvas).offset();
            var x = (e.pageX - offset.left-border - self.translateX) / self.scale;
            var y = (e.pageY - offset.top-border - self.translateY) / self.scale;
            self.behaviour.onMouseMove( x, y );
        }
    });

    $(this.canvas).mousedown( function(e) {
        var offset = $(self.canvas).offset();
        if ( self.behaviour.onMouseDown ) {
            var x = (e.pageX - offset.left-border - self.translateX) / self.scale;
            var y = (e.pageY - offset.top-border - self.translateY) / self.scale;
            self.behaviour.onMouseDown( x, y, e );
        }
        e.preventDefault();
    });

    $(this.canvas).mouseup( function(e) {
        var offset = $(self.canvas).offset();
        if ( self.behaviour.onMouseUp ) {
            var x = (e.pageX - offset.left-border - self.translateX) / self.scale;
            var y = (e.pageY - offset.top-border - self.translateY) / self.scale;
            self.behaviour.onMouseUp( x, y );
        }
        e.preventDefault();
    });

    $(this.canvas).click( function(e) {
        var offset = $(self.canvas).offset();
        $(self.canvas).focus();
        if ( self.behaviour.onMouseClick ) {
            var x = (e.pageX - offset.left-border - self.translateX) / self.scale;
            var y = (e.pageY - offset.top-border - self.translateY) / self.scale;
            self.behaviour.onMouseClick( x, y );
        }
        e.preventDefault();
    });

    $(this.canvas).dblclick( function(e) {
        var offset = $(self.canvas).offset();
        if ( self.behaviour.onDoubleClick ) {
            var x = (e.pageX - offset.left-border - self.translateX) / self.scale;
            var y = (e.pageY - offset.top-border - self.translateY) / self.scale;
            self.behaviour.onDoubleClick( x, y );
        }
        e.preventDefault();
    });

    $(this.canvas).keydown( function(e) {
        if ( self.behaviour.onKeyDown ) {
            self.behaviour.onKeyDown( e );
        }
        e.preventDefault();
        return false;
    });

    $(this.canvas).keyup( function(e) {
        if ( self.behaviour.onKeyUp ) {
            self.behaviour.onKeyUp( e );
        }
        e.preventDefault();
        return false;
    });
}


DrawView.prototype = {

    clear: function()
    {
        this.ctx.setTransform( 1, 0, 0, 1, 0, 0 );
        this.ctx.clearRect( 0, 0, this.canvas.width, this.canvas.height );
    },

    format: function()
    {
        this.doc.root.format(this.ctx);
    },

    update: function()
    {
        this.doc.root.format();
        this.calcSelection();
        this.draw();
    },

    setHintText: function( text )
    {
        this.hintText = text;
        this.hintImage.src = 
                    "button.cgi?" +
                    "font=Arial" +
                    "&size=11" +
                    "&text=" + encodeURIComponent( this.hintText ) +
                    "&fill=rgba(0,0,0,0.5)";
    },


    lineTool: function() 
    {
        if ( this.behaviour instanceof DrawLinesBehaviour ) {
            this.behaviour = this.behaviour.previousBehaviour;
        }

        this.behaviour = new DrawLinesBehaviour( this, this.behaviour, 
                false, false );
    },

    curveTool: function() 
    {
        if ( this.behaviour instanceof DrawLinesBehaviour ) {
            this.behaviour = this.behaviour.previousBehaviour;
        }

        this.behaviour = new DrawLinesBehaviour( this, this.behaviour, 
                true, false );
    },

    arrowTool: function() 
    {
        if ( this.behaviour instanceof DrawLinesBehaviour ) {
            this.behaviour = this.behaviour.previousBehaviour;
        }

        this.behaviour = new DrawLinesBehaviour( this, this.behaviour, 
                true, true );
    },

    // Draw without any control handles.
    drawForSave: function()
    {
        this.ctx.save();
        this.ctx.setTransform( 1.0, 0, 0, 1.0, 0, 0 );
        this.ctx.fillStyle = "#ffffff";
        this.ctx.fillRect( -1, -1, this.canvas.width+1, this.canvas.height+1 );
        this.doc.root.draw( this.ctx );
        this.ctx.restore();
    },

    draw: function()
    {
        this.clear();
        this.ctx.setTransform( this.scale, 0, 0, this.scale, this.translateX,
                this.translateY );
        this.doc.root.draw(this.ctx);

        if ( this.selection.length > 0 ) {
            this.ctx.save();
            this.ctx.strokeStyle = "#000000";
            this.ctx.lineWidth = 1;

            var tr = this.selectHandleTransform.apply( 
                this.selectionBounds.x,
                this.selectionBounds.y );
            var tl = this.selectHandleTransform.apply( 
                this.selectionBounds.x + this.selectionBounds.width,
                this.selectionBounds.y );
            var bl = this.selectHandleTransform.apply( 
                this.selectionBounds.x + this.selectionBounds.width,
                this.selectionBounds.y + this.selectionBounds.height );
            var br = this.selectHandleTransform.apply( 
                this.selectionBounds.x,
                this.selectionBounds.y + this.selectionBounds.height );

            var rh = new Point( this.rotHandleX, this.rotHandleY );

            var r = this.HANDLE_RADIUS;
            this.ctx.strokeRect( tl.x - r, tl.y - r, r * 2, r * 2 );
            this.ctx.strokeRect( tr.x - r, tr.y - r, r * 2, r * 2 );
            this.ctx.strokeRect( br.x - r, br.y - r, r * 2, r * 2 );
            this.ctx.strokeRect( bl.x - r, bl.y - r, r * 2, r * 2 );

            if ( this.enableRotationHandle ) {
                this.ctx.beginPath();
                this.ctx.strokeStyle = "#008000";
                this.ctx.lineWidth = 3;
                this.ctx.moveTo( rh.x, rh.y );
                this.ctx.arc( rh.x, rh.y, 6, 0, 1.5 * Math.PI, false );
                this.ctx.stroke();
            }
            this.ctx.restore();
        }

        if ( this.editNode ) {
            this.editNode.drawEditHandles(this.ctx);
        }

        if ( this.menu ) {
            this.menu.draw( this.ctx );
        }

        if ( this.hintText !== null && this.hintImage.complete ) {
            try {
                this.ctx.drawImage( this.hintImage, 0, 0 );
            } catch( e ) {

            }
        }

        // Draw size handle
        this.ctx.save();
        this.ctx.setTransform(1,0,0,1,0,0);
        this.ctx.lineWidth = 1;
        this.ctx.strokeStyle = "#4fa0d3";
        this.ctx.beginPath();
        for( var i = this.RESIZE_RADIUS; i > 0; i -= 3 ) {
            this.ctx.moveTo( this.canvas.width, this.canvas.height - i );
            this.ctx.lineTo( this.canvas.width - i, this.canvas.height );
        }
        this.ctx.stroke();
        this.ctx.restore();

    },

    selectByRect: function( rect )
    {
        for( var i = 0; i < this.doc.root.children.length; i++ ) {
            var node = this.doc.root.children[i];
            // if node is a group,
            if ( node.children.length > 0 ) {
                var okay = true;
                // check if all members are contained in the rect.
                for ( var j = 0; j < node.children.length; j++ ) {
                    if ( !rect.contains( node.children[j].rect ) ) {
                        okay = false;
                    }
                }

                if ( okay ) {
                    this.addToSelection( node.children[0] );
                }

            } else if ( rect.contains( this.doc.root.children[i].rect ) ) {
                this.addToSelection( node, false );
            }
        }
    },

    transformSelectHandles: function( matrix )
    {
        this.selectHandleTransform = matrix;
    },

    showRotationHandle: function( shown )
    {
        this.enableRotationHandle = shown;
    },

    doneSelecting: function()
    {
        this.calcSelection();
        this.panel.loadFromNodes( this.selection );
    },

    setEditNode: function( node )
    {
        this.editNode = node;
    },

    isNodeSelected: function( node )
    {
        return node.selectGeneration === this.selectGeneration;
    },

    getSelectedNodes: function()
    {
        return this.selection.concat();
    },

    pointInResizeBox: function( x, y )
    {
        x *= this.scale;
        y *= this.scale;
        return x >= this.canvas.width - this.RESIZE_RADIUS &&
               x < this.canvas.width + this.border &&
               y >= this.canvas.height - this.RESIZE_RADIUS &&
               y < this.canvas.height + this.border;
    },
   
    getHandleUnderPoint: function( x, y )
    {
        if ( this.selection.length ) {
            var r = this.HANDLE_RADIUS * 2;
            if ( x >= this.selectionBounds.x - r && x < this.selectionBounds.x + r
                    )
            {
                if ( y >= this.selectionBounds.y - r && 
                    y < this.selectionBounds.y + r )
                {
                    return this.NW;
                } else if ( y >= this.selectionBounds.y +
                        this.selectionBounds.height -r && y <
                        this.selectionBounds.y + this.selectionBounds.height +
                        r
                ) {
                    return this.SW;
                }
            } else if (
                    x >= this.selectionBounds.x + this.selectionBounds.width -r &&
                    x < this.selectionBounds.x + this.selectionBounds.width + r
            ) {
                if ( y >= this.selectionBounds.y -r && 
                    y < this.selectionBounds.y + r )
                {
                    return this.NE;
                } else if ( y >= this.selectionBounds.y +
                        this.selectionBounds.height -r && y <
                        this.selectionBounds.y + this.selectionBounds.height +
                        r
                ) {
                    return this.SE;
                }
            }

            if ( x >= this.rotHandleX - r && x < this.rotHandleX + r &&
                 y >= this.rotHandleY - r && y < this.rotHandleY + r ) 
            {
                return this.ROT;
            }
        }

        return null;
    },    

    numSelected: function()
    {
        return this.selection.length;
    },

    shiftSelection: function( x,y )
    {
        this.selectionBounds.x += x;
        this.selectionBounds.y += y;
    },

    addToSelection: function( node, clearIfNew )
    {
        // selection implies no edit node.
        this.setEditNode( null );

        if ( node.selectGeneration === this.selectGeneration ) {
            // already selected.
            return;
        }

        if ( clearIfNew ) {
            this.clearSelection();
        }

        this.selection.push( node );
        node.selectGeneration = this.selectGeneration;

        // if the node is a group member, then also add its siblings.
        if ( node.parent && node.parent.parent !== null ) {
            var parent = node.parent;
            for ( var i = 0; i < parent.children.length; i++ ) {
                this.addToSelection( parent.children[i] );
            }
        }

        // if the node has children, then also add its children.
        if ( node.children.length > 0 ) {
            this.addToSelection( node.children[0] );
        }
    },

    calcSelection: function()
    {
        if ( this.selection.length === 0 ) {
            return;
        }

        var node = this.selection[0];
        this.selectionBounds.x = node.rect.x;
        this.selectionBounds.y = node.rect.y;
        this.selectionBounds.width = node.rect.width;
        this.selectionBounds.height = node.rect.height;

        for ( var i = 1; i < this.selection.length; i++ ) { 
            this.selectionBounds.union( this.selection[i].rect );
        }

        this.rotHandleX = this.selectionBounds.x +
            this.selectionBounds.width - 20;
        this.rotHandleY = this.selectionBounds.y;
    },
    
    clearSelection: function()
    {
        if ( this.selection.length > 0 ) {
            this.selectGeneration += 1;
            this.selection.length = 0;
        }
    },

    assignNumber: function( node )
    {
        node.sortOrder = this.nextNumber++;
        for( var i = 0; i < node.children.length; i++ ) {
            this.assignNumber( node.children[i] );
        }
    },

    sortNodes: function(nodes)
    {
        // first, number all nodes via depth first search.
        this.nextNumber = 0;
        this.assignNumber( this.doc.root );

        // now, sort the selection with lowest numbers first.
        nodes.sort( function( a, b ) {
            return b.sortOrder - a.sortOrder;
        });
    }
           
};

