// ----- FreeShow -----
// This is for media/file functions

import { get } from "svelte/store"
import type { ContentProviderId } from "../../../electron/contentProviders/base/types"
import { Main } from "../../../types/IPC/Main"
import type { FileFolder, MediaStyle, Subtitle } from "../../../types/Main"
import type { Cropping, Styles } from "../../../types/Settings"
import type { ShowType } from "../../../types/Show"
import { requestMain, sendMain } from "../../IPC/main"
import { audioFolders, cachePath, loadedMediaThumbnails, media, mediaFolders, special } from "../../stores"
import { addToMediaFolder } from "../../utils/cloudSync"
import { isMainWindow, newToast, wait, waitUntilValueIsDefined } from "../../utils/common"
import { audioExtensions, imageExtensions, mediaExtensions, presentationExtensions, videoExtensions } from "../../values/extensions"
import type { API_media, API_slide_thumbnail } from "../actions/api"
import { clone } from "./array"
import { getFirstActiveOutput, getOutputResolution } from "./output"

export function getExtension(path: string): string {
    if (typeof path !== "string") return ""
    if (path.indexOf(".") < 0) return path
    if (path.includes("?")) path = path.slice(0, path.indexOf("?"))
    return path.substring(path.lastIndexOf(".") + 1).toLowerCase()
}

export function removeExtension(name: string): string {
    if (typeof name !== "string" || !name) return ""
    if (name.indexOf(".") < 0) return name
    return name.slice(0, name.lastIndexOf("."))
}

export function isMediaExtension(extension: string, audio = false): boolean {
    let extensions: string[] = [...imageExtensions, ...videoExtensions]
    if (audio) extensions = audioExtensions
    return extensions.includes(extension.toLowerCase())
}

export function getMediaType(extension: string): ShowType {
    if (extension.toLowerCase() === "pdf") return "pdf"
    if (presentationExtensions.includes(extension.toLowerCase())) return "ppt"
    if (audioExtensions.includes(extension.toLowerCase())) return "audio"
    if (videoExtensions.includes(extension.toLowerCase())) return "video"
    return "image"
}

export function getFileName(path: string): string {
    if (typeof path !== "string") return ""
    if (path.indexOf("\\") > -1) return path.substring(path.lastIndexOf("\\") + 1)
    if (path.indexOf("/") > -1) return path.substring(path.lastIndexOf("/") + 1)
    return path
}

let pathJoiner = ""
export function splitPath(path: string): string[] {
    if (typeof path !== "string") return []
    path = path.replace("file://", "")
    if (path.indexOf("\\") > -1) pathJoiner = "\\"
    if (path.indexOf("/") > -1) pathJoiner = "/"
    return path.split(pathJoiner) || []
}

export function joinPath(path: string[]): string {
    if (!pathJoiner) splitPath(path?.[0])
    return path.join(pathJoiner)
}

export function isLocalFile(path: string): boolean {
    if (typeof path !== "string") return false
    if (path.startsWith("http://") || path.startsWith("https://") || path.startsWith("data:") || path.startsWith("blob:") || path.startsWith("freeshow-protected://")) return false
    return true
}

// fix for media files with special characters in file name not playing
export function encodeFilePath(path: string): string {
    if (typeof path !== "string") return ""
    if (!isLocalFile(path)) return path

    if (path.startsWith("file://")) path = path.replace("file://", "")
    try {
        if (path.match(/%[0-9a-fA-F]{2}/)) path = decodeURIComponent(path)
    } catch (e) {
        // malformed percent-encoding: keep the original path
        console.debug("Could not decode media path:", path, e)
    }

    // encode each path segment except for drive letter (e.g. C:) to avoid issues with ":" in file names on Windows
    const encoded = splitPath(path).map((seg, i) => (i === 0 && /^[a-zA-Z]:$/.test(seg) ? seg : encodeURIComponent(seg)))
    let joined = joinPath(encoded)

    // path starting at "/" auto completes to app root, but should be file://
    if (joined.startsWith("/")) joined = `file://${joined}`
    return joined
}

