'use strict';

var Effect = require('./Effect-BIlTHtLK.js');
var index = require('./index-9ZJx0550.js');

/**
 * This module provides types and utility functions to create and work with branded types,
 * which are TypeScript types with an added type tag to prevent accidental usage of a value in the wrong context.
 *
 * The `refined` and `nominal` functions are both used to create branded types in TypeScript.
 * The main difference between them is that `refined` allows for validation of the data, while `nominal` does not.
 *
 * The `nominal` function is used to create a new branded type that has the same underlying type as the input, but with a different name.
 * This is useful when you want to distinguish between two values of the same type that have different meanings.
 * The `nominal` function does not perform any validation of the input data.
 *
 * On the other hand, the `refined` function is used to create a new branded type that has the same underlying type as the input,
 * but with a different name, and it also allows for validation of the input data.
 * The `refined` function takes a predicate that is used to validate the input data.
 * If the input data fails the validation, a `BrandErrors` is returned, which provides information about the specific validation failure.
 *
 * @since 2.0.0
 */
/**
 * @since 2.0.0
 * @category symbols
 */
const RefinedConstructorsTypeId = /*#__PURE__*/Symbol.for("effect/Brand/Refined");
/**
 * Returns a `BrandErrors` that contains a single `RefinementError`.
 *
 * @since 2.0.0
 * @category constructors
 */
const error = (message, meta) => [{
  message,
  meta
}];
function refined(...args) {
  const either = args.length === 2 ? unbranded => args[0](unbranded) ? Effect.right(unbranded) : Effect.left(args[1](unbranded)) : unbranded => {
    return Effect.match(args[0](unbranded), {
      onNone: () => Effect.right(unbranded),
      onSome: Effect.left
    });
  };
  return Object.assign(unbranded => Effect.getOrThrowWith(either(unbranded), Effect.identity), {
    [RefinedConstructorsTypeId]: RefinedConstructorsTypeId,
    option: args => Effect.getRight(either(args)),
    either,
    is: args => Effect.isRight(either(args))
  });
}

