import { Arr, Entry, RimbuError, Token } from '@rimbu/base';
import {
  type ArrayNonEmpty,
  IndexRange,
  OptLazy,
  OptLazyOr,
  Range,
  type RelatedTo,
  type ToJSON,
  TraverseState,
  Update,
} from '@rimbu/common';

import { Stream, type StreamSource } from '@rimbu/stream';
import { isEmptyStreamSourceInstance } from '@rimbu/stream/custom';

import type { SortedMap } from '@rimbu/sorted/map';
import type {
  SortedMapBuilder,
  SortedMapContext,
} from '@rimbu/sorted/map-custom';

import {
  SortedEmpty,
  SortedIndex,
  SortedNonEmptyBase,
  innerDeleteMax,
  innerDeleteMin,
  innerDropInternal,
  innerGetAtIndex,
  innerMutateGetFromLeft,
  innerMutateGetFromRight,
  innerMutateGiveToLeft,
  innerMutateGiveToRight,
  innerMutateJoinLeft,
  innerMutateJoinRight,
  innerMutateSplitRight,
  innerNormalizeDownsizeChild,
  innerNormalizeIncreaseChild,
  innerStreamSliceIndex,
  innerTakeInternal,
  leafDeleteMax,
  leafDeleteMin,
  leafMutateGetFromLeft,
  leafMutateGetFromRight,
  leafMutateGiveToLeft,
  leafMutateGiveToRight,
  leafMutateJoinLeft,
  leafMutateJoinRight,
  leafMutateSplitRight,
} from '@rimbu/sorted/common';

export class SortedMapEmpty<K = any, V = any>
  extends SortedEmpty
  implements SortedMap<K, V>
{
  _NonEmptyType!: SortedMap.NonEmpty<K, V>;

  constructor(readonly context: SortedMapContext<K>) {
    super();
  }

  streamRange(): Stream<readonly [K, V]> {
    return Stream.empty();
  }

  streamKeys(): Stream<K> {
    return Stream.empty();
  }

  streamValues(): Stream<V> {
    return Stream.empty();
  }

  streamSliceIndex(): Stream<readonly [K, V]> {
    return Stream.empty();
  }

  minKey<O>(otherwise?: OptLazy<O>): O {
    return OptLazy(otherwise) as O;
  }

  minValue<O>(otherwise?: OptLazy<O>): O {
    return OptLazy(otherwise) as O;
  }

  maxKey<O>(otherwise?: OptLazy<O>): O {
    return OptLazy(otherwise) as O;
  }

  maxValue<O>(otherwise?: OptLazy<O>): O {
    return OptLazy(otherwise) as O;
  }

  get<_, O>(key: any, otherwise?: OptLazy<O>): O {
    return OptLazy(otherwise) as O;
  }

  hasKey(): false {
    return false;
  }

  findIndex(): number {
    return -1;
  }

  getKeyAtIndex<O>(index: number, otherwise?: OptLazy<O>): O {
    return OptLazy(otherwise) as O;
  }

  getValueAtIndex<O>(index: number, otherwise?: OptLazy<O>): O {
    return OptLazy(otherwise) as O;
  }

  set(key: K, value: V): SortedMap.NonEmpty<K, V> {
    return this.context.leaf([[key, value]]);
  }

  addEntry(entry: readonly [K, V]): SortedMap.NonEmpty<K, V> {
    return this.context.leaf([entry]);
  }

  addEntries(entries: StreamSource<readonly [K, V]>): SortedMap.NonEmpty<K, V> {
    return this.context.from(entries);
  }

  removeKey(): SortedMap<K, V> {
    return this;
  }

  removeKeys(): SortedMap<K, V> {
    return this;
  }

  removeKeyAndGet(): undefined {
    return undefined;
  }

  modifyAt(
    atKey: K,
    options: {
      ifNew?: OptLazyOr<V, Token>;
    }
  ): SortedMap<K, V> {
    if (undefined !== options.ifNew) {
      const value = OptLazyOr<V, Token>(options.ifNew, Token);

      if (Token === value) return this;

      return this.context.leaf([[atKey, value]]);
    }

    return this;
  }

  mapValues<V2>(): SortedMap<K, V2> {
    return this as any;
  }

  updateAt(): SortedMap<K, V> {
    return this;
  }

  slice(): SortedMap<K, V> {
    return this;
  }

  toBuilder(): SortedMapBuilder<K, V> {
    return this.context.builder();
  }

  toString(): string {
    return `SortedMap()`;
  }

  toJSON(): ToJSON<any[]> {
    return {
      dataType: this.context.typeTag,
      value: [],
    };
  }
}

