<!DOCTYPE html>
<html>

  <head>
    <!-- 1. Load libraries -->
    <!-- Polyfill(s) for older browsers -->
    <script src="https://npmcdn.com/core-js/client/shim.min.js"></script>
    <script src="https://npmcdn.com/reflect-metadata@0.1.3"></script>
    <script src="https://npmcdn.com/systemjs@0.19.27/dist/system.src.js"></script>
    
    <script src="https://code.angularjs.org/1.5.8/angular.js"></script>

    <!-- 2. Configure SystemJS -->
    <script src="systemjs.config.js"></script>
    <script>
      System.import('app').catch(function(err){ console.error(err); });
    </script>
    
    <link rel="stylesheet" href="style.css">
  </head>

  <!-- 3. Display the application -->
  <body>
    <my-app>Loading...</my-app>
  </body>

</html>
/* Styles go here */

System.config({
    //use typescript for compilation
    transpiler: 'ts',
    typescriptOptions: {
      tsconfig: true
    },
    meta: {
      'typescript': {
        "exports": "ts"
      }
    },

    //map tells the System loader where to look for things
    map: {
        'app': './app',
        'ng-metadata': 'https://npmcdn.com/ng-metadata',
        'rxjs': 'https://npmcdn.com/rxjs@5.0.0-beta.6',
        'ts': 'https://npmcdn.com/plugin-typescript@4.0.10/lib/plugin.js',
        'typescript': 'https://npmcdn.com/typescript@1.9.0-dev.20160409/lib/typescript.js',
    },
    //packages defines our app package
    packages: {
        app: {
          main: './main.ts',
          defaultExtension: 'ts'
        },
        'ng-metadata': {
          defaultExtension: 'js'
        },
        'rxjs': {
          main: 'index.js',
          defaultExtension: 'js'
        }
    }
});
import { bootstrap } from 'ng-metadata/platform-browser-dynamic';

import { AppComponent, NativeModuleName } from './index';

bootstrap( AppComponent, [NativeModuleName] );
import {Directive, Component, Inject, OnInit, OnDestroy, Input} from 'ng-metadata/core';
import angularStats from "./ng-stats";

var ngCreateCount = 0;
var ngDestroyCount = 0;
var elDestroyCount = 0;

@Directive({
  selector: '[my-directive]'
})
class MyDirective {
  constructor() {
    console.log("MyDirective construct");
  }
}

@Component({
  selector: 'my-component',
  directives:[MyDirective],
  template: '<div my-directive="$ctrl.someValue">MyComponent with MyDirective and got created {{$ctrl.inputVal}} times</div>'
})
class MyComponent implements OnInit, OnDestroy { 
  
  @Input("<")
  public inputVal;
  
  public someValue = "test";
  
  constructor(@Inject("$element") private el) {
  }
  
  ngOnInit() {
    
    console.log("MyComponent OnInit");
    
    ngCreateCount++;
    
    this.el.on("$destroy", function() {
      
      console.log("MyComponent element destroyed");
      
      elDestroyCount++;
    });
  }
  
  ngOnDestroy() {
    
    console.log("MyComponent OnDestroy");
    
    ngDestroyCount++;
  }
}

@Component({
  selector: 'my-app',
  directives:[MyComponent],
  template: '<div><h1>My BugReport Plnkr <small>for ng-metadata!</small></h1><div ng-if="$ctrl.showComponent"><my-component input-val="$ctrl.ngCreateCount"></my-component></div><button ng-click="$ctrl.toggle()">Toggle</button><div>ngOnInit: {{ $ctrl.ngCreateCount }}</div><div>ngOnDestroy: {{ $ctrl.ngDestroyCount }}</div><div>el.$destroy: {{ $ctrl.elDestroyCount }}</div></div><hr/><my-native-app></my-native-app>'
})
export class AppComponent { 
  
  public showComponent:boolean;
  
  public ngCreateCount:number = 0;
  public ngDestroyCount:number = 0;
  public elDestroyCount:number = 0;
  
  constructor(@Inject("$rootScope") rootScope) {
    angularStats({
      rootScope: rootScope,
      position: "bottomleft"
    });
  }
  
  public toggle() {
    this.showComponent = !this.showComponent;
    this.ngCreateCount = ngCreateCount;
    this.ngDestroyCount = ngDestroyCount;
    this.elDestroyCount = elDestroyCount;
  }
}
{
  "compilerOptions": {
    "target": "es5",
    "module": "commonjs",
    "moduleResolution": "node",
    "sourceMap": true,
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "removeComments": false,
    "noImplicitAny": true,
    "suppressImplicitAnyIndexErrors": true
  }
}
export { AppComponent } from './app.component';
export {NativeModuleName} from './app.native.component.ts'

