import {
  NullValue,
  ObjectValue,
  Value,
  type Arguments,
  type FunctionCallContext,
  UndefinedValue,
  type PropertyKeyValue,
  Descriptor,
  SymbolValue,
  JSStringValue,
} from '../value.mts';
import {
  surroundingAgent,
} from '../host-defined/engine.mts';
import { Q, X, type ValueEvaluator } from '../completion.mts';
import { AddEntriesFromIterable } from './Map.mts';
import { bootstrapConstructor } from './bootstrap.mts';
import {
  Assert,
  CreateArrayFromList,
  CreateDataProperty,
  DefinePropertyOrThrow,
  CreateDataPropertyOrThrow,
  EnumerableOwnProperties,
  FromPropertyDescriptor,
  Get,
  HasOwnProperty,
  IsExtensible,
  OrdinaryObjectCreate,
  OrdinaryCreateFromConstructor,
  RequireObjectCoercible,
  SameValue,
  Set,
  SetIntegrityLevel,
  TestIntegrityLevel,
  ToObject,
  ToPropertyDescriptor,
  ToPropertyKey,
  CreateBuiltinFunction,
  Realm,
  type FunctionObject,
  GroupBy,
  type KeyedGroupRecord,
} from '#self';

/** https://tc39.es/ecma262/#sec-object-value */
function* ObjectConstructor([value = Value.undefined]: Arguments, { NewTarget }: FunctionCallContext): ValueEvaluator {
  // 1. If NewTarget is neither undefined nor the active function, then
  if (NewTarget !== Value.undefined && NewTarget !== surroundingAgent.activeFunctionObject) {
    // a. Return ? OrdinaryCreateFromConstructor(NewTarget, "%Object.prototype%").
    return yield* OrdinaryCreateFromConstructor(NewTarget as FunctionObject, '%Object.prototype%');
  }
  // 2. If value is undefined or null, return OrdinaryObjectCreate(%Object.prototype%).
  if (value === Value.null || value === Value.undefined) {
    return OrdinaryObjectCreate(surroundingAgent.intrinsic('%Object.prototype%'));
  }
  // 3. Return ! ToObject(value).
  return X(ToObject(value));
}

/** https://tc39.es/ecma262/#sec-object.assign */
function* Object_assign([target = Value.undefined, ...sources]: Arguments): ValueEvaluator {
  // 1. Let to be ? ToObject(target).
  const to = Q(ToObject(target));
  // 2. If only one argument was passed, return to.
  if (sources.length === 0) {
    return to;
  }
  // 3. Let sources be the List of argument values starting with the second argument.
  // 4. For each element nextSource of sources, in ascending index order, do
  for (const nextSource of (sources as Arguments).values()) {
    // a. If nextSource is neither undefined nor null, then
    if (nextSource !== Value.undefined && nextSource !== Value.null) {
      // i. Let from be ! ToObject(nextSource).
      const from = X(ToObject(nextSource));
      // ii. Let keys be ? from.[[OwnPropertyKeys]]().
      const keys = Q(yield* from.OwnPropertyKeys());
      // iii. For each element nextKey of keys in List order, do
      for (const nextKey of keys) {
        // 1. Let desc be ? from.[[GetOwnProperty]](nextKey).
        const desc = Q(yield* from.GetOwnProperty(nextKey));
        // 2. If desc is not undefined and desc.[[Enumerable]] is true, then
        if (!(desc instanceof UndefinedValue) && desc.Enumerable === Value.true) {
          // a. Let propValue be ? Get(from, nextKey).
          const propValue = Q(yield* Get(from, nextKey));
          // b. Perform ? Set(to, nextKey, propValue, true).
          Q(yield* Set(to, nextKey, propValue, Value.true));
        }
      }
    }
  }
  // 5. Return to.
  return to;
}

/** https://tc39.es/ecma262/#sec-object.create */
function* Object_create([O = Value.undefined, Properties = Value.undefined]: Arguments) {
  // 1. If Type(O) is neither Object nor Null, throw a TypeError exception.
  if (!(O instanceof ObjectValue) && !(O instanceof NullValue)) {
    return surroundingAgent.Throw('TypeError', 'ObjectPrototypeType');
  }
  // 2. Let obj be OrdinaryObjectCreate(O).
  const obj = OrdinaryObjectCreate(O);
  // 3. If Properties is not undefined, then
  if (Properties !== Value.undefined) {
    // a. Return ? ObjectDefineProperties(obj, Properties).
    return Q(yield* ObjectDefineProperties(obj, Properties));
  }
  // 4. Return obj.
  return obj;
}