/** @internal */
const complete = (key, exit, entryStats, timeToLiveMillis) => Effect.struct({
  _tag: "Complete",
  key,
  exit,
  entryStats,
  timeToLiveMillis
});
/** @internal */
const pending = (key, deferred) => Effect.struct({
  _tag: "Pending",
  key,
  deferred
});
/** @internal */
const refreshing = (deferred, complete) => Effect.struct({
  _tag: "Refreshing",
  deferred,
  complete
});
/** @internal */
const MapKeyTypeId = /*#__PURE__*/Symbol.for("effect/Cache/MapKey");
class MapKeyImpl {
  current;
  [MapKeyTypeId] = MapKeyTypeId;
  previous = undefined;
  next = undefined;
  constructor(current) {
    this.current = current;
  }
  [Effect.symbol]() {
    return Effect.pipe(Effect.hash(this.current), Effect.combine(Effect.hash(this.previous)), Effect.combine(Effect.hash(this.next)), Effect.cached(this));
  }
  [Effect.symbol$1](that) {
    if (this === that) {
      return true;
    }
    return isMapKey(that) && Effect.equals(this.current, that.current) && Effect.equals(this.previous, that.previous) && Effect.equals(this.next, that.next);
  }
}
/** @internal */
const makeMapKey = current => new MapKeyImpl(current);
/** @internal */
const isMapKey = u => Effect.hasProperty(u, MapKeyTypeId);
class KeySetImpl {
  head = undefined;
  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 */
const makeKeySet = () => new KeySetImpl();
/**
 * Constructs a new `CacheState` from the specified values.
 *
 * @internal
 */
const makeCacheState = (map, keys, accesses, updating, hits, misses) => ({
  map,
  keys,
  accesses,
  updating,
  hits,
  misses
});
/**
 * Constructs an initial cache state.
 *
 * @internal
 */
const initialCacheState = () => makeCacheState(Effect.empty(), makeKeySet(), Effect.unbounded(), Effect.make(false), 0, 0);
/** @internal */
const CacheSymbolKey = "effect/Cache";
/** @internal */
const CacheTypeId = /*#__PURE__*/Symbol.for(CacheSymbolKey);
const cacheVariance = {
  /* c8 ignore next */
  _Key: _ => _,
  /* c8 ignore next */
  _Error: _ => _,
  /* c8 ignore next */
  _Value: _ => _
};
/** @internal */
const ConsumerCacheSymbolKey = "effect/ConsumerCache";
/** @internal */
const ConsumerCacheTypeId = /*#__PURE__*/Symbol.for(ConsumerCacheSymbolKey);
const consumerCacheVariance = {
  /* c8 ignore next */
  _Key: _ => _,
  /* c8 ignore next */
  _Error: _ => _,
  /* c8 ignore next */
  _Value: _ => _
};
/** @internal */
const makeCacheStats = options => options;
/** @internal */
const makeEntryStats = loadedMillis => ({
  loadedMillis
});
class CacheImpl {
  capacity;
  context;
  fiberId;
  lookup;
  timeToLive;
  [CacheTypeId] = cacheVariance;
  [ConsumerCacheTypeId] = consumerCacheVariance;
  cacheState;
  constructor(capacity, context, fiberId, lookup, timeToLive) {
    this.capacity = capacity;
    this.context = context;
    this.fiberId = fiberId;
    this.lookup = lookup;
    this.timeToLive = timeToLive;
    this.cacheState = initialCacheState();
  }
  get(key) {
    return Effect.map(this.getEither(key), Effect.merge);
  }
  get cacheStats() {
    return Effect.sync(() => makeCacheStats({
      hits: this.cacheState.hits,
      misses: this.cacheState.misses,
      size: Effect.size(this.cacheState.map)
    }));
  }
  getOption(key) {
    return Effect.suspend(() => Effect.match(Effect.get(this.cacheState.map, key), {
      onNone: () => {
        const mapKey = makeMapKey(key);
        this.trackAccess(mapKey);
        this.trackMiss();
        return Effect.succeed$1(Effect.none());
      },
      onSome: value => this.resolveMapValue(value)
    }));
  }
  getOptionComplete(key) {
    return Effect.suspend(() => Effect.match(Effect.get(this.cacheState.map, key), {
      onNone: () => {
        const mapKey = makeMapKey(key);
        this.trackAccess(mapKey);
        this.trackMiss();
        return Effect.succeed$1(Effect.none());
      },
      onSome: value => this.resolveMapValue(value, true)
    }));
  }
  contains(key) {
    return Effect.sync(() => Effect.has(this.cacheState.map, key));
  }
  entryStats(key) {
    return Effect.sync(() => {
      const option = Effect.get(this.cacheState.map, key);
      if (Effect.isSome(option)) {
        switch (option.value._tag) {
          case "Complete":
            {
              const loaded = option.value.entryStats.loadedMillis;
              return Effect.some(makeEntryStats(loaded));
            }
          case "Pending":
            {
              return Effect.none();
            }
          case "Refreshing":
            {
              const loaded = option.value.complete.entryStats.loadedMillis;
              return Effect.some(makeEntryStats(loaded));
            }
        }
      }
      return Effect.none();
    });
  }
  getEither(key) {
    return Effect.suspend(() => {
      const k = key;
      let mapKey = undefined;
      let deferred = undefined;
      let value = Effect.getOrUndefined(Effect.get(this.cacheState.map, k));
      if (value === undefined) {
        deferred = Effect.unsafeMake(this.fiberId);
        mapKey = makeMapKey(k);
        if (Effect.has(this.cacheState.map, k)) {
          value = Effect.getOrUndefined(Effect.get(this.cacheState.map, k));
        } else {
          Effect.set(this.cacheState.map, k, pending(mapKey, deferred));
        }
      }
      if (value === undefined) {
        this.trackAccess(mapKey);
        this.trackMiss();
        return Effect.map(this.lookupValueOf(key, deferred), Effect.right);
      } else {
        return Effect.flatMap(this.resolveMapValue(value), Effect.match({
          onNone: () => this.getEither(key),
          onSome: value => Effect.succeed$1(Effect.left(value))
        }));
      }
    });
  }
  invalidate(key) {
    return Effect.sync(() => {
      Effect.remove(this.cacheState.map, key);
    });
  }
  invalidateWhen(key, when) {
    return Effect.sync(() => {
      const value = Effect.get(this.cacheState.map, key);
      if (Effect.isSome(value) && value.value._tag === "Complete") {
        if (value.value.exit._tag === "Success") {
          if (when(value.value.exit.value)) {
            Effect.remove(this.cacheState.map, key);
          }
        }
      }
    });
  }
  get invalidateAll() {
    return Effect.sync(() => {
      this.cacheState.map = Effect.empty();
    });
  }
  refresh(key) {
    return Effect.clockWith(clock => Effect.suspend(() => {
      const k = key;
      const deferred = Effect.unsafeMake(this.fiberId);
      let value = Effect.getOrUndefined(Effect.get(this.cacheState.map, k));
      if (value === undefined) {
        if (Effect.has(this.cacheState.map, k)) {
          value = Effect.getOrUndefined(Effect.get(this.cacheState.map, k));
        } else {
          Effect.set(this.cacheState.map, k, pending(makeMapKey(k), deferred));
        }
      }
      if (value === undefined) {
        return Effect.asVoid(this.lookupValueOf(key, deferred));
      } else {
        switch (value._tag) {
          case "Complete":
            {
              if (this.hasExpired(clock, value.timeToLiveMillis)) {
                const found = Effect.getOrUndefined(Effect.get(this.cacheState.map, k));
                if (Effect.equals(found, value)) {
                  Effect.remove(this.cacheState.map, k);
                }
                return Effect.asVoid(this.get(key));
              }
              // Only trigger the lookup if we're still the current value, `completedResult`
              return Effect.pipe(this.lookupValueOf(key, deferred), Effect.when(() => {
                const current = Effect.getOrUndefined(Effect.get(this.cacheState.map, k));
                if (Effect.equals(current, value)) {
                  const mapValue = refreshing(deferred, value);
                  Effect.set(this.cacheState.map, k, mapValue);
                  return true;
                }
                return false;
              }), Effect.asVoid);
            }
          case "Pending":
            {
              return Effect._await(value.deferred);
            }
          case "Refreshing":
            {
              return Effect._await(value.deferred);
            }
        }
      }
    }));
  }
  set(key, value) {
    return Effect.clockWith(clock => Effect.sync(() => {
      const now = clock.unsafeCurrentTimeMillis();
      const k = key;
      const lookupResult = Effect.succeed$2(value);
      const mapValue = complete(makeMapKey(k), lookupResult, makeEntryStats(now), now + Effect.toMillis(Effect.decode(this.timeToLive(lookupResult))));
      Effect.set(this.cacheState.map, k, mapValue);
    }));
  }
  get size() {
    return Effect.sync(() => {
      return Effect.size(this.cacheState.map);
    });
  }
  get values() {
    return Effect.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;
    });
  }
  get entries() {
    return Effect.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;
    });
  }
  get keys() {
    return Effect.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;
    });
  }
  resolveMapValue(value, ignorePending = false) {
    return Effect.clockWith(clock => {
      switch (value._tag) {
        case "Complete":
          {
            this.trackAccess(value.key);
            if (this.hasExpired(clock, value.timeToLiveMillis)) {
              Effect.remove(this.cacheState.map, value.key.current);
              return Effect.succeed$1(Effect.none());
            }
            this.trackHit();
            return Effect.map(value.exit, Effect.some);
          }
        case "Pending":
          {
            this.trackAccess(value.key);
            this.trackHit();
            if (ignorePending) {
              return Effect.succeed$1(Effect.none());
            }
            return Effect.map(Effect._await(value.deferred), Effect.some);
          }
        case "Refreshing":
          {
            this.trackAccess(value.complete.key);
            this.trackHit();
            if (this.hasExpired(clock, value.complete.timeToLiveMillis)) {
              if (ignorePending) {
                return Effect.succeed$1(Effect.none());
              }
              return Effect.map(Effect._await(value.deferred), Effect.some);
            }
            return Effect.map(value.complete.exit, Effect.some);
          }
      }
    });
  }
  trackHit() {
    this.cacheState.hits = this.cacheState.hits + 1;
  }
  trackMiss() {
    this.cacheState.misses = this.cacheState.misses + 1;
  }
  trackAccess(key) {
    Effect.offer(this.cacheState.accesses, key);
    if (Effect.compareAndSet(this.cacheState.updating, false, true)) {
      let loop = true;
      while (loop) {
        const key = Effect.poll(this.cacheState.accesses, Effect.EmptyMutableQueue);
        if (key === Effect.EmptyMutableQueue) {
          loop = false;
        } else {
          this.cacheState.keys.add(key);
        }
      }
      let size = Effect.size(this.cacheState.map);
      loop = size > this.capacity;
      while (loop) {
        const key = this.cacheState.keys.remove();
        if (key !== undefined) {
          if (Effect.has(this.cacheState.map, key.current)) {
            Effect.remove(this.cacheState.map, key.current);
            size = size - 1;
            loop = size > this.capacity;
          }
        } else {
          loop = false;
        }
      }
      Effect.set$1(this.cacheState.updating, false);
    }
  }
  hasExpired(clock, timeToLiveMillis) {
    return clock.unsafeCurrentTimeMillis() > timeToLiveMillis;
  }
  lookupValueOf(input, deferred) {
    return Effect.clockWith(clock => Effect.suspend(() => {
      const key = input;
      return Effect.pipe(this.lookup(input), Effect.provideContext(this.context), Effect.exit, Effect.flatMap(exit => {
        const now = clock.unsafeCurrentTimeMillis();
        const stats = makeEntryStats(now);
        const value = complete(makeMapKey(key), exit, stats, now + Effect.toMillis(Effect.decode(this.timeToLive(exit))));
        Effect.set(this.cacheState.map, key, value);
        return Effect.zipRight(Effect.done(deferred, exit), exit);
      }), Effect.onInterrupt(() => Effect.zipRight(Effect.interrupt(deferred), Effect.sync(() => {
        Effect.remove(this.cacheState.map, key);
      }))));
    }));
  }
}
/** @internal */
const make$1 = options => {
  const timeToLive = Effect.decode(options.timeToLive);
  return makeWith({
    capacity: options.capacity,
    lookup: options.lookup,
    timeToLive: () => timeToLive
  });
};
/** @internal */
const makeWith = options => Effect.map(Effect.all([Effect.context(), Effect.fiberId]), ([context, fiberId]) => new CacheImpl(options.capacity, context, fiberId, options.lookup, exit => Effect.decode(options.timeToLive(exit))));

