var _a, _b;
import * as Context from "@effect/data/Context";
import * as Data from "@effect/data/Data";
import * as Debug from "@effect/data/Debug";
import * as Either from "@effect/data/Either";
import * as Equal from "@effect/data/Equal";
import * as Hash from "@effect/data/Hash";
import * as MutableHashMap from "@effect/data/MutableHashMap";
import * as MutableQueue from "@effect/data/MutableQueue";
import * as MutableRef from "@effect/data/MutableRef";
import * as Option from "@effect/data/Option";
import * as Deferred from "@effect/io/Deferred";
import * as Exit from "@effect/io/Exit";
import * as core from "@effect/io/internal_effect_untraced/core";
import * as effect from "@effect/io/internal_effect_untraced/effect";
import { none } from "@effect/io/internal_effect_untraced/fiberId";
/** @internal */
export const complete = (key, exit, entryStats, timeToLiveMillis) => Data.struct({
  _tag: "Complete",
  key,
  exit,
  entryStats,
  timeToLiveMillis
});
/** @internal */
export const pending = (key, deferred) => Data.struct({
  _tag: "Pending",
  key,
  deferred
});
/** @internal */
export const refreshing = (deferred, complete) => Data.struct({
  _tag: "Refreshing",
  deferred,
  complete
});
/** @internal */
export const MapKeyTypeId = /*#__PURE__*/Symbol.for("@effect/cache/Cache/MapKey");
class MapKeyImpl {
  constructor(current) {
    this.current = current;
    this[_a] = MapKeyTypeId;
    this.previous = undefined;
    this.next = undefined;
  }
  [(_a = MapKeyTypeId, Hash.symbol)]() {
    return Hash.combine(Hash.hash(this.next))(Hash.combine(Hash.hash(this.previous))(Hash.hash(this.current)));
  }
  [Equal.symbol](that) {
    if (this === that) {
      return true;
    }
    return isMapKey(that) && Equal.equals(this.current, that.current) && Equal.equals(this.previous, that.previous) && Equal.equals(this.next, that.next);
  }
}
/** @internal */
export const makeMapKey = current => new MapKeyImpl(current);
/** @internal */
export const isMapKey = u => typeof u === "object" && u != null && MapKeyTypeId in u;
class KeySetImpl {
  constructor() {
    this.head = undefined;
    this.tail = undefined;
  }
  add(key) {
    if (key !== this.tail) {
      if (this.tail === undefined) {
        this.head = key;
        this.tail = key;
      } else {
        const previous = key.previous;
        const next = key.next;
        if (next !== undefined) {
          key.next = undefined;
          if (previous !== undefined) {
            previous.next = next;
            next.previous = previous;
          } else {
            this.head = next;
            this.head.previous = undefined;
          }
        }
        this.tail.next = key;
        key.previous = this.tail;
        this.tail = key;
      }
    }
  }
  remove() {
    const key = this.head;
    if (key !== undefined) {
      const next = key.next;
      if (next !== undefined) {
        key.next = undefined;
        this.head = next;
        this.head.previous = undefined;
      } else {
        this.head = undefined;
        this.tail = undefined;
      }
    }
    return key;
  }
}
/** @internal */
export const makeKeySet = () => new KeySetImpl();
/**
 * Constructs a new `CacheState` from the specified values.
 *
 * @internal
 */
export const makeCacheState = (map, keys, accesses, updating, hits, misses) => ({
  map,
  keys,
  accesses,
  updating,
  hits,
  misses
});
/**
 * Constructs an initial cache state.
 *
 * @internal
 */
