//#OPTIONS: CPP

/*
  debugging tools that depend on node.js extensions

  compile with -DGHCJS_DEBUG_ALLOC to use

  compile additionally with -DGHCJS_DEBUG_ALLOC_ALWAYS_ENABLE to enable
  allocation debugging even if the JavaScript runtime does not support
  weak references with observable deadness. This causes allocation
  debugging to run with reduced functionality (h$debugAlloc_shouldBeDead
  is not available) and keeps strong references to everything allocaded
  between h$gc calls.
 */

// public API

// called after (GHCJS) GC, all objects reachable from the Haskell
// runtime must have been marked with the given mark
function h$debugAlloc_verifyReachability(mark) {
#ifdef GHCJS_DEBUG_ALLOC
  h$debugAlloc_init_internal();
  h$debugAlloc_verifyReachability_internal(mark);
#endif
}

// called after creating a new Heap object or RTS primitive
function h$debugAlloc_notifyAlloc(obj) {
#ifdef GHCJS_DEBUG_ALLOC
  h$debugAlloc_init_internal();
  h$debugAlloc_notifyAlloc_internal(obj);
#endif
}

// called when an object is used
function h$debugAlloc_notifyUse(obj) {
#ifdef GHCJS_DEBUG_ALLOC
  h$debugAlloc_init_internal();
  h$debugAlloc_notifyUse_internal(obj);
#endif
}

// private API

#ifdef GHCJS_DEBUG_ALLOC

var h$debugAlloc;

function h$debugAlloc_init_internal() {
  if(!h$debugAlloc) {
    h$debugAlloc =
      { enabled: false
      // Set of weak references to everything that's been allocated
      , allocatedHeapObjects: null
      // reference to the node.js weak module
      , makeWeak: null
      // exception while trying to load the node.js weak module
      , makeWeakError: null
      // each registered allocated heap object gets a unique sequence number
      , allocCount: 0
      // these objects have been seen with the wrong mark at least once (Set of weak refs)
      // each object has a __ghcjsShouldBeDeadSince timestamp indicating the first time
      // the object was unreachable from the Haskell heap
      , shouldBeDead: null
      // these objects have been seen with the wrong mark at least once, and have been
      // used after that (Map of sequence no -> heap object)
      //
      // this set should remain empty.
      , shouldBeDead_reported: null
      // these sequence ids of items that have been in the shouldBeDead_reported set
      // this should also be empty
      , shouldBeDead_reported_ids: null
    };

#ifndef GHCJS_BROWSER
    if(h$isNode()) {
      try {
        // the 'weak' package provides the same functionality, but segfaults
        // try this first
        h$debugAlloc.makeWeak = require('weak-napi');
        h$debugAlloc.enabled = true;
      } catch(e) {
        h$debugAlloc.makeWeakError = e;
      }
      /*
      if(!h$debugAlloc.makeWeak) {
      try {
        // fall back to 'weak'
        h$debugAlloc.makeWeak = require('weak');
        h$debugAlloc.enabled = true;
      } catch(e) {
        h$debugAlloc.makeWeakError = e;
      }
    }*/
    }
#endif
#ifdef GHCJS_DEBUG_ALLOC_ALWAYS_ENABLE
    h$debugAlloc.enabled = true;
#endif
    if(h$debugAlloc.enabled) {
      h$debugAlloc.allocatedHeapObjects = new Set();
      if(h$debugAlloc.makeWeak) {
        h$debugAlloc.shouldBeDead = new Set();
      }
      h$debugAlloc.shouldBeDead_reported = new Map();
      h$debugAlloc.shouldBeDead_reported_ids = new Set();
    }
  }
}

function h$debugAlloc_notifyAlloc_internal(obj) {
  if(!h$debugAlloc.enabled) return;
  // test if already notified
  if(typeof obj.__ghcjsDebugAllocSequenceNo == 'number') return;
  obj.__ghcjsDebugAllocSequenceNo = h$debugAlloc.allocCount++;
  obj.__ghcjsDebugAllocStack = new Error();

  // wrap all own properties in a getter and setter
  // use these to notify the allocation debugger of each use
  for(var p in obj) {
    if(!p.startsWith('__') && obj.hasOwnProperty(p)) {
      (function(pobj, pp) {
        var pw = '__alloc_wrapped_' + pp;
        pobj[pw] = pobj[pp];
        Object.defineProperty(pobj, pp,
          { get: function() {
                   h$debugAlloc_notifyUse_internal(this);
                   return this[pw];
                 }
          , set: function(v) {
                  h$debugAlloc_notifyUse_internal(this);
                  this[pw] = v;
                 }
          });
        })(obj, p);
    }
  }

  // insert into our set of allocated heap objects, use a weak ref if supported
  if(h$debugAlloc.makeWeak) {
    h$debugAlloc.allocatedHeapObjects.add(h$debugAlloc.makeWeak(obj));
  } else {
    h$debugAlloc.allocatedHeapObjects.add(obj);
  }
}

