/*
     Copyright 2012-2013 Jacob Beard

     Licensed under the Apache License, Version 2.0 (the "License");
     you may not use this file except in compliance with the License.
     You may obtain a copy of the License at

             http://www.apache.org/licenses/LICENSE-2.0

     Unless required by applicable law or agreed to in writing, software
     distributed under the License is distributed on an "AS IS" BASIS,
     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     See the License for the specific language governing permissions and
     limitations under the License.
*/

//monkey-patch HTMLCollection prototypes for better portability
HTMLCollection.prototype.slice = HTMLCollection.prototype.slice || function(){
  return Array.prototype.slice.call( this );
};

HTMLCollection.prototype.forEach = HTMLCollection.prototype.forEach || function(fn){
  for(var i = 0; i < this.length; i++){
    fn(this.item(i));
  }
};


function ScxmlViz(domAttachPoint,doc,width,height,options){
    var STATE_NAMES = ['scxml','state','parallel','final','history'];
    var scxmlNs = "http://www.w3.org/2005/07/scxml";

    function getElementByIdWrapper(xmldoc, myId) {
        //this funciton is necessary to allow getElementById to work on xml documents in firefox

        //first try the regular way
        var toReturn = xmldoc.getElementById(myId);

        if(toReturn === null){
            //try the firefox way
            toReturn = xmldoc.evaluate(
                            '//*[@id="'+myId+'"]', 
                            xmldoc, 
                            null,
                            XPathResult.FIRST_ORDERED_NODE_TYPE, 
                            null).singleNodeValue;
        }

        return toReturn;
    }

    function getChildStates(state){
        return d3.selectAll(state.childNodes)[0].
            filter(function(n){return n.nodeType === 1;}).
            filter(function(n){return STATE_NAMES.indexOf(n.localName) > -1;});
    }

    function getTransitionsFromStates(childScxmlNodes){
        var transitions = childScxmlNodes.
            map(function(node){return node.childNodes;}).
            map(function(domNodeList){return Array.prototype.slice.call(domNodeList);}).
            reduce(function(a,b){return a.concat(b);},[]).
            filter(function(node){return node.localName === 'transition';}).
            filter(function(transitionNode){return transitionNode.hasAttribute('target');});    //TODO: find a way to render targetless transitions

        var fakeTransitions = [];

        transitions.forEach(function(transitionNode){
            transitionNode.source = transitionNode.parentNode;
            transitionNode.targets = transitionNode.getAttribute('target').trim().split(/\s+/)
                                        .map(getElementByIdWrapper.bind(null,transitionNode.ownerDocument));
            transitionNode.target = transitionNode.targets[0];

            if(!transitionNode.target) throw new Error("Unable to find target for transition node.");

            transitionNode.targets.slice(1).forEach(function(target){
                //make a fake transition node
                var fakeTransitionNode = transitionNode.cloneNode(true);
                transitionNode.parentNode.appendChild(fakeTransitionNode); 
                fakeTransitionNode.source = transitionNode.parentNode;
                fakeTransitionNode.target = target;
            });
        });

        return transitions.concat(fakeTransitions);
    }

    function traverseAndCountSubElements(node){
        var states = getChildStates(node);
        var sizes = states.map(traverseAndCountSubElements);
        var size = sizes.reduce(function(a,b){return a + b;},0) + 1;
        //console.log('size',size);
        node.size = size;
        if(size > 1) node.isParent = true;
        return size;
    }

    function normalizeInitialStates(node){

        if(node.isParent){
            if(node.localName !== 'parallel'){
                if(node.hasAttribute('initial')){
                    node.ownerDocument.getElementById(node.getAttribute('initial')).isInitial = true;
                }else{
                    var initialState;
                    done:
                    for(var i=0; i < node.childNodes; i++){
                        var state = node.childNodes[i];
                        if(state.localName === 'initial'){
                            for(var j=0; j < state.childNodes; j++){
                                var node2 = state[j];
                                if(node2.localName === 'transition'){
                                    initialState = node.ownerDocument.getElementById(node2.getAttribute('target'));
                                    break done;
                                }
                            }
                        }
                    }

                    if(!initialState){
                        //get the first state
                        initialState = getChildStates(node)[0];
                    }
                    initialState.isInitial = true; 
                }
            }

            getChildStates(node).forEach(normalizeInitialStates);   //recurse
        }
    }

    width = width || 960;
    height = height || 500;

    d3.ns.prefix.scxml = "http://www.w3.org/2005/07/scxml";

    var padding = 20;

    var treemap = d3.layout.treemap()
        .padding(padding)
        .size([width, height])
        .value(function(d) { return d.size; })
        .children(getChildStates);

    var svg = d3.select(domAttachPoint).append("svg")
        .attr("width", "100%")
        .attr("height", "98%")
        .attr("viewBox", "0 0 " + width + ' ' + height)
        .attr("preserveAspectRatio", "xMidYMid meet");

    var g = svg.append("g");

    var defs = svg.append("svg:defs");

    defs.append("svg:marker")
        .attr("id", 'transitionMarker')
        .attr("viewBox", "0 -5 10 10")
        .attr("refX", 10)
        .attr("refY", 0)
        .attr("markerWidth", 6)
        .attr("markerHeight", 6)
        .attr("orient", "auto")
      .append("svg:path")
        .attr("d", "M0,-5L10,0L0,5");


    //TODO: make dynamic based on bbox of the text? or just guess dimensions? maybe ask on the list about this
    var historyDim = 30,
        basicWidth = 10,
        basicHeight = 10,
        initialStateWidth = 10,
        initialStateHeight = 10;

    function getDistance(p1,p2){
        return Math.sqrt(Math.pow(p2[0] - p1[0],2) + Math.pow(p2[1] - p1[1], 2));
    }

    function getCenterPoints(d){
        if(d.isParent){
            //debugger;

            var dx = d.dx/2,
                dy = d.dy/2;

            return [
                [d.x + dx, d.y],     //top-center
                [d.x + dx/2, d.y],
                [d.x + dx + dx/2, d.y],
                [d.x + dx, d.y + d.dy - padding],    //bottom-center
                [d.x + dx/2, d.y + d.dy - padding],    
                [d.x + dx + dx/2, d.y + d.dy - padding],   
                [d.x, d.y + dy],     //left-center
                [d.x, d.y + dy/2],    
                [d.x, d.y + dy + dy/2],     
                [d.x + d.dx - padding, d.y + dy],     //right-center
                [d.x + d.dx - padding, d.y + dy/2],     
                [d.x + d.dx - padding, d.y + dy + dy/2]    
            ]; 
        }else if(d.localName === 'history'){
            var x = getInnerXCoordForBasicRectNode(d) + d.x,
                y = getInnerYCoordForBasicRectNode(d) + d.y + historyDim/2;
            return [[x,y]];
        }else{
            x = getInnerXCoordForBasicRectNode(d) + d.x;
            y = getInnerYCoordForBasicRectNode(d) + d.y;

            dx = basicWidth/2;
            dy = basicHeight/2;

            return [
                [x + dx,y],
                [x + dx,y + basicHeight],
                [x,y + dy],
                [x + basicWidth,y + dy]
            ]; 
        }
    }

    function getSourceAndDest(link,distanceThreshold){

        distanceThreshold = distanceThreshold || 0;

        var sourceCenterPoints = getCenterPoints(link.source),
            targetCenterPoints = getCenterPoints(link.target);

        //console.log(link,sourceCenterPoints,targetCenterPoints);  

        //cartesion product
        var centerPointCombinations = [];
        for(var i = 0; i < sourceCenterPoints.length; i++){
            for(var j = 0; j < targetCenterPoints.length; j++){
                centerPointCombinations.push([sourceCenterPoints[i],targetCenterPoints[j]]);
            } 
        } 

        var minDistance = Number.MAX_VALUE, minDistanceCombo;
        for(var k = 0; k < centerPointCombinations.length; k++){
            var points = centerPointCombinations[k];
            var distance = getDistance.apply(null,points);
            if(distance < minDistance && distance > distanceThreshold){
                minDistance = distance;
                minDistanceCombo = points; 
            }
        }

        return minDistanceCombo;
    }

    function getEdgeD(reverseEdge,d){
        var points = getSourceAndDest(d,5);
        var sourceX = points[0][0], 
            sourceY = points[0][1], 
            destX = points[1][0], 
            destY = points[1][1],   
            dx = destX - sourceX,
            dy = destY - sourceY,
            dr = Math.sqrt(dx * dx + dy * dy);

        return reverseEdge ? 
            "M" + destX + "," + destY + "A" + dr + "," + dr + " 0 0,1 " + sourceX  + "," + sourceY : 
            "M" + sourceX + "," + sourceY + "A" + dr + "," + dr + " 0 0,0 " + destX + "," + destY;
    }

    function getEdgeToSelfD(d){
        //debugger;
        if(d.isParent){
            var source = [d.x + 3 * d.dx/4, d.y];     //top
            var dest = [d.x + d.dx - padding, d.y + d.dy/4];     //right
            var ctrlPt1 = [d.x + 7 * d.dx/8, d.y - d.dy/4];
            var ctrlPt2 = [d.x + 9 * d.dx/8, d.y + d.dy/8];
        }else if(d.localName === 'history'){
            throw new Error('History state should not be source and target of transition.');
        }else{
            var x = getInnerXCoordForBasicRectNode(d) + d.x;
            var y = getInnerYCoordForBasicRectNode(d) + d.y;

            var dx = basicWidth/2;
            var dy = basicHeight/2;

            source = [x + dx,y];                 //top
            dest = [x + basicWidth,y + dy];      //right

            ctrlPt1 = [x + dx, y - basicHeight * 5];
            ctrlPt2 = [x + basicWidth * 6,y + dy];
        }

        
        return "M" + source[0] + "," + source[1] + 
                "C" + ctrlPt1[0] + "," + ctrlPt1[1] + " " +
                ctrlPt2[0] + "," + ctrlPt2[1] + " " + 
                dest[0] + "," + dest[1];
    }

    function edgeLayout(reverseEdge,d){
        //4 possibilities:
            //source is basic, target is basic
            //source is composite, target is composite
            //source is basic, target is composite
            //source is composite, target is basic
        //either way, pick the closest edge, and aim for the center.
        //left-of, right of, above, below, contains. pick the center point on the closest edge.
        //ah, edge routing... we also want to minimize edge crossings, so...
        //TODO: deal with the special case of looping back to ourself

        return d.source === d.target ?
                getEdgeToSelfD(d.source) :
                getEdgeD(reverseEdge,d);
    }


    function getInnerXCoordForBasicRectNode(d){
        return d.dx / 2 - basicWidth/2;
    }

    function getInnerYCoordForBasicRectNode(d){
        return d.dy / 2 - basicHeight/2;
    }


    traverseAndCountSubElements(doc.documentElement);
    normalizeInitialStates(doc.documentElement);

    var nodes = treemap.nodes(doc.documentElement);
    var links = getTransitionsFromStates(nodes);
    //treemap.links(links);     //no reason to set links for this kind of layout

    var cell = svg.selectAll("g")
            .data(nodes)
        .enter().append("g")
            .attr("class", function(node){
                var c;
                if(node.localName === 'history'){
                    c = "cell history";
                }else if(node.localName === 'initial'){
                    c = "cell initial";
                }else if(node.parentNode && node.parentNode.localName === 'parallel'){
                    c = "cell orthogonalComponent";
                }else{
                    c = "cell";
                }

                if(node.isInitial) c += ' initial';

                return c;
            })
            .attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; })
            .attr('id',function(d){return d.getAttribute('id');});

    var xmlSerializer = new XMLSerializer();
    cell.append('title').text(function(d){
        //debugger;
        return d3.selectAll(d.childNodes)[0]
                .filter(function(d){return d.localName === 'onentry' || d.localName === 'onexit';})
                .map(function(onentry){return xmlSerializer.serializeToString(onentry);}).join('\n');
    });

    cell.append("rect")
            .attr("rx", function(d){ return d.localName === 'history' ? d.dx : 10;})
            .attr("ry", function(d){ return d.localName === 'history' ? d.dy : 10;})
            .attr("x", function(d) { return d.isParent ? 0 : getInnerXCoordForBasicRectNode(d);})
            .attr("y", function(d) { return d.isParent ? 0 : getInnerYCoordForBasicRectNode(d);})
            .attr("width", function(d) { 
                if(d.isParent){ 
                    return d.dx - padding;
                }else if(d.localName === 'history'){
                    return historyDim;
                }else {
                    return basicWidth; 
                }
            })  //should be equal to bbox of the text
            .attr("height", function(d) { 
                if(d.isParent){ 
                    return d.dy - padding;
                }else if(d.localName === 'history'){
                    return historyDim;  //should be a circle
                }else {
                    return basicHeight; 
                }
            });

    cell.append("text")
            .attr("x", function(d) { return d.isParent ? 10 : d.dx / 2; })
            .attr("y", function(d) { return d.isParent ? 10 : d.dy / 2; })
            .attr("dy", function(d){ 
                if(d.localName ==='history'){ 
                    return historyDim / 2; 
                }else if(d.isParent){
                    return ".35em";
                }else{
                    return basicHeight + 4;
                }
            })
            .attr('dx',function(d){
                if(d.localName ==='history'){ 
                    //return historyDim / 2; 
                    return 10;
                }else{
                    return 0;
                }
            })
            .attr("text-anchor", function(d){ return d.isParent ? "" : "middle";})
            .text(function(d) { 
                if(d.localName === 'history'){
                    return (d.getAttribute('type') === 'deep' ? 'H*' : 'H'); 
                }else{
                    return d.getAttribute('id'); 
                }
            });

    var edgeDefinition = defs.selectAll('path.transition')
                .data(links)
            .enter().append('path')
                .attr('class','transition')
                .attr("marker-end", function(d) { return "url(#transitionMarker)"; })
                .attr("d", edgeLayout.bind(null,false))
                .attr("id",function(d,i){return 'transition' + i;});



    var reverseEdgeDefinition = defs.selectAll('path.reverseTransition')
                .data(links)
            .enter().append('path')
                .attr('class','reverseTransition')
                .attr("d", edgeLayout.bind(null,true))
                .attr("id",function(d,i){return 'reverseTransition' + i;});

    var edgeGroup = svg.selectAll('g.transition')
                .data(links)
            .enter().append('g')
                .attr('class','transition');

    var edge = edgeGroup.append('use')
                .attr('class','transition')
                .attr("xlink:href",function(d,i){return '#transition' + i;});

    if(!options || !options.hideTransitionLabels){
        var transitionLabels = edgeGroup.append('text')
                    .attr('class','transitionLabel')
                    .attr('dy','1em')
                    .append('textPath')
                    .attr("xlink:href",function(d,i){
                        var points = getSourceAndDest(d,5);
                        if(points[1][0] < points[0][0]){
                            return '#reverseTransition' + i;
                        }else{
                            return '#transition' + i;
                        }
                    })
                    .attr("startOffset",function(d){
                        var points = getSourceAndDest(d,5);
                        if(points[1][0] < points[0][0]){
                            return 30;  //a bit extra because of marker
                        }else{
                            return 10;
                        }
                    })
                    .text(function(d){
                        return (d.hasAttribute('event') ? d.getAttribute('event') : '') + 
                               (d.hasAttribute('cond') ? '[' + d.getAttribute('cond') + ']' : '');
                    });
    }

    edgeGroup.append('title').text(xmlSerializer.serializeToString.bind(xmlSerializer));

    return svg;
}