import { DeepReadonly } from "utility-types";
import ObservableSlim from "observable-slim";

export type DeepMutable<T> = T extends (...args: any[]) => any
  ? T
  : T extends Readonly<[any, ...any[]]>
  ? _DeepMutableObject<T>
  : T extends ReadonlyArray<any>
  ? _DeepMutableArray<T[number]>
  : T extends object
  ? _DeepMutableObject<T>
  : T;

export interface _DeepMutableArray<T> extends Array<DeepMutable<T>> {}

export type _DeepMutableObject<T> = {
  -readonly [P in keyof T]: DeepMutable<T[P]>
};

function shallow_copy<T>(x: T): T {
  if (x instanceof Array) return [...x] as any;
  else if (typeof x === "object") return { ...x };
  else return x;
}

export type Transaction<T extends {}> = {
  source: string;
  changes: ObservableSlim.Change<T>[];
};

export interface Mutate<T extends object> {
  (mutator: (mutable: T) => void, no_log?: boolean): void;
  objs<T extends any[]>(
    objs: T,
    mutator: (...muts: DeepMutable<T>) => void,
    no_log?: boolean
  ): void;
}

export function stasis<T extends {}>(
  state: T,
  on_change?: (tx: Transaction<T>) => unknown
): {
  state: DeepReadonly<T>;
  log: Transaction<T>[];
  mutate: Mutate<T>;
} {
  let current_tx: Transaction<T>;
  let log: Transaction<T>[] = [];

  // let observer = (changes: ObservableSlim.Change<T>[]) => {
  //   current_tx.changes.push(
  //     ...changes.map(change => ({
  //       ...change,
  //       previousValue: shallow_copy(change.previousValue)
  //     }))
  //   );
  // };

  // let proxy = ObservableSlim.create(state, false, observer);

  let mutate = function(mutator, no_log = false) {
    current_tx = { source: mutator.name, changes: [] };
    mutator(state); // proxy
    if (!no_log) log.push(current_tx);
    if (on_change) on_change(current_tx);
  } as Mutate<T>;
  mutate.objs = function<T extends any[]>(
    objs: T,
    mutator: (...muts: DeepMutable<T>) => void,
    no_log = false
  ) {
    current_tx = { source: mutator.name, changes: [] };
    let proxies = objs.map(
      obj =>
        // ObservableSlim.create(obj, false, observer)
        obj as any
    ) as DeepMutable<T>;
    mutator(...proxies);
    if (!no_log) log.push(current_tx);
    if (on_change) on_change(current_tx);
  };

  return {
    state: state as DeepReadonly<T>,
    log,
    mutate
  };
}