// decode only file name in path (not full path)
// export function decodeFilePath(path: string) {
//     let splittedPath = splitPath(path)
//     let fileName = splittedPath.pop() || ""
//     let decodedName = decodeURI(fileName)

//     return joinPath([...splittedPath, decodedName])
// }

export async function getThumbnail(data: API_media) {
    let path = data.path

    const located = await locateMediaFile(path)
    if (!located?.path) return ""
    path = located.path

    if (videoExtensions.includes(getExtension(path))) {
        path = getThumbnailPath(path, mediaSize.drawerSize)
    }

    return await toDataURL(encodeFilePath(path))
}

export async function getSlideThumbnail(data: API_slide_thumbnail, extraOutData: { backgroundImage?: string; overlays?: string[] } = {}, plainSlide = false) {
    const currentOutput = getFirstActiveOutput()
    const outSlide = currentOutput?.out?.slide
    if (!currentOutput) return ""

    if (!data.showId) data.showId = outSlide?.id
    if (!data.layoutId) data.layoutId = outSlide?.layout
    if (data.index === undefined) data.index = outSlide?.index

    if (!data?.showId) return ""

    const output = clone(currentOutput)
    if (!output.out) output.out = {}
    output.out.slide = { id: data.showId, layout: data.layoutId, index: data.index }

    if (plainSlide) {
        output.style = ""
        output.out = { slide: output.out.slide }
    }

    if (extraOutData.backgroundImage) output.out.background = { path: extraOutData.backgroundImage }
    if (extraOutData.overlays) output.out.overlays = extraOutData.overlays

    let resolution: any = getOutputResolution(currentOutput.id)
    resolution = { width: resolution.width * 0.5, height: resolution.height * 0.5 }

    const thumbnail = await requestMain(Main.CAPTURE_SLIDE, { output: { [currentOutput.id]: output }, resolution })
    return thumbnail?.base64 || ""
}

// convert to base64
async function toDataURL(url: string): Promise<string> {
    return new Promise((resolve) => {
        const xhr = new XMLHttpRequest()

        xhr.onload = () => {
            const reader = new FileReader()
            reader.onloadend = () => resolve((reader.result || "").toString())
            reader.readAsDataURL(xhr.response)
        }
        xhr.onerror = () => resolve("")

        xhr.open("GET", url)
        xhr.responseType = "blob"
        xhr.send()
    })
}

// download any online media
// get located media path & generated thumbnail
const replacedPaths = new Map<string, { path: string; altPath?: string; thumbnail: string }>()
let currentlyGetting: string[] = []
export async function getMedia(path: string, size: number = mediaSize.drawerSize) {
    if (typeof path !== "string") return null

    const mediaId = `${path}-${size}`

    const mediaData = clone(get(media)[path])
    if (replacedPaths.has(mediaId)) return { ...replacedPaths.get(mediaId)!, data: mediaData }

    if (currentlyGetting.includes(mediaId)) {
        await waitUntilValueIsDefined(() => replacedPaths.has(mediaId), 50, 10000)
        if (replacedPaths.has(mediaId)) return { ...replacedPaths.get(mediaId)!, data: mediaData }
        return null
    }
    currentlyGetting.push(mediaId)

    try {
        if (path.startsWith("http")) {
            const localPath = (await downloadOnlineMedia(path)) || path
            const thumbnail = mediaData?.contentFile?.thumbnail || localPath

            return finish(localPath, thumbnail, path)
        }

        if (!isLocalFile(path) || path.includes("freeshow-cache") || path.includes("media-cache")) {
            return finish(path, path)
        }

        // lessons
        // let thumbnailPath = getThumbnailPath(path, data.size)
        // // cache after it's downloaded
        // setTimeout(() => loadThumbnail(path, data.size), 1000)

        const located = await locateMediaFile(path)
        if (!located) return finish()

        const newPath = located.path
        if (!located.hasChanged) addToMediaFolder(newPath)

        if (!isMainWindow()) {
            const thumbnail = getThumbnailPath(newPath, size)
            return finish(newPath, thumbnail)
        }

        // generate thumbnails only in main window
        const thumbnail = (await loadThumbnail(newPath, size)) || newPath
        return finish(newPath, thumbnail)
    } finally {
        const index = currentlyGetting.indexOf(mediaId)
        if (index > -1) currentlyGetting.splice(index, 1)
    }

    function finish(path?: string, thumbnail?: string, altPath?: string) {
        if (!path || !thumbnail) return null
        if (altPath) replacedPaths.set(mediaId, { path, altPath, thumbnail })
        else replacedPaths.set(mediaId, { path, thumbnail })
        if (altPath) return { path, altPath, thumbnail, data: mediaData }
        return { path, thumbnail, data: mediaData }
    }
}

