import { surroundingAgent } from '../host-defined/engine.mts';
import {
  NullValue,
  JSStringValue,
  UndefinedValue,
  ObjectValue,
  Value,
  Descriptor,
  wellKnownSymbols,
  type FunctionCallContext,
  type Arguments,
  type ObjectInternalMethods,
} from '../value.mts';
import {
  Q, X, type ValueCompletion, type ValueEvaluator,
} from '../completion.mts';
import { __ts_cast__, type Mutable } from '../helpers.mts';
import { assignProps } from './bootstrap.mts';
import {
  DefinePropertyOrThrow,
  Get,
  HasOwnProperty,
  Invoke,
  IsAccessorDescriptor,
  IsArray,
  IsCallable,
  MakeBasicObject,
  Realm,
  RequireObjectCoercible,
  SameValue,
  SetImmutablePrototype,
  ToObject,
  ToPropertyKey,
  type BuiltinFunctionObject,
  type FunctionObject,
  type ImmutablePrototypeObject,
  type OrdinaryObject,
} from '#self';

/** https://tc39.es/ecma262/#sec-object.prototype.hasownproperty */
function* ObjectProto_hasOwnProperty([V = Value.undefined]: Arguments, { thisValue }: FunctionCallContext): ValueEvaluator {
  // 1. Let P be ? ToPropertyKey(V).
  const P = Q(yield* ToPropertyKey(V));
  // 2. Let O be ? ToObject(this value).
  const O = Q(ToObject(thisValue));
  // 3. Return ? HasOwnProperty(O, P).
  return yield* HasOwnProperty(O, P);
}

/** https://tc39.es/ecma262/#sec-object.prototype.isprototypeof */
function* ObjectProto_isPrototypeOf([V = Value.undefined]: Arguments, { thisValue }: FunctionCallContext): ValueEvaluator {
  // 1. If Type(V) is not Object, return false.
  if (!(V instanceof ObjectValue)) {
    return Value.false;
  }
  // 2. Let O be ? ToObject(this value).
  const O = Q(ToObject(thisValue));
  // 3. Repeat,
  while (true) {
    // a. Set V to ? V.[[GetPrototypeOf]]().
    V = Q(yield* (V as ObjectValue).GetPrototypeOf());
    // b. If V is null, return false.
    if (V === Value.null) {
      return Value.false;
    }
    // c. If SameValue(O, V) is true, return true.
    if (SameValue(O, V) === Value.true) {
      return Value.true;
    }
  }
}

/** https://tc39.es/ecma262/#sec-object.prototype.propertyisenumerable */
function* ObjectProto_propertyIsEnumerable([V = Value.undefined]: Arguments, { thisValue }: FunctionCallContext): ValueEvaluator {
  // 1. Let P be ? ToPropertyKey(V).
  const P = Q(yield* ToPropertyKey(V));
  // 2. Let O be ? ToObject(this value).
  const O = Q(ToObject(thisValue));
  // 3. Let desc be ? O.[[GetOwnProperty]](P).
  const desc = Q(yield* O.GetOwnProperty(P));
  // 4. If desc is undefined, return false.
  if (desc instanceof UndefinedValue) {
    return Value.false;
  }
  // 5. Return desc.[[Enumerable]].
  return desc.Enumerable!;
}

/** https://tc39.es/ecma262/#sec-object.prototype.tolocalestring */
function* ObjectProto_toLocaleString(_argList: Arguments, { thisValue }: FunctionCallContext): ValueEvaluator {
  // 1. Let O be the this value.
  const O = thisValue;
  // 2. Return ? Invoke(O, "toString").
  return Q(yield* Invoke(O, Value('toString')));
}