export abstract class SortedMapNode<K, V>
  extends SortedNonEmptyBase<readonly [K, V], SortedMapNode<K, V>>
  implements SortedMap.NonEmpty<K, V>
{
  _NonEmptyType!: SortedMap.NonEmpty<K, V>;
  abstract get context(): SortedMapContext<K>;
  abstract get size(): number;
  abstract stream(options?: {
    reversed?: boolean;
  }): Stream.NonEmpty<readonly [K, V]>;
  abstract streamSliceIndex(
    range: IndexRange,
    options?: { reversed?: boolean }
  ): Stream<readonly [K, V]>;
  abstract forEach(
    f: (entry: readonly [K, V], index: number, halt: () => void) => void,
    options?: { state?: TraverseState }
  ): void;
  abstract get<U, O>(key: RelatedTo<K, U>, otherwise?: OptLazy<O>): V | O;
  abstract findIndex(key: K): number;
  abstract addInternal(
    entry: readonly [K, V],
    hash?: number
  ): SortedMapNode<K, V>;
  abstract modifyAtInternal(
    atKey: K,
    options: {
      ifNew?: OptLazyOr<V, Token>;
      ifExists?: ((currentEntry: V, remove: Token) => V | Token) | V;
    }
  ): SortedMapNode<K, V>;
  abstract getInsertIndexOf(key: K): number;
  abstract mapValues<V2>(
    mapFun: (value: V, key: K) => V2
  ): SortedMapNode<K, V2>;
  abstract toArray(): ArrayNonEmpty<readonly [K, V]>;
  abstract normalize(): SortedMap<K, V>;
  abstract min(): readonly [K, V];
  abstract max(): readonly [K, V];

  asNormal(): this {
    return this;
  }

  getSliceRange(range: Range<K>): { startIndex: number; endIndex: number } {
    const { start, end } = Range.getNormalizedRange(range);
    let startIndex = 0;
    let endIndex = this.size - 1;

    if (undefined !== start) {
      const [startValue, startInclude] = start;
      startIndex = this.getInsertIndexOf(startValue);

      if (startIndex < 0) {
        startIndex = SortedIndex.next(startIndex);
      } else if (!startInclude) {
        startIndex++;
      }
    }
    if (undefined !== end) {
      const [endValue, endInclude] = end;
      endIndex = this.getInsertIndexOf(endValue);

      if (endIndex < 0) endIndex = SortedIndex.prev(endIndex);
      else if (!endInclude) endIndex--;
    }

    return { startIndex, endIndex };
  }

  streamKeys(options: { reversed?: boolean } = {}): Stream.NonEmpty<K> {
    return this.stream(options).map(Entry.first);
  }

  streamValues(options: { reversed?: boolean } = {}): Stream.NonEmpty<V> {
    return this.stream(options).map(Entry.second);
  }

  streamRange(
    keyRange: Range<K>,
    options: { reversed?: boolean } = {}
  ): Stream<readonly [K, V]> {
    const { startIndex, endIndex } = this.getSliceRange(keyRange);

    return this.streamSliceIndex(
      {
        start: [startIndex, true],
        end: [endIndex, true],
      },
      options
    );
  }

  minKey(): K {
    return this.min()[0];
  }

  minValue(): V {
    return this.min()[1];
  }

  maxKey(): K {
    return this.max()[0];
  }

  maxValue(): V {
    return this.max()[1];
  }

  hasKey<UK>(key: RelatedTo<K, UK>): boolean {
    const token = Symbol();
    return token !== this.get(key, token);
  }

  getKeyAtIndex<O>(index: number, otherwise?: OptLazy<O>): K | O {
    const token = Symbol();
    const result = this.getAtIndex(index, token);
    if (token === result) return OptLazy(otherwise) as O;
    return result[0];
  }

  getValueAtIndex<O>(index: number, otherwise?: OptLazy<O>): V | O {
    const token = Symbol();
    const result = this.getAtIndex(index, token);
    if (token === result) return OptLazy(otherwise) as O;
    return result[1];
  }

  addEntry(entry: readonly [K, V]): SortedMap.NonEmpty<K, V> {
    return this.addInternal(entry).normalize().assumeNonEmpty();
  }

  addEntries(entries: StreamSource<readonly [K, V]>): SortedMap.NonEmpty<K, V> {
    if (isEmptyStreamSourceInstance(entries)) return this;

    const builder = this.toBuilder();
    builder.addEntries(entries);
    return builder.build() as SortedMap.NonEmpty<K, V>;
  }

  modifyAt(
    atKey: K,
    options: {
      ifNew?: OptLazyOr<V, Token>;
      ifExists?: ((currentEntry: V, remove: Token) => V | Token) | V;
    }
  ): SortedMap<K, V> {
    return this.modifyAtInternal(atKey, options).normalize();
  }

  set(key: K, value: V): SortedMap.NonEmpty<K, V> {
    return this.addEntry([key, value]);
  }

  updateAt<U>(
    key: RelatedTo<K, U>,
    update: Update<V>
  ): SortedMap.NonEmpty<K, V> {
    if (!this.context.isValidKey(key)) return this;

    return this.modifyAt(key, {
      ifExists: (value): V => Update(value, update),
    }).assumeNonEmpty();
  }

  removeKey<UK>(key: RelatedTo<K, UK>): SortedMap<K, V> {
    if (!this.context.isValidKey(key)) return this;

    return this.modifyAt(key, {
      ifExists: (_, remove): typeof remove => remove,
    });
  }

  removeKeys<UK>(keys: StreamSource<RelatedTo<K, UK>>): SortedMap<K, V> {
    if (isEmptyStreamSourceInstance(keys)) return this;

    const builder = this.toBuilder();
    builder.removeKeys(keys);
    return builder.build();
  }

  removeKeyAndGet<UK>(key: RelatedTo<K, UK>): [SortedMap<K, V>, V] | undefined {
    if (!this.context.isValidKey(key)) return undefined;

    const token = Symbol();
    let currentValue: V | typeof token = token;

    const newMap = this.modifyAt(key, {
      ifExists: (value, remove): V | typeof remove => {
        currentValue = value;
        return remove;
      },
    });

    if (token === currentValue) {
      return undefined;
    }

    return [newMap, currentValue];
  }

  filter(
    pred: (entry: readonly [K, V], index: number, halt: () => void) => boolean,
    options: { negate?: boolean } = {}
  ): SortedMap<K, V> {
    const builder = this.context.builder<K, V>();
    builder.addEntries(this.stream().filter(pred, options));

    if (builder.size === this.size) return this;

    return builder.build();
  }

  take(amount: number): SortedMap<K, V> | any {
    if (amount === 0) return this.context.empty();
    if (amount >= this.size || -amount > this.size) return this;
    if (amount < 0) return this.drop(this.size + amount);

    return this.takeInternal(amount).normalize();
  }

  drop(amount: number): SortedMap<K, V> {
    if (amount === 0) return this;
    if (amount >= this.size || -amount > this.size) return this.context.empty();
    if (amount < 0) return this.take(this.size + amount);

    return this.dropInternal(amount).normalize();
  }

  sliceIndex(range: IndexRange): SortedMap<K, V> {
    const indexRange = IndexRange.getIndicesFor(range, this.size);

    if (indexRange === 'empty') return this.context.empty();
    if (indexRange === 'all') return this;

    const [start, end] = indexRange;

    return this.drop(start).take(end - start + 1);
  }

  slice(range: Range<K>): SortedMap<K, V> {
    const { startIndex, endIndex } = this.getSliceRange(range);

    return this.sliceIndex({
      start: [startIndex, true],
      end: [endIndex, true],
    });
  }

  toBuilder(): SortedMapBuilder<K, V> {
    return this.context.createBuilder<K, V>(this);
  }

  toString(): string {
    return this.stream().join({
      start: 'SortedMap(',
      sep: ', ',
      end: ')',
      valueToString: (entry) => `${entry[0]} -> ${entry[1]}`,
    });
  }

  toJSON(): ToJSON<(readonly [K, V])[]> {
    return {
      dataType: this.context.typeTag,
      value: this.toArray(),
    };
  }
}

