import { ImageUploader, UploadResult } from "./ImageUploader"
import LinkImageAction from "../Osm/Actions/LinkImageAction"
import FeaturePropertiesStore from "../FeatureSource/Actors/FeaturePropertiesStore"
import { NoteId, OsmId, OsmTags } from "../../Models/OsmFeature"
import ThemeConfig from "../../Models/ThemeConfig/ThemeConfig"
import { Store, UIEventSource } from "../UIEventSource"
import { OsmConnection } from "../Osm/OsmConnection"
import { Changes } from "../Osm/Changes"
import Translations from "../../UI/i18n/Translations"
import { Translation } from "../../UI/i18n/Translation"
import { Feature } from "geojson"
import ImageUploadQueue, { ImageUploadArguments } from "./ImageUploadQueue"
import { GeoOperations } from "../GeoOperations"
import NoteCommentElement from "../../UI/Popup/Notes/NoteCommentElement"
import OsmObjectDownloader from "../Osm/OsmObjectDownloader"
import ExifReader from "exifreader"

/**
 * The ImageUploadManager has a
 */
export class ImageUploadManager {
    private readonly _queue: ImageUploadQueue = ImageUploadQueue.singleton
    private readonly _uploader: ImageUploader
    private readonly _featureProperties: FeaturePropertiesStore
    private readonly _theme: ThemeConfig
    private readonly _gps: Store<GeolocationCoordinates | undefined>
    private readonly _osmConnection: OsmConnection
    private readonly _changes: Changes

    /**
     * Keeps track of the _features_ for which an upload was successfull. Only used to give an indication.
     * Every time an image is uploaded, the featureID is added to the list. Not persisted (and should not be)
     */
    private readonly _successfull: UIEventSource<string[]> = new UIEventSource<string[]>([])
    public readonly successfull: Store<string[]> = this._successfull
    /**
     * Keeps track of the _features_ for which an upload failed. Only used to give an indication to the user.
     * Every time an image upload fails, the featureID is added to the list. Not persisted (and should not be)
     */
    private readonly _fails: UIEventSource<ImageUploadArguments[]> = new UIEventSource<
        ImageUploadArguments[]
    >([])
    public readonly fails: Store<string[]> = this._fails.map((args) => args.map((a) => a.featureId))
    /**
     * FeatureIDs of queued items
     */
    public readonly queued: Store<string[]> = this._queue.imagesInQueue.map((queue) =>
        queue.map((q) => q.featureId)
    )
    public readonly queuedArgs = this._queue.imagesInQueue
    /**
     * The feature for which an upload is currently running
     */
    public readonly _isUploading: UIEventSource<string | undefined> = new UIEventSource(undefined)
    public readonly isUploading: Store<string | undefined> = this._isUploading
    private readonly _reportError: (
        message: string | Error | XMLHttpRequest,
        extramessage?: string
    ) => Promise<void>

    private readonly _progressCurrentImage: UIEventSource<number> = new UIEventSource(0)
    public readonly progressCurrentImage: Store<number> = this._progressCurrentImage

    constructor(
        layout: ThemeConfig,
        uploader: ImageUploader,
        featureProperties: FeaturePropertiesStore,
        osmConnection: OsmConnection,
        changes: Changes,
        gpsLocation: Store<GeolocationCoordinates | undefined>,
        reportError: (
            message: string | Error | XMLHttpRequest,
            extramessage?: string
        ) => Promise<void>
    ) {
        this._uploader = uploader
        this._featureProperties = featureProperties
        this._theme = layout
        this._osmConnection = osmConnection
        this._changes = changes
        this._gps = gpsLocation
        this._reportError = reportError
    }

    public async canBeUploaded(file: File): Promise<true | { error: Translation }> {
        const sizeInBytes = file.size
        if (sizeInBytes > this._uploader.maxFileSizeInMegabytes * 1000000) {
            const error = Translations.t.image.toBig.Subs({
                actual_size: Math.floor(sizeInBytes / 1000000) + "MB",
                max_size: this._uploader.maxFileSizeInMegabytes + "MB",
            })
            return { error }
        }
        const ext = file.name.split(".").at(-1).toLowerCase()
        if (ext !== "jpg" && ext !== "jpeg") {
            return { error: new Translation({ en: "Only JPG-files are allowed" }) }
        }

        const tags = await ExifReader.load(file)
        if (tags.ProjectionType?.value === "cylindrical") {
            return {
                error: new Translation({
                    en: "Cylindrical images (typically created by a Panorama-app) are not supported",
                }),
            }
        }

        return true
    }

    /**
     * Uploads the given image, applies the correct title and license for the known user.
     * Will then add this image to the OSM-feature or the OSM-note automatically, based on the ID of the feature.
     * Does _not_ check 'canBeUploaded'
     * Note: the image will actually be added to the queue. If the image-upload fails, this will be attempted when visiting MC again
     * @param file a jpg file to upload
     * @param tagsStore The tags of the feature
     * @param targetKey Use this key to save the attribute under. Default: 'image'
     * @param noblur if true, then the api call will indicate that the image is already blurred. The server won't apply blurring in this case
     * @param feature the feature this image is about. Will be used as fallback to get the GPS-coordinates
     */
    public uploadImageAndApply(
        file: File,
        tagsStore: UIEventSource<OsmTags>,
        targetKey: string,
        noblur: boolean,
        feature: Feature,
        options: {
            ignoreGPS: boolean | false
        }
    ): void {
        const tags: OsmTags = tagsStore.data
        const featureId = <OsmId | NoteId>tags.id

        const author = this._osmConnection?.userDetails?.data?.name ?? "Anonymous" // Might be a note upload

        /**
         * The location to upload the image with.
         * Note that EXIF-data will always be trusted _more_ by the uploader
         */
        let location: [number, number] = GeoOperations.centerpointCoordinates(feature)
        if (this._gps.data && !options?.ignoreGPS) {
            location = [this._gps.data.longitude, this._gps.data.latitude]
        }

        const args: ImageUploadArguments = {
            location,
            date: new Date().getTime(),
            layoutId: this._theme.id,
            author,
            blob: file,
            featureId,
            noblur,
            targetKey,
        }
        this._queue.add(args)
        this.uploadQueue()
    }

