// public/sw.js
// see: https://stackoverflow.com/questions/40100922/activate-updated-service-worker-on-refresh
// https://developers.google.com/web/ilt/pwa/caching-files-with-service-worker

/* importScripts('./nats.js') // import { connect as natsConnect } from './nats.js' // Firefox does not support import in sw
function testNats() {
	console.debug('NATS connect:', typeof connect)
} */
const CACHE_NAME = 'v01'
const DEV_MODE = 1 // 5 is all messages
const componentCache = {}
const fileTypeToCache = {}
const fileTypeToCacheArr = ['js', 'css', 'png', 'jpg', 'jpeg', 'gif', 'svg', 'woff', 'woff2', 'ttf', 'eot', 'otf', 'ico']
for (const key of fileTypeToCacheArr) {
	fileTypeToCache[key] = true
}
let openedCache
let cacheOpenState = false

// openCache() // start opening caches as soon as possible

function isSafari() {
	return navigator.userAgent.indexOf('Safari') > -1 && navigator.userAgent.indexOf('Chrome') === -1
}

function isFirefox() {
	return navigator.userAgent.indexOf('Firefox') > -1
}

if (isSafari() || isFirefox()) {
	const debugOrig = console.debug
	const warnOrig = console.warn
	console.debug = (...args) => {
		debugOrig.apply(null, args)
		postMessage({ type: 'debug', args })
	}
	console.warn = (...args) => {
		warnOrig.apply(null, args)
		postMessage({ type: 'warn', args })
	}
}

function parseAfter(subjectString, separator) {
	const pos = subjectString.indexOf(separator)
	if (pos >= 0) {
		return subjectString.substring(pos + separator.length)
	}
	return subjectString
}
function endsWith(subjectString, endString) {
	return subjectString.substring(subjectString.length - endString.length) === endString
}
function postMessage(msg) {
	self.clients.matchAll({ includeUncontrolled: true, type: 'window' }).then(clients => {
		if (clients && clients.length > 0) {
			// you need to decide which clients you want to send the message to, we use only first
			const client = clients[0]
			const msg2 = JSON.parse(JSON.stringify(msg)) // prevent DOMException: Failed to execute 'postMessage' on 'Client': Response object could not be cloned.
			client.postMessage(msg2)
		}
	})
}

let installed = false
self.addEventListener('install', event => {
	console.debug(`•• service-worker: install start`)
	installed = true
	cacheOpenState = false
	event.waitUntil(self.skipWaiting())
})

self.addEventListener('activate', event => {
	console.debug(`•• service-worker: activate start: installed ${installed}, cache open ${cacheOpenState}`)
	if (installed || cacheOpenState === false) {
		// && navigator.onLine
		event.waitUntil(
			caches.keys().then(keyList => {
				keyList.map(key => {
					event.waitUntil(caches.delete(key))
					console.debug(`•• service-worker: delete all cache keys, keyList`, keyList)
				})
				openCache('activate after delete keys') // activate does not wait for caches.open
			})
		)
	} else {
		openCache('activate') // activate does not wait for caches.open
	}
	event.waitUntil(self.clients.claim())
	console.debug(`•• service-worker: activate done`)
})

// listen to messages
self.addEventListener('message', event => {
	if (event.data && event.data.type === 'cache-open') {
		// console.debug(`•• service-worker: received message '${event.data && event.data.type}', answering with: ${cacheOpenState}`)
		postMessage({ type: 'cache-open', result: cacheOpenState })
		return
	}
	console.debug(`•• service-worker: received UNANSWERED message '${(event.data && event.data.type) || 'unknown'}'`)
	// postMessage({ type: 'unknown', result: 'unknown event' })
})

let cacheOpenStarted = false
function openCache(from) {
	if (cacheOpenState === true || cacheOpenStarted === true) {
		return
	}
	cacheOpenStarted = true
	console.debug(`•• service-worker: caches.open '${CACHE_NAME}' from '${from}' start`)
	caches
		.open(CACHE_NAME)
		.then(cache => {
			openedCache = cache
			openedCache
				.keys()
				.then(keyList => {
					cacheOpenState = true
					console.debug(`•• service-worker: caches.open '${CACHE_NAME}' from '${from}' done, keys`, keyList)
					postMessage({ type: 'cache-open', result: cacheOpenState })
				})
				.catch(error => {
					console.error(`•• service-worker: caches.open '${CACHE_NAME}'from '${from}' keys error`, error)
					postMessage({ type: 'activate', result: error })
				})
		})
		.catch(error => {
			console.error(`•• service-worker: caches.open error from '${from}'`, error)
		})
}