export class SortedMapLeaf<K, V> extends SortedMapNode<K, V> {
  constructor(
    readonly context: SortedMapContext<K>,
    public entries: readonly (readonly [K, V])[]
  ) {
    super();
  }

  copy(entries: readonly (readonly [K, V])[]): SortedMapLeaf<K, V> {
    if (entries === this.entries) return this;
    return this.context.leaf(entries);
  }

  get size(): number {
    return this.entries.length;
  }

  stream(
    options: { reversed?: boolean } = {}
  ): Stream.NonEmpty<readonly [K, V]> {
    return Stream.fromArray(this.entries, options) as Stream.NonEmpty<[K, V]>;
  }

  streamSliceIndex(
    range: IndexRange,
    options: { reversed?: boolean } = {}
  ): Stream<readonly [K, V]> {
    const { reversed = false } = options;

    return Stream.fromArray(this.entries, { range, reversed });
  }

  min(): readonly [K, V] {
    return this.entries[0];
  }

  max(): readonly [K, V] {
    return Arr.last(this.entries);
  }

  get<UK, O>(key: RelatedTo<K, UK>, otherwise?: OptLazy<O>): V | O {
    if (!this.context.isValidKey(key)) return OptLazy(otherwise) as O;

    const index = this.context.findIndex(key, this.entries);

    if (index < 0) return OptLazy(otherwise) as O;

    return this.entries[index][1];
  }

