import { surroundingAgent } from '../host-defined/engine.mts';
import {
  Value,
  type Arguments,
  type FunctionCallContext,
} from '../value.mts';
import { Q, type ValueCompletion, type ValueEvaluator } from '../completion.mts';
import { bootstrapPrototype } from './bootstrap.mts';
import type { WeakMapObject } from './WeakMap.mts';
import {
  Call,
  IsCallable,
  SameValue,
  RequireInternalSlot,
  CanBeHeldWeakly,
  Realm,
  Throw,
} from '#self';

/** https://tc39.es/ecma262/#sec-weakmap.prototype.delete */
function WeakMapProto_delete([key = Value.undefined]: Arguments, { thisValue }: FunctionCallContext): ValueCompletion {
  // 1. Let M be the this value.
  const M = thisValue as WeakMapObject;
  // 2. Perform ? RequireInternalSlot(M, [[WeakMapData]]).
  Q(RequireInternalSlot(M, 'WeakMapData'));
  // 3. If CanBeHeldWeakly(key) is false, return false.
  if (!CanBeHeldWeakly(key)) {
    return Value.false;
  }
  // 4. For each Record { [[Key]], [[Value]] } p that is an element of entries, do
  const entries = M.WeakMapData;
  for (let i = 0; i < entries.length; i += 1) {
    const p = entries[i];
    // a. If p.[[Key]] is not empty and SameValue(p.[[Key]], key) is true, then
    if (p.Key !== undefined && SameValue(p.Key, key) === Value.true) {
      // i. Set p.[[Key]] to empty.
      Q(surroundingAgent.debugger_tryTouchDuringPreview(M));
      p.Key = undefined;
      // ii. Set p.[[Value]] to empty.
      p.Value = undefined;
      // iii. return true.
      return Value.true;
    }
  }
  // 5. Return false.
  return Value.false;
}

/** https://tc39.es/ecma262/#sec-weakmap.prototype.get */
function WeakMapProto_get([key = Value.undefined]: Arguments, { thisValue }: FunctionCallContext): ValueCompletion {
  // 1. Let m be the this value.
  const M = thisValue as WeakMapObject;
  // 2. Perform ? RequireInternalSlot(M, [[WeakMapData]]).
  Q(RequireInternalSlot(M, 'WeakMapData'));
  // 3. If CanBeHeldWeakly(key) is false, return false.
  if (!CanBeHeldWeakly(key)) {
    return Value.undefined;
  }
  // 4. For each Record { [[Key]], [[Value]] } p of M.[[WeakMapData]], do
  const entries = M.WeakMapData;
  for (const p of entries) {
    // a. If p.[[Key]] is not empty and SameValue(p.[[Key]], key) is true, return p.[[Value]].
    if (p.Key !== undefined && SameValue(p.Key, key) === Value.true) {
      return p.Value!;
    }
  }
  // 6. Return undefined.
  return Value.undefined;
}

/** https://tc39.es/proposal-upsert/#sec-weakmap.prototype.getOrInsert */
function WeakMapProto_getOrInsert([key = Value.undefined, value = Value.undefined]: Arguments, { thisValue }: FunctionCallContext): ValueCompletion {
  // 1. Let m be the this value.
  const M = thisValue as WeakMapObject;
  // 2. Perform ? RequireInternalSlot(M, [[WeakMapData]]).
  Q(RequireInternalSlot(M, 'WeakMapData'));
  // 3. If CanBeHeldWeakly(key) is false, throw a TypeError exception.
  if (!CanBeHeldWeakly(key)) {
    return Throw.TypeError('$1 cannot be used as a WeakMap key', key);
  }
  // 4. For each Record { [[Key]], [[Value]] } p of M.[[WeakMapData]], do
  const entries = M.WeakMapData;
  for (const p of entries) {
    // a. If p.[[Key]] is not empty and SameValue(p.[[Key]], key) is true, return p.[[Value]].
    if (p.Key !== undefined && SameValue(p.Key, key) === Value.true) {
      return p.Value!;
    }
  }
  // 5. Let p be the Record { [[Key]]: key, [[Value]]: value }.
  const p = { Key: key, Value: value };
  // 6. Append p to M.[[WeakMapData]].
  entries.push(p);
  // 7. Return value.
  return value;
}

