import { equals } from "../../Equal.js"
import type { HashMap } from "../../HashMap.js"
import * as O from "../../Option.js"
import { isTagged } from "../../Predicate.js"
import * as Stack from "../stack.js"
import { arraySpliceIn, arraySpliceOut, arrayUpdate } from "./array.js"
import { fromBitmap, hashFragment, toBitmap } from "./bitwise.js"
import { MAX_INDEX_NODE, MIN_ARRAY_NODE, SIZE } from "./config.js"

/** @internal */
export type Node<K, V> =
  | EmptyNode<K, V>
  | LeafNode<K, V>
  | CollisionNode<K, V>
  | IndexedNode<K, V>
  | ArrayNode<K, V>

/** @internal */
export interface SizeRef {
  value: number // mutable by design
}

/** @internal */
export class EmptyNode<out K, out V> {
  readonly _tag = "EmptyNode"

  modify(
    edit: number,
    _shift: number,
    f: HashMap.UpdateFn<V>,
    hash: number,
    key: K,
    size: SizeRef
  ): Node<K, V> {
    const v = f(O.none())
    if (O.isNone(v)) return new EmptyNode()
    ;++size.value
    return new LeafNode(edit, hash, key, v)
  }
}

/** @internal */
export function isEmptyNode(a: unknown): a is EmptyNode<unknown, unknown> {
  return isTagged(a, "EmptyNode")
}

/** @internal */
export function isLeafNode<K, V>(
  node: Node<K, V>
): node is EmptyNode<K, V> | LeafNode<K, V> | CollisionNode<K, V> {
  return isEmptyNode(node) || node._tag === "LeafNode" || node._tag === "CollisionNode"
}

/** @internal */
export function canEditNode<K, V>(node: Node<K, V>, edit: number): boolean {
  return isEmptyNode(node) ? false : edit === node.edit
}

/** @internal */
export class LeafNode<out K, out V> {
  readonly _tag = "LeafNode"

  constructor(
    readonly edit: number,
    readonly hash: number,
    readonly key: K,
    public value: O.Option<V>
  ) {}

  modify(
    edit: number,
    shift: number,
    f: HashMap.UpdateFn<V>,
    hash: number,
    key: K,
    size: SizeRef
  ): Node<K, V> {
    if (equals(key, this.key)) {
      const v = f(this.value)
      if (v === this.value) return this
      else if (O.isNone(v)) {
        ;--size.value
        return new EmptyNode()
      }
      if (canEditNode(this, edit)) {
        this.value = v
        return this
      }
      return new LeafNode(edit, hash, key, v)
    }
    const v = f(O.none())
    if (O.isNone(v)) return this
    ;++size.value
    return mergeLeaves(
      edit,
      shift,
      this.hash,
      this,
      hash,
      new LeafNode(edit, hash, key, v)
    )
  }
}

/** @internal */
export class CollisionNode<out K, out V> {
  readonly _tag = "CollisionNode"

  constructor(
    readonly edit: number,
    readonly hash: number,
    readonly children: Array<Node<K, V>>
  ) {}

  modify(
    edit: number,
    shift: number,
    f: HashMap.UpdateFn<V>,
    hash: number,
    key: K,
    size: SizeRef
  ): Node<K, V> {
    if (hash === this.hash) {
      const canEdit = canEditNode(this, edit)
      const list = this.updateCollisionList(
        canEdit,
        edit,
        this.hash,
        this.children,
        f,
        key,
        size
      )
      if (list === this.children) return this

      return list.length > 1 ? new CollisionNode(edit, this.hash, list) : list[0]! // collapse single element collision list
    }
    const v = f(O.none())
    if (O.isNone(v)) return this
    ;++size.value
    return mergeLeaves(
      edit,
      shift,
      this.hash,
      this,
      hash,
      new LeafNode(edit, hash, key, v)
    )
  }

  updateCollisionList(
    mutate: boolean,
    edit: number,
    hash: number,
    list: Array<Node<K, V>>,
    f: HashMap.UpdateFn<V>,
    key: K,
    size: SizeRef
  ) {
    const len = list.length
    for (let i = 0; i < len; ++i) {
      const child = list[i]!
      if ("key" in child && equals(key, child.key)) {
        const value = child.value
        const newValue = f(value)
        if (newValue === value) return list
        if (O.isNone(newValue)) {
          ;--size.value
          return arraySpliceOut(mutate, i, list)
        }
        return arrayUpdate(mutate, i, new LeafNode(edit, hash, key, newValue), list)
      }
    }

    const newValue = f(O.none())
    if (O.isNone(newValue)) return list
    ;++size.value
    return arrayUpdate(mutate, len, new LeafNode(edit, hash, key, newValue), list)
  }
}

/** @internal */
export class IndexedNode<out K, out V> {
  readonly _tag = "IndexedNode"

  constructor(
    readonly edit: number,
    public mask: number,
    public children: Array<Node<K, V>>
  ) {}