var ngCreateCount = 0;
var ngDestroyCount = 0;
var elDestroyCount = 0;

export var NativeModuleName = angular.module("Native", [])
  .directive('myNativeDirective', function() {
    return {
      controller: function myNativeDirectiveCtrl() {
        console.log("MyNativeDirective construct");
      }
    }
  })
  .component('myNativeComponent', {
    template: '<div my-native-directive>MyNativeComponent with MyNativeDirective and got created {{$ctrl.inputVal}} times</div>',
    bindings: {
      inputVal: "<"
    },
    controller: ["$element",function myNativeComponentCtrl($element) {
      
      this.$onInit = function () {
    
        console.log("MyNativeComponent OnInit");
    
        ngCreateCount++;
    
        $element.on("$destroy", function() {
      
          console.log("MyNativeComponent element destroyed");
      
          elDestroyCount++;
        });
      }
  
      this.$onDestroy = function() {
    
        console.log("MyNativeComponent OnDestroy");
    
        ngDestroyCount++;
      }
    }]
  })
  .component('myNativeApp', {
    template: '<h1>My BugReport Plnkr <small>with native ng!</small></h1><div ng-if="$ctrl.showComponent"><my-native-component input-val="$ctrl.ngCreateCount"></my-native-component></div><button ng-click="$ctrl.toggle()">Toggle</button><div>$onInit: {{ $ctrl.ngCreateCount }}</div><div>$onDestroy: {{ $ctrl.ngDestroyCount }}</div><div>el.$destroy: {{ $ctrl.elDestroyCount }}</div>',
    controller: function myNativeAppCtrl() {
      this.showComponent = false;
  
      this.ngCreateCount = 0;
      this.ngDestroyCount = 0;
      this.elDestroyCount = 0;
  
      this.toggle = function() {
        this.showComponent = !this.showComponent;
        this.ngCreateCount = ngCreateCount;
        this.ngDestroyCount = ngDestroyCount;
        this.elDestroyCount = elDestroyCount;
      }
    }
  })
  .name;

/* istanbul ignore next */
if (!angular.version) {
  // we're doing this because some versions
  // of angular don't expose itself correctly
  angular = window.angular;
}

export default showAngularStats;

var autoloadKey = 'showAngularStats_autoload';
var current = null;
// define the timer function to use based upon whether or not 'performance is available'
var timerNow = window.self.performance && window.self.performance.now
  ? () => window.self.performance.now()
  : () => Date.now();

var lastWatchCountRun = timerNow();
var watchCountTimeout = null;
var lastWatchCount = getWatcherCount() || 0;
var lastDigestLength = 0;
var scopeSelectors = '.ng-scope, .ng-isolate-scope';
var $rootScope;

var digestIsHijacked = false;

var listeners = {
  watchCount: {},
  digestLength: {}
};

// Hijack $digest to time it and update data on every digest.
function hijackDigest() {
  if (digestIsHijacked) {
    return;
  }
  digestIsHijacked = true;
  var scopePrototype = Object.getPrototypeOf(getRootScope());
  var oldDigest = scopePrototype.$digest;
  scopePrototype.$digest = function $digest() {
    var start = timerNow();
    oldDigest.apply(this, arguments);
    var diff = (timerNow() - start);
    updateData(getWatcherCount(), diff);
  };
}

// used to prevent localstorage error in chrome packaged apps
function isChromeApp() {
  return (typeof chrome !== 'undefined' &&
  typeof chrome.storage !== 'undefined' &&
  typeof chrome.storage.local !== 'undefined');
}

// check for autoload
var autoloadOptions = sessionStorage[autoloadKey] || (!isChromeApp() && localStorage[autoloadKey]);
if (autoloadOptions) {
  autoload(JSON.parse(autoloadOptions));
}

function autoload(options) {
  if (window.self.angular && getRootScope()) {
    showAngularStats(options);
  } else {
    // wait for angular to load...
    setTimeout(function() {
      autoload(options);
    }, 200);
  }
}