/**  https://tc39.es/proposal-upsert/#sec-weakmap.prototype.getOrInsertComputed */
function* WeakMapProto_getOrInsertComputed([key = Value.undefined, callbackfn = Value.undefined]: Arguments, { thisValue }: FunctionCallContext): ValueEvaluator {
  // 1. Let m be the this value.
  const M = thisValue as WeakMapObject;
  // 2. Perform ? RequireInternalSlot(M, [[WeakMapData]]).
  Q(RequireInternalSlot(M, 'WeakMapData'));
  // 3. If CanBeHeldWeakly(key) is false, throw a TypeError exception.
  if (!CanBeHeldWeakly(key)) {
    return surroundingAgent.Throw('TypeError', 'NotAWeakKey', key);
  }
  // 4. If IsCallable(callbackfn) is false, throw a TypeError exception.
  if (!IsCallable(callbackfn)) {
    return surroundingAgent.Throw('TypeError', 'NotAFunction', callbackfn);
  }
  // 5. For each Record { [[Key]], [[Value]] } p of M.[[WeakMapData]], do
  const entries = M.WeakMapData;
  for (const p of entries) {
    // a. If p.[[Key]] is not empty and SameValue(p.[[Key]], key) is true, return p.[[Value]].
    if (p.Key !== undefined && SameValue(p.Key, key) === Value.true) {
      return p.Value!;
    }
  }
  // 6. Let value be ? Call(callbackfn, undefined, « key »).
  const value = Q(yield* Call(callbackfn, Value.undefined, [key]));
  // 7. NOTE: The Map may have been modified during execution of callbackfn.
  // 8. For each Record { [[Key]], [[Value]] } p of M.[[WeakMapData]], do
  for (const p of entries) {
    // a. If p.[[Key]] is not empty and SameValue(p.[[Key]], key) is true, then
    if (p.Key !== undefined && SameValue(p.Key, key) === Value.true) {
      // i. Set p.[[Value]] to value.
      p.Value = value;
      // ii. Return value.
      return value;
    }
  }
  // 9. Let p be the Record { [[Key]]: key, [[Value]]: value }.
  const p = { Key: key, Value: value };
  // 10. Append p to M.[[WeakMapData]].
  entries.push(p);
  // 11. Return value.
  return value;
}

/** https://tc39.es/ecma262/#sec-weakmap.prototype.has */
function WeakMapProto_has([key = Value.undefined]: Arguments, { thisValue }: FunctionCallContext): ValueCompletion {
  // 1. Let M be the this value.
  const M = thisValue as WeakMapObject;
  // 2. Perform ? RequireInternalSlot(M, [[WeakMapData]]).
  Q(RequireInternalSlot(M, 'WeakMapData'));
  // 3. If CanBeHeldWeakly(key) is false, return false.
  if (!CanBeHeldWeakly(key)) {
    return Value.false;
  }
  // 4. For each Record { [[Key]], [[Value]] } p of M.[[WeakMapData]], do
  const entries = M.WeakMapData;
  for (const p of entries) {
    // a. If p.[[Key]] is not empty and SameValue(p.[[Key]], key) is true, return true.
    if (p.Key !== undefined && SameValue(p.Key, key) === Value.true) {
      return Value.true;
    }
  }
  // 6. Return false.
  return Value.false;
}

/** https://tc39.es/ecma262/#sec-weakmap.prototype.set */
function WeakMapProto_set([key = Value.undefined, value = Value.undefined]: Arguments, { thisValue }: FunctionCallContext): ValueCompletion {
  // 1. Let M be the this value.
  const M = thisValue as WeakMapObject;
  // 2. Perform ? RequireInternalSlot(M, [[WeakMapData]]).
  Q(RequireInternalSlot(M, 'WeakMapData'));
  // 3. If CanBeHeldWeakly(key) is false, throw a TypeError exception.
  if (!CanBeHeldWeakly(key)) {
    return surroundingAgent.Throw('TypeError', 'WeakCollectionNotObject', key);
  }
  // 4. For each Record { [[Key]], [[Value]] } p that is an element of entries, do
  const entries = M.WeakMapData;
  for (const p of entries) {
    // a. If p.[[Key]] is not empty and SameValue(p.[[Key]], key) is true, then
    if (p.Key !== undefined && SameValue(p.Key, key) === Value.true) {
      // i. Set p.[[Value]] to value.
      Q(surroundingAgent.debugger_tryTouchDuringPreview(M));
      p.Value = value;
      // ii. Return M.
      return M;
    }
  }
  // 5. Let p be the Record { [[Key]]: key, [[Value]]: value }.
  const p = { Key: key, Value: value };
  // 7. Append p as the last element of entries.
  entries.push(p);
  // 7. Return M.
  return M;
}

export function bootstrapWeakMapPrototype(realmRec: Realm) {
  const proto = bootstrapPrototype(realmRec, [
    ['delete', WeakMapProto_delete, 1],
    ['get', WeakMapProto_get, 1],
    ['getOrInsert', WeakMapProto_getOrInsert, 2],
    ['getOrInsertComputed', WeakMapProto_getOrInsertComputed, 2],
    ['has', WeakMapProto_has, 1],
    ['set', WeakMapProto_set, 2],
  ], realmRec.Intrinsics['%Object.prototype%'], 'WeakMap');

  realmRec.Intrinsics['%WeakMap.prototype%'] = proto;
}
