//---------------------------------------------------
//
//  InkCanvas.js
//      Provides the basic inking support for WPF/e
//      by wrapping an InkPresenter and providing
//      WPF InkCanvas like features
//
//  Author: SamGeo
//
//
// --------------------------------------------------

//todo: consider moving strokesChangedCallback and strokesMovedCallback to a set/get property
function InkCanvas(inkPresenter, wpfeObj, strokesChangedCallback, strokesMovedCallback)
{
    //todo: validate that this is an InkPresenter?
    setInkCanvasCallback(inkPresenter, "MouseLeftButtonDown", inkCanvasDelegate(this, this.handleMouseDown));
	setInkCanvasCallback(inkPresenter, "MouseMove", inkCanvasDelegate(this, this.handleMouseMove));
	setInkCanvasCallback(inkPresenter, "MouseLeftButtonUp", inkCanvasDelegate(this, this.handleMouseUp));
		
    this._stroke = null;
    this._editingMode = "Ink";
    this._editingModeInverted = "EraseByStroke";
    this._inkPresenter = inkPresenter;
    this._lastX = 0;
    this._lastY = 0;
    this._xMoveOffset = 0;
    this._yMoveOffset = 0;
    this._isDown = false;
    this._wpfeObj = wpfeObj;
    this._inkCanvasSelection = null;
    this._strokesChangedCallback = strokesChangedCallback;
    this._strokesMovedCallback = strokesMovedCallback;
    this._strokeId = 1;
    //this._defaultDrawingAttributes = wpfeObj.CreateFromXaml("<DrawingAttributes/>");
}

//strokes get
InkCanvas.prototype.getStrokes = function()
{
    return this._inkPresenter.Strokes;
}

//strokes set
InkCanvas.prototype.setStrokes = function(strokes)
{
    this._inkPresenter.Strokes = strokes;
}

//todo: investigate a bug where setting newstroke.DrawingAttributes.OutlineColor = this._defaultDrawingAttributes.OutlineColor
////DefaultDrawingAttributes get
//InkCanvas.prototype.getDefaultDrawingAttributes = function()
//{
//    return this._defaultDrawingAttributes;
//}

////DefaultDrawingAttributes set
//InkCanvas.prototype.setDefaultDrawingAttributes = function(drawingAttributes)
//{
//    this._defaultDrawingAttributes = drawingAttributes;
//}

//EditingMode get
InkCanvas.prototype.getEditingMode = function()
{
    return this._editingMode;
}

//EditingMode set
InkCanvas.prototype.setEditingMode = function(editingMode)
{
    var lowerEditingMode = editingMode.toLowerCase();
    if (lowerEditingMode != "ink" && lowerEditingMode != "erasebystroke" && lowerEditingMode != "select" && lowerEditingMode != "none")
    {
        throw "Unknown EditingMode";
    }
    if (lowerEditingMode == "ink")
    {
        this._editingMode = "Ink";
    }
    else if (lowerEditingMode == "erasebystroke")
    {
        this._editingMode = "EraseByStroke";
    }
    else if (lowerEditingMode == "select")
    {
        this._editingMode = "Select";
    }
    else if (lowerEditingMode == "none")
    {
        this._editingMode = "None";
    }
}

//EditingModeInverted get
InkCanvas.prototype.getEditingModeInverted = function()
{
    return this._editingModeInverted;
}

//EditingModeInverted set
InkCanvas.prototype.setEditingModeInverted = function(editingMode)
{
    var lowerEditingMode = editingMode.toLowerCase();
    if (lowerEditingMode != "ink" && lowerEditingMode != "erasebystroke" && lowerEditingMode != "select" && lowerEditingMode != "none")
    {
        throw "Unknown EditingMode";
    }
    if (lowerEditingMode == "ink")
    {
        this._editingModeInverted = "Ink";
    }
    else if (lowerEditingMode == "erasebystroke")
    {
        this._editingModeInverted = "EraseByStroke";
    }
    else if (lowerEditingMode == "select")
    {
        this._editingModeInverted = "Select";
    }
    else if (lowerEditingMode == "none")
    {
        this._editingModeInverted = "None";
    }
}

//EditingModeInverted get
InkCanvas.prototype.getActiveEditingMode = function()
{
    //todo: when we check inverted, check it here
    return this._editingMode;
}