/* const sleep = ms => new Promise(resolve => setTimeout(resolve, ms))
const maxWaitLoops = 500 // 100 * sleep(0) = about 10 seconds
async function waitCacheOpen() {
	let waitTime = performance && performance.now && performance.now() / 1000
	let loop = 0
	while (!openedCache && openedCache.put && loop < maxWaitLoops) {
		loop++
		await sleep(0)
	}
	if (loop > 0) {
		waitTime = performance.now() / 1000 - waitTime
		waitTime = Math.round((waitTime + Number.EPSILON) * Math.pow(10, 4)) / Math.pow(10, 4) // round to 4 decimals
		console.warn(`••• service worker: wait cache open loops ${loop}, time: ${waitTime} seconds`)
	}
} */

async function componentResponse(event, response) {
	const ret = await response.clone().json()
	try {
		if (ret.include) {
			let baseUrl = self.location.origin + (ret.base_path || ret.rec?.base_path) + 'plugin/'
			for (const item of ret.include) {
				const url = baseUrl + item.path
				componentCache[url] = item.path
				const newRequest = new Request(url)
				const newResponse = new Response(item.js, { headers: { 'Content-Type': 'text/javascript' } })
				/* if (!openedCache || !openedCache.put) {
				console.debug(`•••• service-worker: CACHE IS NOT OPEN,waitCacheOpen()`)
				await waitCacheOpen()
			} */
				if (openedCache && openedCache.put) {
					openedCache.put(newRequest, newResponse)
					if (DEV_MODE > 0) {
						console.debug(`•••• service-worker: ADD TO CACHE ${url}, ${Math.floor((item.js.length / 1024) * 10) / 10} Kb`)
					}
				} else {
					console.debug(`•••• service-worker: ADD TO CACHE FAILED, cache was not open ${url}, ${Math.floor((item.js.length / 1024) * 10) / 10} Kb`)
				}
			}
		}
	} catch (error) {
		console.debug(`•• service-worker: 🚀 ~ componentResponse ERROR, event, response:`, error, event, response)
	}
	return response
}

function fetchResponse(event, response) {
	if (response.ok === false && response.status === 504) {
		// 504 = Gateway Timeout
		if (event.request.url.indexOf('.vite/deps') > -1) {
			console.error(`•• service-worker: .vite/deps fetch FAILED ${event.request.url}, response`, response)
			// debugger
			// removeServiceWorker() /* callback: () => {
			let count = 0
			const intervalID = setInterval(() => {
				count++
				if (typeof window !== 'undefined') {
					clearInterval(intervalID)
					window.location.reload()
				} else if (count > 20) {
					clearInterval(intervalID)
				}
			}, 50)
		}
		console.error(`•• service-worker: fetch FAILED ${event.request.url}, response 504:`, response)
		return response
	}
	if (response.ok === false && response.status !== 304) {
		// 304 = Not Modified
		console.error(`•• service-worker: fetch FAILED ${event.request.url}, response`, response)
		return response
	}
	if (endsWith(event.request.url, '/rest/nc/import/component') || endsWith(event.request.url, '/rest/nc/input')) {
		return componentResponse(event, response)
	}
	const fileType = parseAfter(event.request.url, '.')
	if (event.request.method === 'GET' && event.request.url.indexOf('?') === -1 && fileTypeToCache[fileType] && event.request.url.substring(0, 4) === 'http') {
		// skip chrome extensions do not start with 'http'
		try {
			if (openedCache && openedCache.put) {
				openedCache.put(event.request, response.clone())
				if (DEV_MODE > 3) {
					console.debug(`••• service-worker: fetch ok, cached ${event.request.url}, response: `, response)
				} else if (DEV_MODE > 2) {
					console.debug(`••• service-worker: fetch ok, cached ${event.request.url}`)
				}
			} else {
				if (DEV_MODE > 3) {
					console.debug(`••• service-worker: fetch ok, cache was not open ${event.request.url}, response: `, response)
				} else if (DEV_MODE > 2) {
					console.debug(`••• service-worker: fetch ok, cache was not open ${event.request.url}`)
				}
			}
		} catch (error) {
			console.debug(`•• service-worker: 🚀 ~ fetchResponse ERROR, event, response:`, error, event, response)
		}
	} else {
		if (DEV_MODE > 3) {
			console.debug(
				`•• service-worker: fetch ok, not cached ${event.request.url}, status: ${response.status} ${response.statusText}` // example: status: 200 OK
			)
		}
	}
	return response
}