  findIndex(key: K): number {
    if (!this.context.comp.isComparable(key)) return -1;
    const index = this.context.findIndex(key, this.entries);
    return index < 0 ? -1 : index;
  }

  getAtIndex<O>(index: number, otherwise?: OptLazy<O>): readonly [K, V] | O {
    if (index >= this.size || -index > this.size)
      return OptLazy(otherwise) as O;
    if (index < 0) return this.getAtIndex(this.size + index, otherwise);

    return this.entries[index];
  }

  forEach(
    f: (entry: readonly [K, V], index: number, halt: () => void) => void,
    options: { state?: TraverseState } = {}
  ): void {
    const { state = TraverseState() } = options;

    if (state.halted) return;

    Arr.forEach(this.entries, f, state);
  }

  mapValues<V2>(mapFun: (value: V, key: K) => V2): SortedMapLeaf<K, V2> {
    const newEntries = this.entries.map((entry): [K, V2] => {
      const newValue = mapFun(entry[1], entry[0]);
      return [entry[0], newValue];
    });

    return this.context.leaf(newEntries);
  }

  toArray(): ArrayNonEmpty<[K, V]> {
    return this.entries.slice() as ArrayNonEmpty<[K, V]>;
  }

  // internal methods

  getInsertIndexOf(key: K): number {
    return this.context.findIndex(key, this.entries);
  }

  addInternal(entry: readonly [K, V]): SortedMapNode<K, V> {
    const index = this.context.findIndex(entry[0], this.entries);

    if (index >= 0) {
      const currentEntry = this.entries[index];
      if (Object.is(currentEntry[1], entry[1])) return this;

      const newEntries = Arr.update(this.entries, index, entry);
      return this.copy(newEntries);
    }

    const insertIndex = SortedIndex.next(index);
    const newEntries = Arr.insert(this.entries, insertIndex, entry);
    return this.copy(newEntries);
  }

