import { ImageUploader } from "./ImageUploader"
import { AuthorizedPanoramax, ImageData, Panoramax, PanoramaxXYZ } from "panoramax-js/dist"
import ExifReader from "exifreader"
import ImageProvider, { PanoramaView, ProvidedImage } from "./ImageProvider"
import BaseUIElement from "../../UI/BaseUIElement"
import { LicenseInfo } from "./LicenseInfo"
import { GeoOperations } from "../GeoOperations"
import Constants from "../../Models/Constants"
import { Store, Stores, UIEventSource } from "../UIEventSource"
import SvelteUIElement from "../../UI/Base/SvelteUIElement"
import Panoramax_bw from "../../assets/svg/Panoramax_bw.svelte"
import Link from "../../UI/Base/Link"
import { Feature, Point } from "geojson"
import { AddImageOptions } from "panoramax-js/dist/Panoramax"

export default class PanoramaxImageProvider extends ImageProvider {
    public static readonly singleton: PanoramaxImageProvider = new PanoramaxImageProvider()
    private static readonly xyz = new PanoramaxXYZ()
    private static defaultPanoramax = new AuthorizedPanoramax(
        Constants.panoramax.url,
        Constants.panoramax.token,
        3000
    )

    /**
     *
     * const url = "https://panoramax.mapcomplete.org/api/pictures/e931ce57-4591-4dd5-aa4c-595e89c37e84/hd.jpg"
     * const match = url.match(PanoramaxImageProvider.isDirectLink)
     * match[1] // => "e931ce57-4591-4dd5-aa4c-595e89c37e84"
     */
    public static readonly isDirectLink =
        /https:\/\/panoramax.mapcomplete.org\/api\/pictures\/([0-9a-f-]+)\/(hd)|(sd)|(thumb).jpg/

    public defaultKeyPrefixes: string[] = ["panoramax", "image"]
    public readonly name: string = "panoramax"

    private static knownMeta: Record<
        string,
        { data: Promise<{ data: ImageData; url: string }>; time: Date }
    > = {}

    public SourceIcon(
        img?: { id: string; url: string },
        location?: {
            lon: number
            lat: number
        }
    ): BaseUIElement {
        const host = "https://" + new URL(img.url).host
        const p = new Panoramax(host)
        return new Link(
            new SvelteUIElement(Panoramax_bw),
            p.createViewLink({
                imageId: img?.id,
                location,
            }),
            true
        )
    }

    visitUrl(
        img: { id: string; url: string },
        location?: { lon: number; lat: number }
    ): string | undefined {
        const host = "https://" + new URL(img.url).host
        const p = new Panoramax(host)
        return p.createViewLink({
            imageId: img?.id,
            location,
        })
    }

    public addKnownMeta(meta: ImageData, url?: string) {
        PanoramaxImageProvider.knownMeta[meta.id] = {
            data: Promise.resolve({ data: meta, url }),
            time: new Date(),
        }
    }

    /**
     * Tries to get the entry from the mapcomplete-panoramax instance. Might return undefined
     * @param id
     * @private
     */
    private async getInfoFromMapComplete(id: string): Promise<{ data: ImageData; url: string }> {
        const url = `https://panoramax.mapcomplete.org/`
        const data = await PanoramaxImageProvider.defaultPanoramax.imageInfo(id)
        return { url, data }
    }

    private async getInfoFromXYZ(imageId: string): Promise<{ data: ImageData; url: string }> {
        const data = await PanoramaxImageProvider.xyz.imageInfo(imageId)
        return { data, url: "https://api.panoramax.xyz/" }
    }

    /**
     * Reads a geovisio-somewhat-looking-like-geojson object and converts it to a provided image
     * @private
     */
    private featureToImage(info: { data: ImageData; url: string }) {
        const meta = info?.data
        if (!meta) {
            return undefined
        }

        const url = info.url

        function makeAbsolute(s: string) {
            if (!s.startsWith("https://") && !s.startsWith("http://")) {
                const parsed = new URL(url)
                return parsed.protocol + "//" + parsed.host + s
            }
            return s
        }

        const [lon, lat] = GeoOperations.centerpointCoordinates(meta)
        return <ProvidedImage>{
            id: meta.id,
            url: makeAbsolute(meta.assets.sd.href),
            url_hd: makeAbsolute(meta.assets.hd.href),
            host: meta["links"].find((l) => l.rel === "root")?.href,
            lon,
            lat,
            key: "panoramax",
            provider: this,
            status: meta.properties["geovisio:status"],
            rotation: Number(meta.properties["view:azimuth"]),
            isSpherical: meta.properties.exif["Xmp.GPano.ProjectionType"] === "equirectangular",
            date: new Date(meta.properties.datetime),
        }
    }