export const initialCacheState = () => makeCacheState(MutableHashMap.empty(), makeKeySet(), MutableQueue.unbounded(), MutableRef.make(false), 0, 0);
/** @internal */
const CacheSymbolKey = "@effect/cache/Cache";
/** @internal */
export const CacheTypeId = /*#__PURE__*/Symbol.for(CacheSymbolKey);
const cacheVariance = {
  _Key: _ => _,
  _Error: _ => _,
  _Value: _ => _
};
/** @internal */
export const makeCacheStats = (hits, misses, size) => ({
  hits,
  misses,
  size
});
/** @internal */
export const makeEntryStats = loadedMillis => ({
  loadedMillis
});
class CacheImpl {
  constructor(capacity, context, fiberId, lookup, timeToLive) {
    this.capacity = capacity;
    this.context = context;
    this.fiberId = fiberId;
    this.lookup = lookup;
    this.timeToLive = timeToLive;
    this[_b] = cacheVariance;
    this.cacheState = initialCacheState();
  }
  get(key) {
    return Debug.bodyWithTrace(trace => core.map(this.getEither(key), Either.merge).traced(trace));
  }
  cacheStats() {
    return Debug.bodyWithTrace(trace => core.sync(() => makeCacheStats(this.cacheState.hits, this.cacheState.misses, MutableHashMap.size(this.cacheState.map))).traced(trace));
  }
  getOption(key) {
    return Debug.bodyWithTrace(trace => core.suspend(() => Option.match(MutableHashMap.get(this.cacheState.map, key), () => {
      const mapKey = makeMapKey(key);
      this.trackAccess(mapKey);
      this.trackMiss();
      return core.succeed(Option.none());
    }, value => this.resolveMapValue(value))).traced(trace));
  }
  getOptionComplete(key) {
    return Debug.bodyWithTrace(trace => core.suspend(() => Option.match(MutableHashMap.get(this.cacheState.map, key), () => {
      const mapKey = makeMapKey(key);
      this.trackAccess(mapKey);
      this.trackMiss();
      return core.succeed(Option.none());
    }, value => this.resolveMapValue(value, true))).traced(trace));
  }
  contains(key) {
    return Debug.bodyWithTrace(trace => core.sync(() => MutableHashMap.has(this.cacheState.map, key)).traced(trace));
  }
  entryStats(key) {
    return Debug.bodyWithTrace(trace => core.sync(() => {
      const option = MutableHashMap.get(this.cacheState.map, key);
      if (Option.isSome(option)) {
        switch (option.value._tag) {
          case "Complete":
            {
              const loaded = option.value.entryStats.loadedMillis;
              return Option.some(makeEntryStats(loaded));
            }
          case "Pending":
            {
              return Option.none();
            }
          case "Refreshing":
            {
              const loaded = option.value.complete.entryStats.loadedMillis;
              return Option.some(makeEntryStats(loaded));
            }
        }
      }
      return Option.none();
    }).traced(trace));
  }
  getEither(key) {
    return Debug.bodyWithTrace(trace => core.suspend(() => {
      const k = key;
      let mapKey = undefined;
      let deferred = undefined;
      let value = Option.getOrUndefined(MutableHashMap.get(this.cacheState.map, k));
      if (value === undefined) {
        deferred = Deferred.unsafeMake(this.fiberId);
        mapKey = makeMapKey(k);
        if (MutableHashMap.has(this.cacheState.map, k)) {
          value = Option.getOrUndefined(MutableHashMap.get(this.cacheState.map, k));
        } else {
          MutableHashMap.set(this.cacheState.map, k, pending(mapKey, deferred));
        }
      }
      if (value === undefined) {
        this.trackAccess(mapKey);
        this.trackMiss();
        return core.map(this.lookupValueOf(key, deferred), Either.right);
      } else {
        return core.flatMap(this.resolveMapValue(value), Option.match(() => this.getEither(key), value => core.succeed(Either.left(value))));
      }
    }).traced(trace));
  }
  invalidate(key) {
    return Debug.bodyWithTrace(trace => core.sync(() => {
      MutableHashMap.remove(this.cacheState.map, key);
    }).traced(trace));
  }
  invalidateWhen(key, when) {
    return Debug.bodyWithTrace(trace => core.sync(() => {
      const value = MutableHashMap.get(this.cacheState.map, key);
      if (Option.isSome(value) && value.value._tag === "Complete") {
        if (value.value.exit._tag === "Success") {
          if (when(value.value.exit.value)) {
            MutableHashMap.remove(this.cacheState.map, key);
          }
        }
      }
    }).traced(trace));
  }
  invalidateAll() {
    return Debug.bodyWithTrace(trace => core.sync(() => {
      this.cacheState.map = MutableHashMap.empty();
    }).traced(trace));
  }
  refresh(key) {
    return Debug.bodyWithTrace(trace => effect.clockWith(clock => core.suspend(() => {
      const k = key;
      const deferred = Deferred.unsafeMake(this.fiberId);
      let value = Option.getOrUndefined(MutableHashMap.get(this.cacheState.map, k));
      if (value === undefined) {
        if (MutableHashMap.has(this.cacheState.map, k)) {
          value = Option.getOrUndefined(MutableHashMap.get(this.cacheState.map, k));
        } else {
          MutableHashMap.set(this.cacheState.map, k, pending(makeMapKey(k), deferred));
        }
      }
      if (value === undefined) {
        return core.asUnit(this.lookupValueOf(key, deferred));
      } else {
        switch (value._tag) {
          case "Complete":
            {
              if (this.hasExpired(clock, value.timeToLiveMillis)) {
                const found = Option.getOrUndefined(MutableHashMap.get(this.cacheState.map, k));
                if (Equal.equals(found, value)) {
                  MutableHashMap.remove(this.cacheState.map, k);
                }
                return core.asUnit(this.get(key));
              }
              // Only trigger the lookup if we're still the current value, `completedResult`
              return core.asUnit(effect.when(() => {
                const current = Option.getOrUndefined(MutableHashMap.get(this.cacheState.map, k));
                if (Equal.equals(current, value)) {
                  const mapValue = refreshing(deferred, value);
                  MutableHashMap.set(this.cacheState.map, k, mapValue);
                  return true;
                }
                return false;
              })(this.lookupValueOf(key, deferred)));
            }
          case "Pending":
            {
              return Deferred.await(value.deferred);
            }
          case "Refreshing":
            {
              return Deferred.await(value.deferred);
            }
        }
      }
    })).traced(trace));
  }
  set(key, value) {
    return Debug.bodyWithTrace(trace => effect.clockWith(clock => core.sync(() => {
      const now = clock.unsafeCurrentTimeMillis();
      const k = key;
      const lookupResult = Exit.succeed(value);
      const mapValue = complete(makeMapKey(k), lookupResult, makeEntryStats(now), now + this.timeToLive(lookupResult).millis);
      MutableHashMap.set(this.cacheState.map, k, mapValue);
    })).traced(trace));
  }
  size() {
    return Debug.bodyWithTrace(trace => core.sync(() => {
      return MutableHashMap.size(this.cacheState.map);
    }).traced(trace));
  }
  values() {
    return Debug.bodyWithTrace(trace => core.sync(() => {
      const values = [];
      for (const entry of this.cacheState.map) {
        if (entry[1]._tag === "Complete" && entry[1].exit._tag === "Success") {
          values.push(entry[1].exit.value);
        }
      }
      return values;
    }).traced(trace));
  }
  entries() {
    return Debug.bodyWithTrace(trace => core.sync(() => {
      const values = [];
      for (const entry of this.cacheState.map) {
        if (entry[1]._tag === "Complete" && entry[1].exit._tag === "Success") {
          values.push([entry[0], entry[1].exit.value]);
        }
      }
      return values;
    }).traced(trace));
  }
  keys() {
    return Debug.bodyWithTrace(trace => core.sync(() => {
      const keys = [];
      for (const entry of this.cacheState.map) {
        if (entry[1]._tag === "Complete" && entry[1].exit._tag === "Success") {
          keys.push(entry[0]);
        }
      }
      return keys;
    }).traced(trace));
  }
  resolveMapValue(value, ignorePending = false) {
    return effect.clockWith(clock => {
      switch (value._tag) {
        case "Complete":
          {
            this.trackAccess(value.key);
            this.trackHit();
            if (this.hasExpired(clock, value.timeToLiveMillis)) {
              MutableHashMap.remove(this.cacheState.map, value.key.current);
              return core.succeed(Option.none());
            }
            return core.map(core.done(value.exit), Option.some);
          }
        case "Pending":
          {
            this.trackAccess(value.key);
            this.trackHit();
            if (ignorePending) {
              return core.succeed(Option.none());
            }
            return core.map(Deferred.await(value.deferred), Option.some);
          }
        case "Refreshing":
          {
            this.trackAccess(value.complete.key);
            this.trackHit();
            if (this.hasExpired(clock, value.complete.timeToLiveMillis)) {
              if (ignorePending) {
                return core.succeed(Option.none());
              }
              return core.map(Deferred.await(value.deferred), Option.some);
            }
            return core.map(core.done(value.complete.exit), Option.some);
          }
      }
    });
  }
  trackHit() {
    this.cacheState.hits = this.cacheState.hits + 1;
  }
  trackMiss() {
    this.cacheState.misses = this.cacheState.misses + 1;
  }
  trackAccess(key) {
    MutableQueue.offer(this.cacheState.accesses, key);
    if (MutableRef.compareAndSet(this.cacheState.updating, false, true)) {
      let loop = true;
      while (loop) {
        const key = MutableQueue.poll(this.cacheState.accesses, MutableQueue.EmptyMutableQueue);
        if (key === MutableQueue.EmptyMutableQueue) {
          loop = false;
        } else {
          this.cacheState.keys.add(key);
        }
      }
      let size = MutableHashMap.size(this.cacheState.map);
      loop = size > this.capacity;
      while (loop) {
        const key = this.cacheState.keys.remove();
        if (key !== undefined) {
          if (MutableHashMap.has(this.cacheState.map, key.current)) {
            MutableHashMap.remove(this.cacheState.map, key.current);
            size = size - 1;
            loop = size > this.capacity;
          }
        } else {
          loop = false;
        }
      }
      MutableRef.set(this.cacheState.updating, false);
    }
  }
  hasExpired(clock, timeToLiveMillis) {
    return clock.unsafeCurrentTimeMillis() > timeToLiveMillis;
  }
  lookupValueOf(input, deferred) {
    return effect.clockWith(clock => core.suspend(() => {
      const key = input;
      return core.onInterrupt(() => core.zipRight(Deferred.interrupt(deferred), core.sync(() => {
        MutableHashMap.remove(this.cacheState.map, key);
      })))(core.flatMap(exit => {
        const now = clock.unsafeCurrentTimeMillis();
        const stats = makeEntryStats(now);
        const value = complete(makeMapKey(key), exit, stats, now + this.timeToLive(exit).millis);
        MutableHashMap.set(this.cacheState.map, key, value);
        return core.zipRight(Deferred.done(deferred, exit), core.done(exit));
      })(core.exit(core.provideContext(this.context)(this.lookup(input)))));
    }));
  }
}
_b = CacheTypeId;
/** @internal */
export const make = /*#__PURE__*/Debug.methodWithTrace((trace, restore) => (capacity, timeToLive, lookup) => makeWith(capacity, restore(lookup), () => timeToLive).traced(trace).traced(trace));
/** @internal */
export const makeWith = /*#__PURE__*/Debug.methodWithTrace((trace, restore) => (capacity, lookup, timeToLive) => core.map(effect.all(core.context(), core.fiberId()), ([context, fiberId]) => new CacheImpl(capacity, context, fiberId, restore(lookup), restore(timeToLive))).traced(trace));
/** @internal */
export const unsafeMakeWith = (capacity, lookup, timeToLive) => new CacheImpl(capacity, Context.empty(), none, lookup, timeToLive);
//# sourceMappingURL=cache.mjs.map