  modifyAtInternal(
    key: K,
    options: {
      ifNew?: OptLazyOr<V, Token>;
      ifExists?: ((currentEntry: V, remove: Token) => V | Token) | V;
    }
  ): SortedMapNode<K, V> {
    const entryIndex = this.context.findIndex(key, this.entries);

    if (entryIndex >= 0) {
      if (undefined === options.ifExists) return this;

      const currentEntry = this.entries[entryIndex];
      const currentValue = currentEntry[1];
      const newValue =
        options.ifExists instanceof Function
          ? options.ifExists(currentValue, Token)
          : options.ifExists;

      if (Object.is(newValue, currentValue)) return this;

      if (Token === newValue) {
        const newEntries = Arr.splice(this.mutateEntries, entryIndex, 1);
        return this.copy(newEntries);
      }

      const newEntries = Arr.update(this.entries, entryIndex, [
        key,
        newValue,
      ] as [K, V]);
      return this.copy(newEntries);
    }

    if (undefined === options.ifNew) return this;

    const newValue = OptLazyOr<V, Token>(options.ifNew, Token);

    if (Token === newValue) return this;

    const insertIndex = SortedIndex.next(entryIndex);
    const newEntries = Arr.insert(this.entries, insertIndex, [
      key,
      newValue,
    ] as [K, V]);

    return this.copy(newEntries);
  }

  takeInternal(amount: number): SortedMapLeaf<K, V> {
    return this.context.leaf(this.entries.slice(0, amount));
  }

  dropInternal(amount: number): SortedMapLeaf<K, V> {
    return this.context.leaf(this.entries.slice(amount));
  }

  deleteMin(): [readonly [K, V], SortedMapLeaf<K, V>] {
    return leafDeleteMin<SortedMapLeaf<K, V>, readonly [K, V]>(this);
  }

  deleteMax(): [readonly [K, V], SortedMapLeaf<K, V>] {
    return leafDeleteMax<SortedMapLeaf<K, V>, readonly [K, V]>(this);
  }

  mutateSplitRight(index?: number): [readonly [K, V], SortedMapLeaf<K, V>] {
    return leafMutateSplitRight<SortedMapLeaf<K, V>, readonly [K, V]>(
      this,
      index
    );
  }

  mutateGiveToLeft(
    left: SortedMapLeaf<K, V>,
    toLeft: readonly [K, V]
  ): [readonly [K, V], SortedMapLeaf<K, V>] {
    return leafMutateGiveToLeft(this, left, toLeft);
  }

  mutateGiveToRight(
    right: SortedMapLeaf<K, V>,
    toRight: readonly [K, V]
  ): [readonly [K, V], SortedMapLeaf<K, V>] {
    return leafMutateGiveToRight(this, right, toRight);
  }

  mutateGetFromLeft(
    left: SortedMapLeaf<K, V>,
    toMe: readonly [K, V]
  ): [readonly [K, V], SortedMapLeaf<K, V>] {
    return leafMutateGetFromLeft(this, left, toMe);
  }

  mutateGetFromRight(
    right: SortedMapLeaf<K, V>,
    toMe: readonly [K, V]
  ): [readonly [K, V], SortedMapLeaf<K, V>] {
    return leafMutateGetFromRight(this, right, toMe);
  }

  mutateJoinLeft(left: SortedMapLeaf<K, V>, entry: readonly [K, V]): void {
    return leafMutateJoinLeft(this, left, entry);
  }

  mutateJoinRight(right: SortedMapLeaf<K, V>, entry: readonly [K, V]): void {
    return leafMutateJoinRight(this, right, entry);
  }

