export type NotNullableKeys<T> = { [K in keyof T]: NonNullable<T[K]> }

export type Nullable<T> = { [P in keyof T]: T[P] | null }
export function isNotNullNorUndefined<T>(value: T | null | undefined): value is T {
  return !isNullOrUndefined(value)
}

// @ts-ignore faisons confiance à notre code
export function toSorted<U>(value: Readonly<NonEmptyArray<U>>, comparator?: (a: U, b: U) => number): Readonly<NonEmptyArray<U>>
// @ts-ignore faisons confiance à notre code
export function toSorted<U>(value: Readonly<U[]>, comparator?: (a: U, b: U) => number): Readonly<U[]>
export function toSorted<U>(value: NonEmptyArray<U>, comparator?: (a: U, b: U) => number): NonEmptyArray<U>
export function toSorted<U>(value: U[], comparator?: (a: U, b: U) => number): U[]
export function toSorted<U>(value: U[], comparator?: (a: U, b: U) => number): U[] {
  return [...value].sort(comparator)
}

export function isNotNullNorUndefinedNorEmpty<U>(value: U[] | null | undefined): value is NonEmptyArray<U>
export function isNotNullNorUndefinedNorEmpty(value: string | null | undefined): value is string
export function isNotNullNorUndefinedNorEmpty(value: string | any[] | null | undefined): boolean {
  if (Array.isArray(value)) {
    if (!isNullOrUndefined(value)) {
      return isNonEmptyArray(value)
    }
  } else if (typeof value === 'string') {
    return value.trim() !== ''
  }

  return false
}

export function isNullOrUndefinedOrEmpty<U>(value: Readonly<U[]> | null | undefined): value is null | undefined
export function isNullOrUndefinedOrEmpty(value: string | null | undefined): value is null | undefined
export function isNullOrUndefinedOrEmpty(value: string | Readonly<any[]> | null | undefined): boolean {
  if (value === null || value === undefined) {
    return true
  }

  if (Array.isArray(value)) {
    return value.length === 0
  } else if (typeof value === 'string') {
    return value.trim() === ''
    // unreachable code
    /* v8 ignore next 4 */
  }

  return false
}

export const isNullOrUndefined = <T>(value: T | null | undefined): value is null | undefined => {
  return value === null || value === undefined
}

export const onlyUnique = <T>(value: T, index: number, self: T[]): boolean => {
  return self.indexOf(value) === index
}

export type PartialRecord<K extends keyof any, T> = {
  [P in K]?: T
}

export const getValues = <T>(o: { [s: string]: T } | ArrayLike<T>): T extends unknown ? T[] : never => {
  // @ts-ignore
  return Object.values(o)
}

export const getKeys = <T extends object>(object: T, filter: (key: string) => key is Extract<keyof T, string>): Array<Extract<keyof T, string>> => Object.keys(object).filter(filter)
export const getEntries = <T extends string, U>(object: Record<T, U>, filter: (key: string) => key is T): [T, U][] =>
  Object.entries<U>(object).filter((key: [string, U]): key is [T, U] => filter(key[0]))

// @ts-ignore use with caution
export const getEntriesHardcore = <T extends string, U>(object: Record<T, U>): [T, U][] => Object.entries<U>(object)

export const exhaustiveCheck = (param: never): never => {
  throw new Error(`Unreachable case: ${JSON.stringify(param)}`)
}

export type NonEmptyArray<T> = [T, ...T[]]
export const isNonEmptyArray = <T>(arr: T[]): arr is NonEmptyArray<T> => {
  return arr.length > 0
}

export const map = <T, U>(array: Readonly<NonEmptyArray<T>>, transform: (item: T) => U): Readonly<NonEmptyArray<U>> => {
  const [first, ...rest] = array

  return [transform(first), ...rest.map(transform)]
}

/* v8 ignore next 2 */
export const isTrue = <T extends true>(_t: T): void => {}
export const isFalse = <T extends false>(_t: T): void => {}

export type Expect<T, E> = T extends E ? (E extends T ? true : false) : false

// from https://stackoverflow.com/questions/72789915/typescript-omit-seems-to-transform-an-union-into-an-intersection/72790170#72790170
export type OmitDistributive<T, K extends string> = T extends unknown ? Omit<T, K> : never

export type PickDistributive<T, K extends keyof T> = T extends unknown ? Pick<T, K> : never

export type SimplePromiseFn<T> = () => Promise<T>
export type Memoized<T> = SimplePromiseFn<T> & { _type: 'MEMOIZED' }
export const memoize = <T>(fn: SimplePromiseFn<T>): Memoized<T> => {
  let cache: T | null = null

  return (async () => {
    if (cache === null) {
      cache = await fn()
    }

    return cache
  }) as Memoized<T>
}

export const sleep = (ms: number): Promise<void> => {
  return new Promise(resolve => setTimeout(resolve, ms))
}

export const stringArrayEquals = (array1: string[], array2: string[]): boolean => {
  return array1.length === array2.length && array1.every((value, index) => array2[index] === value)
}

export type RecordPartial<K extends keyof any, T> = {
  [P in K]?: T
}
