// Copyright (C) 2023 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import {assertExists} from './assert';
import {exists} from './utils';

export type PathKey = string | number;
export type Path = PathKey[];

/**
 * Gets the |value| at a |path| of |object|. If a portion of the path doesn't
 * exist, |undefined| is returned.
 *
 * Example:
 * const obj = {
 *   a: [
 *     {b: 'c'},
 *     {d: 'e', f: 123},
 *   ],
 * };
 * getPath(obj, ['a']) -> [{b: 'c'}, {d: 'e', f: 123}]
 * getPath(obj, ['a', 1]) -> {d: 'e', f: 123}
 * getPath(obj, ['a', 1, 'd']) -> 'e'
 * getPath(obj, ['g']) -> undefined
 * getPath(obj, ['g', 'h']) -> undefined
 *
 * Note: This is an appropriate use of `any`, as we are knowingly getting fast
 * and loose with the type system in this function: it's basically JavaScript.
 * Attempting to pretend it's anything else would result in superfluous type
 * assertions which would serve no benefit.
 */
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function getPath<T>(obj: any, path: Path): T | undefined {
  let x = obj;
  for (const node of path) {
    if (x === undefined) return undefined;
    x = x[node];
  }
  return x;
}

/**
 * Sets the |value| at |path| of |object|. If the final node of the path doesn't
 * exist, the value will be created. Otherwise, TypeError is thrown.
 *
 * Example:
 * const obj = {
 *   a: [
 *     {b: 'c'},
 *     {d: 'e', f: 123},
 *   ],
 * };
 * setPath(obj, ['a'], 'foo') -> {a: 'foo'}
 * setPath(obj, ['a', 1], 'foo') -> {a: [{b: 'c'}, 'foo']}
 * setPath(obj, ['g'], 'foo') -> {a: [...], g: 'foo'}
 * setPath(obj, ['g', 'h'], 'foo') -> TypeError!
 */
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function setPath<T>(obj: any, path: Path, value: T): void {
  const pathClone = [...path];
  let o = obj;
  while (pathClone.length > 1) {
    const p = assertExists(pathClone.shift());
    o = o[p];
  }

  const p = pathClone.shift();
  if (!exists(p)) {
    throw TypeError('Path array is empty');
  }
  o[p] = value;
}

export function shallowEquals(a: unknown, b: unknown) {
  if (a === b) {
    return true;
  }
  if (a === undefined || b === undefined) {
    return false;
  }
  if (a === null || b === null) {
    return false;
  }
  const objA = a as {[_: string]: {}};
  const objB = b as {[_: string]: {}};
  for (const key of Object.keys(objA)) {
    if (objA[key] !== objB[key]) {
      return false;
    }
  }
  for (const key of Object.keys(objB)) {
    if (objA[key] !== objB[key]) {
      return false;
    }
  }
  return true;
}

export function isString(s: unknown): s is string {
  return typeof s === 'string' || s instanceof String;
}

// Given a string enum |enum|, check that |value| is a valid member of |enum|.
export function isEnumValue<T extends {}>(
  enm: T,
  value: unknown,
): value is T[keyof T] {
  return Object.values(enm).includes(value);
}
