// (c) SpcFORK - Kaboom Cache System
// 2024-05-31 - 3:00 PM

import * as kbg from "kaplay";

export const HEADER = {
  name: 'Cacher',
  version: '0.0.11'
}

const wait = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));

export class Cacher {
  private cacheName: string;

  private _cache: Cache;
  set cache(v: Cache) {
    this._cache = v;
    localStorage.setItem(this.cacheName, JSON.stringify(v));
    this.initialized = true;
  }

  get cache() {
    return this._cache;
  }

  private initialized = false;

  static nsBuilder(ns: string, text: string) {
    return `${ns}~${text}`;
  }

  constructor(cacheName: string) {
    this.cacheName = cacheName;
  }

  ensureIsInit() {
    if (!this.initialized)
      throw new Error("Cache not initialized, will not function without cache");
  }

  async init() {
    this.cache = await caches.open(this.cacheName);
    this.initialized = true;
  }

  createNamespace(namespace: string): CacherNamespace {
    this.ensureIsInit();
    return new CacherNamespace(this, namespace);
  }

  createKaplayCacher() {
    this.ensureIsInit();
    return new (window as any).KaplayCacher(this);
  }

  createSpriteCacher() {
    this.ensureIsInit();
    return new (window as any).SpriteCacher(this);
  }

  createSoundCacher() {
    this.ensureIsInit();
    return new (window as any).SoundCacher(this);
  }
}

export class CacherNamespace {
  namespace: string;
  parent: Cacher;
  cache: () => Cache;

  constructor(parent: Cacher, namespace: string) {
    parent.ensureIsInit();
    this.namespace = namespace;
    this.parent = parent;
    this.cache = () => this.parent.cache;
  }

  nsBuilder = (name: string) => Cacher.nsBuilder(this.namespace, name);

  async get(name: string): Promise<Response | undefined> {
    return await this.cache().match(this.nsBuilder(name));
  }

  async put(name: string, response: Response) {
    await this.cache().put(this.nsBuilder(name), response);
  }

  makeRollout(rolloutList: string[], rolloutInterval = 0) {
    return new CacheRollout(this, rolloutList, rolloutInterval);
  }
}

export class CacheRollout {
  rolloutList: string[];
  rolloutInterval: number;
  parent: CacherNamespace;
  cache: () => Cache;

  constructor(parent: CacherNamespace, rolloutList: string[], rolloutInterval = 0) {
    this.rolloutList = rolloutList;
    this.rolloutInterval = rolloutInterval;
    this.parent = parent;
    this.cache = () => parent.cache();
  }

  async rollout(
    cb: (name: string, nsName: string, response: Response | void, t: InstanceType<typeof CacheRollout>) => any
  ): Promise<any[]> {
    const results = await Promise.all(this.rolloutList.map(async (name) => {
      const nsName = this.parent.nsBuilder(name);
      const response = await this.cache().match(nsName);
      const result = await cb(name, nsName, response, this);
      if (this.rolloutInterval > 0) await wait(this.rolloutInterval);
      return result;
    }));
    return results;
  }

  // @ Throw out broken Caches
  async tossBrokenCaches(rolloutList: string[]) {
    let ck = await this.cache().keys();
    let arrNotInRLL: string[] = [];
    for (let req of ck) {
      let name = req.url.split('/').pop()?.split('~')[1] as string;
      if (!rolloutList.includes(name)) arrNotInRLL.push(name);
    }
    for (let b of arrNotInRLL) await this.cache().delete(b);
  }
}

/**
 * @ Kaplay Cacher
 *
 * It's required that you change the namespace, loadEntity, and fetchName (If different file-order)
 * in order to create your own.
 */
class KaplayCacher {
  loadEntity: (name: string, reader: any) => Promise<any>
  fetchName = (name: string) => `./${this.namespace}/${name}`;

  cache = () => this.CNS.cache();

  parent: Cacher;
  namespace: string;
  CNS: CacherNamespace;

  nsBuilder = (name: string) => Cacher.nsBuilder(this.namespace, name);

  constructor(parent: Cacher, ns: string) {
    this.parent = parent;
    this.namespace = ns;
    this.CNS = new CacherNamespace(parent, ns);
  }

  async fetchAndCache(name: string): Promise<Response> {
    const response = await fetch(this.fetchName(name));
    const imageBlob = await response.blob();
    const imageUrl = URL.createObjectURL(imageBlob);

    let res = new Response(imageBlob);

    await this.CNS.put(name, res);
    await this.loadEntity(name, { result: imageUrl });

    return res;
  }

  async loadCached(name: string, cachedResponse: Response): Promise<Response> {
    const reader = new FileReader();
    reader.onload = async () => await this.loadEntity(name, reader)
      .catch(async (e) => {
        console.error(`Failed to load cached entity for ${name}:`, e);
        cachedResponse = await this.fetchAndCache(name);
      });

    reader.readAsDataURL(await cachedResponse.blob());
    return cachedResponse;
  }

  async rollout(rolloutList: string[], rolloutInterval = 0) {
    let rl = this.CNS.makeRollout(rolloutList, rolloutInterval);
    let res = await rl.rollout(async (name, _nsName, response, _t) => {
      if (response) await this.loadCached(name, response);
      else await this.fetchAndCache(name);
    })
    await rl.tossBrokenCaches(rolloutList);
    return res;
  }
}

export const staticPlugin = {
  ...HEADER,
  Cacher,
  KaplayCacher,
  CacherNamespace,
  CacheRollout,
  CacherPlugin
}

{ (window as any).KapCacher = staticPlugin }

export function CacherPlugin(kbg: kbg.KaboomCtx) {

  class SpriteCacher extends KaplayCacher {
    static namespace = 'sprites';
    constructor(parent: Cacher) { super(parent, new.target.namespace) }

    fetchName = (name: string) => `./${this.namespace}/${name}.png`
    loadEntity = async (name: string, reader: FileReader) =>
      await kbg.loadSprite(name, reader.result as string);
  }

  class SoundCacher extends KaplayCacher {
    static namespace = 'sounds';
    constructor(parent: Cacher) { super(parent, new.target.namespace) }

    fetchName = (name: string) => `./${this.namespace}/${name}.mp3`
    loadEntity = async (name: string, reader: FileReader) =>
      await kbg.loadSound(name, reader.result as string);
  }

  return {
    ...staticPlugin,
    KaplayCacher,
    SpriteCacher,
    SoundCacher
  };
}