    private async getInfoForUncached(id: string) {
        try {
            return await this.getInfoFromMapComplete(id)
        } catch (e) {
            console.debug(e)
        }
        try {
            return await this.getInfoFromXYZ(id)
        } catch (e) {
            console.debug(e)
        }
        return undefined
    }

    private async getInfoFor(id: string): Promise<{ data: ImageData; url: string }> {
        if (!id.match(/^[a-zA-Z0-9-]+$/)) {
            return undefined
        }
        const cached = PanoramaxImageProvider.knownMeta[id]
        if (cached) {
            if (new Date().getTime() - cached.time.getTime() < 5000) {
                return await cached.data
            }
        }
        const promise: Promise<{ data: ImageData; url: string }> = this.getInfoForUncached(id)
        PanoramaxImageProvider.knownMeta[id] = {
            time: new Date(),
            data: promise,
        }
        return await promise
    }

    public async ExtractUrls(key: string, value: string): Promise<ProvidedImage[]> {
        const match = value.match(PanoramaxImageProvider.isDirectLink)
        let alt_id = undefined
        if (match) {
            alt_id = value
            value = match[1] // The ID
        }
        if (!Panoramax.isId(value)) {
            return undefined
        }
        const providedImage = await this.getInfo(value)
        providedImage.alt_id = alt_id
        providedImage.originalAttribute = {key, value}
        return [providedImage]
    }

    public async getInfo(hash: string): Promise<ProvidedImage> {
        const r: { data: ImageData; url: string } = await this.getInfoFor(hash)
        return this.featureToImage(r)
    }

    getRelevantUrls(tags: Record<string, string>, prefixes: string[]): Store<ProvidedImage[]> {
        const source = UIEventSource.FromPromise(super.getRelevantUrlsFor(tags, prefixes))
        function hasLoading(data: ProvidedImage[]) {
            if (data === undefined) {
                return true
            }
            return data?.some(
                (img) =>
                    img?.status !== undefined &&
                    img?.status !== "ready" &&
                    img?.status !== "broken" &&
                    img?.status !== "hidden"
            )
        }

        Stores.Chronic(5000, () => hasLoading(source.data)).addCallback(() => {
            super.getRelevantUrlsFor(tags, prefixes).then((data) => {
                source.set(data)
                return !hasLoading(data)
            })
        })

        return Stores.ListStabilized(source)
    }

    public async DownloadAttribution(providedImage: {
        id: string
    }): Promise<LicenseInfo> {
        const meta = await this.getInfoFor(providedImage.id)

        return {
            artist: meta.data.providers.at(-1).name, // We take the last provider, as that one probably contain the username of the uploader
            date: new Date(meta.data.properties["datetime"]),
            licenseShortName: meta.data.properties["geovisio:license"],
        }
    }

    public apiUrls(): string[] {
        return ["https://panoramax.mapcomplete.org", "https://panoramax.xyz"]
    }

    public static getPanoramaxInstance(host: string) {
        host = new URL(host).host
        if (new URL(this.defaultPanoramax.host).host === host) {
            return this.defaultPanoramax
        }
        if (new URL(this.xyz.host).host === host) {
            return this.xyz
        }
        return new Panoramax(host)
    }

    public async getPanoramaInfo(image: {
        id: string
    }): Promise<Feature<Point, PanoramaView>> | undefined {
        const imageInfo = await PanoramaxImageProvider.xyz.imageInfo(image.id)
        const url = (imageInfo.assets.sd ?? imageInfo.assets.thumb ?? imageInfo.assets.hd).href
        const northOffset = imageInfo.properties["view:azimuth"]
        const pitchOffset = Number(imageInfo.properties.exif["Xmp.GPano.PosePitchDegrees"])
        return <Feature<Point, PanoramaView>>{
            type: "Feature",
            geometry: imageInfo.geometry,
            properties: <PanoramaView>{
                url,
                northOffset,
                pitchOffset,
                provider: this,
                imageMeta: imageInfo
            },
        }
    }
}