function initOptions(opts) {

  // Remove autoload if they did not specifically request it
  if (opts === false || !opts.autoload) {
    sessionStorage.removeItem(autoloadKey);
    localStorage.removeItem(autoloadKey);
    // do nothing if the argument is false
    if (opts === false) {
      return;
    }
  }

  opts.position = opts.position || 'top-left';
  opts = angular.extend({
    htmlId: null,
    rootScope: undefined,
    digestTimeThreshold: 16,
    watchCountThreshold: 2000,
    autoload: false,
    trackDigest: false,
    trackWatches: false,
    logDigest: false,
    logWatches: false,
    styles: {
      position: 'fixed',
      background: 'black',
      borderBottom: '1px solid #666',
      borderRight: '1px solid #666',
      color: '#666',
      fontFamily: 'Courier',
      width: 130,
      zIndex: 9999,
      textAlign: 'right',
      top: opts.position.indexOf('top') === -1 ? null : 0,
      bottom: opts.position.indexOf('bottom') === -1 ? null : 0,
      right: opts.position.indexOf('right') === -1 ? null : 0,
      left: opts.position.indexOf('left') === -1 ? null : 0
    }
  }, opts || {});

  // for ionic support
  if (opts.rootScope) {
    $rootScope = opts.rootScope;
  }

  return opts;
}

function showAngularStats(opts) {
  /* eslint max-statements:[2, 45] */
  /* eslint complexity:[2, 18] */
  /* eslint consistent-return:0 */
  // TODO ^^ fix these things...
  opts = opts !== undefined ? opts : {};
  var returnData = {
    listeners: listeners
  };
  // delete the previous one
  if (current) {
    current.$el && current.$el.remove();
    current.active = false;
    current = null;
  }

  // Implemented in separate function due to webpack's statement count limit
  opts = initOptions(opts);

  if(!opts) {
    return;
  }

  hijackDigest();

  // setup the state
  var state = current = {active: true};

  // auto-load on startup
  if (opts.autoload) {
    if (opts.autoload === 'localStorage') {
      localStorage.setItem(autoloadKey, JSON.stringify(opts));
    } else if (opts.autoload === 'sessionStorage' || typeof opts.autoload === 'boolean') {
      sessionStorage.setItem(autoloadKey, JSON.stringify(opts));
    } else {
      throw new Error(
        'Invalid value for autoload: ' + opts.autoload + ' can only be "localStorage" "sessionStorage" or boolean.'
      );
    }
  }

  // general variables
  var bodyEl = angular.element(document.body);
  var noDigestSteps = 0;

  // add the DOM element
  var htmlId = opts.htmlId ? (' id="' + opts.htmlId + '"') : '';
  state.$el = angular.element('<div' + htmlId +
      '><canvas></canvas><div><span></span> | <span></span></div></div>').css(opts.styles);
  bodyEl.append(state.$el);
  var $watchCount = state.$el.find('span');
  var $digestTime = $watchCount.next();

  // initialize the canvas
  var graphSz = {width: 130, height: 40};
  var cvs = state.$el.find('canvas').attr(graphSz)[0];


  // add listeners
  listeners.digestLength.ngStatsAddToCanvas = function(digestLength) {
    addDataToCanvas(null, digestLength);
  };

  listeners.watchCount.ngStatsAddToCanvas = function(watchCount) {
    addDataToCanvas(watchCount);
  };

  track('digest', listeners.digestLength);
  track('watches', listeners.watchCount, true);

  log('digest', listeners.digestLength);
  log('watches', listeners.watchCount, true);

  function track(thingToTrack, listenerCollection, diffOnly) {
    var capThingToTrack = thingToTrack.charAt(0).toUpperCase() + thingToTrack.slice(1);
    if (opts['track' + capThingToTrack]) {
      returnData[thingToTrack] = [];
      listenerCollection['track + capThingToTrack'] = function(tracked) {
        if (!diffOnly || returnData[thingToTrack][returnData.length - 1] !== tracked) {
          returnData[thingToTrack][returnData.length - 1] = tracked;
          returnData[thingToTrack].push(tracked);
        }
      };
    }
  }

  function log(thingToLog, listenerCollection, diffOnly) {
    var capThingToLog = thingToLog.charAt(0).toUpperCase() + thingToLog.slice(1);
    if (opts['log' + capThingToLog]) {
      var last;
      listenerCollection['log' + capThingToLog] = function(tracked) {
        if (!diffOnly || last !== tracked) {
          last = tracked;
          var color = colorLog(thingToLog, tracked);
          if (color) {
            console.log('%c' + thingToLog + ':', color, tracked);
          } else {
            console.log(thingToLog + ':', tracked);
          }
        }
      };
    }
  }

  function getColor(metric, threshold) {
    if (metric > threshold) {
      return 'red';
    } else if (metric > 0.7 * threshold) {
      return 'orange';
    }
    return 'green';
  }

  function colorLog(thingToLog, tracked) {
    var color;
    if (thingToLog === 'digest') {
      color = 'color:' + getColor(tracked, opts.digestTimeThreshold);
    } else if (thingToLog === 'watches') {
      color = 'color:' + getColor(tracked, opts.watchCountThreshold);
    }
    return color;
  }

  function addDataToCanvas(watchCount, digestLength) {
    var averageDigest = digestLength || lastDigestLength;
    var digestColor = getColor(averageDigest, opts.digestTimeThreshold);
    lastWatchCount = nullOrUndef(watchCount) ? lastWatchCount : watchCount;
    var watchColor = getColor(lastWatchCount, opts.watchCountThreshold);
    lastDigestLength = nullOrUndef(digestLength) ? lastDigestLength : digestLength;
    $watchCount.text(lastWatchCount).css({color: watchColor});
    $digestTime.text(lastDigestLength.toFixed(2)).css({color: digestColor});

    if (!digestLength) {
      return;
    }

    // color the sliver if this is the first step
    var ctx = cvs.getContext('2d');
    if (noDigestSteps > 0) {
      noDigestSteps = 0;
      ctx.fillStyle = '#333';
      ctx.fillRect(graphSz.width - 1, 0, 1, graphSz.height);
    }

    // mark the point on the graph
    ctx.fillStyle = digestColor;
    ctx.fillRect(graphSz.width - 1, Math.max(0, graphSz.height - averageDigest), 2, 2);
  }

  // Shift the canvas to the left.
  function shiftLeft() {
    if (state.active) {
      setTimeout(shiftLeft, 250);
      var ctx = cvs.getContext('2d');
      var imageData = ctx.getImageData(1, 0, graphSz.width - 1, graphSz.height);
      ctx.putImageData(imageData, 0, 0);
      ctx.fillStyle = ((noDigestSteps++) > 2) ? 'black' : '#333';
      ctx.fillRect(graphSz.width - 1, 0, 1, graphSz.height);
    }
  }

  // start everything
  shiftLeft();
  if (!$rootScope.$$phase) {
    $rootScope.$digest();
  }

  return returnData;
}