//MouseDownLeftButtonDown handler
InkCanvas.prototype.handleMouseDown = function(sender, eventArgs) 
{
    var activeEditingMode = this.getActiveEditingMode();
	if (activeEditingMode != "None" && sender.CaptureMouse())
	{
        if (activeEditingMode == "Ink")
        {
            
            this._strokeId++;
            var uniqueName = this._inkPresenter.Name + "Stroke" + this._strokeId;
            var strokeXaml = "<Stroke xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml' x:Name='" + uniqueName + "'/>";
            this._stroke = this._wpfeObj.content.createFromXAML(strokeXaml);
            this._inkPresenter.Strokes.Add(this._stroke);
            this._stroke.DrawingAttributes.OutlineColor = "white";
            this._stroke.DrawingAttributes.Color = "black";
            var stylusPoints = 
                eventArgs.GetStylusPoints(sender);
            this._stroke.StylusPoints.AddStylusPoints(stylusPoints);
        }
        else if (activeEditingMode == "EraseByStroke" || activeEditingMode == "Select")
        {
            this._isDown = true;
            this._lastX = eventArgs.X;
            this._lastY = eventArgs.Y;
        }
        
        //do we have a selection that we need to clear?
        if (this._inkCanvasSelection != null)
        {
            //time to re-merge selection
            //todo: do this in the correct index / order
            var mergedStrokes = 
                this._wpfeObj.content.CreateFromXAML("<StrokeCollection/>");
            
            for (var i = 0; i < this._inkPresenter.Strokes.Count; i++)
            {
                mergedStrokes.Add(this._inkPresenter.Strokes.GetItem(i));
            }
            
            var selectionInkPresenter = this._inkCanvasSelection.getInkPresenter();
            var xOffset = selectionInkPresenter.GetValue("Canvas.Left");
            var yOffset = selectionInkPresenter.GetValue("Canvas.Top");
            
            var unselectedStrokes = 
                this._inkCanvasSelection.getSelectedStrokes(xOffset, yOffset);
            
            var movedStrokes = this._wpfeObj.content.createFromXaml("<StrokeCollection/>");
            for (var j = 0; j < unselectedStrokes.Count; j++)
            {
                var stroke = unselectedStrokes.GetItem(j);
                movedStrokes.Add(stroke);
                mergedStrokes.Add(stroke);
            }
            
            this._inkPresenter.Strokes = mergedStrokes;

            this._inkPresenter.Children.Remove(this._inkCanvasSelection.getInkPresenter());
            this._inkCanvasSelection = null;
            
            this.raiseStrokesMoved(movedStrokes, xOffset - this._xMoveOffset, yOffset - this._yMoveOffset);
            
            this._xMoveOffset = 0;
            this._yMoveOffset = 0;
        }
    }
}

