(function ($) {

    // The single diagram global object
    // It's properties are listed here for reference.
    var DIAGRAM = {};
    DIAGRAM.status = 'pointer';
    DIAGRAM.statusDetail = null;
    DIAGRAM.startCoords = {canvas:{x:0, y:0}, model:{x:0, y:0}};
    DIAGRAM.currentCoords = {canvas:{x:0, y:0}, model:{x:0, y:0}};
    DIAGRAM.previousCoords = {canvas:{x:0, y:0}, model:{x:0, y:0}};
    DIAGRAM.hitItem = null;
    DIAGRAM.draggingArcId = null;
    DIAGRAM.startNodeId = null;
    DIAGRAM.prevMousedownTime = 0;
    
  /***********************************************************
   *         diagram widget
   ***********************************************************
   */
    $.widget('systo.diagram', {
        options: {
            allowEditing:false,
            model:'',
            canvasColour: 'white',
            canvasWidth: 700,
            canvasHeight: 400,
            offsetx: 0,
            offsety: 0,
            scale: 1,
            selectNode: function (node) {
                return true;
            },
            selectArc: function (arc) {
                return true;
            }
        },

        widgetEventPrefix: 'diagram:',

        _create: function () {
            var self = this;
            console.debug(self);
            this.element.addClass('diagram-1');

            $(this.element).                
                bind( "resize", function(event, ui) {
                    var canvas = $(self.element).find('canvas');
                    canvas.attr('width',$(self.element).width());
                    canvas.attr('height',$(self.element).height());
                    var context = canvas[0].getContext("2d");
                    var model = SYSTO.models[self.options.model];
                    redraw(model, context, self, self.options)
                });

            var div = $('<div style="width:100%; height:100%;"></div>');
            var canvas = $('<canvas></canvas>').
                mousedown(function(event) {
                    mouseDown(event, self, canvas[0]);
                }).
                mousemove(function(event) {
                    mouseMove(event, self, canvas[0]);
                }).
                mouseup(function(event) {
                    mouseUp(event, self, canvas[0]);
                });

            $(div).append(canvas);

            var dummy = $('<div class="diagram_listener" style="display:none;" >Click me!</div>').
                click(function(event) {
                    var context = canvas[0].getContext("2d");
                    var model = SYSTO.models[self.options.model];
                    redraw(model, context, self, self.options)
                });
            $(div).append(dummy);

            //this._container = $(this.element).append(div);
            var dummy = $('<div class="event_listener" '+
                    'style="display:none;" >Click me!</div>').
                click(function(event) {
                    //console.debug('diagram listener');
                });
            $(div).append(dummy);
            //this._container = $(this.element).append(dummy);

            var buttonZoomin = $('<button style="position:absolute; width:25px; height:25px; left:0px; top:0px; font-size:16px;" title="Zoom in"><b>+</b></button>').
                mousedown(function(event) {
                    var scalingFactor = 1.2;
                    self.options.scale = self.options.scale*scalingFactor;
                    var canvasWidth = $(canvas).width();
                    var canvasHeight = $(canvas).height();
                    var w2 = canvasWidth/2;
                    var h2 = canvasHeight/2;
                    self.options.offsetx = -1*((w2-self.options.offsetx)*scalingFactor - w2);
                    self.options.offsety = -1*((h2-self.options.offsety)*scalingFactor - h2);
                    var context = canvas[0].getContext("2d");
                    context.setTransform(self.options.scale, 0, 0, self.options.scale, self.options.offsetx, self.options.offsety);
                    var model = SYSTO.models[self.options.model];
                    clearCanvas(context, self);
                    renderArcs(model, self.options, context);
                    renderNodes(model, self.options, context);
                });
            var buttonZoomout = $('<button style="position:absolute; width:25px; height:25px; left:0px; top:24px; font-size:18px;" title="Zoom out"><b>-</b></button>').
                mousedown(function(event) {
                    var scalingFactor = 1.2;
                    self.options.scale = self.options.scale/scalingFactor;
                    var canvasWidth = $(canvas).width();
                    var canvasHeight = $(canvas).height();
                    var w2 = canvasWidth/2;
                    var h2 = canvasHeight/2;
                    self.options.offsetx = -1*((w2-self.options.offsetx)/scalingFactor - w2);
                    self.options.offsety = -1*((h2-self.options.offsety)/scalingFactor - h2);
                    var context = canvas[0].getContext("2d");
                    context.setTransform(self.options.scale, 0, 0, self.options.scale, self.options.offsetx, self.options.offsety);
                    var model = SYSTO.models[self.options.model];
                    clearCanvas(context, self);
                    renderArcs(model, self.options, context);
                    renderNodes(model, self.options, context);
                });
            var buttonZoomtofit = $('<button style="position:absolute; padding:0px; width:25px; height:25px; left:0px; top:48px;" title = "Zoom to fit"><b>[ ]</b></button>').
                mousedown(function(event) {
                    var model = SYSTO.models[self.options.model];
                    var canvasWidth = $(canvas).width();
                    var canvasHeight = $(canvas).height();
                    var modelSize = maxXY(model);
                    var modelWidth = modelSize.xmax - modelSize.xmin;
                    var modelHeight = modelSize.ymax - modelSize.ymin;
                    var canvasRatio = canvasHeight/canvasWidth;
                    var modelRatio = modelHeight/modelWidth;
                    if (modelRatio > canvasRatio) {
                        self.options.scale = canvasHeight/modelHeight;
                    } else {
                        self.options.scale = canvasWidth/modelWidth;
                    }
                    canvasCentrex = canvasWidth/2;
                    canvasCentrey = canvasHeight/2;
                    modelCentrex = (modelSize.xmin+modelSize.xmax)/2;
                    modelCentrey = (modelSize.ymin+modelSize.ymax)/2;
                    var w2 = canvasWidth/2;
                    var h2 = canvasHeight/2;
                    //this.offsetx = -1*((w2)/scalingFactor - w2);
                    //this.offsety = -1*((h2)/scalingFactor - h2);
                    self.options.offsetx = 0 - modelSize.xmin*self.options.scale;
                    self.options.offsety = 0 - modelSize.ymin*self.options.scale;
                    var context = canvas[0].getContext("2d");
                    context.setTransform(self.options.scale, 0, 0, self.options.scale, self.options.offsetx, self.options.offsety);
                    clearCanvas(context, self);
                    renderArcs(model, self.options, context);
                    renderNodes(model, self.options, context);
                });
            $(div).append(buttonZoomin).append(buttonZoomout).append(buttonZoomtofit);
/*
            // This is preliminary code for a zoom slider - probably won't go down that path.
            var zoomSlider = $('<div class="slider_slider" style="float:left; width:150px; height:11px;"></div>').
                slider({
                    orientation: "horizontal",
                    min: 0.2,
                    max: 2,
                    step: 0.02,
                    value: 1,
                    animate: 'fast',
                    slide: function (event, ui) {
                        var scalingFactor = ui.value;
                        self.options.scale = scalingFactor;
                        var canvasWidth = $(canvas).width();
                        var canvasHeight = $(canvas).height();
                        var w2 = canvasWidth/2;
                        var h2 = canvasHeight/2;
                        //self.options.offsetx = -1*((w2-self.options.offsetx)/scalingFactor - w2);
                        //self.options.offsety = -1*((h2-self.options.offsety)/scalingFactor - h2);
                        var context = canvas[0].getContext("2d");
                        context.setTransform(self.options.scale, 0, 0, self.options.scale, self.options.offsetx, self.options.offsety);
                        var model = SYSTO.models[self.options.model];
                        clearCanvas(context, self);
                        renderArcs(model, self.options, context);
                        renderNodes(model, self.options, context);
                    }
                });
            $(div).append(zoomSlider);
*/

            // Buttons for adding the diagramming toolbar: adding nodes and arcs.

            // First, we add a 'pointer' button, which is common to all languages.
            var left = 40;
            var button = $('<button style="position:absolute; width:25px; height:25px; left:'+left+'px; top:0px; font-size:16px;" title="Pointer"><b>P</b></button>').
                mousedown(function(event) {
                    DIAGRAM.status = 'pointer';
                });
            $(div).append(button);
            left += 30;

            // Now we add (first) the nodes and (second) the arcs which are specific to
            // a paticular language, if 'allowEditing' is true for this diagram.
            if (this.options.allowEditing) {
                var model = SYSTO.models[this.options.model];
                console.debug(model);
                var language = SYSTO.languages[model.meta.language];

                var nodeTypes = language.NodeType;
                console.debug(language);
                for (var nodeTypeId in nodeTypes) {
                    var nodeType = nodeTypes[nodeTypeId];
                    console.debug(nodeType);
                    if (nodeType.has_button) {
                        var firstLetter = nodeType.button_label.substring(0,2);
                    } else {
                        continue;
                    }
                    button = $('<button style="position:absolute; width:35px; height:25px; left:'+left+'px; top:0px; font-size:16px;" title="nodeTypeId"><b>'+firstLetter+'</b></button>').
                    mousedown({nodeTypeId:nodeTypeId}, function( event) {
                        DIAGRAM.status = 'add_node';
                        DIAGRAM.statusDetail = event.data.nodeTypeId;
                        console.debug(event.data.nodeTypeId);
                    });
                    $(div).append(button);
                    left += 34;
                }

                var arcTypes = language.ArcType;
                console.debug(language);
                left += 5;
                for (var arcTypeId in arcTypes) {
                    var arcType = arcTypes[arcTypeId];
                    console.debug(arcType);
                    if (arcType.has_button) {
                        var firstLetter = arcType.button_label.substring(0,2);
                    } else {
                        continue;
                    }
                    button = $('<button style="position:absolute; width:35px; height:25px; left:'+left+'px; top:0px; font-size:16px;" title="nodeTypeId"><b>'+firstLetter+'</b></button>').
                    mousedown({arcTypeId:arcTypeId}, function(event) {
                        DIAGRAM.status = 'add_arc';
                        DIAGRAM.statusDetail = event.data.arcTypeId;
                    });
                    $(div).append(button);
                    left += 34;
                }
            }


            // Node dialogue window
            var nodeDialogue = $(
                '<div id="node_dialogue" style="visibility:hidden; position:absolute; width:400px; height:100px; left:100px; top:100px; font-size:16px; background:white; border:solid 1px black; padding:20px;">'+
                    '<p>Fred</p>'+
                            '<label for="name">Equation </label>'+
                            '<input type="text" name="equation" id="equation" style="width:300px; border:solid 1px gray" value="12345"/>'+
                        '<button class="ok">OK</button>'+
                        '<button class="cancel">Cancel</button>'+
                '</div>)');

            nodeDialogue.find('.ok').click(function(event) {
                var nodeId = $('#node_dialogue').find('p').text();
                var equation = $('#node_dialogue').find('#equation');
                var model = SYSTO.models[self.options.model];
                var node = model.nodes[nodeId];
                node.extras.equation.value = equation.val();
                generateSimulationFunction(model);
                resultsObject = simulate(model);
                SYSTO.results = resultsObject.results;
                SYSTO.resultStats = resultsObject.resultStats;
                $('.event_listener').trigger('click');
                $('#node_dialogue').css('visibility','hidden');
            });
            nodeDialogue.find('.cancel').click(function(event) {
                $('#node_dialogue').css('visibility','hidden');
            });

            $(div).append(nodeDialogue);

            this._container = $(this.element).append(div);

            var model = SYSTO.models[this.options.model];

            var context = canvas[0].getContext("2d");
            clearCanvas(context, this);
            renderArcs(model, self.options, context);
            renderNodes(model, self.options, context);

            this._setOptions({
            });
        },

        _destroy: function () {
            this.element.removeClass('diagram-1');
            this.element.empty();
            this._super();
        },
        _setOption: function (key, value) {
            var self = this;
            var prev = this.options[key];
            var fnMap = {
                selectNode:function() {
                    value();
                    var canvas = $(self.element).find('canvas');
                    var context = canvas[0].getContext("2d");
                    var model = SYSTO.models[self.options.model];
                    clearCanvas(context, self);
                    renderArcs(model, self.options, context);
                    renderNodes(model, self.options, context);
                },
                selectArc:function() {
                    value();
                    var canvas = $(self.element).find('canvas');
                    var context = canvas[0].getContext("2d");
                    var model = SYSTO.models[self.options.model];
                    clearCanvas(context, self);
                    renderArcs(model, self.options, context);
                    renderNodes(model, self.options, context);
                }
            };

            // base
            this._super(key, value);

            if (key in fnMap) {
                fnMap[key]();

                // Fire event
                this._triggerOptionChanged(key, prev, value);
            }
        },

        _triggerOptionChanged: function (optionKey, previousValue, currentValue) {
            this._trigger('setOption', {type: 'setOption'}, {
                option: optionKey,
                previous: previousValue,
                current: currentValue
            });
        }
    });




function redraw(model, context, widget, options) {
    clearCanvas(context, widget);
    renderArcs(model, options, context);
    renderNodes(model, options, context);
}



function clearCanvas(context, widget) {

    var options = widget.options;
    context.canvas.width = $(widget.element).width();
    context.canvas.height = $(widget.element).height();
    context.save();
    context.setTransform(1,0,0,1,0,0);
    context.clearRect(0,0,context.canvas.width,context.canvas.height);
    context.fillStyle = 'white';
    context.fillRect(0,0,context.canvas.width,context.canvas.height);
    context.restore();
    context.setTransform(options.scale, 0, 0, options.scale, options.offsetx, options.offsety);
}


function renderNodes(model, options, context) {

    var nodeList = model.nodes;

    for (var nodeId in nodeList) {
        var node = nodeList[nodeId];
        if (options.selectNode(node)) {
            var nodeTypeId = node.type;
            var nodeType = SYSTO.languages[model.meta.language].NodeType[nodeTypeId];
            context.strokeStyle = nodeType.border_colour.set.normal;
            context.lineWidth = nodeType.line_width.set.normal;
            context.fillStyle = nodeType.fill_colour.set.normal;

            if (nodeType.shape === 'rectangle') {
                var width = nodeType.width;
                var height = nodeType.height;
                context.beginPath();
                context.fillRect(node.centrex-width/2, node.centrey-height/2, width, height);
                context.strokeRect(node.centrex-width/2, node.centrey-height/2, width, height);
                if (nodeType.has_label) {
                    context.beginPath();
                    context.fillStyle = 'black';
                    context.textAlign = 'center';
                    context.textBaseline = 'middle';
                    context.lineWidth = 1;
                    this.labelWidth = printAtWordWrap(context, node.label, node.centrex+nodeType.text_shiftx, 
                        node.centrey+nodeType.text_shifty, 15, 100);
                }

            } else if (nodeType.shape === 'circle') {
                var r = nodeType.radius;
                context.beginPath();
                context.arc(node.centrex, node.centrey, r, 0, Math.PI*2, true);   
                context.stroke();  
                context.fill(); 
                if (nodeType.has_label) {
                    context.fillStyle = 'black';
                    context.textAlign = 'center';
                    context.textBaseline = 'middle';
                    context.lineWidth = 1;
                    this.labelWidth = printAtWordWrap(context, node.label, node.centrex+nodeType.text_shiftx, 
                        node.centrey+nodeType.text_shifty, 15, 100);
                }
            }
        }
    }
}


function renderArcs(model, options, context) {
    var nodeList = model.nodes;
    var arcList = model.arcs

    for (var arcId in arcList) {
        var arc = arcList[arcId];
        if (options.selectArc(arc)) {
            var arcTypeId = arc.type;
            var arcType = SYSTO.languages[model.meta.language].ArcType[arcTypeId];
            context.strokeStyle = arcType.line_colour.set.normal;
            context.lineWidth = arcType.linewidth;
            context.fillStyle = arcType.fill_colour.set.normal;

            if (arcType.shape === 'straight' || arcType.shape === 'curved') {    // TODO: handle curved arcs below!
                var startNode = nodeList[arc.start_node_id];
                var endNode = nodeList[arc.end_node_id];
                var endNodeType = SYSTO.languages[model.meta.language].NodeType[endNode.type];
                if (endNodeType.shape === 'rectangle') {
                    var w2 = endNodeType.width/2;
                    var h2 = endNodeType.height/2;
                    var ratio = h2/w2;
                    var coords = interceptRectangle(startNode.centrex, startNode.centrey, 
                        endNode.centrex, endNode.centrey, w2, h2, ratio);
                } else if (endNodeType.shape === 'circle') {
                    var radius = endNodeType.radius;
                    var coords = interceptCircle(startNode.centrex, startNode.centrey, 
                        endNode.centrex, endNode.centrey, radius);
                }
                var arrowheadCoords = arrowhead(startNode.centrex, startNode.centrey, coords.x, coords.y, 
                    arcType.arrow_length, arcType.arrow_width, arcType.arrow_length);

                context.beginPath();
                context.moveTo(startNode.centrex, startNode.centrey);
                context.lineTo(arrowheadCoords.basex, arrowheadCoords.basey);
                context.lineTo(arrowheadCoords.leftx, arrowheadCoords.lefty);
                context.lineTo(coords.x, coords.y);
                context.lineTo(arrowheadCoords.rightx, arrowheadCoords.righty);
                context.lineTo(arrowheadCoords.basex, arrowheadCoords.basey);
                context.stroke();
                context.fill();
                context.closePath();
            }
        } else if (arcType.shape === 'curved') {
        }
    }
}






function interceptRectangle(x0, y0, x1, y1, w2, h2, rectangle_ratio) {

    var abs_dx;
    var abs_dy;
    var borderx;
    var bordery;
    var dx = x0 - x1;
    var dy = y0 - y1;
    var line_ratio;
    var signx;
    var signy;

    if (dx>=0) {
        signx = 1;
    } else {
        signx = -1;
    }
    if (dy>=0) {
        signy = 1;
    } else {
        signy = -1;
    }
    abs_dx = Math.abs(dx);
    abs_dy = Math.abs(dy);
    if (abs_dx>0) {
        line_ratio = abs_dy / abs_dx;
    } else {
        line_ratio = 9999;
    }
    if (line_ratio<rectangle_ratio) {
        borderx = signx * w2;
        bordery = signy * abs_dy * w2 / abs_dx;
    } else {
        borderx = signx * abs_dx * h2 / abs_dy;
        bordery = signy * h2;
    }  
    return {x: x1 + borderx,
            y: y1 + bordery}
}



function interceptCircle(x0, y0, x1, y1, radius) {

    var angle = Math.atan2(y0 - y1, x0 - x1);

    return {x: x1 + radius * Math.cos(angle),
            y: y1 + radius * Math.sin(angle)}
}

        
        
function arrowhead(originx, originy, tipx, tipy, arrowLength, arrowWidth, centreLength) {
    var angle1 = Math.atan2(tipy-originy,tipx-originx);
    var angle2 = Math.atan2(arrowWidth,arrowLength);
    var hypot  = Math.sqrt(arrowWidth*arrowWidth+arrowLength*arrowLength);
    var leftx  = tipx-hypot*Math.cos(angle1+angle2);
    var lefty  = tipy-hypot*Math.sin(angle1+angle2);
    var rightx = tipx-hypot*Math.cos(angle1-angle2);
    var righty = tipy-hypot*Math.sin(angle1-angle2);
    var basex  = tipx-centreLength*Math.cos(angle1);
    var basey  = tipy-centreLength*Math.sin(angle1);
    return {leftx:leftx, lefty:lefty, rightx:rightx, righty:righty, basex:basex, basey:basey};
}



function printAtWordWrap(context, text, x, y, lineHeight, fitWidth) {
   fitWidth = fitWidth || 0;
    
   if (fitWidth <= 0)
   {
      context.fillText( text, x, y );
      return;
   }
   var words = text.split(' ');
   var currentLine = 0;
   var idx = 1;
   while (words.length > 0 && idx <= words.length)
   {
      var str = words.slice(0,idx).join(' ');
      var w = context.measureText(str).width;
      if ( w > fitWidth )
      {
         if (idx==1)
         {
                idx=2;
         }
         context.fillText( words.slice(0,idx-1).join(' '), x, y + (lineHeight*currentLine) );
         currentLine++;
         words = words.splice(idx-1);
         idx = 1;
         var returnWidth = fitWidth;
      }
      else
      {  idx++;
         returnWidth = w;
}
   }
   if  (idx > 0) context.fillText( words.join(' '), x, y + (lineHeight*currentLine) );

    return returnWidth;
}



// ======================================== MOUSE EVENT HANDLER ==========================

// Note that DIAGRAM.startCoords, DIAGRAM.currentCoords and DIAGRAM.previousCoords are global.
// All 3 are object literals with structure  {canvas:{x:-,y:-}, model:{x:-,y:-}}
// for canvas and model coordinates respectively.

// Note that DIAGRAM.hitItem is global.

function mouseDown(event, widget, canvas) {
    var options = widget.options;
    var model = SYSTO.models[options.model];
    var context = canvas.getContext("2d");

    var canvasCoords = eventToCanvas(event, canvas);
    DIAGRAM.startCoords.canvas.x = canvasCoords.x - options.offsetx;
    DIAGRAM.startCoords.canvas.y = canvasCoords.y - options.offsety;
    DIAGRAM.startCoords.model = canvasToModel(canvasCoords, options);

    DIAGRAM.hitItem = getHitItem(DIAGRAM.startCoords.model, model);   
    console.debug(DIAGRAM.hitItem);

    if (DIAGRAM.status === 'pointer' && DIAGRAM.hitItem.typeId === 'canvas') {
        DIAGRAM.status = 'start_pan';

    } else if (DIAGRAM.status === 'pointer' && DIAGRAM.hitItem.typeId === 'node') {
        if (DIAGRAM.hitItem.object.type !== 'cloud') {  // TODO: generalise!
            var node = DIAGRAM.hitItem.object;
            node.selected = true;   // TODO: 'node' should have a 'local' poperty object,
                    // to hold all properties set by Systo.
            model.selectedNodes = {}; // This creates a new property OR clears an existing one.
            model.selectedNodes[node.id] = true;  // TODO: develop a proper mechanism for
                    // handling selections.
                    // This needs to be a list, since there can be lots of them.

            var mousedownTime = new Date();
            if (mousedownTime - DIAGRAM.prevMousedownTime<500) {
                $('#node_dialogue').find('p').text(DIAGRAM.hitItem.object.id);
                $('#node_dialogue').find('#equation').
                    val(DIAGRAM.hitItem.object.extras.equation.value);
                $('#node_dialogue').css('visibility','visible');
                DIAGRAM.status = 'pointer';
                DIAGRAM.prevMousedownTime = mousedownTime;
                return;
            }
            DIAGRAM.prevMousedownTime = mousedownTime;
            DIAGRAM.status = "hit_node";
        };
        if (DIAGRAM.hitItem.object.type !== 'valve') {  // TODO: generalise!
            DIAGRAM.status = 'hit_node';
        }

    } else if (DIAGRAM.status === 'add_node') {    // TODO: generalise this.
        var newNodeId = getNewNodeId(model, DIAGRAM.statusDetail);
        model.nodes[newNodeId] = createNode(newNodeId, DIAGRAM.statusDetail, DIAGRAM.startCoords.model);
        //redraw(model, context, widget, options)
        $('.diagram_listener').trigger('click');
        DIAGRAM.status = 'pointer';

    } else if (DIAGRAM.status === 'add_arc') {
        createDotNodeType(model.meta.language);
        model.nodes['dot1'] = createNode('dot1', 'dot', DIAGRAM.startCoords.model);
        var newArcId = getNewArcId(model, DIAGRAM.statusDetail);
        DIAGRAM.draggingArcId = newArcId;
        DIAGRAM.status = 'start_arc';
        if (DIAGRAM.hitItem.typeId === 'node') {
            model.arcs[newArcId] = createArc(newArcId, DIAGRAM.statusDetail, DIAGRAM.hitItem.object);
            DIAGRAM.startNodeId = DIAGRAM.hitItem.object.id;
        } else {
            var newNodeId = getNewNodeId(model, 'cloud');     // TODO: Generalise this!
            model.nodes[newNodeId] = createNode(newNodeId, 'cloud', DIAGRAM.startCoords.model);
            model.arcs[newArcId] = createArc(newArcId, DIAGRAM.statusDetail, model.nodes[newNodeId]);
            DIAGRAM.startNodeId = newNodeId;
        }

    } else if (DIAGRAM.status === 'add_flow') {
        createDotNodeType();
        model.nodes['dot1'] = createNode('dot1', 'dot', DIAGRAM.startCoords.model);
        var newArcId = getNewArcId(model, 'flow');
        DIAGRAM.draggingArcId = newArcId;
        DIAGRAM.status = 'start_arc';
        if (DIAGRAM.hitItem.typeId === 'node') {
            model.arcs[newArcId] = createArc(newArcId, 'flow', DIAGRAM.hitItem.object);
            DIAGRAM.startNodeId = DIAGRAM.hitItem.object.id;
        } else {
            var newNodeId = getNewNodeId(model, 'cloud');
            model.nodes[newNodeId] = createNode(newNodeId, 'cloud', DIAGRAM.startCoords.model);
            model.arcs[newArcId] = createArc(newArcId, 'flow', model.nodes[newNodeId]);
            DIAGRAM.startNodeId = newNodeId;
        }

    } else if (DIAGRAM.status === 'add_influence') {
        if (DIAGRAM.hitItem.typeId === 'node') {
            createDotNodeType();
            model.nodes['dot1'] = createNode('dot1', 'dot', DIAGRAM.startCoords.model);
            var newArcId = getNewArcId(model, 'influence');
            DIAGRAM.draggingArcId = newArcId;
            model.arcs[newArcId] = createArc(newArcId, 'influence', DIAGRAM.hitItem.object);
            DIAGRAM.startNodeId = DIAGRAM.hitItem.object.id;
            DIAGRAM.status = 'start_arc';

        } else {
            alert('ERROR: You must start an influence arrow on a node.\n\nTry starting near the centre of the node rather than the edge, to make sure you hit it.');
            DIAGRAM.status = 'pointer';
        }
    }
    DIAGRAM.previousCoords = JSON.parse(JSON.stringify(DIAGRAM.startCoords));
}



function mouseMove(event, widget, canvas) {
    if (DIAGRAM.status === 'pointer' || DIAGRAM.status === 'stock') return;

    var options = widget.options;
    var model = SYSTO.models[options.model];
    var nodeList= model.nodes;
    var arcList = model.arcs;

    var context = canvas.getContext("2d");

    DIAGRAM.currentCoords.canvas = eventToCanvas(event, canvas);
    DIAGRAM.currentCoords.model = canvasToModel(DIAGRAM.currentCoords.canvas, options);

    // Separate section to handle change in DIAGRAM.status from mousedown to mousemoving.
    if (DIAGRAM.status === 'start_pan') {
        DIAGRAM.status = 'panning';
    } else if (DIAGRAM.status === 'hit_node' && shiftFromStart()>5) {
        DIAGRAM.status = 'dragging_node';
    } else if (DIAGRAM.status === 'start_arc') {
        DIAGRAM.status = 'dragging_arc';
    }

    // Now you can handle the mousemoving DIAGRAM.status
    if (DIAGRAM.status === 'panning') {
        options.offsetx = DIAGRAM.currentCoords.canvas.x - DIAGRAM.startCoords.canvas.x;
        options.offsety = DIAGRAM.currentCoords.canvas.y - DIAGRAM.startCoords.canvas.y;

        // TODO: Check why not do a simple transform?
        redraw(model, context, widget, options)
    } else if (DIAGRAM.status === 'dragging_node') {
        var dx = DIAGRAM.currentCoords.model.x-DIAGRAM.previousCoords.model.x;
        var dy = DIAGRAM.currentCoords.model.y-DIAGRAM.previousCoords.model.y;
        var node = DIAGRAM.hitItem.object;
        node.centrex += dx;
        node.centrey += dy;
        if (node.type === 'stock' || node.type === 'cloud') {
            for (var arcId in arcList) {
                var arc = arcList[arcId];
                if (arc.type === 'flow') {
                    if (node.id === arc.start_node_id) {
                        var endNode = nodeList[arc.end_node_id];
                        var valveNode = nodeList[arc.node_id];
                        valveNode.centrex = (node.centrex+endNode.centrex)/2;
                        valveNode.centrey = (node.centrey+endNode.centrey)/2;
                    } else if (node.id === arc.end_node_id) {
                        var startNode = nodeList[arc.start_node_id];
                        var valveNode = nodeList[arc.node_id];
                        valveNode.centrex = (node.centrex+startNode.centrex)/2;
                        valveNode.centrey = (node.centrey+startNode.centrey)/2;
                    }
                }
            }
        }
        //redraw(model, context, widget, options);
        $('.diagram_listener').trigger('click');


    } else if (DIAGRAM.status === 'dragging_arc') {
        var dx = DIAGRAM.currentCoords.model.x-DIAGRAM.previousCoords.model.x;
        var dy = DIAGRAM.currentCoords.model.y-DIAGRAM.previousCoords.model.y;
        var node = nodeList['dot1'];
        node.centrex += dx;
        node.centrey += dy;
        //redraw(model, context, widget, options)
        $('.diagram_listener').trigger('click');
    }

    // One of the ways of copyng one object into another.
    DIAGRAM.previousCoords = JSON.parse(JSON.stringify(DIAGRAM.currentCoords));
}



function mouseUp(event, widget, canvas) {
    var options = widget.options;
    var model = SYSTO.models[options.model];

    var context = canvas.getContext("2d");

    var endCoords = {canvas:{}, model:{}};

    var canvasCoords = eventToCanvas(event, canvas);
    endCoords.canvas.x = canvasCoords.x - options.offsetx;
    endCoords.canvas.y = canvasCoords.y - options.offsety;
    endCoords.model = canvasToModel(canvasCoords, options);

    delete model.nodes.dot1;

    DIAGRAM.hitItem = getHitItem(endCoords.model, model);   

    if (DIAGRAM.status === 'panning' || DIAGRAM.status === 'start_pan') {
        DIAGRAM.status = 'pointer';

    } else if (DIAGRAM.status === 'dragging_node') {
        DIAGRAM.status = 'pointer';

    } else if (DIAGRAM.status === 'dragging_arc') {
        var arc = model.arcs[DIAGRAM.draggingArcId];
        if (DIAGRAM.hitItem.typeId === 'node') {
            var endNode = DIAGRAM.hitItem.object;
            arc.end_node_id = endNode.id;
            createInternodeIfRequired(model, arc)
        } else {
            var newNodeId = getNewNodeId(model, 'cloud');  // TODO: generalise!
            arc.end_node_id = newNodeId;
            model.nodes[newNodeId] = createNode(newNodeId, 'cloud', endCoords.model);
            createInternodeIfRequired(model, arc)
        }
        //redraw(model, context, widget, options)
        $('.diagram_listener').trigger('click');
        DIAGRAM.status = 'pointer';

    } else {
        DIAGRAM.status = 'pointer';
    }
}




function createInternodeIfRequired(model, arc) {
    var startNode = model.nodes[arc.start_node_id];
    var endNode = model.nodes[arc.end_node_id];
    var arcType = SYSTO.languages[model.meta.language].ArcType[arc.type];
    if (arcType.node_type !== null) {
        var midx = (startNode.centrex+endNode.centrex)/2;
        var midy = (startNode.centrey+endNode.centrey)/2;
        var newInternodeId = getNewNodeId(model, arcType.node_type); 
        model.nodes[newInternodeId] = createNode(newInternodeId, arcType.node_type, {x:midx, y:midy}); 
        model.arcs[DIAGRAM.draggingArcId].node_id = newInternodeId;
    }
}


function getHitItem(modelCoords, model) {
    var nodeList = model.nodes;
    var modelx = modelCoords.x;
    var modely = modelCoords.y;
    var nodeTypeList = SYSTO.languages[model.meta.language].NodeType;   

    for (var nodeId in nodeList) {
        var node = nodeList[nodeId];
        var nodeType = nodeTypeList[node.type];
        var centrex = node.centrex;
        var centrey = node.centrey;
        var shape = nodeType.shape;
        if (shape === 'rectangle') {
            var w2 = nodeType.width/2
            var h2 = nodeType.height/2;
            if (modelx >= centrex-w2 && modelx <= centrex+w2 && modely >= centrey-h2 && modely <= centrey+h2) {
                return {typeId:'node', object:node, typeObject:nodeType};
            }
        } else if (shape === 'circle') {
            var diffx = modelx-centrex;
            var diffy = modely-centrey;
            var hypot = Math.sqrt(diffx*diffx+diffy*diffy);
            if (hypot < nodeType.radius) {
                return {typeId:'node', object:node, typeObject:nodeType};
            }
        } 
    }

    return {typeId:'canvas'};
}



function getNewNodeId(model, requiredNodeType) {
    var nodeList = model.nodes;

    var imax = 0;
    for (var nodeId in nodeList) {
        var matches = nodeId.match(/([a-z]+|[0-9]+)/gi);
        var thisNodeType = matches[0];
        if (thisNodeType === requiredNodeType) {
            var i = parseInt(matches[1]);
            if (i > imax) {
                imax = i;
            }
        }
    }
    inew = imax+1;
    return requiredNodeType+inew;
}



function getNewArcId(model, requiredArcType) {
    var arcList = model.arcs;

    var imax = 0;
    for (var arcId in arcList) {
        var matches = arcId.match(/([a-z]+|[0-9]+)/gi);
        var thisArcType = matches[0];
        if (thisArcType === requiredArcType) {
            var i = parseInt(matches[1]);
            if (i > imax) {
                imax = i;
            }
        }
    }
    inew = imax+1;
    return requiredArcType+inew;
}



// This is involved in dragging a new arc.
// Rather than having to put special blocks of code in the arc-drawing code,
// we temporarily create a new type of node ("dot"), and an instance of this node 
// type ("dot1").   It is a small circle.    The arc-drwaing code then handles this 
// in the same way as any other arc.
// The function below creates the temporary node type.   The actual instance is create
// using createNode(), as usual.
// Some of the properties set here are un-needed, but it was just simplest to
// leave them in.

function createDotNodeType(language) {
    SYSTO.languages[language].NodeType.dot = {
        has_button: false,
        has_label: false,
        default_label_root: 'dot',
        shape: 'circle',
        radius: 2,
        border_colour: {set:   {normal:'black',   selected:'blue',    highlight:'green'},
                        unset: {normal:'red',     selected:'blue',    highlight:'green'}},
        fill_colour:   {set:   {normal:'black',   selected:'white',   highlight:'white'},
                        unset: {normal:'white',   selected:'white',   highlight:'white'}},
        line_width:    {set:   {normal:1.5,       selected:5,         highlight:5},
                        unset: {normal:3.5,       selected:5,         highlight:5}},
        display_colour: 'black',
        text_shiftx: 0,
        text_shifty: 0
    }
}




function createNode(newNodeId, nodeTypeId, modelCoords) {
    return {
        id:newNodeId, 
        type:nodeTypeId, 
        label:newNodeId, 
        centrex:modelCoords.x, 
        centrey:modelCoords.y, 
        text_shiftx:-12, 
        text_shifty:-25, 
        equation:'15', 
        extras:{
            equation:{type:'long_text', default_value:'', value:''}, 
            min_value:{type:'short_text', default_value:'0', value:'0'}, 
            max_value:{type:'short_text', default_value:'100', value:'100'}, 
            documentation:{type:'long_text', default_value:'', value:''}, 
            comments:{type:'long_text', default_value:'', value:''}}};
}


/*
        "flow4": {"id":"flow4", "type":"flow", "label":"predator_birth_fraction", "start_node_id":"stock2", "end_node_id":"cloud4", "node_id":"valve4"},
        "influence1": {"id":"influence1", "type":"influence", "label":"predator_birth_fraction", "start_node_id":"variable4", "end_node_id":"valve1", "curvature":0.3, "along":0.5},
*/

function createArc(newArcId, arcTypeId, startNode) {

    return {
        id:newArcId, 
        type:arcTypeId, 
        label:newArcId, 
        start_node_id:startNode.id, 
        end_node_id:'dot1', 
        curvature:0.3,
        along:0.5};
}




function shiftFromStart() {
    var dx = Math.abs(DIAGRAM.startCoords.canvas.x - DIAGRAM.currentCoords.canvas.x);
    var dy = Math.abs(DIAGRAM.startCoords.canvas.y - DIAGRAM.currentCoords.canvas.y);
    return Math.max(dx,dy);    // Should be quicker than using Pythagoras!
}

// ============================== COORDINATE CONVERSIONS ======================


// The following is a useful link for general issues about getting window sizes etc:
// http://www.howtocreate.co.uk/tutorials/javascript/browserwindow

// There is quite a lot of stuff out there on getting mouse coordinates in a canvas 
// (or, more generally, an HTML element, typically a div) from the event properties.
// It's generally recognised as being a messy problem, since there is (or seems to be)
// no standard method whoch works across all browsers for simply getting the coordinates
// of a mouse event in a particular HTML element.

// The following is pasted here as a reminder about document.body.scroll(Left,Top) - could
// be required if I ever allow scrolling of elements inside <body>.
// x = window.pageXOffset - containerPos.left + 0*document.body.scrollLeft + evt.clientX;
// y = window.pageYOffset - containerPos.top + 0*document.body.scrollTop + evt.clientY;
// In current tests:
//    window.pageXOffset equals document.body.scrollLeft, and
//    window.pageYOffset equals document.body.scrollTop.

// The following function gets the canvas coordinates (i.e. with the top-left corner being 0,0) from
// the mouse event properties.    It allows for 3 different methods, with the actual method
// used being determined by a SYSTOGRAM.diagramMeta.canvasCoordsMethod:
// 'eventClient' - uses evt.clientX, evt.clientY
// 'eventOffset' - uses evt.offsetX, evt.offsetY
// 'eventLayer'  - uses evt.layerX, evt.layerY

// The 3 methods are made available so that it will be easy to provide a preferences setting
// to switch between them, in case a particular browser does not support one or the other.
// Obviously, I should be checking that the properties being used are available in the
// user's browser., and allowing for an automatic fall-back to an alternative method if necessary.





function eventToCanvas(evt, canvas) {

    var canvasx;
    var canvasy;

    var canvasCoordsMethod = 'eventClient';

    if (canvasCoordsMethod === 'eventClient') {
        containerPos = getContainerPos(canvas);
        canvasx = window.pageXOffset - containerPos.left + evt.clientX;
        canvasy = window.pageYOffset - containerPos.top + evt.clientY;

    } else if (canvasCoordsMethod === 'eventOffset') {
        canvasx = evt.offsetX;
        canvasy = evt.offsetY;

    } else if (canvasCoordsMethod === 'eventLayer') {
        containerPos = this.getContainerPos();
        canvasx = evt.layerX - containerPos.left;
        canvasy = evt.layerY - containerPos.top;
    }
/*
        console.debug('');
        console.debug('++++++++++++++++++++++++++++++++++++++++++++++++');
        console.debug('window.page(X,Y)Offset:        '+window.pageXOffset+', '+window.pageYOffset);
        console.debug('containerPos.(left,top):       '+containerPos.left+', '+containerPos.top);
        console.debug('document.body.scroll(Left,Top):'+document.body.scrollLeft+', '+document.body.scrollTop);
        console.debug('evt.client(X,Y):               '+evt.clientX+', '+evt.clientY);
        console.debug('evt.layer(X,Y):                '+evt.layerX+', '+evt.layerY);
        console.debug('evt.offset(X,Y):               '+evt.offsetX+', '+evt.offsetY);
        console.debug('eventClient returned(x,y):     '+canvasx+', '+canvasy);
        console.debug('++++++++++++++++++++++++++++++++++++++++++++++++');
        console.debug('');
*/

    return {x: canvasx, y: canvasy};
};




function getContainerPos(canvas){
    var obj = canvas;
    var top = 0;
    var left = 0;
    while (obj.tagName !== "BODY") {
        top += obj.offsetTop;
        left += obj.offsetLeft;
        obj = obj.offsetParent;
    }
    return {
        left: left,
        top: top
    };
};


function canvasToModel(canvasCoords, options) {
    var modelx;
    var modely;

    modelx = Math.round((canvasCoords.x-options.offsetx)/options.scale);
    modely = Math.round((canvasCoords.y-options.offsety)/options.scale);
    //modelx = Math.round((canvasCoords.x)/options.scale);
    //modely = Math.round((canvasCoords.y)/options.scale);
    return {x: modelx, y: modely};
};




function modelToCanvas(modelCoords, options) {
    var canvasx;
    var canvasy;

    canvasx = options.scale*modelCoords.x+options.offsetx;
    canvasy = options.scale*modelCoords.y+options.offsety;
    return {x: canvasx, y: canvasy};
};



function maxXY(model) {
    var xmin = 0;
    var xmax = 500;
    var ymin = 0;
    var ymax = 500;

    var nodeList = model.nodes;

    var first = true;
    for (var nodeId in nodeList) {
        if (nodeList.hasOwnProperty(nodeId)) {
            node = nodeList[nodeId];
            if (first) {
                xmin = node.centrex;
                xmax = node.centrex;
                ymin = node.centrey;
                ymax = node.centrey;
                first = false;
            } else {  
                if (node.centrex < xmin) {
                    xmin = node.centrex;
                } else if (node.centrex > xmax) {
                     xmax = node.centrex;
                }
                if (node.centrey < ymin) {
                    ymin = node.centrey;
                } else if (node.centrey > ymax) {
                    ymax = node.centrey;
                }
            }
        }
    }

    xmin -= 50;
    ymin -=50;
    xmax += 50;
    ymax += 50;
    return {xmin: xmin, xmax: xmax, ymin: ymin, ymax:ymax};
}



})(jQuery);