// export function isMediaCached(path: string, size: number = mediaSize.drawerSize) {
//     const mediaId = `${path}-${size}`
//     return replacedPaths.has(mediaId)
// }

export async function getMediaCached(path: string, size: number = mediaSize.drawerSize) {
    const mediaData = clone(get(media)[path])

    const mediaId = `${path}-${size}`
    if (replacedPaths.has(mediaId)) return { ...replacedPaths.get(mediaId)!, data: mediaData }

    if (currentlyGetting.includes(mediaId)) {
        await waitUntilValueIsDefined(() => replacedPaths.has(mediaId), 50, 10000)
        if (replacedPaths.has(mediaId)) return { ...replacedPaths.get(mediaId)!, data: mediaData }
    }

    return null
}

export async function locateMediaFile(path: string) {
    if (path.startsWith("http") || path.startsWith("data:") || path.startsWith("blob:") || path.startsWith("freeshow-protected://")) return { path, hasChanged: false }

    let folders: string[] = []
    if (get(special).autoLocateMedia !== false) {
        const mediaType = getMediaType(getExtension(path))
        if (mediaType === "audio") folders = Object.values(get(audioFolders)).map((a) => a.path!)
        else folders = Object.values(get(mediaFolders)).map((a) => a.path!)
    }

    const result = await requestMain(Main.LOCATE_MEDIA_FILE, { filePath: path, folders })

    // if (!result) newToast("error.media")
    // else if (result.hasChanged) newToast("toast.media_replaced")

    return result
}

const existingMedia: string[] = []
export async function doesMediaExist(path: string, noCache = false) {
    if (existingMedia.includes(path)) return true

    if (noCache) {
        const existsDataNoCache = await requestMain(Main.DOES_MEDIA_EXIST, { path, noCache })
        if (!existsDataNoCache?.exists) return false

        existingMedia.push(path)
        return true
    }

    const creationTime = get(media)[path]?.creationTime || 0
    const existsData = await requestMain(Main.DOES_MEDIA_EXIST, { path, creationTime })
    if (!existsData) return false

    // update "media"
    if (!existsData.exists || !creationTime) {
        media.update((a) => {
            if (existsData.exists && a[path]) {
                a[path].creationTime = existsData.creationTime
            } else {
                a[path] = { creationTime: existsData.creationTime }
            }

            return a
        })
    }

    if (existsData.exists) existingMedia.push(path)
    return existsData.exists
}

export async function getMediaInfo(path: string): Promise<{ codecs: string[]; mimeType: string; mimeCodec: string } | null> {
    let info: { path?: string; codecs: string[]; mimeType: string; mimeCodec: string } | null = null
    if (typeof path !== "string") return info
    if (!isLocalFile(path)) return info

    const cachedInfo = get(media)[path]?.info
    if (cachedInfo?.codecs?.length) return cachedInfo

    try {
        info = (await requestMain(Main.MEDIA_CODEC, { path })) || null
    } catch (err) {
        return info
    }

    if (!info) return info

    delete info.path

    if (JSON.stringify(get(media)[path]?.info) === JSON.stringify(info)) return info

    media.update((a) => {
        if (!a[path]) a[path] = {}
        a[path].info = info
        return a
    })

    return info
}