//MouseMove handler
InkCanvas.prototype.handleMouseMove = function(sender, eventArgs) 
{
    var activeEditingMode = this.getActiveEditingMode();
    if (activeEditingMode == "None")
    {
        return;
    }
    if (activeEditingMode == "Ink")
    {
        if (this._stroke != null)
        {
            var stylusPoints = 
                eventArgs.GetStylusPoints(sender);
            this._stroke.StylusPoints.AddStylusPoints(stylusPoints);
        }
    }
    else if (activeEditingMode == "EraseByStroke")
    {
        if (this._isDown)
        {
            var hitStrokes = 
                this.hitTest(this._lastX,
                             this._lastY,
                             eventArgs.X,
                             eventArgs.Y);
                             
            if (hitStrokes.length > 0)
            {
                var removedStrokes = this._wpfeObj.content.createFromXaml("<StrokeCollection/>");
            
                var newStrokeCollection = this._wpfeObj.content.createFromXAML("<StrokeCollection/>");
                for (var i = 0; i < this._inkPresenter.Strokes.Count; i++)
                {
                    var skip = false;
                    for (var j = 0; j < hitStrokes.length; j++)
                    {
                        if (i == hitStrokes[j])
                        {
                            
                            skip = true;
                            break; //don't copy this stroke
                        }
                    }
                    if (skip)
                    {
                        //we'll be removing this...
                        removedStrokes.Add(this._inkPresenter.Strokes.GetItem(i));    
                    }
                    else
                    {
                        newStrokeCollection.Add(this._inkPresenter.Strokes.GetItem(i));
                    }
                }
                this._inkPresenter.Strokes = newStrokeCollection;
                this.raiseStrokesChanged(null, removedStrokes);
            }
        
            this._lastX = eventArgs.X;
            this._lastY = eventArgs.Y;
        }
    }
    else if (activeEditingMode == "Select")
    {
        if (this._isDown)
        {
            if (this._inkCanvasSelection == null)
            {
                var inkPresenter = 
                    this._wpfeObj.content.createFromXAML("<InkPresenter Opacity='0.7' Background='LightBlue' />");
                this._inkPresenter.Children.Add(inkPresenter);
                this._inkCanvasSelection = new InkCanvasSelection(inkPresenter, this._wpfeObj);
            }
            
            var minX = Math.min(this._lastX, eventArgs.X);
            var minY = Math.min(this._lastY, eventArgs.Y);
            var maxX = Math.max(this._lastX, eventArgs.X);
            var maxY = Math.max(this._lastY, eventArgs.Y);
            var selectionInkPresenter = this._inkCanvasSelection.getInkPresenter();
            selectionInkPresenter.SetValue("Canvas.Left", minX);
            selectionInkPresenter.SetValue("Canvas.Top", minY);
            selectionInkPresenter.SetValue("Width", maxX - minX);
            selectionInkPresenter.SetValue("Height", maxY - minY);
        }
    }
}


//MouseDownLeftButtonUp handler
InkCanvas.prototype.handleMouseUp = function(sender, eventArgs) 
{
    var activeEditingMode = this.getActiveEditingMode();
    if (activeEditingMode == "None")
    {
        return;
    }
    
    sender.ReleaseMouseCapture();
    if (activeEditingMode == "Select" &&
        this._inkCanvasSelection != null && 
        !this._inkCanvasSelection.getHasSelection())
    {
        var minX = Math.min(this._lastX, eventArgs.X);
        var minY = Math.min(this._lastY, eventArgs.Y);
        var maxX = Math.max(this._lastX, eventArgs.X);
        var maxY = Math.max(this._lastY, eventArgs.Y);
        var selectionRect = new Rect();
        selectionRect.X = minX;
        selectionRect.Y = minY;
        selectionRect.Width = maxX - minX;
        selectionRect.Height = maxY - minY;
        
        var selectionResults = this.getStrokesInSelection(selectionRect);
        if (selectionResults.SelectedStrokes.Count > 0)
        {
            //we have a selection folks...
            
            var strokesBounds = this.getStrokeCollectionBounds(selectionResults.SelectedStrokes);
            var selectionInkPresenter = this._inkCanvasSelection.getInkPresenter();
            selectionInkPresenter.SetValue("Canvas.Left", strokesBounds.X);
            selectionInkPresenter.SetValue("Canvas.Top", strokesBounds.Y);
            selectionInkPresenter.SetValue("Width", strokesBounds.Width);
            selectionInkPresenter.SetValue("Height", strokesBounds.Height);
            
            this._inkPresenter.Strokes = selectionResults.UnselectedStrokes;
            this._inkCanvasSelection.setSelectedStrokes( selectionResults.SelectedStrokes,
                                                         strokesBounds.X,
                                                         strokesBounds.Y);
                                                         
            this._xMoveOffset = strokesBounds.X;
            this._yMoveOffset = strokesBounds.Y;
        }
        else if (this._inkCanvasSelection != null)
        {
            //remove... nothing was selected
            this._inkPresenter.Children.Remove(this._inkCanvasSelection.getInkPresenter());
            this._inkCanvasSelection = null;
        }
    }
    if (activeEditingMode == "Ink")
    {
        var addedStrokes = this._wpfeObj.content.createFromXaml("<StrokeCollection/>");
        addedStrokes.Add(this._stroke);
        this.raiseStrokesChanged(addedStrokes, null);
        this._stroke = null;
    }
    else if (activeEditingMode == "EraseByStroke" || activeEditingMode == "Select")
    {
        this._lastX = 0;
        this._lastY = 0;
        this._isDown = false;
    }
}