function h$debugAlloc_filterReport(obj) {
  var stk = obj.__ghcjsDebugAllocStack ? obj.__ghcjsDebugAllocStack.stack : '';
  if(!(stk.indexOf('h$runThreadSlice') >= 0)) return true;

  // if(obj.hasOwnProperty('f') && obj.hasOwnProperty('d1') && obj.hasOwnProperty('d2')) {
  // }
  // if(stk.indexOf(' at h$d ') >= 0) return true;
  // if(stk.indexOf(' at h$makeEnum ') >= 0) return true;
  return false;
}

var h$debugAlloc_notify_recursive = false;
function h$debugAlloc_notifyUse_internal(obj) {
  if(!h$debugAlloc.enabled) return;
  if(h$debugAlloc_notify_recursive) return;
  try {
    h$debugAlloc_notify_recursive = true;
    if(typeof obj === 'object' && obj && typeof obj.__ghcjsShouldBeDeadSince === 'number' && !h$debugAlloc_filterReport(obj)) {
      var seqNo = obj.__ghcjsDebugAllocSequenceNo;
      if(!h$debugAlloc.shouldBeDead_reported_ids.has(seqNo)) {
        if(h$debugAlloc.makeWeak) {
          h$debugAlloc.shouldBeDead_reported.set(seqNo, obj); // h$debugAlloc.makeWeak(obj));
        }
        h$debugAlloc.shouldBeDead_reported_ids.add(seqNo);
        // should we do more reporting, output the CCS or JS stack of the allocation point perhaps?
      }
    }
  } finally {
    h$debugAlloc_notify_recursive = false;
  }
}

// call this immediately after h$gc
function h$debugAlloc_verifyReachability_internal(mark) {
  if(!h$debugAlloc.enabled) return;
  if(typeof global == 'object' && global && global.gc) {
    global.gc();
  }
  var timestamp = Date.now();
  var weak, elem, key;
  function getMark(obj) {
    if(typeof obj.m === 'number') {
      return obj.m;
    } else if(typeof elem.m === 'object' && elem.m) {
      return obj.m.m;
    } else {
      return -1;
    }
  }
  if(h$debugAlloc.makeWeak) {
    // keep track of things with assistance from weaks in the JS runtime, yay!
    for(weak of h$debugAlloc.allocatedHeapObjects) {
      elem = h$debugAlloc.makeWeak.get(weak);
      if(elem === undefined) {
        h$debugAlloc.allocatedHeapObjects.delete(weak);
      } else {
        if(getMark(elem) !== mark) {
          elem.__ghcjsShouldBeDeadSince = timestamp;
          h$debugAlloc.shouldBeDead.add(h$debugAlloc.makeWeak(elem));
        }
      }
    }
    // clean up dead refs
    for(weak of h$debugAlloc.shouldBeDead) {
      if(h$debugAlloc.makeWeak.isDead(weak)) {
        h$debugAlloc.shouldBeDead.delete(weak);
      }
    }
    /* we store the original objects now
    for([key, weak] of h$debugAlloc.shouldBeDead_reported) {
      if(h$debugAlloc.makeWeak.isDead(weak)) {
        h$debugAlloc.shouldBeDead_reported.delete(key);
      }
    } */
  } else {
    // no support for weaks in the JS runtime
    // we don't keep track of shouldBeDead, since that would leak everything ever allocated
    // but we can still record use-after-unreachable cases
    for(elem of h$debugAlloc.allocatedHeapObjects) {
      if(getMark(elem) !== mark) {
        h$debugAlloc.allocatedHeapObjects.delete(elem);
        elem.__ghcjsShouldBeDeadSince = timestamp;
      }
    }
  }
}
#endif

/*
var h$debug = {};

function h$loadDebugTools() {
  function load(m, p) {
    if(h$isNode()) {
      try {
        var t = require(m);
        h$debug[p] = t;
      } catch(e) { }
    }
  }
  load('gc-stats', 'gcStats');
  load('v8-natives', 'v8');
  var t;
  if(h$isNode()) {
    try {
      t = require('gc-stats');
      h$debug.gcStats = t;
    } catch(e) { }
    try {
      require('');
    } catch(e) { }
  }
}
*/