/**
 * Constructs a new cache with the specified capacity, time to live, and
 * lookup function.
 *
 * @since 2.0.0
 * @category constructors
 */
const make = make$1;

/**
 * Provides a constructor for a Case Class.
 *
 * @since 2.0.0
 * @category constructors
 */
const Error$1 = /*#__PURE__*/function () {
  const plainArgsSymbol = /*#__PURE__*/Symbol.for("effect/Data/Error/plainArgs");
  const O = {
    BaseEffectError: class extends Effect.YieldableError {
      constructor(args) {
        super(args?.message, args?.cause ? {
          cause: args.cause
        } : undefined);
        if (args) {
          Object.assign(this, args);
          // @effect-diagnostics-next-line floatingEffect:off
          Object.defineProperty(this, plainArgsSymbol, {
            value: args,
            enumerable: false
          });
        }
      }
      toJSON() {
        return {
          ...this[plainArgsSymbol],
          ...this
        };
      }
    }
  };
  return O.BaseEffectError;
}();
/**
 * @since 2.0.0
 * @category constructors
 */
const TaggedError = tag => {
  const O = {
    BaseEffectError: class extends Error$1 {
      _tag = tag;
    }
  };
  O.BaseEffectError.prototype.name = tag;
  return O.BaseEffectError;
};