export async function isVideoSupported(path: string) {
    const info = await getMediaInfo(path)
    if (!info?.codecs) return true

    // HEVC (H.265) / Timecode
    const unsupportedCodecs = /(hevc|hvc1|ap4h|tmcd)/i
    const isUnsupported = info.codecs.length && info.codecs.every((codec) => unsupportedCodecs.test(codec))

    // not reliable:
    // const isSupported = MediaSource.isTypeSupported(info.mimeCodec)

    if (isUnsupported) newToast("toast.unsupported_video")
    return !isUnsupported
}

export function setMediaTracks(data: { path: string; tracks: Subtitle[] }) {
    const path: string = data.path || ""

    media.update((a) => {
        if (!a[path]) a[path] = {}
        a[path].tracks = data.tracks
        return a
    })
}

export function enableSubtitle(video: HTMLVideoElement, languageId: string) {
    if (!video) return
    const tracks = [...(video.textTracks || [])]

    const enabled = tracks.find((a) => a.mode !== "disabled")
    if (enabled) enabled.mode = "disabled"

    if (!languageId) return

    const newTrack = tracks.find((a) => a.language === languageId)
    if (newTrack) newTrack.mode = "showing"
}

export function getMediaStyle(mediaObj: MediaStyle | undefined, currentStyle: Styles | undefined) {
    const fitOptions = {
        blurAmount: currentStyle?.blurAmount ?? 6,
        blurOpacity: currentStyle?.blurOpacity || 0.3
    }

    const mediaStyle: MediaStyle = {
        filter: "",
        flipped: false,
        flippedY: false,
        blend: "",
        fit: currentStyle?.fit || "contain",
        fitOptions,
        volume: currentStyle?.volume ?? 100,
        speed: "1",
        fromTime: 0,
        toTime: 0,
        softLoop: 0,
        videoType: "",
        cropping: {}
    }

    if (!mediaObj && !currentStyle) return mediaStyle

    Object.keys(mediaStyle).forEach((key) => {
        if (!mediaObj?.[key]) return
        mediaStyle[key] = mediaObj[key]
    })

    return mediaStyle
}

export function getMediaLayerType(path: string, style: MediaStyle | null): "" | "background" | "foreground" {
    if (!path) return ""
    if (style?.videoType) return style.videoType as "background" | "foreground"

    // get multiple matching folder paths if children are added
    let allMatchingFolderPaths: string[] = []
    const mediaFolderPaths = Object.values(get(mediaFolders)).map((a) => a.path || "")
    mediaFolderPaths.forEach((folderPath) => {
        if (path.startsWith(folderPath)) allMatchingFolderPaths.push(folderPath)
    })
    if (!allMatchingFolderPaths.length) return ""

    // get longest matching folder path (the closest to the actual file)
    let matchedFolderPath = ""
    allMatchingFolderPaths.forEach((folderPath) => {
        if (folderPath.length > matchedFolderPath.length) matchedFolderPath = folderPath
    })
    if (!matchedFolderPath) return ""

    const folderData = Object.values(get(mediaFolders)).find((a) => a.path === matchedFolderPath)
    if (!folderData) return ""

    return folderData.mediaType || ""
}

export const mediaSize = {
    big: 900, // stage & editor
    slideSize: 500, // slide + remote
    drawerSize: 250, // drawer media
    small: 100 // show tools
}