  normalize(): SortedMap<K, V> {
    if (this.entries.length === 0) return this.context.empty();
    if (this.entries.length <= this.context.maxEntries) return this;
    const size = this.size;
    const [upEntry, rightNode] = this.mutateSplitRight();
    return this.context.inner([upEntry], [this, rightNode], size);
  }
}

export class SortedMapInner<K, V> extends SortedMapNode<K, V> {
  constructor(
    readonly context: SortedMapContext<K>,
    public entries: readonly (readonly [K, V])[],
    public children: readonly SortedMapNode<K, V>[],
    readonly size: number
  ) {
    super();
  }

  get mutateChildren(): SortedMapNode<K, V>[] {
    return this.children as SortedMapNode<K, V>[];
  }

  copy(
    entries: readonly (readonly [K, V])[] = this.entries,
    children: readonly SortedMapNode<K, V>[] = this.children,
    size: number = this.size
  ): SortedMapInner<K, V> {
    if (
      entries === this.entries &&
      children === this.children &&
      size === this.size
    )
      return this;
    return this.context.inner(entries, children, size);
  }

  stream(
    options: { reversed?: boolean } = {}
  ): Stream.NonEmpty<readonly [K, V]> {
    const token = Symbol();
    return Stream.zipAll(
      token,
      Stream.fromArray(this.children, options),
      Stream.fromArray(this.entries, options)
    ).flatMap(([child, e]): Stream.NonEmpty<readonly [K, V]> => {
      if (token === child) RimbuError.throwInvalidStateError();
      if (token === e) return child.stream(options);
      return child.stream(options).append(e);
    }) as Stream.NonEmpty<readonly [K, V]>;
  }

  streamSliceIndex(
    range: IndexRange,
    options: { reversed?: boolean } = {}
  ): Stream<readonly [K, V]> {
    const { reversed = false } = options;
    return innerStreamSliceIndex<readonly [K, V]>(this, range, reversed);
  }

  min(): readonly [K, V] {
    return this.children[0].min();
  }

  max(): readonly [K, V] {
    return Arr.last(this.children).max();
  }

  get<UK, O>(key: RelatedTo<K, UK>, otherwise?: OptLazy<O>): V | O {
    if (!this.context.isValidKey(key)) return OptLazy(otherwise) as O;

    const index = this.context.findIndex(key, this.entries);

    if (index >= 0) return this.entries[index][1];

    const childIndex = SortedIndex.next(index);
    const child = this.children[childIndex];

    return child.get(key, otherwise);
  }

  findIndex(key: K): number {
    if (!this.context.comp.isComparable(key)) return -1;
    const index = this.context.findIndex(key, this.entries);
    if (index >= 0)
      return (
        this.children.slice(0, index + 1).reduce((x, y) => x + y.size, 0) +
        index
      );
    const childIndex = SortedIndex.next(index);
    const child = this.children[childIndex];
    const index$ = child.findIndex(key);
    if (index$ >= 0) {
      return (
        index$ +
        (this.children.slice(0, childIndex).reduce((x, y) => x + y.size, 0) -
          index -
          1)
      );
    }

    return -1;
  }

  getAtIndex<O>(index: number, otherwise?: OptLazy<O>): readonly [K, V] | O {
    return innerGetAtIndex<readonly [K, V], O>(this, index, otherwise);
  }

  forEach(
    f: (entry: readonly [K, V], index: number, halt: () => void) => void,
    options: { state?: TraverseState } = {}
  ): void {
    const { state = TraverseState() } = options;

    let i = -1;
    const entryLength = this.entries.length;
    const { halt } = state;

    while (!state.halted && i < entryLength) {
      if (i >= 0) f(this.entries[i], state.nextIndex(), halt);
      else {
        const childIndex = SortedIndex.next(i);
        this.children[childIndex].forEach(f, { state });
      }
      i = SortedIndex.next(i);
    }
  }

