// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

export const mod = (a: number, n: number): number => {
  return ((a % n) + n) % n;
};

export function assert<T>(
    predicate: T,
    message = 'Assertion failed!',
    ): asserts predicate {
  if (!predicate) {
    throw new Error(message);
  }
}

export type Keys<T> = T extends T ? keyof T : never;

export type RequiredKeys<T> = {
  [K in keyof T] -?: Record<string|number|symbol, unknown> extends Pick<T, K>? never : K;
}[keyof T];

export type OptionalKeys<T> = {
  [K in keyof T] -?: Record<string|number|symbol, unknown> extends Pick<T, K>? K : never;
}[keyof T];

export type DeepImmutable<T> = {
  readonly[K in keyof T]: DeepImmutable<T[K]>;
};

export type DeepMutable<T> = {
  -readonly[K in keyof T]: DeepMutable<T[K]>;
};

export type DeepPartial<T> = {
  [K in keyof T]?: DeepPartial<Exclude<T[K], undefined>>;
};

export type Mutable<T> = {
  -readonly[K in keyof T]: T[K];
};

export const deepFreeze = <T extends object>(object: T): DeepImmutable<T> => {
  for (const name of Reflect.ownKeys(object)) {
    const value = object[name as keyof T];
    if ((value && typeof value === 'object') || typeof value === 'function') {
      deepFreeze(value);
    }
  }
  return Object.freeze(object);
};

export class InsertAssignment<T> {
  value: T;
  constructor(value: T) {
    this.value = value;
  }
}

export class ArrayAssignments<T> {
  value: Record<number, T>;
  constructor(value: Record<number, T>) {
    this.value = value;
  }
}

export type Assignments<T> = T extends Readonly<Array<infer R>>?
    R[]|ArrayAssignments<Assignments<R>|InsertAssignment<R>>:
    {[K in keyof T]: Assignments<T[K]>};

export const immutableDeepAssign = <T>(
    object: DeepImmutable<T>,
    assignments: DeepImmutable<DeepPartial<Assignments<T>>>,
    ): DeepImmutable<T> => {
  if (assignments instanceof ArrayAssignments) {
    assert(Array.isArray(object), `Expected an array. Got ${typeof object}.`);
    const updatedObject = [...object] as Mutable<typeof object>;
    const keys = Object.keys(assignments.value)
                     .sort(
                         (a, b) => Number(b) - Number(a),
                         ) as Array<keyof typeof updatedObject>;
    for (const key of keys) {
      const update = assignments.value[Number(key)];
      if (update === undefined) {
        updatedObject.splice(Number(key), 1);
      } else if (update instanceof InsertAssignment) {
        updatedObject.splice(Number(key), 0, update.value);
      } else {
        updatedObject[Number(key)] = immutableDeepAssign(
            updatedObject[key],
            update,
        );
      }
    }
    return Object.freeze(updatedObject);
  }
  if (typeof assignments === 'object' && !Array.isArray(assignments)) {
    assert(!Array.isArray(object), 'Expected an object. Got an array.');
    const updatedObject = {...object} as Mutable<typeof object>;
    const keys = Object.keys(assignments) as Array<keyof typeof assignments&keyof typeof updatedObject>;
    for (const key of keys) {
      const update = assignments[key];
      if (update === undefined) {
        delete updatedObject[key];
      } else {
        updatedObject[key] = immutableDeepAssign(
            updatedObject[key],
            update as typeof updatedObject[typeof key],
        );
      }
    }
    return Object.freeze(updatedObject);
  }
  return assignments as DeepImmutable<T>;
};