InkCanvas.prototype.getStrokeCollectionBounds = function(strokeCollection)
{
    var rect = new Rect();
    if (strokeCollection.Count > 0)
    {
        rect = this.getStrokeBounds(strokeCollection.GetItem(0));
        for (var i = 1; i < strokeCollection.Count; i++)
        {
            var stroke = strokeCollection.GetItem(i);
            rect = rect.getUnion(this.getStrokeBounds(stroke));
        }
    }
    return rect;
}

InkCanvas.prototype.getStrokeBounds = function(stroke)
{
    var minX = 99999999;
    var maxX = -99999999;
    var minY = 99999999;
    var maxY = -99999999;
    
    for (var i = 0; i < stroke.StylusPoints.Count; i++)
    {
        var stylusPoint = stroke.StylusPoints.GetItem(i);
        if (stylusPoint.X < minX)
        {
            minX = stylusPoint.X;
        }
        if (stylusPoint.X > maxX)
        {
            maxX = stylusPoint.X;
        }
        if (stylusPoint.Y < minY)
        {
            minY = stylusPoint.Y;
        }
        if (stylusPoint.Y > maxY)
        {
            maxY = stylusPoint.Y;
        }
    }
    
    //inflate for pen width
    minX -= (stroke.DrawingAttributes.Width / 2) + 1;
    minY -= (stroke.DrawingAttributes.Height / 2) + 1;
    maxX += stroke.DrawingAttributes.Width + 2;
    maxY += stroke.DrawingAttributes.Height + 2;
    
    var rect = new Rect()
    rect.X = minX;
    rect.Y = minY;
    rect.Width = maxX - minX;
    rect.Height = maxY - minY;
    
    return rect;
}

//returns the selected and unselected strokes
InkCanvas.prototype.getStrokesInSelection = function(rect)
{
    var selectionResults = new SelectionResults();
    selectionResults.SelectedStrokes = this._wpfeObj.content.createFromXAML("<StrokeCollection/>");
    selectionResults.UnselectedStrokes = this._wpfeObj.content.createFromXAML("<StrokeCollection/>");
    
    for (var i = 0; i < this._inkPresenter.Strokes.Count; i++)
    {
        var stroke = this._inkPresenter.Strokes.GetItem(i);
        var strokeRect = this.getStrokeBounds(stroke);
        if (rect.contains(strokeRect))
        {
            selectionResults.SelectedStrokes.Add(stroke);
        }
        else
        {
            selectionResults.UnselectedStrokes.Add(stroke);
        }
    }
    return selectionResults;
}


//simple hit test method
InkCanvas.prototype.hitTest = function(x1, y1, x2, y2)
{
    var hitStrokeIndexes = new Array();
    for (var i = 0; i < this._inkPresenter.Strokes.Count; i++)
    {
        var stroke = this._inkPresenter.Strokes.GetItem(i);
        if (stroke.StylusPoints.Count > 0)
        {
            var lastStylusPoint = stroke.StylusPoints.GetItem(0);
            for (var j = 1; j < stroke.StylusPoints.Count; j++)
            {
                var stylusPoint = stroke.StylusPoints.GetItem(j);
                if (this.doLinesIntersect(lastStylusPoint.X,
                                          lastStylusPoint.Y,
                                          stylusPoint.X,
                                          stylusPoint.Y,
                                          x1,
                                          y1,
                                          x2,
                                          y2))
                {
                    hitStrokeIndexes[hitStrokeIndexes.length] = i;
                    break;
                }      
                lastStylusPoint = stylusPoint;   
            }
        }
    }
    return hitStrokeIndexes;
}

//calls the strokes changed callback
InkCanvas.prototype.raiseStrokesChanged = function(addedStrokes, removedStrokes)
{
    if (this._strokesChangedCallback != null)
    {
        this._strokesChangedCallback(addedStrokes, removedStrokes);
    }
} 

//calls the strokes moved callback
InkCanvas.prototype.raiseStrokesMoved = function(movedStrokes, xOffset, yOffset)
{
    if (this._strokesMovedCallback != null)
    {
        this._strokesMovedCallback(movedStrokes, xOffset, yOffset);
    }
} 

