import { Mutex, tryAcquire } from "async-mutex";

/**
 * An abstraction over the client's revision number cache. Provides a cache,
 * keyed by public key and data key and protected by a mutex to guard against
 * concurrent access to the cache. Each cache entry also has its own mutex, to
 * protect against concurrent access to that entry.
 */
export class RevisionNumberCache {
  private mutex: Mutex;
  private cache: { [key: string]: CachedRevisionNumber };

  /**
   * Creates the `RevisionNumberCache`.
   */
  constructor() {
    this.mutex = new Mutex();
    this.cache = {};
  }

  /**
   * Gets the revision cache key for the given public key and data key.
   *
   * @param publicKey - The given public key.
   * @param dataKey - The given data key.
   * @returns - The revision cache key.
   */
  static getCacheKey(publicKey: string, dataKey: string): string {
    return `${publicKey}/${dataKey}`;
  }

  /**
   * Gets an object containing the cached revision and the mutex for the entry.
   * The revision and mutex will be initialized if the entry is not yet cached.
   *
   * @param publicKey - The given public key.
   * @param dataKey - The given data key.
   * @returns - The cached revision entry object.
   */
  async getRevisionAndMutexForEntry(publicKey: string, dataKey: string): Promise<CachedRevisionNumber> {
    const cacheKey = RevisionNumberCache.getCacheKey(publicKey, dataKey);

    // Block until the mutex is available for the cache.
    return await this.mutex.runExclusive(async () => {
      if (!this.cache[cacheKey]) {
        this.cache[cacheKey] = new CachedRevisionNumber();
      }
      return this.cache[cacheKey];
    });
  }

  /**
   * Calls `exclusiveFn` with exclusive access to the given cached entry. The
   * revision number of the entry can be safely updated in `exclusiveFn`.
   *
   * @param publicKey - The given public key.
   * @param dataKey - The given data key.
   * @param exclusiveFn - A function to call with exclusive access to the given cached entry.
   * @returns - A promise containing the result of calling `exclusiveFn`.
   */
  async withCachedEntryLock<T>(
    publicKey: string,
    dataKey: string,
    exclusiveFn: (cachedRevisionEntry: CachedRevisionNumber) => Promise<T>
  ): Promise<T> {
    // Safely get or create mutex for the requested entry.
    const cachedRevisionEntry = await this.getRevisionAndMutexForEntry(publicKey, dataKey);

    try {
      return await tryAcquire(cachedRevisionEntry.mutex).runExclusive(async () => exclusiveFn(cachedRevisionEntry));
    } catch (e) {
      // Change mutex error to be more descriptive and user-friendly.
      if ((e as Error).message.includes("mutex already locked")) {
        throw new Error(
          `Concurrent access prevented in SkyDB for entry { publicKey: ${publicKey}, dataKey: ${dataKey} }`
        );
      } else {
        throw e;
      }
    }
  }
}

/**
 * An object containing a cached revision and a corresponding mutex. The
 * revision can be internally updated and it will reflect in the client's cache.
 */
export class CachedRevisionNumber {
  mutex: Mutex;
  revision: bigint;

  /**
   * Creates a `CachedRevisionNumber`.
   */
  constructor() {
    this.mutex = new Mutex();
    this.revision = BigInt(-1);
  }
}