class FetchError extends TaggedError('FetchError') {
}
class NotFoundError extends TaggedError('NotFoundError') {
}
const httpClient = (input, init) => Effect.gen(function* () {
    const response = yield* Effect.tryPromise({
        try: () => fetch(input, init),
        catch: error => new FetchError({ cause: error }),
    });
    if (response.status === 404) {
        return yield* new NotFoundError({ url: response.url });
    }
    return response;
});
class ResponseTextError extends TaggedError('ResponseTextError') {
    constructor(error) {
        super(new Error(`Failed to read response text`, { cause: error }));
    }
}
const responseAsText = (res) => Effect.tryPromise({
    try: () => res.text(),
    catch: error => new ResponseTextError(error),
});

class URLCreationError extends TaggedError('URLCreationError') {
}
const createAssetUrlWithBasePath = (file, basePath) => Effect.try_(() => index.getAssetPath(['/assets', file].filter(Boolean).join('/').replace('//', '/'))).pipe(Effect.tapError(() => Effect.logWarning(`Failed to create URL for icon "${file}".
           Please provide an absolute URL in your app's 'setAssetPath(...)' configuration!
           Falling back to /assets/`)), Effect.orElseSucceed(() => window.location.origin + '/assets/' + file));

const SVGString = refined(isValidSVG, () => error('SVG data is malformed'));
class BrandValidationError extends TaggedError('BrandValidationError') {
    constructor(error, type) {
        super(new Error(`Brand validation failed for type ${type}.`, { cause: error }));
    }
}
const createValidSVGString = (svg) => Effect.try_({
    try: () => SVGString(svg),
    catch: error => new BrandValidationError(error, 'SVGString'),
});
function isValidSVG(svg) {
    if (typeof svg !== 'string') {
        return false;
    }
    try {
        const parser = new DOMParser();
        const doc = parser.parseFromString(svg, 'image/svg+xml');
        return doc.documentElement.nodeName === 'svg';
    }
    catch {
        return false;
    }
}

const emptyDefaultSvg = () => SVGString(`<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"></svg>`);
const createIconCache = () => make({
    capacity: 200,
    timeToLive: Effect.infinity,
    lookup: (key) => Effect.gen(function* () {
        const name = key;
        const assetPath = yield* createAssetUrlWithBasePath(`./icons/${name}.svg`);
        const res = yield* httpClient(assetPath);
        const responseString = yield* responseAsText(res);
        return yield* createValidSVGString(responseString);
    }),
});
let cacheInstance = null;
function fetchIconSvg(name) {
    const key = name;
    return Effect.gen(function* () {
        if (!cacheInstance) {
            cacheInstance = yield* createIconCache();
        }
        return yield* cacheInstance.get(key);
    });
}
function clearIconCache() {
    return Effect.gen(function* () {
        if (cacheInstance) {
            yield* cacheInstance.invalidateAll;
        }
        else {
            Effect.logWarning('Attempted to clear icon cache before it was initialized');
        }
    });
}

exports.clearIconCache = clearIconCache;
exports.emptyDefaultSvg = emptyDefaultSvg;
exports.fetchIconSvg = fetchIconSvg;
