let lastUsedId = 0;

export const getUniqueID = () => {
  return lastUsedId++;
};

export class Util {
  static MinBy<T>(list: T[], fn: (T: T) => number): T | null {
    let lowestT: T | null = null;
    let lowestValue: number | null = null;

    for (const item of list) {
      const value = fn(item);

      if (lowestValue === null || value < lowestValue) {
        lowestT = item;
        lowestValue = value;
      }
    }

    return lowestT;
  }

  static MinByAndValue<T>(
    list: T[],
    fn: (T: T) => number
  ): { obj: T; value: number } | null {
    let lowestT: T | null = null;
    let lowestValue: number | null = null;

    for (const item of list) {
      const value = fn(item);

      if (lowestValue === null || value < lowestValue) {
        lowestT = item;
        lowestValue = value;
      }
    }

    return lowestT === null || lowestValue === null
      ? null
      : { obj: lowestT, value: lowestValue };
  }

  static MaxBy<T>(list: T[], fn: (T: T) => number): T | null {
    let highestT: T | null = null;
    let highestValue: number | null = null;

    for (const item of list) {
      const value = fn(item);

      if (highestValue === null || value > highestValue) {
        highestT = item;
        highestValue = value;
      }
    }

    return highestT;
  }

  static RandRange(low: number, high: number): number {
    return Math.floor(Math.random() * (high - low) + low);
  }

  public static SortByKey<T>(array: T[], key: (x: T) => number): T[] {
    return array.sort((a, b) => {
      return key(a) - key(b);
    });
  }

  public static ReplaceAll(
    str: string,
    mapObj: { [key: string]: string }
  ): string {
    const re = new RegExp(Object.keys(mapObj).join("|"), "gi");

    return str.replace(re, (matched) => {
      return mapObj[matched.toLowerCase()];
    });
  }

  public static Debounce<F extends (...args: any[]) => void>(
    func: F,
    waitMilliseconds = 50,
    options = {
      isImmediate: false,
    }
  ): F {
    let timeoutId: any; // types are different on node vs client, so we have to use any.

    const result = (...args: any[]) => {
      const doLater = () => {
        timeoutId = undefined;
        if (!options.isImmediate) {
          func.apply(this, args);
        }
      };

      const shouldCallNow = options.isImmediate && timeoutId === undefined;

      if (timeoutId !== undefined) {
        clearTimeout(timeoutId);
      }

      timeoutId = setTimeout(doLater, waitMilliseconds);

      if (shouldCallNow) {
        func.apply(this, args);
      }
    };

    return result as any;
  }

  public static FormatDate(d: Date): string {
    const monthName = [
      "Jan",
      "Feb",
      "Mar",
      "Apr",
      "May",
      "Jun",
      "Jul",
      "Aug",
      "Sep",
      "Oct",
      "Nov",
      "Dec",
    ][d.getMonth()];

    return `${monthName} ${d.getDate()}, ${("00" + d.getHours()).substr(-2)}:${(
      "00" + d.getMinutes()
    ).substr(-2)}:${("00" + d.getSeconds()).substr(-2)}`;
  }

  public static FlattenByOne<T>(arr: T[][]): T[] {
    let result: T[] = [];

    for (const obj of arr) {
      result = result.concat(obj);
    }

    return result;
  }

  public static PadString(
    string: string,
    length: number,
    intersperse = "",
    character = " "
  ) {
    return string + intersperse + character.repeat(length - string.length);
  }
}

/**
 * A deep readonly type - given an object type, all subobjects and their subobjects are also marked as readonly.
 */
export type Const<T> = T extends Function
  ? T
  : {
      readonly [P in keyof T]: T[P] extends { [k: string]: any }
        ? Const<T[P]>
        : T[P];
    };

const assertOnlyCalledOnceData: { [k: string]: [string, number] } = {};

/**
 * Asserts that a function is not called more than twice. Useful for debugging react lifecycle which may be creating more objects than you realize, impacting performance.
 * @param id identifier
 */