angular.module('angularStats', []).directive('angularStats', function() {
  var index = 1;
  return {
    scope: {
      digestLength: '@',
      watchCount: '@',
      watchCountRoot: '@',
      onDigestLengthUpdate: '&?',
      onWatchCountUpdate: '&?'
    },
    link: function(scope, el, attrs) {
      hijackDigest();
      var directiveIndex = index++;

      setupDigestLengthElement();
      setupWatchCountElement();
      addWatchCountListener();
      addDigestLengthListener();
      scope.$on('$destroy', destroyListeners);

      function setupDigestLengthElement() {
        if (attrs.hasOwnProperty('digestLength')) {
          var digestEl = el;
          if (attrs.digestLength) {
            digestEl = angular.element(el[0].querySelector(attrs.digestLength));
          }
          listeners.digestLength['ngStatsDirective' + directiveIndex] = function(length) {
            window.dirDigestNode = digestEl[0];
            digestEl.text((length || 0).toFixed(2));
          };
        }
      }

      function setupWatchCountElement() {
        if (attrs.hasOwnProperty('watchCount')) {
          var watchCountRoot;
          var watchCountEl = el;
          if (scope.watchCount) {
            watchCountEl = angular.element(el[0].querySelector(attrs.watchCount));
          }

          if (scope.watchCountRoot) {
            if (scope.watchCountRoot === 'this') {
              watchCountRoot = el;
            } else {
              // In the case this directive is being compiled and it's not in the dom,
              // we're going to do the find from the root of what we have...
              var rootParent;
              if (attrs.hasOwnProperty('watchCountOfChild')) {
                rootParent = el[0];
              } else {
                rootParent = findRootOfElement(el);
              }
              watchCountRoot = angular.element(rootParent.querySelector(scope.watchCountRoot));
              if (!watchCountRoot.length) {
                throw new Error('no element at selector: ' + scope.watchCountRoot);
              }
            }
          }

          listeners.watchCount['ngStatsDirective' + directiveIndex] = function(count) {
            var watchCount = count;
            if (watchCountRoot) {
              watchCount = getWatcherCountForElement(watchCountRoot);
            }
            watchCountEl.text(watchCount);
          };
        }
      }

      function addWatchCountListener() {
        if (attrs.hasOwnProperty('onWatchCountUpdate')) {
          listeners.watchCount['ngStatsDirectiveUpdate' + directiveIndex] = function(count) {
            scope.onWatchCountUpdate({watchCount: count});
          };
        }
      }

      function addDigestLengthListener() {
        if (attrs.hasOwnProperty('onDigestLengthUpdate')) {
          listeners.digestLength['ngStatsDirectiveUpdate' + directiveIndex] = function(length) {
            scope.onDigestLengthUpdate({digestLength: length});
          };
        }
      }

      function destroyListeners() {
        delete listeners.digestLength['ngStatsDirectiveUpdate' + directiveIndex];
        delete listeners.watchCount['ngStatsDirectiveUpdate' + directiveIndex];
        delete listeners.digestLength['ngStatsDirective' + directiveIndex];
        delete listeners.watchCount['ngStatsDirective' + directiveIndex];
      }
    }
  };

  function findRootOfElement(el) {
    var parent = el[0];
    while (parent.parentElement) {
      parent = parent.parentElement;
    }
    return parent;
  }
});