InkCanvas.prototype.doLinesIntersect = function(line1StartX, line1StartY, line1EndX, line1EndY, line2StartX, line2StartY, line2EndX, line2EndY)
{
      var a1 = line1EndY - line1StartY;
      var a2 = line1StartX - line1EndX;
      var a3 = (line1EndX * line1StartY) - (line1StartX * line1EndY);
      var a4 = line2EndY - line2StartY;
      var a5 = line2StartX - line2EndX;
      var a6 = (line2EndX * line2StartY) - (line2StartX * line2EndY);
      var a7 = (a1 * a5) - (a4 * a2);
      if (a7 != 0)
      {
            var num4 = 0;
            var num5 = 0;
            var num6 = 0;
            var num7 = 0;
            var num8 = 0;
            var num9 = 0;
            var num10 = 0;
            var num11 = 0;
            var num2 = ((a2 * a6) - (a5 * a3)) / a7;
            var num1 = ((a4 * a3) - (a1 * a6)) / a7;
            if (line1StartX < line1EndX)
            {
                  num11 = Math.floor(line1StartX);
                  num10 = Math.ceil(line1EndX);
            }
            else
            {
                  num11 = Math.floor(line1EndX);
                  num10 = Math.ceil(line1StartX);
            }
            if (line2StartX < line2EndX)
            {
                  num9 = Math.floor(line2StartX);
                  num8 = Math.ceil(line2EndX);
            }
            else
            {
                  num9 = Math.floor(line2EndX);
                  num8 = Math.ceil(line2StartX);
            }
            if (line1StartY < line1EndY)
            {
                  num7 = Math.floor(line1StartY);
                  num6 = Math.ceil(line1EndY);
            }
            else
            {
                  num7 = Math.floor(line1EndY);
                  num6 = Math.ceil(line1StartY);
            }
            if (line2StartY < line2EndY)
            {
                  num5 = Math.floor(line2StartY);
                  num4 = Math.ceil(line2EndY);
            }
            else
            {
                  num5 = Math.floor(line2EndY);
                  num4 = Math.ceil(line2StartY);
            }
            if ((((num11 <= num2) && (num2 <= num10)) && ((num7 <= num1) && (num1 <= num6))) && (((num9 <= num2) && (num2 <= num8)) && ((num5 <= num1) && (num1 <= num4))))
            {
                  return true;
            }
      }
      if (line1EndX == line2StartX && line1EndY == line2StartY)
      {
            return true;
      }
      return false;
}




// Helper method for creating per-object callbacks
/// @param target object to invoke the callback on
/// @param callback method to be called on the object
function inkCanvasDelegate(target, callback) {
	var func = function() {
		callback.apply(target, arguments);
	}
	return func;
}

/// Hook up the specified event to the specified inkCanvasDelegate.
/// Allows javascript inkCanvasDelegates to be used and dynamically
/// creates the global method.
/// @param target WPF/e element to set the event on
/// @param eventName name of the event to set, ie "MouseEnter"
/// @param inkCanvasDelegate function to call when the event occurs.
function setInkCanvasCallback(target, eventName, inkCanvasDelegate) {
	if (!window.methodID)
		window.methodID = 0;
	
	var callbackName = "uniqueCallback" + (window.methodID++);
	eval(callbackName + " = inkCanvasDelegate;");
	
	target.addEventListener(eventName, "javascript:" + callbackName);
}

function InkCanvasSelection(inkPresenter, wpfeObj)
{
    setInkCanvasCallback(inkPresenter, "MouseLeftButtonDown", inkCanvasDelegate(this, this.handleMouseDown));
	setInkCanvasCallback(inkPresenter, "MouseMove", inkCanvasDelegate(this, this.handleMouseMove));
	setInkCanvasCallback(inkPresenter, "MouseLeftButtonUp", inkCanvasDelegate(this, this.handleMouseUp));
    this._inkPresenter = inkPresenter;
    this._isDown = false;
    this._isEnabled = true;
    this._lastX = 0;
    this._lastY = 0;
    this._wpfeObj = wpfeObj;
    this._hasSelection = false;
}

//simple helper to ref the inkPresenter
InkCanvasSelection.prototype.getInkPresenter = function()
{
    return this._inkPresenter;
}

InkCanvasSelection.prototype.getHasSelection = function()
{
    return this._hasSelection;   
}