/** https://tc39.es/ecma262/#sec-object.prototype.tostring */
function* ObjectProto_toString(_argList: Arguments, { thisValue }: FunctionCallContext): ValueEvaluator {
  // 1. If the this value is undefined, return "[object Undefined]".
  if (thisValue === Value.undefined) {
    return Value('[object Undefined]');
  }
  // 2. If the this value is null, return "[object Null]".
  if (thisValue === Value.null) {
    return Value('[object Null]');
  }
  // 3. Let O be ! ToObject(this value).
  const O = X(ToObject(thisValue));
  // 4. Let isArray be ? IsArray(O).
  const isArray = Q(IsArray(O));
  let builtinTag;
  // 5. If isArray is true, let builtinTag be "Array".
  if (isArray === Value.true) {
    builtinTag = 'Array';
  } else if ('ParameterMap' in O) { // 6. Else if O has a [[ParameterMap]] internal slot, let builtinTag be "Arguments".
    builtinTag = 'Arguments';
  } else if ('Call' in O) { // 7. Else if O has a [[Call]] internal method, let builtinTag be "Function".
    builtinTag = 'Function';
  } else if ('ErrorData' in O) { // 8. Else if O has an [[ErrorData]] internal slot, let builtinTag be "Error".
    builtinTag = 'Error';
  } else if ('BooleanData' in O) { // 9. Else if O has a [[BooleanData]] internal slot, let builtinTag be "Boolean".
    builtinTag = 'Boolean';
  } else if ('NumberData' in O) { // 10. Else if O has a [[NumberData]] internal slot, let builtinTag be "Number".
    builtinTag = 'Number';
  } else if ('StringData' in O) { // 11. Else if O has a [[StringData]] internal slot, let builtinTag be "String".
    builtinTag = 'String';
  } else if ('DateValue' in O) { // 12. Else if O has a [[DateValue]] internal slot, let builtinTag be "Date".
    builtinTag = 'Date';
  } else if ('RegExpMatcher' in O) { // 13. Else if O has a [[RegExpMatcher]] internal slot, let builtinTag be "RegExp".
    builtinTag = 'RegExp';
  } else { // 14. Else, let builtinTag be "Object".
    builtinTag = 'Object';
  }
  // 15. Let tag be ? Get(O, @@toStringTag).
  const tag = Q(yield* Get(O, wellKnownSymbols.toStringTag));
  let tagStr;
  // 16. If Type(tag) is not String, set tag to builtinTag.
  if (!(tag instanceof JSStringValue)) {
    tagStr = builtinTag;
  } else {
    tagStr = tag.stringValue();
  }
  // 17. Return the string-concatenation of "[object ", tag, and "]".
  return Value(`[object ${tagStr}]`);
}

/** https://tc39.es/ecma262/#sec-object.prototype.valueof */
function ObjectProto_valueOf(_argList: Arguments, { thisValue }: FunctionCallContext): ValueCompletion {
  // 1. Return ? ToObject(this value).
  return Q(ToObject(thisValue));
}

/** https://tc39.es/ecma262/#sec-object.prototype.__defineGetter__ */
function* ObjectProto__defineGetter__([P = Value.undefined, getter = Value.undefined]: Arguments, { thisValue }: FunctionCallContext): ValueEvaluator {
  // 1. Let O be ? ToObject(this value).
  const O = Q(ToObject(thisValue));
  // 2. If IsCallable(getter) is false, throw a TypeError exception.
  if (!IsCallable(getter)) {
    return surroundingAgent.Throw('TypeError', 'NotAFunction', getter);
  }
  // 3. Let desc be PropertyDescriptor { [[Get]]: getter, [[Enumerable]]: true, [[Configurable]]: true }.
  const desc = Descriptor({
    Get: getter,
    Enumerable: Value.true,
    Configurable: Value.true,
  });
  // 4. Let key be ? ToPropertyKey(P).
  const key = Q(yield* ToPropertyKey(P));
  // 5. Perform ? DefinePropertyOrThrow(O, key, desc).
  Q(yield* DefinePropertyOrThrow(O, key, desc));
  // 6. Return undefined.
  return Value.undefined;
}