/** https://tc39.es/ecma262/#sec-object.defineproperties */
function* Object_defineProperties([O = Value.undefined, Properties = Value.undefined]: Arguments): ValueEvaluator {
  // 1. Return ? ObjectDefineProperties(O, Properties).
  return Q(yield* ObjectDefineProperties(O, Properties));
}

/** https://tc39.es/ecma262/#sec-objectdefineproperties ObjectDefineProperties */
function* ObjectDefineProperties(O: Value, Properties: Value) {
  // 1. If Type(O) is not Object, throw a TypeError exception.
  if (!(O instanceof ObjectValue)) {
    return surroundingAgent.Throw('TypeError', 'NotAnObject', O);
  }
  // 2. Let props be ? ToObject(Properties).
  const props = Q(ToObject(Properties));
  // 3. Let keys be ? props.[[OwnPropertyKeys]]().
  const keys = Q(yield* props.OwnPropertyKeys());
  // 4. Let descriptors be a new empty List.
  const descriptors: [PropertyKeyValue, Descriptor][] = [];
  // 5. For each element nextKey of keys in List order, do
  for (const nextKey of keys) {
    // a. Let propDesc be ? props.[[GetOwnProperty]](nextKey).
    const propDesc = Q(yield* props.GetOwnProperty(nextKey));
    // b. If propDesc is not undefined and propDesc.[[Enumerable]] is true, then
    if (!(propDesc instanceof UndefinedValue) && propDesc.Enumerable === Value.true) {
      // i. Let descObj be ? Get(props, nextKey).
      const descObj = Q(yield* Get(props, nextKey));
      // ii. Let desc be ? ToPropertyDescriptor(descObj).
      const desc = Q(yield* ToPropertyDescriptor(descObj));
      // iii. Append the pair (a two element List) consisting of nextKey and desc to the end of descriptors.
      descriptors.push([nextKey, desc]);
    }
  }
  // 6. For each pair from descriptors in list order, do
  for (const pair of descriptors) {
    // a. Let P be the first element of pair.
    const P = pair[0];
    // b. Let desc be the second element of pair.
    const desc = pair[1];
    // c. Perform ? DefinePropertyOrThrow(O, P, desc).
    Q(yield* DefinePropertyOrThrow(O, P, desc));
  }
  // 7. Return O.
  return O;
}

/** https://tc39.es/ecma262/#sec-object.defineproperty */
function* Object_defineProperty([O = Value.undefined, P = Value.undefined, Attributes = Value.undefined]: Arguments) {
  // 1. If Type(O) is not Object, throw a TypeError exception.
  if (!(O instanceof ObjectValue)) {
    return surroundingAgent.Throw('TypeError', 'NotAnObject', O);
  }
  // 2. Let key be ? ToPropertyKey(P).
  const key = Q(yield* ToPropertyKey(P));
  // 3. Let desc be ? ToPropertyDescriptor(Attributes).
  const desc = Q(yield* ToPropertyDescriptor(Attributes));
  // 4. Perform ? DefinePropertyOrThrow(O, key, desc).
  Q(yield* DefinePropertyOrThrow(O, key, desc));
  // 5. Return O.
  return O;
}

/** https://tc39.es/ecma262/#sec-object.entries */
function* Object_entries([O = Value.undefined]: Arguments): ValueEvaluator {
  // 1. Let obj be ? ToObject(O).
  const obj = Q(ToObject(O));
  // 2. Let nameList be ? EnumerableOwnPropertyNames(obj, key+value).
  const nameList = Q(yield* EnumerableOwnProperties(obj, 'key+value'));
  // 3. Return CreateArrayFromList(nameList).
  return CreateArrayFromList(nameList);
}

