
class Media {
    #date = 0;
    md5sum = "";
    fixedSum = "";
    path = "";
    fileName = "";
    meta = {};
    fixedTags = [];
    version = 0;
    tags = [];
    writeAccess = false;
    thumbnail = "";
    original = "";
    ui = null;

    constructor(data) {
        this.#date = data.date;
        this.md5sum = data.md5sum;
        this.fixedSum = data.fixedSum;
        this.path = data.path;
        this.fileName = data.fileName;
        this.meta = data.meta || {};
        this.fixedTags = [];
        this.version = data.version;

        this.tags = [];
        this.writeAccess = data.writeAccess !== undefined ? data.writeAccess : (data.accessType === 2);
        this.thumbnail = `/api/media/thumbnail/${data.fixedSum}.jpg`;
        this.original = `/api/media/original/${data.fixedSum}`;
        this.ui = null;

        this.setTags(data.fixedTags || [], data.tags || []);

        for (let i in this.meta) {
            if (this.meta[i].type === 'date')
                this.meta[i].value = new Date(parseInt(this.meta[i].value));
            else if (this.meta[i].type === 'number' || this.meta[i].type === 'octet')
                this.meta[i].value = parseInt(this.meta[i].value);
            else if (this.meta[i].type === 'string')
                this.meta[i].value = '' + this.meta[i].value;
        }
    }

    getDateTs() {
        return this.#date;
    }

    getDate() {
        return new Date(this.#date);
    }

    resize(maxWidth, maxHeight) {
        let ratio = Math.min(1, Math.max(
            maxWidth / (this.meta?.width?.value || maxWidth),
            maxHeight / (this.meta?.height?.value || maxHeight)));
        let result = {
            width: Math.floor(this.meta.width?.value *ratio),
            height: Math.floor(this.meta.height?.value *ratio),
        };
        if (isNaN(result.width) || isNaN(result.height) || !result.height || !result.width) {
            console.error("Failed to resize image ", this);
            return null;
        }
        return result;
    }

    setTags(fixedTags, tags) {
        this.tags = tags.reduce((acc, tag) => { acc.add(tag.replaceAll(/\/\/+/gi, '/')); return acc; }, new Set());
        this.fixedTags = fixedTags.reduce((acc, tag) => { acc.add(tag.replaceAll(/\/\/+/gi, '/')); return acc; }, new Set());
    }

    allTags() {
        return Array.from(new Set([...this.fixedTags, ...this.tags])).sort();
    }
}

function tryLoadMedia(md5sum) {
    return new Promise((ok, ko) => {
        $.get("/api/media/" +md5sum, data => {
            let item = new Media(data);
            MediaStorage.Instance.pushAll([item], true);
            ok(item);
        }).fail(err => {
            console.error("Trying to get media with md5sum " +md5sum +" failed:", err.responseText);
            ok(null);
        });
    });
}

class MediaStorage extends EventTarget
{
    allMeta = {};
    allMetaTypes = {};
    allTags = new Set();
    medias = [];
    oldest = null;
    newest = null;
    #dbVersion = 0;
    loadingVersion = 0;

    constructor() {
        super();
        this.#reset();
    }

    #reset() {
        this.allMeta = {};
        this.allMetaTypes = {};
        this.allTags = new Set();
        this.medias = [];
        this.oldest = null;
        this.newest = null;
        this.#dbVersion = 0;
        this.loadingVersion = 0;
    }

    async rebuildMetaList() {
        this.#reset();
        this.dispatchEvent(new CustomEvent("rebuildMedia"));
        window.chronology.reset();
        this.downloadMetaList();
    }

    update() {
        this.downloadMetaList(true);
    }

    async restoreMetaList() {
        if (this.isLoading())
            return;
        this.#isLoading = true;
        document.getElementById("pch-infiniteScrollLoading").classList.remove("hidden");
        let hasData = false;
        try {
            await LoadingTasks.push(async () => {
                let data = await window.indexedData.listMedias();
                this.pushAll(data.medias.map(x => new Media(x)));
                this.#updateDbVersion(data.version);
                this.#isLoading = false;
                hasData = !!(data.medias?.length);
            });
        } catch (err) {
            console.error(err);
        }
        this.downloadMetaList(hasData);
    }