/** https://tc39.es/ecma262/#sec-object.prototype.__defineSetter__ */
function* ObjectProto__defineSetter__([P = Value.undefined, setter = Value.undefined]: Arguments, { thisValue }: FunctionCallContext): ValueEvaluator {
  // 1. Let O be ? ToObject(this value).
  const O = Q(ToObject(thisValue));
  // 2. If IsCallable(setter) is false, throw a TypeError exception.
  if (!IsCallable(setter)) {
    return surroundingAgent.Throw('TypeError', 'NotAFunction', setter);
  }
  // 3. Let desc be PropertyDescriptor { [[Set]]: setter, [[Enumerable]]: true, [[Configurable]]: true }.
  const desc = Descriptor({
    Set: setter,
    Enumerable: Value.true,
    Configurable: Value.true,
  });
  // 4. Let key be ? ToPropertyKey(P).
  const key = Q(yield* ToPropertyKey(P));
  // 5. Perform ? DefinePropertyOrThrow(O, key, desc).
  Q(yield* DefinePropertyOrThrow(O, key, desc));
  // 6. Return undefined.
  return Value.undefined;
}

/** https://tc39.es/ecma262/#sec-object.prototype.__lookupGetter__ */
function* ObjectProto__lookupGetter__([P = Value.undefined]: Arguments, { thisValue }: FunctionCallContext): ValueEvaluator {
  // 1. Let O be ? ToObject(this value).
  let O: NullValue | ObjectValue = Q(ToObject(thisValue));
  // 2. Let key be ? ToPropertyKey(P).
  const key = Q(yield* ToPropertyKey(P));
  // 3. Repeat,
  while (true) {
    __ts_cast__<ObjectValue>(O);
    // a. Let desc be ? O.[[GetOwnProperty]](key).
    const desc = Q(yield* O.GetOwnProperty(key));
    // b. If desc is not undefined, then
    if (!(desc instanceof UndefinedValue)) {
      // i. If IsAccessorDescriptor(desc) is true, return desc.[[Get]].
      if (IsAccessorDescriptor(desc)) {
        return desc.Get;
      }
      // ii. Return undefined.
      return Value.undefined;
    }
    // c. Set O to ? O.[[GetPrototypeOf]]().
    O = Q(yield* O.GetPrototypeOf());
    // d. If O is null, return undefined.
    if (O === Value.null) {
      return Value.undefined;
    }
  }
}

/** https://tc39.es/ecma262/#sec-object.prototype.__lookupSetter__ */
function* ObjectProto__lookupSetter__([P = Value.undefined]: Arguments, { thisValue }: FunctionCallContext): ValueEvaluator {
  // 1. Let O be ? ToObject(this value).
  let O: NullValue | ObjectValue = Q(ToObject(thisValue));
  // 2. Let key be ? ToPropertyKey(P).
  const key = Q(yield* ToPropertyKey(P));
  // 3. Repeat,
  while (true) {
    __ts_cast__<ObjectValue>(O);
    // a. Let desc be ? O.[[GetOwnProperty]](key).
    const desc = Q(yield* O.GetOwnProperty(key));
    // b. If desc is not undefined, then
    if (!(desc instanceof UndefinedValue)) {
      // i. If IsAccessorDescriptor(desc) is true, return desc.[[Set]].
      if (IsAccessorDescriptor(desc)) {
        return desc.Set;
      }
      // ii. Return undefined.
      return Value.undefined;
    }
    // c. Set O to ? O.[[GetPrototypeOf]]().
    O = Q(yield* O.GetPrototypeOf());
    // d. If O is null, return undefined.
    if (O === Value.null) {
      return Value.undefined;
    }
  }
}

/** https://tc39.es/ecma262/#sec-get-object.prototype.__proto__ */
function* ObjectProto__proto__Get(_args: Arguments, { thisValue }: FunctionCallContext): ValueEvaluator {
  // 1. Let O be ? ToObject(this value).
  const O = Q(ToObject(thisValue));
  // 2. Return ? O.[[GetPrototypeOf]]().
  return Q(yield* O.GetPrototypeOf());
}

