import {
  array as A,
  ord as Ord,
  readonlyArray as RA,
  option as O,
  function as F,
} from 'fp-ts';
import { Predicate } from 'fp-ts/lib/Predicate';

export function shuffle<T>(arr: T[]): T[] {
  const source = [...arr];
  for (let i = source.length - 1; i > 0; i--) {
    const j = Math.floor(Math.random() * (i + 1));
    [source[i], source[j]] = [source[j], source[i]];
  }

  return source;
}

/**
 * The parameter `arr` should be sorted for correct results.
 */
export function insertSorted<T>(arr: T[], item: T, ord: Ord.Ord<T>): T[] {
  if (arr.length === 0) {
    return [item];
  }

  const arrOrd = ord.compare(arr[0], arr[arr.length - 1]);
  // Array containing the same items
  if (arrOrd === 0) {
    if (ord.compare(arr[0], item) === -1) {
      return [...arr, item];
    } else {
      return [item, ...arr];
    }
  }

  // TODO: could be improved with binary search
  for (let i = 0; i < arr.length; i++) {
    if (ord.compare(arr[i], item) !== arrOrd) {
      return [...A.takeLeft(i)(arr), item, ...A.takeRight(arr.length - i)(arr)];
    }
  }

  return [...arr, item];
}

/**
 * Replace when existent, otherwise add at the end.
 */
export const repsertReadonlyArr =
  <K, V>(predicate: Predicate<K>, val: V, mkKey: () => K) =>
  (arr: ReadonlyArray<readonly [K, V]>): ReadonlyArray<readonly [K, V]> => {
    const idx = F.pipe(
      arr,
      RA.findIndex(([key, _]) => predicate(key)),
    );

    const res = F.pipe(
      idx,
      O.chain((i) =>
        F.pipe(
          arr,
          RA.modifyAt(i, ([key, _]) => [key, val]),
        ),
      ),
    );

    return F.pipe(
      res,
      O.getOrElse<ReadonlyArray<readonly [K, V]>>(() => [
        ...arr,
        [mkKey(), val],
      ]),
    );
  };

export const repsertWithReadonlyArr =
  <K, V>(
    predicate: Predicate<K>,
    replaceVal: (oldVal: V) => V,
    newVal: () => V,
    mkKey: () => K,
  ) =>
  (arr: ReadonlyArray<readonly [K, V]>): ReadonlyArray<readonly [K, V]> => {
    const idx = F.pipe(
      arr,
      RA.findIndex(([key, _]) => predicate(key)),
    );

    const res = F.pipe(
      idx,
      O.chain((i) =>
        F.pipe(
          arr,
          RA.modifyAt(i, ([key, oldVal]) => [key, replaceVal(oldVal)]),
        ),
      ),
    );

    return F.pipe(
      res,
      O.getOrElse<ReadonlyArray<readonly [K, V]>>(() => [
        ...arr,
        [mkKey(), newVal()],
      ]),
    );
  };