InkCanvasSelection.prototype.setSelectedStrokes = function(strokes, xOffset, yOffset)
{
    //translate from InkCanvas to InkCanvasSelection relative coordinates
    //xOffset is the dist from InkCanvas's origin to InkCanvasSelection.Left
    for (var i = 0; i < strokes.Count; i++)
    {
        var stroke = strokes.GetItem(i);
       
        for (var j = 0; j < stroke.StylusPoints.Count; j++)
        {
            var stylusPoint = stroke.StylusPoints.GetItem(j);
            stylusPoint.X -= xOffset;
            stylusPoint.Y -= yOffset;
        }
    }
    
    this._inkPresenter.Strokes = strokes;
    this._hasSelection = true;
}

InkCanvasSelection.prototype.getSelectedStrokes = function(xOffset, yOffset)
{
    var newStrokes = this._wpfeObj.content.CreateFromXAML("<StrokeCollection/>");
    for (var i = 0; i < this._inkPresenter.Strokes.Count; i++)
    {
        var stroke = this._inkPresenter.Strokes.GetItem(i);
        for (var j = 0; j < stroke.StylusPoints.Count; j++)
        {
            var stylusPoint = stroke.StylusPoints.GetItem(j);
            stylusPoint.X += xOffset;
            stylusPoint.Y += yOffset;
        }
        newStrokes.Add(stroke);
    }
    
    this._hasSelection = false;
    return newStrokes;
}

//MouseDownLeftButtonDown handler
InkCanvasSelection.prototype.handleMouseDown = function(sender, eventArgs) 
{
    if (this._isEnabled)
    {
        if (sender.CaptureMouse())
	    {
	        this._isDown = true;
	        this._lastX = eventArgs.X;
	        this._lastY = eventArgs.Y;
	    }
	}
}

//MouseMove handler
InkCanvasSelection.prototype.handleMouseMove = function(sender, eventArgs) 
{
    if (this._isEnabled && this._isDown)
    {
        var xDelta = eventArgs.X - this._lastX;
        var yDelta = eventArgs.Y - this._lastY;
        
        var left = this._inkPresenter.GetValue("Canvas.Left");
        var top = this._inkPresenter.GetValue("Canvas.Top");
        
        this._inkPresenter.SetValue("Canvas.Left", left + xDelta)
        this._inkPresenter.SetValue("Canvas.Top", top + yDelta)
        
        this._lastX = eventArgs.X;
        this._lastY = eventArgs.Y;
    }
}

InkCanvasSelection.prototype.handleMouseUp = function(sender, eventArgs) 
{
    if (this._isEnabled)
    {
        this._isDown = false;
    }
}

///rect primitive
function Rect()
{
    this.X = 0;
    this.Y = 0;
    this.Height = 0;
    this.Width = 0;
}

Rect.prototype.getIntersection = function(rect)
{
    var retRect = new Rect();
    var minX = Math.max(rect.X, this.X);
    var minY = Math.max(rect.Y, this.Y);
    var maxX = Math.min(rect.X + rect.Width, this.X + this.Width);
    var maxY = Math.min(rect.Y + rect.Height, this.Y + this.Height);
    
    if (minX <= maxX && minY <= maxY)
    {
        retRect.X = minX;
        retRect.Y = minY;
        retRect.Width = maxX - retRect.X;
        retRect.Height = maxY - retRect.Y;
    }
    return retRect;
}

Rect.prototype.getUnion = function(rect)
{
    var retRect = new Rect();
    var minX = Math.min(rect.X, this.X);
    var minY = Math.min(rect.Y, this.Y);
    var maxX = Math.max(rect.X + rect.Width, this.X + this.Width);
    var maxY = Math.max(rect.Y + rect.Height, this.Y + this.Height);
    
    if (minX <= maxX && minY <= maxY)
    {
        retRect.X = minX;
        retRect.Y = minY;
        retRect.Width = maxX - retRect.X;
        retRect.Height = maxY - retRect.Y;
    }
    return retRect;
}

Rect.prototype.contains = function(rect)
{
    if (rect.X >= this.X &&
        rect.Y >= this.Y &&
        rect.X + rect.Width <= this.X + this.Width &&
        rect.Y + rect.Height <= this.Y + this.Height)
    {
        return true;
    } 
    return false;    
}

function SelectionResults()
{
    this.SelectedStrokes = null;
    this.UnselectedStrokes = null;
}