export function assertOnlyCalledOnce(id: string | number) {
  let k = id.toString();
  if (assertOnlyCalledOnceData[k] !== undefined) {
    if (assertOnlyCalledOnceData[k][1] === 1) {
      assertOnlyCalledOnceData[k][1] = 2;
    } else {
      throw new Error(
        "Error, called more than twice with same id: " +
          k +
          " , callback the first time was : " +
          assertOnlyCalledOnceData[k]
      );
    }
  } else {
    const stacktrace = new Error().stack!;
    assertOnlyCalledOnceData[k] = [stacktrace, 1];
  }
}

/**
 * Class representing a value which is only computed when used.
 *
 * Usage: const lazy = new Lazy(() => thingThatReturnsSomething()).
 * Then thingThatReturnsSomething() will only get called on the first time lazy.get() is called.
 * On the second and subsequent times, lazy.get() will return the same object - the factory method is not called again.
 */
export class Lazy<T> {
  private _wasConstructed: boolean = false;
  private _value: T | undefined = undefined;
  private _factory: () => T;

  constructor(
    factory: () => T,
    // structure?: T extends { [key: string]: any } ? T : void
  ) {
    this._factory = factory;
  }
  public get(): T {
    // T might have undefined as a valid value
    if (this._value !== undefined || this._wasConstructed === true) {
      return this._value!;
    } else {
      this._value = this._factory();
      this._wasConstructed = true;
      return this._value;
    }
  }
  public wasConstructed(): boolean {
    return this._wasConstructed;
  }
  // public async getAsync(): Promise<T> {
  //   if (this._value !== undefined || this._wasConstructed === true) {
  //     return Promise.resolve(this._value!);
  //   } else {
  //     return new Promise<T>((resolve, reject) => {
  //       this._value = this._factory();
  //       this._wasConstructed = true;
  //       resolve(this._value);
  //     });
  //   }
  // }
}

export function LazyProxy<
  T extends { [key: string]: any } | { [i: number]: any }
>(factory: () => T): Const<T> {
  return (new Proxy(new Lazy(factory), {
    get: (target, property, receiver) => {
      if (property === "toJSON") {
        return () => {
          if (target.wasConstructed()) {
            return target.get();
          } else {
            return "[Object Lazy]";
          }
        };
      }
      const targetValue = target.get();
      return Reflect.get(targetValue, property);
    },
    ownKeys: (target) => {
      const targetValue = target.get();
      return Reflect.ownKeys(targetValue);
    },
    getOwnPropertyDescriptor: (target, property) => {
      /**
       * https://stackoverflow.com/questions/40352613/why-does-object-keys-and-object-getownpropertynames-produce-different-output
       */

      return Object.getOwnPropertyDescriptor(target.get(), property);
      
    },
    has: (target, property) => {
      // This is called when iterating over array i.e. array.forEach()
      return property in target.get()
    }
  }) as unknown) as Const<T>;
}

/**
 * Multiplies colors (0xFFFFFF === 1). use for applying tints manually.
 * @param color1 A base color
 * @param color2 A tint
 */
export function multiplyColor(color1: number, color2: number): number {
  let reds = [color1 & 0xff0000, color2 & 0xff0000];
  let blues = [color1 & 0x0000ff, color2 & 0x0000ff];
  let greens = [color1 & 0x00ff00, color2 & 0x00ff00];
  let out = Math.round(((reds[0] / 0x010000) * reds[1]) / 0xffffff) * 0x010000;
  out += Math.round(((greens[0] / 0x000100) * greens[1]) / 0x00ff00) * 0x000100;
  out += Math.round((blues[0] * blues[1]) / 0x0000ff);
  return out;
}

export function enumKeys<T extends string>(enm: { [key in T]: T }): T[] {
  return Object.keys(enm) as T[];
}

// export function enumKeys<T extends string>(enm: { [key: string]: string }) : T[] {
//   return Object.keys(enm) as T[];
// }