  mapValues<V2>(mapFun: (value: V, key: K) => V2): SortedMapInner<K, V2> {
    const newEntries = this.entries.map((entry): [K, V2] => {
      const newValue = mapFun(entry[1], entry[0]);
      return [entry[0], newValue];
    });
    const newChildren = this.children.map(
      (child): SortedMapNode<K, V2> => child.mapValues(mapFun)
    );

    return this.context.inner(newEntries, newChildren, this.size);
  }

  toArray(): ArrayNonEmpty<readonly [K, V]> {
    let i = -1;
    let result: (readonly [K, V])[] = [];

    while (i < this.entries.length) {
      if (i >= 0) result.push(this.entries[i]);
      else {
        const childIndex = SortedIndex.next(i);
        result = result.concat(this.children[childIndex].toArray());
      }
      i = SortedIndex.next(i);
    }

    return result as ArrayNonEmpty<[K, V]>;
  }

  // internal methods

  getInsertIndexOf(key: K): number {
    let index = 0;

    for (let i = 0; i < this.entries.length; i++) {
      const comp = this.context.comp.compare(key, this.entries[i][0]);
      const child = this.children[i];

      if (comp < 0) {
        const insertIndex = child.getInsertIndexOf(key);

        if (insertIndex < 0) return -index + insertIndex;
        return index + insertIndex;
      }

      index += child.size + 1;

      if (comp === 0) return index - 1;
    }

    const insertIndex = Arr.last(this.children).getInsertIndexOf(key);

    if (insertIndex < 0) return -index + insertIndex;
    return index + insertIndex;
  }

  addInternal(entry: readonly [K, V]): SortedMapInner<K, V> {
    const entryIndex = this.context.findIndex(entry[0], this.entries);

    if (entryIndex >= 0) {
      const newEntries = Arr.update(
        this.entries,
        entryIndex,
        (currentEntry): readonly [K, V] => {
          if (Object.is(currentEntry[1], entry[1])) return currentEntry;
          return entry;
        }
      );

      return this.copy(newEntries);
    }

    const childIndex = SortedIndex.next(entryIndex);
    const child = this.children[childIndex];

    const newChild = child.addInternal(entry);
    if (newChild === child) return this;

    const newSize = this.size + newChild.size - child.size;

    if (newChild.entries.length <= this.context.maxEntries) {
      // no need to shift
      const newChildren = Arr.update(this.children, childIndex, newChild);
      return this.copy(undefined, newChildren, newSize);
    }

    return this.normalizeDownsizeChild(childIndex, newChild, newSize);
  }

  modifyAtInternal(
    key: K,
    options: {
      ifNew?: OptLazyOr<V, Token>;
      ifExists?: ((currentEntry: V, remove: Token) => V | Token) | V;
    }
  ): SortedMapInner<K, V> {
    const entryIndex = this.context.findIndex(key, this.entries);

    if (entryIndex >= 0) {
      if (undefined === options.ifExists) return this;

      const currentEntry = this.entries[entryIndex];
      const currentValue = currentEntry[1];
      const newValue =
        options.ifExists instanceof Function
          ? options.ifExists(currentValue, Token)
          : options.ifExists;

      if (Object.is(newValue, currentValue)) return this;

      if (Token === newValue) {
        // remove inner entry
        const leftChild = this.children[entryIndex];
        const rightChild = this.children[entryIndex + 1];

        if (leftChild.entries.length >= rightChild.entries.length) {
          const [max, newLeft] = leftChild.deleteMax();
          const newEntries = Arr.update(this.entries, entryIndex, max);
          const newSelf = this.copy(newEntries);
          return newSelf.normalizeIncreaseChild(
            entryIndex,
            newLeft,
            this.size - 1
          );
        }

        const [min, newRight] = rightChild.deleteMin();
        const newEntries = Arr.update(this.entries, entryIndex, min);
        const newSelf = this.copy(newEntries);
        return newSelf.normalizeIncreaseChild(
          entryIndex + 1,
          newRight,
          this.size - 1
        );
      }

      // update inner entry
      const newEntry: [K, V] = [key, newValue];
      const newEntries = Arr.update(this.entries, entryIndex, newEntry);
      return this.copy(newEntries);
    }

    const childIndex = SortedIndex.next(entryIndex);
    const child = this.children[childIndex];

    const newChild = child.modifyAtInternal(key, options);
    const newSize = this.size + newChild.size - child.size;

    if (newChild.entries.length < this.context.minEntries) {
      return this.normalizeIncreaseChild(childIndex, newChild, newSize);
    }
    if (newChild.entries.length > this.context.maxEntries) {
      return this.normalizeDownsizeChild(childIndex, newChild, newSize);
    }

    const newChildren = Arr.update(this.children, childIndex, newChild);
    return this.copy(
      undefined,
      newChildren,
      this.size + newChild.size - child.size
    );
  }