/** https://tc39.es/ecma262/#sec-object.freeze */
function* Object_freeze([O = Value.undefined]: Arguments) {
  // 1. If Type(O) is not Object, return O.
  if (!(O instanceof ObjectValue)) {
    return O;
  }
  // 2. Let status be ? SetIntegrityLevel(O, frozen).
  const status = Q(yield* SetIntegrityLevel(O, 'frozen'));
  // 3. If status is false, throw a TypeError exception.
  if (status === Value.false) {
    return surroundingAgent.Throw('TypeError', 'UnableToFreeze', O);
  }
  // 4. Return O.
  return O;
}

/** https://tc39.es/ecma262/#sec-object.fromentries */
function* Object_fromEntries([iterable = Value.undefined]: Arguments): ValueEvaluator {
  // 1. Perform ? RequireObjectCoercible(iterable).
  Q(RequireObjectCoercible(iterable));
  // 2. Let obj be ! OrdinaryObjectCreate(%Object.prototype%).
  const obj = X(OrdinaryObjectCreate(surroundingAgent.intrinsic('%Object.prototype%')));
  // 3. Assert: obj is an extensible ordinary object with no own properties.
  Assert(obj.Extensible === Value.true && obj.properties.size === 0);
  // 4. Let closure be a new Abstract Closure with parameters (key, value) that captures obj and performs the following steps when called:
  function* closure([key = Value.undefined, value = Value.undefined]: Arguments): ValueEvaluator {
    // a. Let propertyKey be ? ToPropertyKey(key).
    const propertyKey = Q(yield* ToPropertyKey(key));
    // b. Perform ! CreateDataPropertyOrThrow(obj, propertyKey, value).
    X(CreateDataPropertyOrThrow(obj, propertyKey, value));
    // c. Return undefined.
    return Value.undefined;
  }
  // 5. Let adder be ! CreateBuiltinFunction(closure, 2, "", « »).
  const adder = X(CreateBuiltinFunction(closure, 2, Value(''), []));
  // 6. Return ? AddEntriesFromIterable(obj, iterable, adder).
  return Q(yield* AddEntriesFromIterable(obj, iterable, adder));
}

/** https://tc39.es/ecma262/#sec-object.getownpropertydescriptor */
function* Object_getOwnPropertyDescriptor([O = Value.undefined, P = Value.undefined]: Arguments): ValueEvaluator {
  // 1. Let obj be ? ToObject(O).
  const obj = Q(ToObject(O));
  // 2. Let key be ? ToPropertyKey(P).
  const key = Q(yield* ToPropertyKey(P));
  // 3. Let desc be ? obj.[[GetOwnProperty]](key).
  const desc = Q(yield* obj.GetOwnProperty(key));
  // 4. Return FromPropertyDescriptor(desc).
  return FromPropertyDescriptor(desc);
}

/** https://tc39.es/ecma262/#sec-object.getownpropertydescriptors */
function* Object_getOwnPropertyDescriptors([O = Value.undefined]: Arguments): ValueEvaluator {
  // 1. Let obj be ? ToObject(O).
  const obj = Q(ToObject(O));
  // 2. Let ownKeys be ? obj.[[OwnPropertyKeys]]().
  const ownKeys = Q(yield* obj.OwnPropertyKeys());
  // 3. Let descriptors be ! OrdinaryObjectCreate(%Object.prototype%).
  const descriptors = X(OrdinaryObjectCreate(surroundingAgent.intrinsic('%Object.prototype%')));
  // 4. For each element key of ownKeys in List order, do
  for (const key of ownKeys) {
    // a. Let desc be ? obj.[[GetOwnProperty]](key).
    const desc = Q(yield* obj.GetOwnProperty(key));
    // b. Let descriptor be ! FromPropertyDescriptor(desc).
    const descriptor = X(FromPropertyDescriptor(desc));
    // c. If descriptor is not undefined, perform ! CreateDataPropertyOrThrow(descriptors, key, descriptor).
    if (descriptor !== Value.undefined) {
      X(CreateDataProperty(descriptors, key, descriptor));
    }
  }
  // 5. Return descriptors.
  return descriptors;
}

