'use strict'

const v8Util = process.electronBinding('v8_util')

const getOwnerKey = (webContents, contextId) => {
  return `${webContents.id}-${contextId}`
}

class ObjectsRegistry {
  constructor () {
    this.nextId = 0

    // Stores all objects by ref-counting.
    // (id) => {object, count}
    this.storage = {}

    // Stores the IDs + refCounts of objects referenced by WebContents.
    // (ownerKey) => { id: refCount }
    this.owners = {}
  }

  // Register a new object and return its assigned ID. If the object is already
  // registered then the already assigned ID would be returned.
  add (webContents, contextId, obj) {
    // Get or assign an ID to the object.
    const id = this.saveToStorage(obj)

    // Add object to the set of referenced objects.
    const ownerKey = getOwnerKey(webContents, contextId)
    let owner = this.owners[ownerKey]
    if (!owner) {
      owner = this.owners[ownerKey] = new Map()
      this.registerDeleteListener(webContents, contextId)
    }
    if (!owner.has(id)) {
      owner.set(id, 0)
      // Increase reference count if not referenced before.
      this.storage[id].count++
    }

    owner.set(id, owner.get(id) + 1)
    return id
  }

  // Get an object according to its ID.
  get (id) {
    const pointer = this.storage[id]
    if (pointer != null) return pointer.object
  }

  // Dereference an object according to its ID.
  // Note that an object may be double-freed (cleared when page is reloaded, and
  // then garbage collected in old page).
  // rendererSideRefCount is the ref count that the renderer process reported
  // at time of GC if this is different to the number of references we sent to
  // the given owner then a GC occurred between a ref being sent and the value
  // being pulled out of the weak map.
  // In this case we decrement out ref count and do not delete the stored
  // object
  // For more details on why we do renderer side ref counting see
  // https://github.com/electron/electron/pull/17464
  remove (webContents, contextId, id, rendererSideRefCount) {
    const ownerKey = getOwnerKey(webContents, contextId)
    const owner = this.owners[ownerKey]
    if (owner && owner.has(id)) {
      const newRefCount = owner.get(id) - rendererSideRefCount

      // Only completely remove if the number of references GCed in the
      // renderer is the same as the number of references we sent them
      if (newRefCount <= 0) {
        // Remove the reference in owner.
        owner.delete(id)
        // Dereference from the storage.
        this.dereference(id)
      } else {
        owner.set(id, newRefCount)
      }
    }
  }

  // Clear all references to objects refrenced by the WebContents.
  clear (webContents, contextId) {
    const ownerKey = getOwnerKey(webContents, contextId)
    const owner = this.owners[ownerKey]
    if (!owner) return

    for (const id of owner.keys()) this.dereference(id)

    delete this.owners[ownerKey]
  }

  // Private: Saves the object into storage and assigns an ID for it.
  saveToStorage (object) {
    let id = v8Util.getHiddenValue(object, 'atomId')
    if (!id) {
      id = ++this.nextId
      this.storage[id] = {
        count: 0,
        object: object
      }
      v8Util.setHiddenValue(object, 'atomId', id)
    }
    return id
  }

  // Private: Dereference the object from store.
  dereference (id) {
    const pointer = this.storage[id]
    if (pointer == null) {
      return
    }
    pointer.count -= 1
    if (pointer.count === 0) {
      v8Util.deleteHiddenValue(pointer.object, 'atomId')
      delete this.storage[id]
    }
  }

  // Private: Clear the storage when renderer process is destroyed.
  registerDeleteListener (webContents, contextId) {
    // contextId => ${processHostId}-${contextCount}
    const processHostId = contextId.split('-')[0]
    const listener = (event, deletedProcessHostId) => {
      if (deletedProcessHostId &&
          deletedProcessHostId.toString() === processHostId) {
        webContents.removeListener('render-view-deleted', listener)
        this.clear(webContents, contextId)
      }
    }
    // Note that the "render-view-deleted" event may not be emitted on time when
    // the renderer process get destroyed because of navigation, we rely on the
    // renderer process to send "ELECTRON_BROWSER_CONTEXT_RELEASE" message to
    // guard this situation.
    webContents.on('render-view-deleted', listener)
  }
}

module.exports = new ObjectsRegistry()