  modify(
    edit: number,
    shift: number,
    f: HashMap.UpdateFn<V>,
    hash: number,
    key: K,
    size: SizeRef
  ): Node<K, V> {
    const mask = this.mask
    const children = this.children
    const frag = hashFragment(shift, hash)
    const bit = toBitmap(frag)
    const indx = fromBitmap(mask, bit)
    const exists = mask & bit
    const canEdit = canEditNode(this, edit)

    if (!exists) {
      const _newChild = new EmptyNode<K, V>().modify(edit, shift + SIZE, f, hash, key, size)
      if (!_newChild) return this
      return children.length >= MAX_INDEX_NODE ?
        expand(edit, frag, _newChild, mask, children) :
        new IndexedNode(edit, mask | bit, arraySpliceIn(canEdit, indx, _newChild, children))
    }

    const current = children[indx]!
    const child = current.modify(edit, shift + SIZE, f, hash, key, size)

    if (current === child) return this
    let bitmap = mask
    let newChildren
    if (isEmptyNode(child)) {
      // remove
      bitmap &= ~bit
      if (!bitmap) return new EmptyNode()
      if (children.length <= 2 && isLeafNode(children[indx ^ 1]!)) {
        return children[indx ^ 1]! // collapse
      }

      newChildren = arraySpliceOut(canEdit, indx, children)
    } else {
      // modify
      newChildren = arrayUpdate(canEdit, indx, child, children)
    }

    if (canEdit) {
      this.mask = bitmap
      this.children = newChildren
      return this
    }

    return new IndexedNode(edit, bitmap, newChildren)
  }
}

/** @internal */
export class ArrayNode<out K, out V> {
  readonly _tag = "ArrayNode"

  constructor(
    readonly edit: number,
    public size: number,
    public children: Array<Node<K, V>>
  ) {}

  modify(
    edit: number,
    shift: number,
    f: HashMap.UpdateFn<V>,
    hash: number,
    key: K,
    size: SizeRef
  ): Node<K, V> {
    let count = this.size
    const children = this.children
    const frag = hashFragment(shift, hash)
    const child = children[frag]
    const newChild = (child || new EmptyNode<K, V>()).modify(
      edit,
      shift + SIZE,
      f,
      hash,
      key,
      size
    )

    if (child === newChild) return this

    const canEdit = canEditNode(this, edit)
    let newChildren
    if (isEmptyNode(child) && !isEmptyNode(newChild)) {
      // add
      ;++count
      newChildren = arrayUpdate(canEdit, frag, newChild, children)
    } else if (!isEmptyNode(child) && isEmptyNode(newChild)) {
      // remove
      ;--count
      if (count <= MIN_ARRAY_NODE) {
        return pack(edit, count, frag, children)
      }
      newChildren = arrayUpdate(canEdit, frag, new EmptyNode<K, V>(), children)
    } else {
      // modify
      newChildren = arrayUpdate(canEdit, frag, newChild, children)
    }

    if (canEdit) {
      this.size = count
      this.children = newChildren
      return this
    }
    return new ArrayNode(edit, count, newChildren)
  }
}

function pack<K, V>(
  edit: number,
  count: number,
  removed: number,
  elements: Array<Node<K, V>>
) {
  const children = new Array<Node<K, V>>(count - 1)
  let g = 0
  let bitmap = 0
  for (let i = 0, len = elements.length; i < len; ++i) {
    if (i !== removed) {
      const elem = elements[i]
      if (elem && !isEmptyNode(elem)) {
        children[g++] = elem
        bitmap |= 1 << i
      }
    }
  }
  return new IndexedNode(edit, bitmap, children)
}

function expand<K, V>(
  edit: number,
  frag: number,
  child: Node<K, V>,
  bitmap: number,
  subNodes: Array<Node<K, V>>
) {
  const arr = []
  let bit = bitmap
  let count = 0
  for (let i = 0; bit; ++i) {
    if (bit & 1) arr[i] = subNodes[count++]!
    bit >>>= 1
  }
  arr[frag] = child
  return new ArrayNode(edit, count + 1, arr)
}

function mergeLeavesInner<K, V>(
  edit: number,
  shift: number,
  h1: number,
  n1: Node<K, V>,
  h2: number,
  n2: Node<K, V>
): Node<K, V> | ((child: Node<K, V>) => Node<K, V>) {
  if (h1 === h2) return new CollisionNode(edit, h1, [n2, n1])
  const subH1 = hashFragment(shift, h1)
  const subH2 = hashFragment(shift, h2)

  if (subH1 === subH2) {
    return (child) => new IndexedNode(edit, toBitmap(subH1) | toBitmap(subH2), [child])
  } else {
    const children = subH1 < subH2 ? [n1, n2] : [n2, n1]
    return new IndexedNode(edit, toBitmap(subH1) | toBitmap(subH2), children)
  }
}

function mergeLeaves<K, V>(
  edit: number,
  shift: number,
  h1: number,
  n1: Node<K, V>,
  h2: number,
  n2: Node<K, V>
): Node<K, V> {
  let stack: Stack.Stack<(node: Node<K, V>) => Node<K, V>> | undefined = undefined
  let currentShift = shift

  while (true) {
    const res = mergeLeavesInner(edit, currentShift, h1, n1, h2, n2)

    if (typeof res === "function") {
      stack = Stack.make(res, stack)
      currentShift = currentShift + SIZE
    } else {
      let final = res
      while (stack != null) {
        final = stack.value(final)
        stack = stack.previous
      }
      return final
    }
  }
}