export async function loadThumbnail(input: string, size: number = mediaSize.drawerSize) {
    if (typeof input !== "string") return ""
    if (!isLocalFile(input)) return input

    // already encoded (this could cause an infinite loop)
    if (input.includes("freeshow-cache") || input.includes("media-cache")) return input

    const loadedPath = get(loadedMediaThumbnails)[getThumbnailId({ input, size })]
    if (loadedPath !== undefined) return loadedPath

    const data = await requestMain(Main.GET_THUMBNAIL, { input, size })
    if (!data) return ""

    thumbnailLoaded(data)
    return data.output
}

export function getThumbnailPath(input: string, size: number) {
    if (!input) return ""
    if (!isLocalFile(input)) return input

    // already encoded
    if (input.includes("freeshow-cache") || input.includes("media-cache")) return input

    const loadedPath = get(loadedMediaThumbnails)[getThumbnailId({ input, size })]
    if (loadedPath !== undefined) return loadedPath

    const encodedPath: string = joinPath([get(cachePath), getThumbnailFileName(hashCode(input))])
    return encodedPath

    function getThumbnailFileName(path: string) {
        return `${path}-${size}.png`
    }
}

// same as electron/thumbnails.ts
function hashCode(str: string) {
    if (!str) return ""
    let hash = 0

    for (let i = 0; i < str.length; i++) {
        const chr = str.charCodeAt(i)
        hash = (hash << 5) - hash + chr // bit shift
        hash |= 0 // convert to 32bit integer
    }

    if (hash < 0) return "i" + hash.toString().slice(1)
    return "a" + hash.toString()
}

export function thumbnailLoaded(data: { input: string; output: string; size: number }) {
    loadedMediaThumbnails.update((a) => {
        a[getThumbnailId(data)] = data.output
        return a
    })
}

function getThumbnailId(data: { input: string; size: number }) {
    return `${data.input}-${data.size}`
}

// convert path to base64
export async function getBase64Path(path: string, size: number = mediaSize.big) {
    if (typeof path !== "string" || !mediaExtensions.includes(getExtension(path))) return ""
    if (!isLocalFile(path)) return path

    const thumbnailPath = await loadThumbnail(path, size)
    if (!thumbnailPath) return ""

    // wait if thumbnail is not generated yet
    await checkThatMediaExists(thumbnailPath)

    const base64Path = await toDataURL(thumbnailPath)

    // "data:image/png;base64," +
    return base64Path || thumbnailPath
}

// check multiple times as thumbnail should be created if it does not exist
export async function checkThatMediaExists(path: string, iteration = 1): Promise<boolean> {
    if (iteration > 8) return false

    const exists = await doesMediaExist(path, true)
    if (!exists) {
        await wait(500 * iteration)
        return checkThatMediaExists(path, iteration + 1)
    }

    return exists
}

const cachedDurations: { [key: string]: number } = {}
export function getVideoDuration(path: string): Promise<number> {
    return new Promise((resolve) => {
        if (cachedDurations[path]) {
            resolve(cachedDurations[path])
            return
        }

        const video = document.createElement("video")
        video.src = encodeFilePath(path)
        video.preload = "metadata"

        let loaded = false
        video.onloadedmetadata = () => {
            const duration = video.duration
            loaded = true

            // clean up references
            video.src = ""

            cachedDurations[path] = duration
            resolve(duration)
        }

        video.onerror = () => {
            if (loaded) return

            console.error(new Error("Failed to load video. Check the path or file format."))
            resolve(0)
        }
    })
}

// Custom Icons (Actions)

export async function convertImagePathToIcon(path: string, size = 64): Promise<string> {
    if (!path) return ""

    return new Promise((resolve) => {
        const img = new Image()
        img.onload = () => {
            const canvas = document.createElement("canvas")
            canvas.width = size
            canvas.height = size

            const context = canvas.getContext("2d")
            if (!context) return resolve(path)

            const ratio = Math.min(size / img.width, size / img.height)
            const drawWidth = img.width * ratio
            const drawHeight = img.height * ratio
            const x = (size - drawWidth) / 2
            const y = (size - drawHeight) / 2

            context.clearRect(0, 0, size, size)
            context.drawImage(img, x, y, drawWidth, drawHeight)
            resolve(canvas.toDataURL("image/png"))
        }

        img.onerror = () => resolve(path)
        img.src = encodeFilePath(path)
    })
}

