/* eslint-disable no-constant-condition */
import { promises as fs, accessSync, constants, writeFileSync } from 'fs';
import { promisify } from 'util';
import { gzip, gzipSync, unzip } from 'zlib';

type JSONData = [{ key: string, value: any }?];


/**
 * Persistant local JSON-based storage.
 * - auto-locking system to prevent corrupted data
 * - uses gzip compression to keep DB storage space utilization low
 * @author Matthew <matthew@staff.libraryofcode.org>
 */
export default class LocalStorage {
  protected storagePath: string;

  private locked: boolean = false;

  constructor(dbName: string, dir = `${__dirname}/../../localstorage`) {
    this.storagePath = `${dir}/${dbName}.json.gz`;
    this.init();
  }

  private init() {
    try {
      accessSync(this.storagePath, constants.F_OK);
    } catch {
      const setup = [];
      const data = gzipSync(JSON.stringify(setup));
      writeFileSync(this.storagePath, data);
    }
    return this;
  }

  /**
   * Compresses data using gzip.
   * @param data The data to be compressed.
   * ```ts
   * await LocalStorage.compress('hello!');
   * ```
   */
  static async compress(data: string): Promise<Buffer> {
    const func = promisify(gzip);
    const comp = <Buffer>await func(data);
    return comp;
  }

  /**
   * Decompresses data using gzip.
   * @param data The data to be decompressed.
   * ```ts
   * const compressed = await LocalStorage.compress('data');
   * const decompressed = await LocalStorage.decompress(compressed);
   * console.log(decompressed); // logs 'data';
   * ```
   */
  static async decompress(data: Buffer): Promise<string> {
    const func = promisify(unzip);
    const uncomp = <Buffer>await func(data);
    return uncomp.toString();
  }

  /**
   * Retrieves one data from the store.
   * If the store has multiple entries for the same key, this function will only return the first entry.
   * ```ts
   * await LocalStorage.get<type>('data-key');
   * ```
   * @param key The key for the data entry.
   */
  public async get<T>(key: string): Promise<T> {
    while (true) {
      if (!this.locked) break;
    }
    this.locked = true;

    const file = await fs.readFile(this.storagePath);
    const uncomp = await LocalStorage.decompress(file);
    this.locked = false;
    const json: JSONData = JSON.parse(uncomp);
    const result = json.filter((data) => data.key === key);
    if (!result[0]) return null;
    return result[0].value;
  }

  /**
   * Retrieves multiple data keys/values from the store.
   * This function will return all of the values matching the key you provided exactly. Use `LocalStorage.get();` if possible.
   * ```ts
   * await LocalStorage.get<type>('data-key');
   * @param key The key for the data entry.
   */
  public async getMany<T>(key: string): Promise<{ key: string, value: T }[]> {
    while (true) {
      if (!this.locked) break;
    }
    this.locked = true;

    const file = await fs.readFile(this.storagePath);
    const uncomp = await LocalStorage.decompress(file);
    this.locked = false;
    const json: JSONData = JSON.parse(uncomp);
    const result = json.filter((data) => data.key === key);
    if (result.length < 1) return null;
    return result;
  }

  /**
   * Sets a key/value pair and creates a new data entry.
   * @param key The key for the data entry.
   * @param value The value for the data entry, can be anything that is valid JSON.
   * @param options.override [DEPRECATED] By default, this function will error if the key you're trying to set already exists. Set this option to true to override that setting.
   * ```ts
   * await LocalStorage.set('data-key', 'test');
   * ```
   */
  public async set(key: string, value: any): Promise<void> {
    while (true) {
      if (!this.locked) break;
    }
    this.locked = true;

    const file = await fs.readFile(this.storagePath);
    const uncomp = await LocalStorage.decompress(file);
    const json: JSONData = JSON.parse(uncomp);
    json.push({ key, value });
    const comp = await LocalStorage.compress(JSON.stringify(json));
    await fs.writeFile(this.storagePath, comp);
    this.locked = false;
  }

  /**
   * Deletes the data for the specified key.
   * **Warning:** This function will delete ALL matching entries.
   * ```ts
   * await LocalStorage.del('data-key');
   * ```
   * @param key The key for the data entry.
   */
  public async del(key: string): Promise<void> {
    while (true) {
      if (!this.locked) break;
    }
    this.locked = true;

    const file = await fs.readFile(this.storagePath);
    const uncomp = await LocalStorage.decompress(file);
    const json: JSONData = JSON.parse(uncomp);
    const filtered = json.filter((data) => data.key !== key);
    const comp = await LocalStorage.compress(JSON.stringify(filtered));
    await fs.writeFile(this.storagePath, comp);
    this.locked = false;
  }
}
