type SimpleData = { id: string; name: string; children?: SimpleData[] };

export class SimpleTree<T extends SimpleData> {
  root: SimpleNode<T>;
  constructor(data: T[]) {
    this.root = createRoot<T>(data);
  }

  get data() {
    return this.root.children?.map((node) => node.data) ?? [];
  }

  create(args: { parentId: string | null; index: number; data: T }) {
    const parent = args.parentId ? this.find(args.parentId) : this.root;
    if (!parent) return null;
    parent.addChild(args.data, args.index);
  }

  move(args: { id: string; parentId: string | null; index: number }) {
    const src = this.find(args.id);
    const parent = args.parentId ? this.find(args.parentId) : this.root;
    if (!src || !parent) return;
    parent.addChild(src.data, args.index);
    src.drop();
  }

  update(args: { id: string; changes: Partial<T> }) {
    const node = this.find(args.id);
    if (node) node.update(args.changes);
  }

  drop(args: { id: string }) {
    const node = this.find(args.id);
    if (node) node.drop();
  }

  find(id: string, node: SimpleNode<T> = this.root): SimpleNode<T> | null {
    if (!node) return null;
    if (node.id === id) return node as SimpleNode<T>;
    if (node.children) {
      for (let child of node.children) {
        const found = this.find(id, child);
        if (found) return found;
      }
      return null;
    }
    return null;
  }
}

function createRoot<T extends SimpleData>(data: T[]) {
  const root = new SimpleNode<T>({ id: "ROOT" } as T, null);
  root.children = data.map((d) => createNode(d as T, root));
  return root;
}

function createNode<T extends SimpleData>(data: T, parent: SimpleNode<T>) {
  const node = new SimpleNode<T>(data, parent);
  if (data.children)
    node.children = data.children.map((d) => createNode<T>(d as T, node));
  return node;
}

class SimpleNode<T extends SimpleData> {
  id: string;
  children?: SimpleNode<T>[];
  constructor(public data: T, public parent: SimpleNode<T> | null) {
    this.id = data.id;
  }

  hasParent(): this is this & { parent: SimpleNode<T> } {
    return !!this.parent;
  }

  get childIndex(): number {
    return this.hasParent() ? this.parent.children!.indexOf(this) : -1;
  }

  addChild(data: T, index: number) {
    const node = createNode(data, this);
    this.children = this.children ?? [];
    this.children.splice(index, 0, node);
    this.data.children = this.data.children ?? [];
    this.data.children.splice(index, 0, data);
  }

  removeChild(index: number) {
    this.children?.splice(index, 1);
    this.data.children?.splice(index, 1);
  }

  update(changes: Partial<T>) {
    if (this.hasParent()) {
      const i = this.childIndex;
      this.parent.addChild({ ...this.data, ...changes }, i);
      this.drop();
    }
  }

  drop() {
    if (this.hasParent()) this.parent.removeChild(this.childIndex);
  }
}