// CACHE

// const jpegQuality = 90 // 0-100
const capturing = new Set<string>()
const retries: { [key: string]: number } = {}
export function captureCanvas(data: { input: string; output: string; size: any; extension: string; config: any; id: string; seek?: number }) {
    let completed = false
    if (capturing.has(data.output)) return exit()
    capturing.add(data.output)

    const canvas = document.createElement("canvas")

    const isImage = imageExtensions.includes(data.extension)
    const mediaElem = document.createElement(isImage ? "img" : "video")

    // Only request CORS for non-local sources.
    if (!isLocalFile(data.input)) (mediaElem as any).crossOrigin = "anonymous"

    mediaElem.addEventListener(isImage ? "load" : "loadeddata", async () => {
        const currentMediaSize = isImage ? { width: (mediaElem as HTMLImageElement).naturalWidth, height: (mediaElem as HTMLImageElement).naturalHeight } : { width: (mediaElem as HTMLVideoElement).videoWidth, height: (mediaElem as HTMLVideoElement).videoHeight }
        const newSize = getNewSize(currentMediaSize, data.size || {})
        canvas.width = newSize.width
        canvas.height = newSize.height

        // seek video
        if (!isImage) {
            const seekTime = (mediaElem as HTMLVideoElement).duration * (data.seek ?? 0.5)
            ;(mediaElem as HTMLVideoElement).currentTime = isFinite(seekTime) ? seekTime : 3

            await new Promise((resolve) => {
                const handler = () => {
                    mediaElem.removeEventListener("seeked", handler)
                    resolve(null)
                }
                mediaElem.addEventListener("seeked", handler)
            })

            await new Promise(requestAnimationFrame)
            await new Promise(requestAnimationFrame)
        }

        // wait until loaded
        const hasLoaded = await waitUntilValueIsDefined(() => (isImage ? (mediaElem as HTMLImageElement).complete : (mediaElem as HTMLVideoElement).readyState === 4), 20)
        if (!hasLoaded) return exit()

        captureCanvasData(currentMediaSize)
    })

    // this should not get called becaues the file is checked existing, but here in case
    mediaElem.addEventListener("error", (err) => {
        if (!mediaElem.src || completed) return

        console.error("Could not load media:", data.input, err)
        if (!retries[data.input]) retries[data.input] = 0
        retries[data.input]++

        if (retries[data.input] > 2) return exit()
        else setTimeout(() => (isImage ? "" : (mediaElem as HTMLVideoElement).load()), 3000)
    })

    mediaElem.src = encodeFilePath(data.input)
    // document.body.appendChild(mediaElem) // DEBUG

    async function captureCanvasData(currentMediaSize) {
        const ctx = canvas.getContext("2d")
        if (!ctx || completed) return exit()

        // ensure lessons are downloaded and loaded before capturing
        const isLessons = data.input.includes("Lessons")
        const loading = isLessons ? 3000 : 0
        await wait(loading)
        ctx.drawImage(mediaElem, 0, 0, currentMediaSize.width, currentMediaSize.height, 0, 0, canvas.width, canvas.height)
        await new Promise(requestAnimationFrame)

        try {
            canvas.toBlob((blob) => {
                if (!blob) return exit()
                blob.arrayBuffer()
                    .then((buffer) => {
                        sendMain(Main.SAVE_IMAGE, { id: data.id, path: data.output, buffer })
                        completed = true

                        // unload
                        capturing.delete(data.output)
                        mediaElem.src = ""
                    })
                    .catch(() => exit())
            })
        } catch (error) {
            // Source is likely cross-origin without CORS if failing
            console.warn("Could not export thumbnail canvas.:", data.input, error)
            exit()
        }
    }

    function exit() {
        if (completed) return

        completed = true
        sendMain(Main.SAVE_IMAGE, { id: data.id })
        capturing.delete(data.output)

        // unload
        mediaElem.onload = null
        mediaElem.onerror = null
        mediaElem.src = ""
    }
}