    /**
     * Attempts to upload all items in the queue
     */
    private uploadingAll = false

    public async uploadQueue() {
        if (this.uploadingAll) {
            return
        }
        const queue = this._queue.imagesInQueue.data ?? []
        if (queue.length === 0) {
            return
        }
        console.log("Checking image upload queue and uploading if needed")
        this.uploadingAll = true
        try {
            while (queue.length > 0) {
                const currentItem = queue[0]
                await this.handleQueueItem(currentItem)
            }
        } catch (e) {
            console.error("Error while handling the queue:", e)
            await this._reportError("Image Upload Manager: queue stopped working:", e)
        } finally {
            this.uploadingAll = false
        }
    }

    /**
     * Handles a queue item:
     * - starts upload
     * - indicates that the upload is busy
     * - Applies the action to the correct element
     * - indicates failure
     *
     * Modifies the queue: if the upload is successfull, deletes the item from the queue
     * @private
     */
    private async handleQueueItem(args: ImageUploadArguments): Promise<void> {
        console.log("Handling queue item", args.blob.name, args)
        if (!args) {
            return
        }
        this._isUploading.set(args.featureId)

        let result: UploadResult = undefined
        let attempts = 2
        while (attempts > 0 && result === undefined) {
            attempts--
            const doReport = attempts == 0
            try {
                result = await this.attemptSingleUpload(args, doReport)
            } catch (e) {
                console.error("Uploading failed with error", e)
            }
            if (!result) {
                console.log("Upload attempt failed, attempts left:", attempts)
            }
        }
        this._isUploading.set(undefined)
        this._fails.set(this._fails.data.filter((a) => a !== args))
        if (result === undefined) {
            this._fails.data.push(args)
            this._fails.ping()
            return
        }
        let properties: UIEventSource<Record<string, string>> = this._featureProperties.getStore(
            args.featureId
        )

        if (!isNaN(Number(args.featureId))) {
            // This is an OSM-note
            const url = result.absoluteUrl
            await this._osmConnection.addCommentToNote(args.featureId, url)
            const properties: UIEventSource<Record<string, string>> =
                this._featureProperties.getStore(args.featureId)
            if (properties) {
                // Properties will not be defined if the note isn't loaded, but that is no problem as the below code is only relevant if the note is shown
                NoteCommentElement.addCommentTo(url, properties, {
                    osmConnection: this._osmConnection,
                })
            }
        } else {
            if (properties === undefined) {
                const downloaded = await new OsmObjectDownloader(
                    this._osmConnection.Backend(),
                    this._changes
                ).DownloadObjectAsync(args.featureId)
                if (downloaded === "deleted") {
                    this._queue.delete(args)
                    return
                }
                this._featureProperties.trackFeature(downloaded.asGeoJson())
                properties = this._featureProperties.getStore(args.featureId)
            }
            const action = new LinkImageAction(
                args.featureId,
                result.key,
                result.value,
                properties,
                {
                    theme: args.layoutId ?? properties?.data?.["_orig_theme"] ?? this._theme.id,
                    changeType: "add-image",
                }
            )
            await this._changes.applyAction(action)
            await this._changes.flushChanges("Image upload completed")
        }

        this._queue.delete(args)
    }

    /**
     * Attempts to upload the image (once).
     * Returns 'undefined' if failed
     * @param featureId
     * @param author
     * @param blob
     * @param targetKey
     * @param noblur
     * @param lastGpsLocation
     * @param ignoreGps
     * @param layoutId
     * @param date
     * @param reportOnFail If set, reports an error to the mapcomplete server so that pietervdvn can fix it
     * @private
     */
    private async attemptSingleUpload(
        { featureId, author, blob, targetKey, noblur, location }: ImageUploadArguments,
        reportOnFail: boolean
    ): Promise<UploadResult | undefined> {
        let key: string
        let value: string
        let absoluteUrl: string

        try {
            ;({ key, value, absoluteUrl } = await this._uploader.uploadImage(
                blob,
                location,
                author,
                noblur,
                this._progressCurrentImage
            ))
        } catch (e) {
            console.error("Could again not upload image due to", e)
            if (reportOnFail) {
                await this._reportError(
                    e,
                    JSON.stringify({
                        ctx: "While uploading an image in the Image Upload Manager",
                        featureId,
                        author,
                        targetKey,
                    })
                )
            }
            return undefined
        }
        key = targetKey ?? key
        if (targetKey && targetKey.indexOf(key) < 0) {
            // This is a non-standard key, so we use the image link directly
            value = absoluteUrl
        }
        return { key, absoluteUrl, value }
    }
}