/** https://tc39.es/ecma262/#sec-getownpropertykeys */
function* GetOwnPropertyKeys(O: Value, type: 'String' | 'Symbol'): ValueEvaluator {
  // 1. Let obj be ? ToObject(O).
  const obj = Q(ToObject(O));
  // 2. Let keys be ? obj.[[OwnPropertyKeys]]().
  const keys = Q(yield* obj.OwnPropertyKeys());
  // 3. Let nameList be a new empty List.
  const nameList: PropertyKeyValue[] = [];
  // 4. For each element nextKey of keys in List order, do
  keys.forEach((nextKey) => {
    // a. If nextKey is a Symbol and type is symbol, or if nextKey is a String and type is string, then
    if ((type === 'Symbol' && nextKey instanceof SymbolValue) || (type === 'String' && nextKey instanceof JSStringValue)) {
      // i. Append nextKey as the last element of nameList.
      nameList.push(nextKey);
    }
  });
  return CreateArrayFromList(nameList);
}

/** https://tc39.es/ecma262/#sec-object.getownpropertynames */
function* Object_getOwnPropertyNames([O = Value.undefined]: Arguments): ValueEvaluator {
  // 1. Return ? GetOwnPropertyKeys(O, string).
  return Q(yield* GetOwnPropertyKeys(O, 'String'));
}

/** https://tc39.es/ecma262/#sec-object.getownpropertysymbols */
function* Object_getOwnPropertySymbols([O = Value.undefined]: Arguments): ValueEvaluator {
  // 1. Return ? GetOwnPropertyKeys(O, symbol).
  return Q(yield* GetOwnPropertyKeys(O, 'Symbol'));
}

/** https://tc39.es/ecma262/#sec-object.getprototypeof */
function* Object_getPrototypeOf([O = Value.undefined]: Arguments): ValueEvaluator {
  // 1. Let obj be ? ToObject(O).
  const obj = Q(ToObject(O));
  // 2. Return ? obj.[[GetPrototypeOf]]().
  return Q(yield* obj.GetPrototypeOf());
}

/** https://tc39.es/ecma262/#sec-object.groupby */
function* Object_groupBy([items = Value.undefined, callback = Value.undefined]: Arguments): ValueEvaluator {
  /*
  1. Let groups be ? GroupBy(items, callback, property).
  2. Let obj be OrdinaryObjectCreate(null).
  3. For each Record { [[Key]], [[Elements]] } g of groups, do
    a. Let elements be CreateArrayFromList(g.[[Elements]]).
    b. Perform ! CreateDataPropertyOrThrow(obj, g.[[Key]], elements).
  4. Return obj.
  */
  const groups: KeyedGroupRecord[] = Q(yield* GroupBy(items, callback, 'property'));
  const obj = OrdinaryObjectCreate(Value.null);
  for (const g of groups) {
    const elements = CreateArrayFromList(g.Elements);
    X(CreateDataPropertyOrThrow(obj, g.Key, elements));
  }
  return obj;
}

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

/** https://tc39.es/ecma262/#sec-object.is */
function Object_is([value1 = Value.undefined, value2 = Value.undefined]: Arguments) {
  // 1. Return SameValue(value1, value2).
  return SameValue(value1, value2);
}

/** https://tc39.es/ecma262/#sec-object.isextensible */
function* Object_isExtensible([O = Value.undefined]: Arguments): ValueEvaluator {
  // 1. If Type(O) is not Object, return false.
  if (!(O instanceof ObjectValue)) {
    return Value.false;
  }
  // 2. Return ? IsExtensible(O).
  return Q(yield* IsExtensible(O));
}

/** https://tc39.es/ecma262/#sec-object.isfrozen */
function* Object_isFrozen([O = Value.undefined]: Arguments): ValueEvaluator {
  // 1. If Type(O) is not Object, return true.
  if (!(O instanceof ObjectValue)) {
    return Value.true;
  }
  // 2. Return ? TestIntegrityLevel(O, frozen).
  return Q(yield* TestIntegrityLevel(O, 'frozen'));
}

/** https://tc39.es/ecma262/#sec-object.issealed */
function* Object_isSealed([O = Value.undefined]: Arguments): ValueEvaluator {
  // 1. If Type(O) is not Object, return true.
  if (!(O instanceof ObjectValue)) {
    return Value.true;
  }
  // 2. Return ? TestIntegrityLevel(O, sealed).
  return Q(yield* TestIntegrityLevel(O, 'sealed'));
}

