import type { Token } from '@rimbu/base';
import type { RMapBase } from '@rimbu/collection-types/map-custom';
import type {
  OptLazy,
  OptLazyOr,
  RelatedTo,
  TraverseState,
} from '@rimbu/common';
import { type HashMap } from '@rimbu/hashed/map';
import type { StreamSource } from '@rimbu/stream';

import type { ProximityMap } from '@rimbu/proximity/map';
import { wrapHashMap } from './wrapping.mjs';

/**
 * Mutable builder used to efficiently construct new immutable {@link ProximityMap} instances.<br/>
 * <br/>
 * The builder stores entries in an internal `HashMap.Builder` and only wraps the result
 * into a `ProximityMap` when {@link ProximityMapBuilder.build | build} (or
 * {@link ProximityMapBuilder.buildMapValues | buildMapValues}) is called.
 *
 * @typeparam K - the key type
 * @typeparam V - the value type
 */
export class ProximityMapBuilder<K, V> implements ProximityMap.Builder<K, V> {
  private readonly internalBuilder: HashMap.Builder<K, V>;

  constructor(
    readonly context: ProximityMap.Context<K>,
    private source?: ProximityMap.NonEmpty<K, V>
  ) {
    this.internalBuilder = context.hashMapContext.builder();
    this.internalBuilder.addEntries(source);
  }

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

  get isEmpty(): boolean {
    return this.internalBuilder.isEmpty;
  }

  /**
   * Applying `get()` to the Builder does NOT apply the proximity algorithm - which would
   * be pointless at this construction stage; the internal, hash-based builder
   * is queried instead
   *
   */
  get = <UK, O>(key: RelatedTo<K, UK>, otherwise?: OptLazy<O>): V | O => {
    if (undefined !== this.source) return this.source.get(key, otherwise!);

    return this.internalBuilder.get(key, otherwise!);
  };

  // prettier-ignore
  hasKey = <UK = K,>(key: RelatedTo<K, UK>): boolean => {
    return this.internalBuilder.hasKey(key);
  };

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

  addEntry = (entry: readonly [K, V]): boolean => {
    const hasChanged = this.internalBuilder.addEntry(entry);

    if (hasChanged) {
      this.source = undefined;
    }

    return hasChanged;
  };

  addEntries = (entries: StreamSource<readonly [K, V]>): boolean => {
    const hasChanged = this.internalBuilder.addEntries(entries);

    if (hasChanged) {
      this.source = undefined;
    }

    return hasChanged;
  };

  set = (key: K, value: V): boolean => {
    const hasChanged = this.internalBuilder.set(key, value);

    if (hasChanged) {
      this.source = undefined;
    }

    return hasChanged;
  };

  removeKey = <UK, O>(key: RelatedTo<K, UK>, otherwise?: OptLazy<O>): V | O => {
    if (this.internalBuilder.hasKey(key)) {
      this.source = undefined;
    }

    return this.internalBuilder.removeKey(key, otherwise!);
  };

  // prettier-ignore
  removeKeys = <UK = K,>(keys: StreamSource<RelatedTo<K, UK>>): boolean => {
    const hasChanged = this.internalBuilder.removeKeys(keys);

    if (hasChanged) {
      this.source = undefined;
    }

    return hasChanged;
  };

  modifyAt = (
    key: K,
    options: {
      ifNew?: OptLazyOr<V, typeof Token>;
      ifExists?:
        | (<V2 extends V = V>(
            currentValue: V & V2,
            remove: typeof Token
          ) => V | typeof Token)
        | V;
    }
  ): boolean => {
    const hasChanged = this.internalBuilder.modifyAt(key, options);

    if (hasChanged) {
      this.source = undefined;
    }

    return hasChanged;
  };

  // prettier-ignore
  updateAt = <O,>(
    key: K,
    update: RMapBase.Update<V>,
    otherwise?: OptLazy<O>
  ): V | O => {
    const previousValue = this.internalBuilder.updateAt(
      key,
      update,
      otherwise!
    );

    const newValue = this.internalBuilder.get(key);

    if (!Object.is(previousValue, newValue)) {
      this.source = undefined;
    }

    return previousValue;
  };

  build = (): ProximityMap<K, V> => {
    return this.source !== undefined
      ? this.source
      : wrapHashMap(this.context, this.internalBuilder.build());
  };

  // prettier-ignore
  buildMapValues = <V2,>(
    mapFun: (value: V, key: K) => V2
  ): ProximityMap<K, V2> => {
    return wrapHashMap(
      this.context,
      this.internalBuilder.buildMapValues(mapFun)
    );
  };
}
