export function findDeepFirst<T extends { items?: T[] }>(
  list: T[],
  cb: (item: T) => boolean,
): T | undefined {
  for (const item of list) {
    if (cb(item) === true) {
      return item;
    }
    if (item.items) {
      const nestedRes = findDeepFirst(item.items, cb);
      if (nestedRes !== undefined) return nestedRes;
    }
  }
}

export function toStringIfDefined(value: unknown): string | undefined {
  return value == undefined ? undefined : String(value);
}

/**
 * Returns true when the function executed in browser.
 */
export function isBrowser() {
  return typeof window !== 'undefined' && 'HTMLElement' in window;
}

export function isPlainObject(obj: unknown): boolean {
  return (
    typeof obj === 'object' &&
    obj !== null &&
    !Array.isArray(obj) &&
    Object.prototype.toString.call(obj) === '[object Object]'
  );
}

/**
 * Returns the previous and next values in an array for a given index.
 *
 * Edge cases:
 * - If the index is `0`, `prev` will be `null`.
 * - If the index is the last position in the array, `next` will be `null`.
 * - If the index is out of bounds (negative or greater than the last index), both `prev` and `next` will be `null`.
 *
 * @example
 * const array = [10, 20, 30, 40];
 * getAdjacentValues(array, 2);
 * // returns: { prev: 20, next: 40 }
 *
 * getAdjacentValues(array, 0);
 * // returns: { prev: null, next: 20 }
 *
 * getAdjacentValues(array, 3);
 * // returns: { prev: 30, next: null }
 *
 * getAdjacentValues(array, -1);
 * // returns: { prev: null, next: null }
 *
 * getAdjacentValues(array, 4);
 * // returns: { prev: null, next: null }
 */
export function getAdjacentValues<T>(
  array: T[],
  index: number,
): { prev: T | null; next: T | null } {
  if (index < 0 || index >= array.length) {
    return { prev: null, next: null };
  }

  const prevValue = index > 0 ? array[index - 1] : null;
  const nextValue = index < array.length - 1 ? array[index + 1] : null;

  return { prev: prevValue, next: nextValue };
}

/**
 * Inserts an element at a given index in an array. Returns a new array with the element inserted.
 *
 * @example
 * const array = [10, 20, 30, 40];
 * insertAt(array, 2, 25);
 * // returns: [10, 20, 25, 30, 40]
 */
export function insertAt<T>(array: T[], index: number, newElement: T): T[] {
  const result = array.concat();
  result.splice(index, 0, newElement);
  return result;
}

/**
 * Removes an element from an array. Returns a new array with the element removed.
 *
 * @example
 * const array = [10, 20, 30, 40];
 * removeElement(array, 20);
 * // returns: [10, 30, 40]
 */
export function removeElement<T>(array: T[], item: T): T[] {
  const index = array.indexOf(item);
  if (index === -1) return array;

  const result = array.slice();
  result.splice(index, 1);
  return result;
}