/** https://tc39.es/ecma262/#sec-object.keys */
function* Object_keys([O = Value.undefined]: Arguments): ValueEvaluator {
  // 1. Let obj be ? ToObject(O).
  const obj = Q(ToObject(O));
  // 2. Let nameList be ? EnumerableOwnPropertyNames(obj, key).
  const nameList = Q(yield* EnumerableOwnProperties(obj, 'key'));
  // 3. Return CreateArrayFromList(nameList).
  return CreateArrayFromList(nameList);
}

/** https://tc39.es/ecma262/#sec-object.preventextensions */
function* Object_preventExtensions([O = Value.undefined]: Arguments) {
  // 1. If Type(O) is not Object, return O.
  if (!(O instanceof ObjectValue)) {
    return O;
  }
  // 2. Let status be ? O.[[PreventExtensions]]().
  const status = Q(yield* O.PreventExtensions());
  // 3. If status is false, throw a TypeError exception.
  if (status === Value.false) {
    return surroundingAgent.Throw('TypeError', 'UnableToPreventExtensions', O);
  }
  // 4. Return O.
  return O;
}

/** https://tc39.es/ecma262/#sec-object.seal */
function* Object_seal([O = Value.undefined]: Arguments) {
  // 1. If Type(O) is not Object, return O.
  if (!(O instanceof ObjectValue)) {
    return O;
  }
  // 2. Let status be ? SetIntegrityLevel(O, sealed).
  const status = Q(yield* SetIntegrityLevel(O, 'sealed'));
  // 3. If status is false, throw a TypeError exception.
  if (status === Value.false) {
    return surroundingAgent.Throw('TypeError', 'UnableToSeal', O);
  }
  // 4. Return O.
  return O;
}

/** https://tc39.es/ecma262/#sec-object.setprototypeof */
function* Object_setPrototypeOf([O = Value.undefined, proto = Value.undefined]: Arguments) {
  // 1. Perform ? RequireObjectCoercible(O).
  Q(RequireObjectCoercible(O));
  // 2. If Type(proto) is neither Object nor Null, throw a TypeError exception.
  if (!(proto instanceof ObjectValue) && !(proto instanceof NullValue)) {
    return surroundingAgent.Throw('TypeError', 'ObjectPrototypeType');
  }
  // 3. If Type(O) is not Object, return O.
  if (!(O instanceof ObjectValue)) {
    return O;
  }
  // 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 O.
  return O;
}

/** https://tc39.es/ecma262/#sec-object.values */
function* Object_values([O = Value.undefined]: Arguments): ValueEvaluator {
  // 1. Let obj be ? ToObject(O).
  const obj = Q(ToObject(O));
  // 2. Let nameList be ? EnumerableOwnPropertyNames(obj, value).
  const nameList = Q(yield* EnumerableOwnProperties(obj, 'value'));
  // 3. Return CreateArrayFromList(nameList).
  return CreateArrayFromList(nameList);
}

export function bootstrapObject(realmRec: Realm) {
  const objectConstructor = bootstrapConstructor(realmRec, ObjectConstructor, 'Object', 1, realmRec.Intrinsics['%Object.prototype%'], [
    ['assign', Object_assign, 2],
    ['create', Object_create, 2],
    ['defineProperties', Object_defineProperties, 2],
    ['defineProperty', Object_defineProperty, 3],
    ['entries', Object_entries, 1],
    ['freeze', Object_freeze, 1],
    ['fromEntries', Object_fromEntries, 1],
    ['getOwnPropertyDescriptor', Object_getOwnPropertyDescriptor, 2],
    ['getOwnPropertyDescriptors', Object_getOwnPropertyDescriptors, 1],
    ['getOwnPropertyNames', Object_getOwnPropertyNames, 1],
    ['getOwnPropertySymbols', Object_getOwnPropertySymbols, 1],
    ['getPrototypeOf', Object_getPrototypeOf, 1],
    ['groupBy', Object_groupBy, 2],
    ['hasOwn', Object_hasOwn, 2],
    ['is', Object_is, 2],
    ['isExtensible', Object_isExtensible, 1],
    ['isFrozen', Object_isFrozen, 1],
    ['isSealed', Object_isSealed, 1],
    ['keys', Object_keys, 1],
    ['preventExtensions', Object_preventExtensions, 1],
    ['seal', Object_seal, 1],
    ['setPrototypeOf', Object_setPrototypeOf, 2],
    ['values', Object_values, 1],
  ]);

  realmRec.Intrinsics['%Object%'] = objectConstructor;
}