/** https://tc39.es/ecma262/#sec-set-object.prototype.__proto__ */
function* ObjectProto__proto__Set([proto = Value.undefined]: Arguments, { thisValue }: FunctionCallContext): ValueEvaluator {
  // 1. Let O be the *this* value.
  // 2. Perform ? RequireObjectCoercible(this value).
  const O = thisValue;
  Q(RequireObjectCoercible(O));
  // 2. If Type(proto) is neither Object nor Null, return undefined.
  if (!(proto instanceof ObjectValue) && !(proto instanceof NullValue)) {
    return Value.undefined;
  }
  // 3. If Type(O) is not Object, return undefined.
  if (!(O instanceof ObjectValue)) {
    return Value.undefined;
  }
  // 4. Let status be ? O.[[SetPrototypeOf]](proto).
  const status = Q(yield* O.SetPrototypeOf(proto));
  // 5. If status is false, throw a TypeError exception.
  if (status === Value.false) {
    return surroundingAgent.Throw('TypeError', 'ObjectSetPrototype');
  }
  // 6. Return undefined.
  return Value.undefined;
}

const InternalMethods = {
  /** https://tc39.es/ecma262/multipage/ordinary-and-exotic-objects-behaviours.html#sec-immutable-prototype-exotic-objects-setprototypeof-v */
  * SetPrototypeOf(V) {
    // 1. Return ? SetImmutablePrototype(O, V).
    return Q(yield* SetImmutablePrototype(this, V));
  },
} satisfies Partial<ObjectInternalMethods<ImmutablePrototypeObject>>;

/** https://tc39.es/ecma262/multipage/fundamental-objects.html#sec-properties-of-the-object-prototype-object */
export function makeObjectPrototype(realmRec: Realm) {
  // The Object prototype object:
  const proto = MakeBasicObject(['Prototype', 'Extensible']) as Mutable<ImmutablePrototypeObject & OrdinaryObject>;

  // * has an [[Extensible]] internal slot whose value is true.
  proto.Extensible = Value.true;

  // * has a [[Prototype]] internal slot whose value is null.
  proto.Prototype = Value.null;

  // * has the internal methods defined for ordinary objects, except for the [[SetPrototypeOf]] method, which is as defined in 10.4.7.1.
  //   (Thus, it is an immutable prototype exotic object.)
  proto.SetPrototypeOf = InternalMethods.SetPrototypeOf;

  // * is %Object.prototype%.
  realmRec.Intrinsics['%Object.prototype%'] = proto;
}

export function bootstrapObjectPrototype(realmRec: Realm) {
  const proto = realmRec.Intrinsics['%Object.prototype%'];

  assignProps(realmRec, proto, [
    ['hasOwnProperty', ObjectProto_hasOwnProperty, 1],
    ['isPrototypeOf', ObjectProto_isPrototypeOf, 1],
    ['propertyIsEnumerable', ObjectProto_propertyIsEnumerable, 1],
    ['toLocaleString', ObjectProto_toLocaleString, 0],
    ['toString', ObjectProto_toString, 0],
    ['valueOf', ObjectProto_valueOf, 0],
    ['__defineGetter__', ObjectProto__defineGetter__, 2],
    ['__defineSetter__', ObjectProto__defineSetter__, 2],
    ['__lookupGetter__', ObjectProto__lookupGetter__, 1],
    ['__lookupSetter__', ObjectProto__lookupSetter__, 1],
    ['__proto__', [ObjectProto__proto__Get, ObjectProto__proto__Set]],
  ]);

  realmRec.Intrinsics['%Object.prototype.toString%'] = X(Get(proto, Value('toString'))) as BuiltinFunctionObject;
  realmRec.Intrinsics['%Object.prototype.valueOf%'] = X(Get(proto, Value('valueOf'))) as FunctionObject;
}