function getNewSize(contentSize: { width: number; height: number }, newSize: { width?: number; height?: number }) {
    if (!contentSize.width) contentSize.width = 1920
    if (!contentSize.height) contentSize.height = 1080

    const ratio = contentSize.width / contentSize.height

    let width = newSize.width
    let height = newSize.height
    if (!width) width = height ? Math.floor(height * ratio) : contentSize.width
    if (!height) height = newSize.width ? Math.floor(width / ratio) : contentSize.height

    return { width, height }
}

// CROP

export function cropImageToBase64(imagePath: string, crop: Partial<Cropping> | undefined, percentageScaling: boolean = false): Promise<string> {
    return new Promise((resolve) => {
        if (!crop) return resolve("")
        if (!crop.bottom && !crop.left && !crop.right && !crop.top) return resolve("")

        const img = new Image()

        // set crossOrigin to prevent canvas tainting
        img.crossOrigin = "anonymous"

        // needed if loading from local path
        img.src = encodeFilePath(imagePath)
        img.onload = () => {
            const iw = img.naturalWidth
            const ih = img.naturalHeight

            const left = (crop.left || 0) * (percentageScaling ? iw * 0.01 : 1)
            const right = (crop.right || 0) * (percentageScaling ? iw * 0.01 : 1)
            const top = (crop.top || 0) * (percentageScaling ? ih * 0.01 : 1)
            const bottom = (crop.bottom || 0) * (percentageScaling ? ih * 0.01 : 1)

            const sx = left
            const sy = top
            const sWidth = iw - left - right
            const sHeight = ih - top - bottom

            if (sWidth <= 0 || sHeight <= 0) return resolve("")

            const canvas = document.createElement("canvas")
            const ctx = canvas.getContext("2d")!

            if (percentageScaling) {
                canvas.width = iw
                canvas.height = ih

                const scaleX = canvas.width / sWidth
                const scaleY = canvas.height / sHeight

                ctx.save()

                // Office transform
                ctx.scale(scaleX, scaleY)
                ctx.translate(-left, -top)

                ctx.drawImage(img, 0, 0)

                ctx.restore()
            } else {
                canvas.width = sWidth
                canvas.height = sHeight

                ctx.drawImage(img, sx, sy, sWidth, sHeight, 0, 0, canvas.width, canvas.height)
            }

            const base64 = canvas.toDataURL("image/png")
            resolve(base64)
        }
        img.onerror = () => resolve("")
    })
}

export async function downloadOnlineMedia(url: string) {
    if (!url?.startsWith("http")) return url

    const mediaData = get(media)[url]
    const downloadedPath = await requestMain(Main.MEDIA_IS_DOWNLOADED, { url, contentFile: mediaData?.contentFile })

    if (downloadedPath?.isDownloading) return url

    const providerId = mediaData?.contentFile?.providerId
    const mediaId = mediaData?.contentFile?.mediaId
    const needsLicense = !!(providerId && mediaId)

    if (downloadedPath?.protectedUrl) {
        if (!needsLicense || isLicenseValid(mediaData) || !navigator.onLine) return downloadedPath.protectedUrl
        const refreshed = await refreshMediaLicense(url, providerId, mediaId)
        if (refreshed) return downloadedPath.protectedUrl
        return url
    }
    if (downloadedPath?.path) return downloadedPath.path

    if (needsLicense && !isLicenseValid(mediaData) && navigator.onLine) await refreshMediaLicense(url, providerId, mediaId)

    const updatedMediaData = get(media)[url]
    sendMain(Main.MEDIA_DOWNLOAD, { url, contentFile: updatedMediaData?.contentFile })
    return url
}