export class PanoramaxUploader implements ImageUploader {
    public readonly panoramax: AuthorizedPanoramax
    maxFileSizeInMegabytes = 100 * 1000 * 1000 // 100MB
    private readonly _targetSequence?: Store<string>

    constructor(url: string, token: string, targetSequence?: Store<string>) {
        this._targetSequence = targetSequence
        this.panoramax = new AuthorizedPanoramax(url, token)
    }

    async uploadImage(
        blob: File,
        currentGps: [number, number],
        author: string,
        noblur: boolean = false,
        progress?: UIEventSource<number | undefined>,
        sequenceId?: string,
        datetime?: string
    ): Promise<{
        key: string
        value: string
        absoluteUrl: string
    }> {
        // https://panoramax.openstreetmap.fr/api/docs/swagger#/

        let [lon, lat] = currentGps ?? [undefined, undefined]
        datetime ??= new Date().toISOString()
        try {
            const tags = await ExifReader.load(blob)
            if (tags.ProjectionType.value === "cylindrical") {
                throw "Unsupported image format: cylindrical images (panorama images) are currently not supported"
            }
            if (tags?.GPSLatitude?.value && tags?.GPSLongitude?.value) {
                const [[latD], [latM], [latS, latSDenom]] = <
                    [[number, number], [number, number], [number, number]]
                >tags?.GPSLatitude?.value
                const [[lonD], [lonM], [lonS, lonSDenom]] = <
                    [[number, number], [number, number], [number, number]]
                >tags?.GPSLongitude?.value

                const exifLat = latD + latM / 60 + latS / (3600 * latSDenom)
                const exifLon = lonD + lonM / 60 + lonS / (3600 * lonSDenom)
                if (
                    typeof exifLat === "number" &&
                    !isNaN(exifLat) &&
                    typeof exifLon === "number" &&
                    !isNaN(exifLon) &&
                    !(exifLat === 0 && exifLon === 0)
                ) {
                    lat = exifLat
                    lon = exifLon
                    if (tags?.GPSLatitudeRef?.value?.[0] === "S") {
                        lat *= -1
                    }
                    if (tags?.GPSLongitudeRef?.value?.[0] === "W") {
                        lon *= -1
                    }
                }
            }
            const dateTime = (
                tags.DateTime.value[0] ??
                tags.DateTimeOriginal.value[0] ??
                tags.GPSDateStamp ??
                tags.CreateDate ??
                tags["Date Created"]
            )?.split(" ")
            if (dateTime) {
                const [date, time] = dateTime
                const exifDatetime = new Date(date.replaceAll(":", "-") + "T" + time)
                if (exifDatetime.getFullYear() === 1970) {
                    // The data probably got reset to the epoch
                    // we don't use the value
                    console.log(
                        "Datetime from picture is probably invalid:",
                        exifDatetime,
                        "using 'now' instead"
                    )
                } else {
                    datetime = exifDatetime.toISOString()
                }
            }
        } catch (e) {
            console.warn("Could not read EXIF-tags due to", e)
        }

        const p = this.panoramax
        sequenceId ??= this._targetSequence?.data ?? Constants.panoramax.sequence
        const sequence: { id: string; "stats:items": { count: number } } = (
            await p.mySequences()
        ).find((s) => s.id === sequenceId)
        const options: AddImageOptions = {
            lon,
            lat,
            datetime,
            isBlurred: noblur,
            onProgress: undefined,
            indexInSequence: sequence["stats:items"].count + 1, // stats:items is '1'-indexed, so .count is also the last index
            exifOverride: {
                Artist: author,
            },
        }
        if (progress) {
            options.onProgress = (e: ProgressEvent) => {
                if (e.lengthComputable) {
                    const percentage = (e.loaded / e.total) * 100
                    progress.set(Math.round(percentage))
                }
            }
            progress.set(0)
        }
        const img = <ImageData>await p.addImage(blob, sequenceId, options)
        PanoramaxImageProvider.singleton.addKnownMeta(img)
        progress.set(undefined)
        return {
            key: "panoramax",
            value: img.id,
            absoluteUrl: img.assets.hd.href,
        }
    }
}
