doctype html html

head
  title Source Route Result
  link rel="stylesheet" href="https://cdn.rawgit.com/Urigo/angular-spinkit/master/build/angular-spinkit.min.css"
  link rel="stylesheet" href="https://cdn.rawgit.com/raykin/json-formatter/master/dist/json-formatter.min.css"
  link rel="stylesheet" href="https://cdn.jsdelivr.net/semantic-ui/2.2.6/semantic.min.css"

  script src="http://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.2/lodash.js"
  script src="http://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.4/jquery.min.js"
  script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.6.0/angular.min.js"
  script async=true src="https://cdn.jsdelivr.net/semantic-ui/2.2.6/semantic.min.js"

  css:
    .call-level-0 {}
    .item.trace.call-level-1 { padding-left: 50px }
    .item.trace.call-level-2 { padding-left: 100px }
    .item.trace.call-level-3 { padding-left: 150px }
    .item.trace.call-level-4 { padding-left: 200px }
    .item.trace.call-level-5 { padding-left: 250px }
    .item.trace.call-level-6 { padding-left: 300px }
    .item.trace.call-level-7 { padding-left: 300px }
    .trace .main .ui.label .icon {
      margin-right: 0;
    }
    // level more than 7 seems not reasonable

body(ng-app="SourceRoute" ng-controller="MainCtrl" ng-cloak)

  .ui.menu.pointing.stackable.attached
    .ui.container
      .item
        a.navbar-brand(href="#" ng-click="::resetTraceFilter()") ALL
      .item(ng-repeat="event in tpEvents" ng-class="{active: event == traceFilter.event}")
        a(href="#" ng-click="traceFilter.event = event" ng-bind="::event")
      .item
        .ui.buttons
          button.ui.labeled.icon.button.olive(ng-click="outlineTrace()" ng-class="{loading: outlineTraceLoading}")
            i.angle.double.right.icon
            span OutLine
          .or
          button.ui.right.labeled.icon.button.green(ng-click="expandAllTrace()" ng-class="{loading: expandAllTraceLoading}")
            i.angle.double.down.icon
            span Expand
      .right.menu
        .item
          span Trace Count
          .ui.teal.left.pointing.label(ng-bind="currentCounter()")

  .ui.container
    .row
    .ui.relaxed.items(ng-class="{{traceFilter.event}}")
      // track by trace.order_id doesn't always work. because order_id doesn't always exists
      .item.trace(ng-repeat="trace in traces | filter:traceFilter:true | filter:childParentFilterFn" ng-class="callLevelClass(trace)" ng-controller="TpTraceCtrl")
        .content(ng-init="showMoreDetail = false" style="display: flex; justify-content: space-between; align-items: center;")
          .ui.segment.padded.main
            a.ui.top.right.attached.label.mini(ng-show="::containsDetail(trace)" ng-click="showMoreDetail = !showMoreDetail")
              i.sidebar.icon
            .header
              span.bold.ui.label>(ng-bind="::(trace.order_id || '>')")
              span(ng-bind="::tpSelfList[trace.tp_self_refer]")
              i.circle.icon.grey(style="font-size: 0.3em")
              span.method-value(ng-bind="::trace.method_id")
            a.ui.bottom.right.attached.label.mini(ng-if="::hasChild()" ng-click="toggleChild()" ng-class="{loading: togglingChild}")
              i.icon.angle.down(ng-show="trace.childOpened" style="font-size: 10px")
              i.icon.angle.right(ng-hide="trace.childOpened" style="font-size: 14px")
              / pulse-spinner(ng-show="togglingChild")
            / workaround for return_value is 'false' and return_value always to be string when existed
            .meta(ng-if="trace.hasOwnProperty('return_value')")
              i.icon.pointing.right.small
              json-formatter(json="::trimString(trace.return_value, 30)" title="{{::trace.return_value_class}}" style="display: inline-block")
          .description(style="margin-left: 20px")
            .details.right.floated(ng-if="showMoreDetail")
              .ui.segments(style="border-color: blue")
                .ui.segment(ng-if="::trace.params_var")
                  .ui.teal.left.ribbon.label Parameters
                  json-formatter(open="1" json="::trace.params_var" title="{{::trace.params_var_class}}")
                .ui.segment(ng-if="::trace.hasOwnProperty('return_value')")
                  .ui.grey.left.ribbon.label Return Value
                  json-formatter(open="1" json="::trace.return_value" title="{{::trace.return_value_class}}")
                .ui.segment(ng-if="::trace.local_var")
                  .ui.teal.left.ribbon.label Local Variables
                  json-formatter(open="1" json="::trace.local_var" title="{{::trace.local_var_class}}")
                .ui.segment(ng-if="::trace.instance_var")
                  .ui.blue.left.ribbon.label Instance Variables
                  json-formatter(open="1" json="::trace.instance_var" title="{{::trace.instance_var_class}}")
                .ui.segment(ng-if="::containsOtherAttrs(trace)")
                  .ui.orange.left.ribbon.label Trace Attributes
                  json-formatter(open="1" json="::plusTraceAttrs[trace.order_id]" title="TracePoint")

  script src="https://cdn.rawgit.com/Urigo/angular-spinkit/master/build/angular-spinkit.min.js"
  script src="https://cdn.rawgit.com/raykin/json-formatter/master/dist/json-formatter.js"
  // script async src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.3/angular-sanitize.js"

  javascript:
    sourceRoute = angular.module('SourceRoute', ['jsonFormatter', 'angular-spinkit'])
    sourceRoute.controller('MainCtrl', function($scope, $filter, $timeout) {

      // setup different color on menu item may not be a good solution
      // $scope.menuColorList = ['yellow', 'olive', 'green', 'teal', 'violet', 'purple', 'brown']
      $scope.trimString = function(str, length) {
        return str.length > length ? str.substring(0, length - 3) + '...' : str;
      }

      $scope.traces = angular.element("#trace-data").data('trace')
      $scope.tpSelfList = angular.element("#trace-data").data('tp-self-caches')
      $scope.tpEvents = angular.element("#trace-data").data('tp-events')
      $scope.childParentFilter = { hide_trace_ids: [] }

      $scope.childParentFilterFn = function(trace) {
        if (!trace.hasOwnProperty('parent_ids')) {
          return true;
        }
        if (trace.parent_ids.length == 0) {
          return true;
        } else {
          var shared_hide_parents = _.intersection(trace.parent_ids, $scope.childParentFilter.hide_trace_ids);
          if (shared_hide_parents.length > 0 ) {
            return false;
          }
        }
        return true;
      }

      $scope.expandAllTrace = function() {
        $scope.expandAllTraceLoading = true
        $timeout(function() {
          _.each($scope.traces, function(trace) { trace.childOpened = true; });
          $scope.childParentFilter.hide_trace_ids = [];
          $scope.expandAllTraceLoading = false
          $scope.expandAllTraceLoading = false
        }, 100)
      }

      $scope.outlineTrace = function() {
        $scope.outlineTraceLoading = true
        $scope.childParentFilter.hide_trace_ids = [];
        _.chain($scope.traces).filter({parent_length: 0, event: $scope.traceFilter.event})
          .each(function(trace) {
            trace.childOpened = false;
            $scope.childParentFilter.hide_trace_ids.push(trace.order_id) }
          ).value()
        $scope.outlineTraceLoading = false
      }

      $scope.traceFilter = {event: $scope.tpEvents[0]}
      if ($scope.tpEvents.length == 1 && angular.isUndefined($scope.traces[0].event)) {
        _.each($scope.traces, function(trace) {
          trace.event = $scope.tpEvents[0]
        })
      }

      $scope.definedClasses = _.uniq(_.map($scope.traces, 'defined_class'))

      $scope.callLevelClass = function(trace) {
        if (trace.parent_length > 7) {
          return 'call-level-7';
        } else {
          return 'call-level-' + trace.parent_length;
        }
      }

      $scope.resetTraceFilter = function() {
        $scope.traceFilter = {};
      }

      $scope.currentCounter = function() {
        return $filter('filter')($scope.traces, $scope.traceFilter, true).length;
      }

      $scope.containsOtherAttrs = function(trace) {
        return trace.hasOwnProperty('path') || trace.hasOwnProperty('lineno')
      }

      $scope.containsDetail = function(trace) {
        return $scope.containsOtherAttrs(trace) || trace.hasOwnProperty('local_var') ||
          trace.hasOwnProperty('instance_var')
      }

      combinedAttrs = function(trace) {
        var attrs = {}

        if (trace.hasOwnProperty('lineno') && trace.hasOwnProperty('path')) {
          attrs.method_defined_on = _.replace(trace.path, /.*gems\//, '') + ":" + trace.lineno;
        } else if (trace.hasOwnProperty('path')) {
          attrs.method_defined_on = _.replace(trace.path, /.*gems\//, '');
        }
        if (trace.hasOwnProperty('defined_class')) {
          attrs.method_defined_in = trace.defined_class;
        }
        return attrs;
      }

      $scope.plusTraceAttrs = _.map($scope.traces, combinedAttrs)

      $scope.outlineTrace();
    })

    sourceRoute.controller('TpTraceCtrl', function($scope, $timeout) {

      $scope.toggleChild = function() {
        $scope.togglingChild = true
        $timeout(function() {
          if ($scope.trace.childOpened) {
            $scope.hideChild();
            $scope.togglingChild = false
          } else {
            $scope.showChild();
            $scope.togglingChild = false
          }
        }, 0)
      }

      $scope.showChild = function() {
        $scope.trace.childOpened = true;
        _.pull($scope.childParentFilter.hide_trace_ids, $scope.trace.order_id);
        if ($scope.trace.direct_child_order_ids.length > 0) {
          $scope.childParentFilter.hide_trace_ids.push($scope.trace.direct_child_order_ids);
          $scope.childParentFilter.hide_trace_ids = _.chain($scope.childParentFilter.hide_trace_ids).flatten().uniq().value();
        }
      }

      $scope.hideChild = function() {
        $scope.trace.childOpened = false;
        $scope.childParentFilter.hide_trace_ids.push($scope.trace.order_id);
        if ($scope.trace.direct_child_order_ids.length > 0) {
          _.each($scope.trace.direct_child_order_ids, function(ele) {
            _.pull($scope.childParentFilter.hide_trace_ids, ele);
          });
        }
      }

      $scope.hasChild = function() {
        return _.find($scope.traces, function(trace) {
          return _.includes(trace.parent_ids, $scope.trace.order_id)
        });
      }
    })

  .data-collect
    / dont use local_trace_data.to_json, because ActiveSupport override it and can introduce unexpected crash for some data
    #trace-data(data-trace="#{jsonify_trace_chain}"
      data-tp-events="#{jsonify_events}"
      data-tp-self-caches="#{jsonify_tp_self_caches}")