import webpDataUrl from 'url-loader!../codecs/tiny.webp';

// Give TypeScript the correct global.
declare var self: ServiceWorkerGlobalScope;

export function cacheOrNetwork(event: FetchEvent): void {
  event.respondWith(async function () {
    const cachedResponse = await caches.match(event.request, { ignoreSearch: true });
    return cachedResponse || fetch(event.request);
  }());
}

export function cacheOrNetworkAndCache(event: FetchEvent, cacheName: string): void {
  event.respondWith(async function () {
    const { request } = event;
    // Return from cache if possible.
    const cachedResponse = await caches.match(request);
    if (cachedResponse) return cachedResponse;

    // Else go to the network.
    const response = await fetch(request);
    const responseToCache = response.clone();

    event.waitUntil(async function () {
      // Cache what we fetched.
      const cache = await caches.open(cacheName);
      await cache.put(request, responseToCache);
    }());

    // Return the network response.
    return response;
  }());
}

export function serveShareTarget(event: FetchEvent): void {
  const dataPromise = event.request.formData();

  // Redirect so the user can refresh the page without resending data.
  // @ts-ignore It doesn't like me giving a response to respondWith, although it's allowed.
  event.respondWith(Response.redirect('/?share-target'));

  event.waitUntil(async function () {
    // The page sends this message to tell the service worker it's ready to receive the file.
    await nextMessage('share-ready');
    const client = await self.clients.get(event.resultingClientId);
    const data = await dataPromise;
    const file = data.get('file');
    client.postMessage({ file, action: 'load-image' });
  }());
}

export function cleanupCache(event: FetchEvent, cacheName: string, keepAssets: string[]) {
  event.waitUntil(async function () {
    const cache = await caches.open(cacheName);

    // Clean old entries from the dynamic cache.
    const requests = await cache.keys();
    const promises = requests.map((cachedRequest) => {
      // Get pathname without leading /
      const assetPath = new URL(cachedRequest.url).pathname.slice(1);
      // If it isn't one of our keepAssets, we don't need it anymore.
      if (!keepAssets.includes(assetPath)) return cache.delete(cachedRequest);
    });

    await Promise.all<any>(promises);
  }());
}

function getAssetsWithPrefix(assets: string[], prefixes: string[]) {
  return assets.filter(
    asset => prefixes.some(prefix => asset.startsWith(prefix)),
  );
}

export async function cacheBasics(cacheName: string, buildAssets: string[]) {
  const toCache = ['/', '/assets/favicon.ico'];

  const prefixesToCache = [
    // Main app JS & CSS:
    'main-app.',
    // Service worker handler:
    'offliner.',
    // Little icons for the demo images on the homescreen:
    'icon-demo-',
    // Site logo:
    'logo.',
  ];

  const prefixMatches = getAssetsWithPrefix(buildAssets, prefixesToCache);

  toCache.push(...prefixMatches);

  const cache = await caches.open(cacheName);
  await cache.addAll(toCache);
}

export async function cacheAdditionalProcessors(cacheName: string, buildAssets: string[]) {
  let toCache = [];

  const prefixesToCache = [
    // Worker which handles image processing:
    'processor-worker.',
    // processor-worker imports:
    'process-',
  ];

  const prefixMatches = getAssetsWithPrefix(buildAssets, prefixesToCache);
  const wasm = buildAssets.filter(asset => asset.endsWith('.wasm'));

  toCache.push(...prefixMatches, ...wasm);

  const supportsWebP = await (async () => {
    if (!self.createImageBitmap) return false;
    const response = await fetch(webpDataUrl);
    const blob = await response.blob();
    return createImageBitmap(blob).then(() => true, () => false);
  })();

  // No point caching the WebP decoder if it's supported natively:
  if (supportsWebP) {
    toCache = toCache.filter(asset => !/webp[\-_]dec/.test(asset));
  }

  const cache = await caches.open(cacheName);
  await cache.addAll(toCache);
}

const nextMessageResolveMap = new Map<string, (() => void)[]>();

/**
 * Wait on a message with a particular event.data value.
 *
 * @param dataVal The event.data value.
 */
function nextMessage(dataVal: string): Promise<void> {
  return new Promise((resolve) => {
    if (!nextMessageResolveMap.has(dataVal)) {
      nextMessageResolveMap.set(dataVal, []);
    }
    nextMessageResolveMap.get(dataVal)!.push(resolve);
  });
}

self.addEventListener('message', (event) => {
  const resolvers = nextMessageResolveMap.get(event.data);
  if (!resolvers) return;
  nextMessageResolveMap.delete(event.data);
  for (const resolve of resolvers) resolve();
});