  takeInternal(amount: number): SortedMapNode<K, V> {
    return innerTakeInternal<SortedMapInner<K, V>, readonly [K, V]>(
      this,
      amount
    );
  }

  dropInternal(amount: number): SortedMapNode<K, V> {
    return innerDropInternal<SortedMapInner<K, V>, readonly [K, V]>(
      this,
      amount
    );
  }

  deleteMin(): [readonly [K, V], SortedMapInner<K, V>] {
    return innerDeleteMin<SortedMapInner<K, V>, readonly [K, V]>(this);
  }

  deleteMax(): [readonly [K, V], SortedMapInner<K, V>] {
    return innerDeleteMax<SortedMapInner<K, V>, readonly [K, V]>(this);
  }

  mutateSplitRight(index?: number): [readonly [K, V], SortedMapInner<K, V>] {
    return innerMutateSplitRight<SortedMapInner<K, V>, readonly [K, V]>(
      this,
      index
    );
  }

  mutateGiveToLeft(
    left: SortedMapInner<K, V>,
    toLeft: readonly [K, V]
  ): [readonly [K, V], SortedMapInner<K, V>] {
    return innerMutateGiveToLeft(this, left, toLeft);
  }

  mutateGiveToRight(
    right: SortedMapInner<K, V>,
    toRight: readonly [K, V]
  ): [readonly [K, V], SortedMapInner<K, V>] {
    return innerMutateGiveToRight(this, right, toRight);
  }

  mutateGetFromLeft(
    left: SortedMapInner<K, V>,
    toMe: readonly [K, V]
  ): [readonly [K, V], SortedMapInner<K, V>] {
    return innerMutateGetFromLeft(this, left, toMe);
  }

  mutateGetFromRight(
    right: SortedMapInner<K, V>,
    toMe: readonly [K, V]
  ): [readonly [K, V], SortedMapInner<K, V>] {
    return innerMutateGetFromRight(this, right, toMe);
  }

  mutateJoinLeft(left: SortedMapInner<K, V>, entry: readonly [K, V]): void {
    return innerMutateJoinLeft(this, left, entry);
  }

  mutateJoinRight(right: SortedMapInner<K, V>, entry: readonly [K, V]): void {
    return innerMutateJoinRight(this, right, entry);
  }

  normalizeDownsizeChild(
    childIndex: number,
    newChild: SortedMapNode<K, V>,
    newSize: number
  ): SortedMapInner<K, V> {
    return innerNormalizeDownsizeChild<SortedMapInner<K, V>, readonly [K, V]>(
      this,
      childIndex,
      newChild,
      newSize
    );
  }

  normalizeIncreaseChild(
    childIndex: number,
    newChild: SortedMapNode<K, V>,
    newSize: number
  ): SortedMapInner<K, V> {
    return innerNormalizeIncreaseChild<SortedMapInner<K, V>, readonly [K, V]>(
      this,
      childIndex,
      newChild,
      newSize
    );
  }

  normalize(): SortedMap<K, V> {
    if (this.entries.length === 0) return this.children[0].normalize();

    if (this.entries.length <= this.context.maxEntries) return this;

    const size = this.size;
    const [upEntry, rightNode] = this.mutateSplitRight();

    return this.copy([upEntry], [this, rightNode], size);
  }
}
