/// <reference path="camelInsightPlugin.ts"/>
module Camin {

    _module.controller("Camin.Controller", ["$scope", "jolokia", "localStorage", "$routeParams", "ejsResource", ($scope, jolokia, localStorage, $routeParams, ejsResource) => {

        $scope.query = "";
        $scope.breadcrumbs = [ ];

        $scope.onQueryChange = function() {
            $('#gantt').html('');
            $('#diagram').html('');
            $scope.breadcrumbs = [ $scope.query ];
            searchRequest();
        }

        var log = Logger.get("Camin");

        var esUrl = new Jolokia(Core.getJolokiaUrl()).execute("io.fabric8.insight:type=Elasticsearch","getRestUrl","insight");
        var esClient = ejsResource(esUrl);

        var searchRequest = function() {
            var queryStr = "exchange.id:\"" + $scope.breadcrumbs + "\"";

            var request = ejs.Request().types('camel');

            var searchPromise = request
                .from(0).size(1).query(ejs.QueryStringQuery(queryStr))
                .doSearch();

            searchPromise.then(function(data) {
                if(!(angular.isUndefined(data.error))) {
                    log.error(data.error);
                    return;
                }
                log.debug("Results", data);

                if (data.hits.hits.length == 1) {
                  var breadcrumbQueryStr = "exchange.in.headers.breadcrumbId:\"" + data.hits.hits[0]._source.exchange.in.headers.breadcrumbId + "\"";
                  var breadcrumbSearchPromise = request
                    .from(0).size(1000).query(ejs.QueryStringQuery(breadcrumbQueryStr))
                    .doSearch();
                  breadcrumbSearchPromise.then(function(data) {
                    log.debug("Results", data);
                    if(!(angular.isUndefined(data.error))) {
                      log.error(data.error);
                      return;
                    }

                    log.debug("Found " + data.hits.total + " exchanges");
                    var events = [ ];
                    for (var i = 0; i < data.hits.hits.length; i++) {
                      var e = data.hits.hits[i]._source;
                      events.push( e );
                    }
                    draw(events);
                  });
                }
            });
        }

        var isoDate = function(date: string): number {
            var timestamp, struct, minutesOffset = 0;
            var numericKeys = [ 1, 4, 5, 6, 7, 10, 11 ];
            // ES5 §15.9.4.2 states that the string should attempt to be parsed as a Date Time String Format string
            // before falling back to any implementation-specific date parsing, so that’s what we do, even if native
            // implementations could be faster
            //              1 YYYY                2 MM       3 DD           4 HH    5 mm       6 ss        7 msec        8 Z 9 ±    10 tzHH    11 tzmm
            if ((struct = /^(\d{4}|[+\-]\d{6})(?:-(\d{2})(?:-(\d{2}))?)?(?:T(\d{2}):(\d{2})(?::(\d{2})(?:\.(\d{3}))?)?(?:(Z)|([+\-])(\d{2})(?::(\d{2}))?)?)?$/.exec(date))) {
                // avoid NaN timestamps caused by “undefined” values being passed to Date.UTC
                for (var i = 0, k; (k = numericKeys[i]); ++i) {
                    struct[k] = +struct[k] || 0;
                }

                // allow undefined days and months
                struct[2] = (+struct[2] || 1) - 1;
                struct[3] = +struct[3] || 1;

                if (struct[8] !== 'Z' && struct[9] !== undefined) {
                    minutesOffset = struct[10] * 60 + struct[11];

                    if (struct[9] === '+') {
                        minutesOffset = 0 - minutesOffset;
                    }
                }

                timestamp = Date.UTC(struct[1], struct[2], struct[3], struct[4], struct[5] + minutesOffset, struct[6], struct[7]);
            }
            else {
                timestamp = Date.parse(date);
            }

            return timestamp;
        }

        var buildSequence = function(events) {
            var sequence = new Sequence();
            var exchangeToExec = { };
            // Sort events
            events = events.sort(function(a:any,b:any) {
              var result = isoDate(a['@timestamp']) - isoDate(b['@timestamp']);
              if (result  == 0) {
                if (a.exchange.id > b.exchange.id) {
                  result = 1;
                } else if (a.exchange.id < b.exchange.id) {
                  result = -1;
                }
              }
              return result ;
            });
            // Extract endpoints and executions
            for (var i = 0; i < events.length; i++) {
                if (events[i].event === 'Created') {
                    var evtCreated: any = events[i];
                    var evtCompleted: any = _.find(events, function(value: any, index) {
                      return value.event === 'Completed' && evtCreated.exchange.id === value.exchange.id;
                    });

                    if (evtCompleted === null) {
                        log.debug('Could not find matching Completed exchange for ' + evtCreated.exchange.id);
                        continue;
                    }
                    // We use the completed event here because the created event may miss the routeId information
                    var endpoint = sequence.endpoint(evtCompleted.exchange.fromEndpoint,
                                                     evtCompleted.exchange.routeId,
                                                     evtCompleted.exchange.contextId,
                                                     evtCompleted.host);
                    var exec = sequence.exec( evtCreated.exchange.id, endpoint, isoDate(evtCreated['@timestamp']), isoDate(evtCompleted['@timestamp']) );
                    exchangeToExec[ evtCreated.exchange.id ] = exec;
                }
            }
            // Extract calls
            var calls = { };
            for (var i = 0; i < events.length; i++) {
                if (events[i].event === 'Sending' && events[i].exchange.in && events[i].exchange.in.headers) {
                    var callId = events[i].exchange.in.headers.AuditCallId;
                    if (callId && !calls[callId]) {
                        var evtSending:any = events[i];
                        var evtSent:any    = _.find(events, function(value: any, index) {
                          return value.event === 'Sent' && evtSending.exchange.id === value.exchange.id;
                        });
                        var evtCreated: any = _.find(events, function(value: any, index) {
                          return value.event === 'Created' && evtSending.exchange.id !== value.exchange.id
                            && evtSending.exchange.in.headers.AuditCallId === value.exchange.in.headers.AuditCallId;
                        });
                        var execA = exchangeToExec[ evtSending.exchange.id ];
                        var execB = evtCreated ? exchangeToExec[ evtCreated.exchange.id ] : null;
                        if (evtSent !== null && evtCreated !== null && execA !== null && execB != null) {
                            var call = sequence.call( callId, execA, execB, isoDate(evtSending['@timestamp']), isoDate(evtSent['@timestamp']) );
                            calls[callId] = call;
                        } else {
                            log.debug("Could not find Execution for exchange " + evtSending.exchange.id);
                        }
                    }
                }
            }
            return sequence;
        }

        var buildDiagram = function(sequence) {
            var diagram = new Diagram();
            var actors = { };
            var signals = [ ];
            var base = sequence.start();
            for (var i = 0; i < sequence.endpoints.length; i++) {
                var actor = diagram.actor("ep" + i);
                var ep = sequence.endpoints[i];
                var key = ep.url + "|" + ep.routeId + "|" + ep.contextId + "|" + ep.host;
                actors[ key ] = actor;
            }
            for (var i = 0; i < sequence.calls.length; i++) {
                var call   = sequence.calls[i];
                if ( call.execB ) {
                    var epA    = call.execA.endpoint;
                    var keyA   = epA.url + "|" + epA.routeId + "|" + epA.contextId + "|" + epA.host;
                    var epB    = call.execB.endpoint;
                    var keyB   = epB.url + "|" + epB.routeId + "|" + epB.contextId + "|" + epB.host;
                    var actorA = actors[ keyA ];
                    var actorB = actors[ keyB ];
                    var start1 = call.start - base;
                    var stop1  = call.execB.start - base;
                    var start2 = call.execB.stop - base;
                    var stop2  = call.stop - base;
                    signals.push( { actorA: actorA,
                                    actorB: actorB,
                                    message: start1 + "ms - " + stop1 + "ms",
                                    timestamp: start1 });
                    signals.push( { actorA: actorB,
                                    actorB: actorA,
                                    message: start2 + "ms - " + stop2 + "ms",
                                    timestamp: start2 });
                }
            }
            signals = signals.sort(function(a,b) { return a['@timestamp'] - b['@timestamp'] });
            for (var i = 0; i < signals.length; i++) {
                diagram.signal( signals[i].actorA, signals[i].actorB, signals[i].message );
            }
            return diagram;
        }

        var buildGantt = function( sequence ) {
            var gantt = new Gantt();
            for (var i = 0; i < sequence.endpoints.length; i++) {
                var endpoint = sequence.endpoints[i];
                var resource = gantt.resource(endpoint);
                for (var j = 0; j < sequence.execs.length; j++) {
                    var exec = sequence.execs[j];
                    if (exec.endpoint === endpoint) {
                        gantt.task(resource, exec.start, exec.stop, exec);
                    }
                }
            }
            for (var i = 0; i < sequence.calls.length; i++) {
                var call = sequence.calls[i];
                if (call.execB) {
                    var taskA = gantt.taskByData(call.execA);
                    var taskB = gantt.taskByData(call.execB);
                    gantt.link(call.start, taskA, call.stop, taskB, call);
                }
            }
            gantt.layout();
            return gantt;
        }

        var eventTypeValue = { "Created": 0, "Sending": 1, "Sent": 2, "Completed": 3 };

        var draw = function(events) {
            $scope.definition = "";

            events = events.sort(function (a,b) { return isoDate(a['@timestamp']) - isoDate(b['@timestamp']); });
            log.debug( "Events", events );

            var sequence = buildSequence( events );
            log.debug( "Sequence", sequence );

            var gantt = buildGantt( sequence );
            log.debug( "Gantt", gantt );
            $('#gantt').html('');
            drawGantt('#gantt', gantt);

            var diagram = buildDiagram( sequence );
            log.debug( "Diagram", diagram );
            $('#diagram').html('');
            drawDiagram('#diagram', diagram);
        }

        var drawDiagram = function(container, diagram) {

            var arrow_size = 10;
            var margin = 10;
            var actor_width = 100;
            var actor_margin = 30;
            var actor_height = 40;
            var signal_height = 30;
            var actor_font = 20;
            var signal_font = 14;
            var width = diagram.actors.length * (actor_width + actor_margin * 2);
            var height = (diagram.signals.length + 1) * signal_height + actor_height * 2 + margin * 2;

            var svg = d3.select(container)
                .append('svg')
                .attr('width', width + 2 * margin)
                .attr('height', height + 2 * margin);
            var g = svg
                .append('g')
                .attr('text-anchor', 'middle');
            for (var i = 0; i < diagram.actors.length; i++) {
                var actor = diagram.actors[i];
                var gu = g
                    .append('g')
                    .attr('transform', 'translate(' + (i * (actor_width + actor_margin * 2) + actor_margin) + ',' + actor_height + ')');
                gu.append('rect')
                    .attr('width', actor_width)
                    .attr('height', actor_height)
                    .attr('stroke', '#000')
                    .attr('stroke-width', '2')
                    .attr('fill', '#FFFFFF');
                gu.append('text')
                    .attr('x', actor_width/2)
                    .attr('y', actor_height/2)
                    .attr('stroke-width', '0')
                    .attr('dominant-baseline', 'middle')
                    .attr('font-size', actor_font)
                    .text(actor.name);

                g.append('line')
                    .attr('x1', i * (actor_width + actor_margin * 2) + actor_width / 2 + actor_margin)
                    .attr('y1', actor_height * 2)
                    .attr('x2', i * (actor_width + actor_margin * 2) + actor_width / 2 + actor_margin)
                    .attr('y2', height - actor_height)
                    .attr('stroke', '#000')
                    .attr('stroke-width', '2');

                var gu = g
                    .append('g')
                    .attr('transform', 'translate(' + (i * (actor_width + actor_margin * 2) + actor_margin) + ',' + (height - actor_height) + ')');
                gu.append('rect')
                    .attr('width', actor_width)
                    .attr('height', actor_height)
                    .attr('stroke', '#000')
                    .attr('stroke-width', '2')
                    .attr('fill', 'white');
                gu.append('text')
                    .attr('x', actor_width/2)
                    .attr('y', actor_height/2)
                    .attr('stroke-width', '0')
                    .attr('dominant-baseline', 'middle')
                    .attr('font-size', actor_font)
                    .text(actor.name);
            }
            for (var i = 0; i < diagram.signals.length; i++) {
                var x;
                var y;
                var length;
                var direction;
                var text;

                x = diagram.signals[i].actorA.index * (actor_width + actor_margin * 2) + actor_width / 2 + actor_margin;
                y = (i + 1) * signal_height + actor_height * 2;
                length = Math.abs(diagram.signals[i].actorA.index - diagram.signals[i].actorB.index) * (actor_width + actor_margin * 2);
                direction = diagram.signals[i].actorB.index > diagram.signals[i].actorA.index ? +1 : -1;
                text = diagram.signals[i].message;

                var gu = g
                    .append('g')
                    .attr('transform', 'translate(' + x + ',' + y + ')')
                    .attr('stroke-width', '2');
                gu.append('rect')
                    .attr('x', Math.min(3, length*direction+3))
                    .attr('y', '-16')
                    .attr('width', Math.abs((length-6)*direction))
                    .attr('height', '19')
                    .attr('stroke', 'white')
                    .attr('stroke-width', '0')
                    .attr('fill', 'white');
                gu.append('line')
                    .attr('x1', 0)
                    .attr('y1', 0)
                    .attr('x2', length*direction)
                    .attr('y2', 0)
                    .attr('stroke', '#000')
                    .attr('stroke-width', '2');
                gu.append('line')
                    .attr('x1', length*direction - arrow_size*direction)
                    .attr('y1', -arrow_size)
                    .attr('x2', length*direction)
                    .attr('y2', 0)
                    .attr('stroke', '#000')
                    .attr('stroke-width', '2');
                gu.append('line')
                    .attr('x1', length*direction)
                    .attr('y1', 0)
                    .attr('x2', length*direction - arrow_size*direction)
                    .attr('y2', arrow_size)
                    .attr('stroke', '#000')
                    .attr('stroke-width', '2');
                gu.append('text')
                    .attr('x', length*direction/2)
                    .attr('y', -8)
                    .attr('stroke-width', '0')
                    .attr('dominant-baseline', 'middle')
                    .attr('font-size', signal_font)
                    .text(text);
            }
        }

        var drawGantt = function(container, gantt) {
            var lineHeight = 35;
            var lineMargin = 3;
            var arrowWidth = 4;

            var width = 800;
            var height = lineHeight * gantt.resources.length;
            var margin = {
            	top : 20,
            	right : 40,
            	bottom : 20,
            	left : 250
            };

            var begin = gantt.start;
            var end   = gantt.stop;

            var x = d3.scale.linear().domain([ begin - (end - begin) * 0.1, end + (end - begin) * 0.1 ]).range([ 0, width ]);
            var yt = function(t) { return t.resource.index * lineHeight + lineMargin + t.index * (lineHeight - 2 * lineMargin) / (t.max + 1); };
            var ht = function(t) { return 2 * (lineHeight - 2 * lineMargin) / (t.max + 1); };

            var svg = d3.select(container)
              .append('svg')
              .attr('width', width + margin.left + margin.right)
              .attr('height', height + margin.top + margin.bottom);

            var text = svg.append('g')
              .attr('width', width)
              .attr('height', height)
              .attr('transform', 'translate(0,' + margin.top + ')')
              .selectAll('text')
              .data(gantt.resources).enter();
            text
              .append('text')
              .attr('x', 0)
              .attr('y', function(r) { return r.index * lineHeight + lineHeight / 2; })
              .attr('dy', '-0.2em')
              .attr('text-anchor', 'start')
              .text(function(r) {
                var endpoint = r.data;
                var text = endpoint.url;
                if (text.indexOf("Endpoint[") == 0) {
                    text = text.substring(9, text.length - 1);
                }
                return text;
              });
            text
              .append('text')
              .attr('x', 0)
              .attr('y', function(r) { return r.index * lineHeight + lineHeight / 2; })
              .attr('dy', '0.8em')
              .attr('text-anchor', 'start')
              .text(function(r) {
                var endpoint = r.data;
                return endpoint.host + "/" + endpoint.contextId + "/" + endpoint.routeId;
              });

            var g = svg.append('g')
              .attr('width', width)
              .attr('height', height)
              .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');

            g.append('g')
              .attr('width', width)
              .attr('height', height)
              .selectAll('rect')
              .data(gantt.tasks).enter()
              .append('rect')
              .attr('rx', lineMargin * 2)
              .attr('ry', lineMargin * 2)
              .attr('x',      function(t) { return x(t.start); })
              .attr('y',      yt)
              .attr('height', ht)
              .attr('width',  function(t) { return x(t.stop) - x(t.start); })
              .attr('stroke', '#000000')
              .attr('stroke-width', '2')
              .attr('fill',   function(t) { return d3.hsl(Math.random() * 360, 0.8, 0.8).toString(); });

            var lines = g.append('g')
              .attr('width', width)
              .attr('height', height)
              .attr('stroke', '#404040')
              .attr('stroke-width', '2')
              .selectAll('line')
              .data(gantt.links).enter();
            lines.append('line')
              .attr('x1', function(l) { return x(l.start); })
              .attr('y1', function(l) { return yt(l.taskA) + ht(l.taskA); })
              .attr('x2', function(l) { return x(l.start); })
              .attr('y2', function(l) { return yt(l.taskB) + ht(l.taskB) / 2; });
            lines.append('line')
              .attr('x1', function(l) { return x(l.start); })
              .attr('y1', function(l) { return yt(l.taskB) + ht(l.taskB) / 2; })
              .attr('x2', function(l) { return x(l.taskB.start); })
              .attr('y2', function(l) { return yt(l.taskB) + ht(l.taskB) / 2; });
            lines.append('line')
              .attr('x1', function(l) { return x(l.taskB.start); })
              .attr('y1', function(l) { return yt(l.taskB) + ht(l.taskB) / 2; })
              .attr('x2', function(l) { return x(l.taskB.start) - arrowWidth; })
              .attr('y2', function(l) { return yt(l.taskB) + ht(l.taskB) / 2 - arrowWidth; });
            lines.append('line')
              .attr('x1', function(l) { return x(l.taskB.start); })
              .attr('y1', function(l) { return yt(l.taskB) + ht(l.taskB) / 2; })
              .attr('x2', function(l) { return x(l.taskB.start) - arrowWidth; })
              .attr('y2', function(l) { return yt(l.taskB) + ht(l.taskB) / 2 + arrowWidth; });
          /*lines.append('line')
              .attr('x1', function(l) { return x(l.taskB.stop); })
              .attr('y1', function(l) { return yt(l.taskB) + ht(l.taskB) / 2; })
              .attr('x2', function(l) { return x(l.stop); })
              .attr('y2', function(l) { return yt(l.taskB) + ht(l.taskB) / 2; });
            lines.append('line')
              .attr('x1', function(l) { return x(l.stop); })
              .attr('y1', function(l) { return yt(l.taskB) + ht(l.taskB) / 2; })
              .attr('x2', function(l) { return x(l.stop); })
              .attr('y2', function(l) { return yt(l.taskA) + ht(l.taskA); });
            lines.append('line')
              .attr('x1', function(l) { return x(l.stop); })
              .attr('y1', function(l) { return yt(l.taskA) + ht(l.taskA); })
              .attr('x2', function(l) { return x(l.stop) - arrowWidth; })
              .attr('y2', function(l) { return yt(l.taskA) + ht(l.taskA) + arrowWidth; });
            lines.append('line')
              .attr('x1', function(l) { return x(l.stop); })
              .attr('y1', function(l) { return yt(l.taskA) + ht(l.taskA); })
              .attr('x2', function(l) { return x(l.stop) + arrowWidth; })
              .attr('y2', function(l) { return yt(l.taskA) + ht(l.taskA) + arrowWidth; });*/
        }

        if ($routeParams["exchangeId"]) {
            $scope.query = $routeParams["exchangeId"];
            $scope.onQueryChange();
        }

    }]);
}