    #doDownloadMetaList(isUpdate) {
        this.#isLoading = true;
        document.getElementById("pch-infiniteScrollLoading").classList.remove("hidden");
        LoadingTasks.push(() => {
            return new Promise(ok => {
                let chronology = window.chronology.isInitialized() ? "" : "&chronology"
                let oldest = isUpdate !== true ? (this.oldest?.getDateTs() || 0) : 0;
                let oldestArg = oldest ? `&from=${oldest}` : "";
                let requestCount = 300;
                $.get(`/api/media/list?count=${requestCount}${chronology}${oldestArg}&version=${this.#dbVersion}`, data => {
                    this.pushAll(data.data.map(i => new Media(i)));
                    if (data.first || data.last)
                        window.chronology.rebuildRange(data.first, data.last);
                    if (data.maxVersion)
                        this.loadingVersion = parseInt(data.maxVersion);
                    if ((data.data?.length || 0) < requestCount) {
                        this.#isLoading = false;
                        this.#updateDbVersion(this.loadingVersion);
                        window.ReloadFilters(MediaStorage.Instance);
                        this.dispatchEvent(new CustomEvent("doneLoading"));
                        document.getElementById("pch-infiniteScrollLoading").classList.add("hidden");
                    } else {
                        this.#doDownloadMetaList(false);
                    }
                    ok();
                });
            });
        });
    }

    downloadMetaList(isUpdate) {
        if (this.isLoading())
            return;
        this.#doDownloadMetaList(isUpdate);
    }

    getDbVersion() { return this.#dbVersion; }

    #updateDbVersion(version) {
        this.#dbVersion = Math.max(this.#dbVersion, version);
    }

    #pushMeta(metaKey, metaVal) {
        if (metaKey === 'dateTime')
            return;
        if (!this.allMeta[metaKey])
            this.allMeta[metaKey] = new Set();
        this.allMeta[metaKey].add(metaVal.value);
        if (!this.allMetaTypes[metaKey])
            this.allMetaTypes[metaKey] = { type: metaVal.type, canBeEmpty: !!this.medias.length, canWrite: metaVal.canWrite };
    }

    #pushTag(tag, first) {
        while (tag.length && tag.endsWith('/'))
            tag = tag.substr(0, tag.length -1);
        this.allTags.add(tag);
        let index = tag.lastIndexOf('/');
        if (index >= 0)
            this.#pushTag(tag.substr(0, index));
    }

    #pushUnique(media) {
        for (let i of media.tags)
            this.#pushTag(i, true);
        for (let i of media.fixedTags)
            this.#pushTag(i, true);
        for (let key in media.meta)
            this.#pushMeta(key, media.meta[key]);
        for (let key in this.allMetaTypes)
            if (!media.meta[key])
                this.allMetaTypes[key].canBeEmpty = true;
        this.medias.push(media);
    }

    #isLoading = false;
    isLoading() { return this.#isLoading; }

    pushAll(arr, partialLoad) {
        let reorder = false;
        let newItems = [];

        for (let i of arr) {
            this.loadingVersion = Math.max(this.loadingVersion, i.version);
            if (partialLoad !== true) {
                this.oldest = !this.oldest || this.oldest.getDateTs() > i.getDateTs() ? i : this.oldest;
                this.newest = !this.newest || this.newest.getDateTs() < i.getDateTs() ? i : this.newest;
            }
            if (this.medias.length && this.medias[this.medias.length -1].getDateTs() < i.getDateTs())
                reorder = true;
            let previous = this.medias.find(x => x.fixedSum === i.fixedSum);
            if (previous) {
                this.medias = this.medias.filter(x => x.fixedSum !== i.fixedSum);
                i.ui = previous.ui;
            } else {
                newItems.push(i);
            }
            this.#pushUnique(i);
        }
        for (let i of newItems)
            this.dispatchEvent(new CustomEvent("newMedia", { detail: i }));
        if (reorder) {
            this.medias.sort((a, b) => b.getDateTs() - a.getDateTs());
            this.dispatchEvent(new CustomEvent("rebuildMedia"));
        }
    }

    onFilterUpdated() {
        this.dispatchEvent(new CustomEvent("rebuildMedia"));
    }

    getMediaIndex(media) {
        return this.medias.indexOf(media);
    }

    nextMedia(current) {
        return this.medias[this.getMediaIndex(current) +1];
    }

    previousMedia(current) {
        return this.medias[this.getMediaIndex(current) -1];
    }

    getMediaBetweenIndexes(a, b) {
        if (a > b)
            return this.getMediaBetweenIndexes(b, a);
        return this.medias.slice(a, b +1);
    }

    getMediaBetween(a, b) {
        let aIndex = this.medias.indexOf(a);
        let bIndex = this.medias.indexOf(b);
        if (aIndex < 0 || bIndex < 0 || aIndex === bIndex)
            return [];
        return this.getMediaBetweenIndexes(aIndex, bIndex);
    }

    getMediaLocal(md5sum) {
        return this.medias.find(x => x.fixedSum === md5sum);
    }

    async getMedia(md5sum) {
        let media = this.medias.find(x => x.fixedSum === md5sum);
        if (media)
            return media;
        return await tryLoadMedia(md5sum);
    }

    setMetaValue(md5sum, key, value) {
        let md5arr = undefined;
        if (Array.isArray(md5sum)) {
            md5arr = md5sum;
            md5sum = "list";
        }
        return LoadingTasks.push(() => {
            return new Promise(ok => {
                let mediaCount = (md5arr || [ md5sum ]).map(checksum => this.medias.find(x => x.fixedSum === checksum)).filter(x => x.writeAccess).length;
                if (mediaCount != (md5arr || [ md5sum ]).length)
                    return ok(false);
                $.ajax({
                    url: `/api/media/${encodeURIComponent(md5sum)}/meta/${encodeURIComponent(key)}`,
                    type: "PATCH",
                    data: { value: value, list: md5arr },
                    success: allData => {
                        allData.forEach(data => {
                            let media = this.medias.find(x => x.fixedSum === data.fixedSum);
                            let meta = data.meta[key] || { type: 'string', value: value, canWrite: true };
                            meta.value = value;
                            this.#pushMeta(key, meta);
                            media.meta[key] = meta;
                        });
                        window.ReloadFilters(this);
                        ok(true);
                    },
                    error: err => ok(false),
                });
            });
        });
    }

    removeTag(md5sum, tagName) {
        let md5arr = undefined;
        if (Array.isArray(md5sum)) {
            md5arr = md5sum;
            md5sum = "list";
        }
        return LoadingTasks.push(() => {
            return new Promise(ok => {
                let mediaCount = (md5arr || [ md5sum ]).map(checksum => this.medias.find(x => x.fixedSum === checksum)).filter(x => x.writeAccess).length;
                if (mediaCount != (md5arr || [ md5sum ]).length)
                    return ok(false);
                $.ajax({
                    url: `/api/media/${encodeURIComponent(md5sum)}/tag/del/${encodeURIComponent(tagName)}`,
                    type: "POST",
                    data: { list: md5arr || [0] },
                    success: allData => {
                        allData.forEach(data => {
                            let media = this.medias.find(x => x.fixedSum === data.fixedSum);
                            media.setTags(data.fixedTags, data.tags);
                            for (let i of data.tags)
                                this.#pushTag(i, true);
                            for (let i of data.fixedTags)
                                this.#pushTag(i, true);
                        });
                        ok(true);
                    },
                    error: err => ok(false),
                });
            });
        });
    }

    addTag(md5sum, tagName) {
        let md5arr = undefined;
        if (Array.isArray(md5sum)) {
            md5arr = md5sum;
            md5sum = "list";
        }
        return LoadingTasks.push(() => {
            return new Promise(ok => {
                let mediaCount = (md5arr || [ md5sum ]).map(checksum => this.medias.find(x => x.fixedSum === checksum)).filter(x => x.writeAccess).length;
                if (mediaCount != (md5arr || [ md5sum ]).length)
                    return ok(false);
                $.ajax({
                    url: `/api/media/${encodeURIComponent(md5sum)}/tag`,
                    type: "PUT",
                    data: { tag: tagName, list: md5arr },
                    success: allData => {
                        allData.forEach(data => {
                            let media = this.medias.find(x => x.fixedSum === data.fixedSum);
                            media.setTags(data.fixedTags, data.tags);
                            for (let i of data.tags)
                                this.#pushTag(i, true);
                            for (let i of data.fixedTags)
                                this.#pushTag(i, true);
                        });
                        ok(true);
                    },
                    error: err => ok(false),
                });
            });
        });
    }
}

MediaStorage.Instance = new MediaStorage();
setInterval(MediaStorage.Instance.update.bind(MediaStorage.Instance), 60000);

