/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

"use strict";

class WatchpointMap {
  constructor(threadActor) {
    this.threadActor = threadActor;
    this._watchpoints = new Map();
  }

  _setWatchpoint(objActor, data) {
    const { property, label, watchpointType } = data;
    const { rawObj } = objActor;

    const desc = objActor.obj.getOwnPropertyDescriptor(property);

    if (
      this.has(rawObj, property) ||
      desc.set ||
      desc.get ||
      !desc.configurable
    ) {
      return null;
    }

    function getValue() {
      return typeof desc.value === "object" && desc.value
        ? desc.value.unsafeDereference()
        : desc.value;
    }

    function setValue(v) {
      desc.value = objActor.obj.makeDebuggeeValue(v);
    }

    const maybeHandlePause = type => {
      const frame = this.threadActor.dbg.getNewestFrame();

      if (
        this.threadActor.shouldSkipAnyBreakpoint ||
        !this.threadActor.hasMoved(frame, type) ||
        this.threadActor.sourcesManager.isFrameBlackBoxed(frame)
      ) {
        return;
      }

      this.threadActor._pauseAndRespond(frame, {
        type,
        message: label,
      });
    };

    if (watchpointType === "get") {
      objActor.obj.defineProperty(property, {
        configurable: desc.configurable,
        enumerable: desc.enumerable,
        set: objActor.obj.makeDebuggeeValue(v => {
          setValue(v);
        }),
        get: objActor.obj.makeDebuggeeValue(() => {
          maybeHandlePause("getWatchpoint");
          return getValue();
        }),
      });
    }

    if (watchpointType === "set") {
      objActor.obj.defineProperty(property, {
        configurable: desc.configurable,
        enumerable: desc.enumerable,
        set: objActor.obj.makeDebuggeeValue(v => {
          maybeHandlePause("setWatchpoint");
          setValue(v);
        }),
        get: objActor.obj.makeDebuggeeValue(() => {
          return getValue();
        }),
      });
    }

    if (watchpointType === "getorset") {
      objActor.obj.defineProperty(property, {
        configurable: desc.configurable,
        enumerable: desc.enumerable,
        set: objActor.obj.makeDebuggeeValue(v => {
          maybeHandlePause("setWatchpoint");
          setValue(v);
        }),
        get: objActor.obj.makeDebuggeeValue(() => {
          maybeHandlePause("getWatchpoint");
          return getValue();
        }),
      });
    }

    return desc;
  }

  add(objActor, data) {
    // Get the object's description before calling setWatchpoint,
    // otherwise we'll get the modified property descriptor instead
    const desc = this._setWatchpoint(objActor, data);
    if (!desc) {
      return;
    }

    const objWatchpoints = this._watchpoints.get(objActor.rawObj) || new Map();

    objWatchpoints.set(data.property, { ...data, desc });
    this._watchpoints.set(objActor.rawObj, objWatchpoints);
  }

  has(obj, property) {
    const objWatchpoints = this._watchpoints.get(obj);
    return objWatchpoints && objWatchpoints.has(property);
  }

  get(obj, property) {
    const objWatchpoints = this._watchpoints.get(obj);
    return objWatchpoints && objWatchpoints.get(property);
  }

  remove(objActor, property) {
    const { rawObj } = objActor;

    // This should remove watchpoints on all of the object's properties if
    // a property isn't passed in as an argument
    if (!property) {
      for (const objProperty in rawObj) {
        this.remove(objActor, objProperty);
      }
    }

    if (!this.has(rawObj, property)) {
      return;
    }

    const objWatchpoints = this._watchpoints.get(rawObj);
    const { desc } = objWatchpoints.get(property);

    objWatchpoints.delete(property);
    this._watchpoints.set(rawObj, objWatchpoints);

    // We should stop keeping track of an object if it no longer
    // has a watchpoint
    if (objWatchpoints.size == 0) {
      this._watchpoints.delete(rawObj);
    }

    objActor.obj.defineProperty(property, desc);
  }

  removeAll(objActor) {
    const objWatchpoints = this._watchpoints.get(objActor.rawObj);
    if (!objWatchpoints) {
      return;
    }

    for (const objProperty in objWatchpoints) {
      this.remove(objActor, objProperty);
    }
  }
}

exports.WatchpointMap = WatchpointMap;