// UTILITY FUNCTIONS

function getRootScope() {
  if ($rootScope) {
    return $rootScope;
  }
  var scopeEl = document.querySelector(scopeSelectors);
  if (!scopeEl) {
    return null;
  }
  $rootScope = angular.element(scopeEl).scope().$root;
  return $rootScope;
}

// Uses timeouts to ensure that this is only run every 300ms (it's a perf bottleneck)
function getWatcherCount() {
  clearTimeout(watchCountTimeout);
  var now = timerNow();
  if (now - lastWatchCountRun > 300) {
    lastWatchCountRun = now;
    lastWatchCount = getWatcherCountForScope();
  } else {
    watchCountTimeout = setTimeout(function() {
      updateData(getWatcherCount());
    }, 350);
  }
  return lastWatchCount;
}

function getWatcherCountForElement(element) {
  var startingScope = getClosestChildScope(element);
  return getWatcherCountForScope(startingScope);
}

function getClosestChildScope(element) {
  element = angular.element(element);
  var scope = element.scope();
  if (!scope) {
    element = angular.element(element.querySelector(scopeSelectors));
    scope = element.scope();
  }
  return scope;
}

function getWatchersFromScope(scope) {
  return scope && scope.$$watchers ? scope.$$watchers : [];
}

// iterate through listeners to call them with the watchCount and digestLength
function updateData(watchCount, digestLength) {
  // update the listeners
  if (!nullOrUndef(watchCount)) {
    angular.forEach(listeners.watchCount, function(listener) {
      listener(watchCount);
    });
  }
  if (!nullOrUndef(digestLength)) {
    angular.forEach(listeners.digestLength, function(listener) {
      listener(digestLength);
    });
  }
}

function nullOrUndef(item) {
  return item === null || item === undefined;
}

function getWatcherCountForScope(scope) {
  var count = 0;
  iterateScopes(scope, function(childScope) {
    count += getWatchersFromScope(childScope).length;
  });
  return count;
}

function iterateScopes(currentScope, fn) {
  if (typeof currentScope === 'function') {
    fn = currentScope;
    currentScope = null;
  }
  currentScope = currentScope || getRootScope();
  currentScope = _makeScopeReference(currentScope);
  if (!currentScope) {
    return;
  }
  var ret = fn(currentScope);
  if (ret === false) {
    return ret;
  }
  return iterateChildren(currentScope, fn);
}

function iterateSiblings(start, fn) {
  var ret;
  /* eslint no-extra-boolean-cast:0 */
  while (!!(start = start.$$nextSibling)) {
    ret = fn(start);
    if (ret === false) {
      break;
    }

    ret = iterateChildren(start, fn);
    if (ret === false) {
      break;
    }
  }
  return ret;
}

function iterateChildren(start, fn) {
  var ret;
  while (!!(start = start.$$childHead)) {
    ret = fn(start);
    if (ret === false) {
      break;
    }

    ret = iterateSiblings(start, fn);
    if (ret === false) {
      break;
    }
  }
  return ret;
}


function getScopeById(id) {
  var myScope = null;
  iterateScopes(function(scope) {
    if (scope.$id === id) {
      myScope = scope;
      return false;
    }
  });
  return myScope;
}

function _makeScopeReference(scope) {
  if (_isScopeId(scope)) {
    scope = getScopeById(scope);
  }
  return scope;
}

function _isScopeId(scope) {
  return typeof scope === 'string' || typeof scope === 'number';
}