// const errorResponse = new Response(`console.error('failed response')`, { headers: { 'Content-Type': 'text/javascript' } })
function setupFetch() {
	console.debug(`•• service-worker: start fetch listener`)
	// openCache()
	let cacheOpenCalled = false
	self.addEventListener('fetch', event => {
		/* if (DEV_MODE > 1) {
			console.debug(`•••• service-worker: request: ${event.request.url}`)
		} */
		if (event.request.url.indexOf('vite') > -1 || (event.request.url.indexOf('.vue') > -1 && event.request.url.indexOf('/assets/') === -1)) {
			// /assets/ is in dist version, no in development version
			if (DEV_MODE > 3) {
				console.debug(`•••• service-worker: direct dev request: ${event.request.url}`)
			}
			return fetch(event.request) // must not be event.request.url because request may be PUSH, not GET
				.then(fetchRes => {
					return fetchResponse(event, fetchRes)
				})
				.catch(error => {
					console.warn(`•• service-worker: direct dev request fetch ERROR ${event.request.url}, error`, error)
					// return error // event.cancelResponse()
				})
		}
		if (cacheOpenState === true) {
			if (DEV_MODE > 3) {
				console.debug(`•• service-worker: cache is open, start to match cache ${event.request.url}`)
			}
			return event.respondWith(
				openedCache
					.match(event.request.url, { ignoreSearch: false, ignoreMethod: true, ignoreVary: true })
					.then(response => {
						if (response) {
							// we found a cached response
							// https://stackoverflow.com/questions/56654119/how-do-you-set-the-url-when-creating-a-new-response
							// Object.defineProperty(response, 'url', { value: event.request.url })
							if (DEV_MODE > 0 && componentCache[event.request.url]) {
								console.debug(`••• service-worker: SERVED COMPONENT FROM CACHE ${event.request.url}`)
							} else if (DEV_MODE > 1) {
								console.debug(`••• service-worker: SERVED FROM CACHE ${event.request.url}`)
							}
							return response
						}
						if (DEV_MODE > 4) {
							console.debug(`••• service-worker: URL WAS NOT FOUND FROM CACHE ${event.request.url}`)
						}
						// do normal fetch() because url was not in the cache
						return fetch(event.request) // must not be event.request.url because request may be PUSH, not GET
							.then(fetchRes => {
								return fetchResponse(event, fetchRes)
							})
							.catch(error => {
								console.error(`•• service-worker: fetch ERROR ${event.request.url}, error`, error)
								// return error // event.cancelResponse()
							})
					})
					.catch(error => {
						console.error(`•• service-worker: event.respondWith match ERROR ${event.request.url}, error`, error)
						// return error // event.cancelResponse()
					})
			)
		}
		// fallback method 'Cache falling back to the network'
		if (!cacheOpenCalled) {
			cacheOpenCalled = true
			openCache('fetch')
		}
		if (DEV_MODE > 2) {
			console.debug(`•• service-worker: CACHE IS NOT OPEN, fetch ${event.request.url}`)
		}
		return event.respondWith(
			caches.match(event.request).then(response => {
				if (response) {
					if (DEV_MODE > 1) {
						console.debug(`•• service-worker: CACHE IS NOT OPEN, SERVED FROM CACHE ${event.request.url}`)
					}
					return response
				}
				return fetch(event.request)
					.then(fetchRes => {
						return fetchResponse(event, fetchRes)
					})
					.catch(error => {
						console.error(`•• service-worker: CACHE IS NOT OPEN, fetch ERROR ${event.request.url}, error`, error)
						// return error // event.cancelResponse()
					})
			})
		)
	})
}
setupFetch()

/* function removeServiceWorker() {
	self.registration
		.unregister()
		.then(function () {
			return self.clients.matchAll()
		})
		.then(function (clients) {
			clients.forEach(client => client.navigate(client.url))
		})
} */