function isLicenseValid(mediaData: MediaStyle | undefined): boolean {
    const exp = mediaData?.licenseExpiresAt
    return typeof exp === "number" && exp > Date.now()
}

// share one IPC call instead of racing.
const licenseRefreshInFlight = new Map<string, Promise<boolean>>()

function refreshMediaLicense(url: string, providerId: ContentProviderId, mediaId: string): Promise<boolean> {
    const existing = licenseRefreshInFlight.get(url)
    if (existing) return existing

    const promise = (async () => {
        const license = await requestMain(Main.CHECK_MEDIA_LICENSE, { providerId, mediaId })
        if (!license) {
            if (get(media)[url]?.licenseExpiresAt !== undefined) {
                media.update((m) => {
                    if (m[url]) delete m[url].licenseExpiresAt
                    return m
                })
            }
            return false
        }

        media.update((m) => {
            if (!m[url]) m[url] = {}
            m[url].licenseExpiresAt = license.expiresAt
            if (!m[url].contentFile) m[url].contentFile = {}
            m[url].contentFile.pingbackUrl = license.pingbackUrl
            return m
        })
        return true
    })().finally(() => {
        licenseRefreshInFlight.delete(url)
    })

    licenseRefreshInFlight.set(url, promise)
    return promise
}

export async function getMediaFileFromClipboard(e: ClipboardEvent): Promise<string | null> {
    const items = e.clipboardData?.items
    if (!items) return null

    return new Promise((resolve) => {
        for (const item of items) {
            if (!item.type.startsWith("image/")) continue

            const file = item.getAsFile()
            if (!file) return resolve(null)

            const reader = new FileReader()
            reader.onload = async (event) => {
                // base64 image data
                const dataUrl = event.target?.result as string
                const compressed = await compressImage(dataUrl)
                resolve(compressed)
            }
            reader.readAsDataURL(file)
        }
    })
}

function compressImage(dataUrl: string, maxWidth = 1920, maxHeight = 1080, quality = 0.8): Promise<string> {
    return new Promise((resolve) => {
        const img = new Image()

        // set crossOrigin to prevent canvas tainting
        img.crossOrigin = "anonymous"

        img.onload = () => {
            let { width, height } = img
            if (width > maxWidth || height > maxHeight) {
                const ratio = Math.min(maxWidth / width, maxHeight / height)
                width = Math.round(width * ratio)
                height = Math.round(height * ratio)
            }

            const canvas = document.createElement("canvas")
            canvas.width = width
            canvas.height = height

            const ctx = canvas.getContext("2d")
            ctx?.drawImage(img, 0, 0, width, height)

            // use png if image has transparency
            const imageData = ctx?.getImageData(0, 0, width, height)
            const hasTransparency = imageData?.data.some((_, i) => i % 4 === 3 && imageData.data[i] < 255)

            if (hasTransparency) resolve(canvas.toDataURL("image/png"))
            else resolve(canvas.toDataURL("image/jpeg", quality))
        }

        img.src = dataUrl
    })
}

export function countFolderMediaItems(folderPath: string, folderContents: FileFolder[]) {
    const folderFiles = (folderContents.find((a) => a.path === folderPath) as any)?.files || []
    let count = { folder: 0, audio: 0, video: 0, image: 0 }

    folderFiles.forEach((filePath: string) => {
        if (filePath === folderPath) return

        const file = folderContents.find((a) => a.path === filePath)
        if (file?.isFolder) {
            if (file.files?.length) count.folder++
            return
        }

        if (videoExtensions.includes(getExtension(filePath))) count.video++
        else if (imageExtensions.includes(getExtension(filePath))) count.image++
        else if (audioExtensions.includes(getExtension(filePath))) count.audio++
    })

    return count
}
