"use strict";
/*
 Copyright (C) 2021, 2024 3NSoft Inc.
 
 This program is free software: you can redistribute it and/or modify it under
 the terms of the GNU General Public License as published by the Free Software
 Foundation, either version 3 of the License, or (at your option) any later
 version.
 
 This program is distributed in the hope that it will be useful, but
 WITHOUT ANY WARRANTY; without even the implied warranty of
 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 See the GNU General Public License for more details.
 
 You should have received a copy of the GNU General Public License along with
 this program. If not, see <http://www.gnu.org/licenses/>.
*/
class InfoStorage {
    constructor() {
        this.fs = w3n.storage.getAppSyncedFS();
    }
    async getPlatformChannel() {
        const fs = await this.fs;
        const channel = await fs.readTxtFile(`channels/platform`)
            .catch((exc) => {
            if (exc.notFound) {
                return undefined;
            }
            else {
                throw exc;
            }
        });
        return channel;
    }
    async savePlatformChannel(channel) {
        const fs = await this.fs;
        await fs.writeTxtFile(`channels/platform`, channel);
    }
    async getAppChannel(appId) {
        const fs = await this.fs;
        const channel = await fs.readTxtFile(`channels/apps/${appId}`)
            .catch((exc) => {
            if (exc.notFound) {
                return undefined;
            }
            else {
                throw exc;
            }
        });
        return channel;
    }
    async saveAppChannel(appId, channel) {
        const fs = await this.fs;
        await fs.writeTxtFile(`channels/apps/${appId}`, channel);
    }
}
function logInfo(msg, cause) {
    console.info(msg, cause);
}
function logError(msg, err) {
    console.error(msg, err);
}
class PlatformUpdater {
    constructor(storage) {
        this.storage = storage;
        this.elem = undefined;
        this.version = undefined;
    }
    setElem(newElem) {
        if (!this.elem) {
            this.elem = document.getElementById("platform-display");
        }
        this.elem.replaceWith(newElem);
        this.elem = newElem;
    }
    async setupAndRender() {
        this.version = await w3n.apps.platform.getCurrentVersion();
        this.channel = await this.storage.getPlatformChannel();
        this.render();
    }
    render() {
        if (!this.version) {
            throw new Error(`Platform info elements are not set`);
        }
        const newElem = makeFromTemplate("platform-template");
        newElem.querySelector(".platform-version")
            .textContent = `Current version ${this.version}`;
        this.addUpdatesDisplayTo(newElem);
        if (this.actionInProcess) {
            this.actionInProcess.renderIn(newElem.querySelector(".platform-progress"));
        }
        else {
            this.addActionsDisplayTo(newElem);
        }
        this.setElem(newElem);
    }
    addUpdatesDisplayTo(newElem) {
        newElem.querySelector(".platform-channel")
            .textContent = (this.channel ? `Update channel ${this.channel}` : null);
        const updatesElem = newElem.querySelector(".platform-available-updates");
        if (this.appliedVersion) {
            updatesElem.textContent = `New version ${this.appliedVersion} is ready to run after restart`;
        }
        else if (this.availableUpdates) {
            if (this.availableUpdates === 'up-to-date') {
                updatesElem.textContent = `Platform is up-to-date`;
            }
            else if (this.availableUpdates === 'none-found') {
                updatesElem.textContent = `Update information not found`;
            }
            else {
                for (const [channel, ver] of this.availableUpdates.entries()) {
                    const verElem = document.createElement('div');
                    verElem.textContent = `${channel} channel has newer version ${ver}`;
                    updatesElem.appendChild(verElem);
                }
            }
        }
    }
    addActionsDisplayTo(newElem) {
        const actions = newElem.querySelector(".platform-actions");
        if (this.appliedVersion) {
            console.error(`don't have simple restart function to add restart action`);
        }
        else if (!this.availableUpdates) {
            const checkUpdatesBtn = document.createElement('button');
            checkUpdatesBtn.textContent = `Check Platform Updates`;
            checkUpdatesBtn.onclick = ev => this.checkForUpdate();
            actions.appendChild(checkUpdatesBtn);
        }
        else if (typeof this.availableUpdates === 'object') {
            for (const [channel, ver] of this.availableUpdates.entries()) {
                const updateBtn = document.createElement('button');
                updateBtn.textContent = ((this.channel === channel) ?
                    `Update to version ${ver}` :
                    `Update to version ${ver} in ${channel} channel`);
                updateBtn.onclick = ev => this.downloadAndApplyUpdate(channel, ver);
                actions.appendChild(updateBtn);
            }
        }
    }
    async checkForUpdate() {
        if (this.actionInProcess) {
            return;
        }
        this.actionInProcess = new StaticTextProgressComponent(`Checking for Platform update ...`);
        this.render();
        try {
            const availableVersions = new Map();
            if (this.channel) {
                try {
                    const ver = await w3n.apps.platform.getLatestVersion(this.channel);
                    availableVersions.set(this.channel, ver);
                }
                catch (err) {
                    logInfo(`Can't get latest version in platform channel ${this.channel}`, err);
                }
            }
            else {
                await this.initDistributionChannelsInfo();
                const channels = (this.distChannels ?
                    Object.keys(this.distChannels.channels) : []);
                for (const channel of channels) {
                    try {
                        const ver = await w3n.apps.platform.getLatestVersion(channel);
                        availableVersions.set(channel, ver);
                    }
                    catch (err) {
                        logInfo(`Can't get latest version in platform channel ${channel}`, err);
                    }
                }
            }
            if (availableVersions.size > 0) {
                const newVersions = new Map();
                const currentSymVer = toSymVer(this.version);
                for (const [channel, ver] of availableVersions.entries()) {
                    const symVer = toSymVer(ver);
                    if (symVer && (compareVersions(symVer, currentSymVer) > 0)) {
                        newVersions.set(channel, ver);
                    }
                }
                this.availableUpdates = ((newVersions.size > 0) ?
                    newVersions : 'up-to-date');
            }
            else if (!this.availableUpdates) {
                this.availableUpdates = 'none-found';
            }
        }
        finally {
            this.actionInProcess = undefined;
            this.render();
        }
    }
    async initDistributionChannelsInfo() {
        try {
            this.distChannels = await w3n.apps.platform.getChannels();
            return true;
        }
        catch (err) {
            logInfo(`Can't fetch platform distribution channels`, err);
            return false;
        }
    }
    async downloadAndApplyUpdate(channel, version) {
        if (this.actionInProcess || this.appliedVersion) {
            return;
        }
        const actionInProcess = new PlatformDownloadAndApplyingProgress(channel);
        this.actionInProcess = actionInProcess;
        this.render();
        try {
            await (new Promise((complete, error) => w3n.apps.platform.downloadAndApplyUpdate(channel, {
                next: p => actionInProcess.update(p),
                error,
                complete
            })));
            this.appliedVersion = version;
            if (this.channel !== channel) {
                this.channel = channel;
                await this.storage.savePlatformChannel(this.channel);
            }
        }
        finally {
            this.actionInProcess = undefined;
            this.render();
        }
    }
}
class StaticTextProgressComponent {
    constructor(txt) {
        this.txt = txt;
        this.elem = undefined;
    }
    renderIn(newElem) {
        this.elem = newElem;
        this.elem.textContent = this.txt;
    }
}
function makeFromTemplate(templateId) {
    const template = document.getElementById(templateId);
    // note that content.cloneNode(true) produces document fragment
    return template.content.firstElementChild.cloneNode(true);
}
function compareVersions(v1, v2) {
    for (let i = 0; i < 3; i += 1) {
        const delta = v1[i] - v2[i];
        if (delta === 0) {
            continue;
        }
        else if (delta > 0) {
            return 1;
        }
        else if (delta < 0) {
            return -1;
        }
    }
    return 0;
}
const symVerRegExp = /\d+\.\d+\.\d+/;
function toSymVer(v) {
    const match = v.match(symVerRegExp);
    if (!match) {
        return;
    }
    return match[0].split('.').map((s) => parseInt(s));
}
function appStateFromInfo(info) {
    if (info.current) {
        return 'installed';
    }
    else if (info.bundled) {
        return 'bundled';
    }
    else if (info.packs) {
        return 'downloaded';
    }
    else {
        throw new Error(`Application ${info.id} is neither installed, nor bundled, nor downloaded`);
    }
}
function latestVersionIn(versions) {
    if (versions.length === 0) {
        throw new Error(`Versions array is empty`);
    }
    else {
        let latest = versions[versions.length - 1];
        let latestSV = toSymVer(latest);
        for (let i = versions.length - 2; i >= 0; i -= 1) {
            const v = versions[i];
            const sv = toSymVer(v);
            if (latestSV) {
                if (sv && (compareVersions(sv, latestSV) > 1)) {
                    latest = v;
                    latestSV = sv;
                }
            }
            else {
                if (sv || (latest < v)) {
                    latest = v;
                }
            }
        }
        return latest;
    }
}
class AppUpdater {
    constructor(storage) {
        this.storage = storage;
        this.info = undefined;
        this.state = undefined;
        this.elem = makeFromTemplate("app-template");
    }
    setElem(newElem) {
        if (this.elem) {
            this.elem.replaceWith(newElem);
        }
        this.elem = newElem;
    }
    get displayElem() {
        return this.elem;
    }
    get id() {
        return this.info.id;
    }
    render() {
        const newElem = makeFromTemplate("app-template");
        newElem.querySelector(".app-domain").textContent = this.id;
        newElem.querySelector(".app-version").textContent = (this.info.current ?
            `Installed version ${this.info.current}` :
            (this.info.bundled ?
                `Bundled version ${this.info.bundled}` :
                `Downloaded version(s) ${this.info.packs.join(', ')}`));
        this.addUpdatesDisplayTo(newElem);
        if (this.actionInProcess) {
            this.actionInProcess.renderIn(newElem.querySelector(".app-progress"));
        }
        else {
            this.addActionsDisplayTo(newElem);
        }
        this.setElem(newElem);
    }
    addUpdatesDisplayTo(newElem) {
        const updatesElem = newElem.querySelector(".app-available-updates");
        if (this.state === 'installed') {
            newElem.querySelector(".app-channel")
                .textContent = (this.channel ? `Update channel ${this.channel}` : null);
            if (this.availableUpdates) {
                if (this.availableUpdates === 'up-to-date') {
                    updatesElem.textContent = `${this.id} is up-to-date`;
                }
                else if (this.availableUpdates === 'none-found') {
                    updatesElem.textContent = `Update information not found`;
                }
                else {
                    for (const [channel, ver] of this.availableUpdates.entries()) {
                        const verElem = document.createElement('div');
                        verElem.textContent = `${channel} channel has newer version ${ver}`;
                        updatesElem.appendChild(verElem);
                    }
                }
            }
        }
        else {
            if (this.availableUpdates) {
                if (this.availableUpdates === 'up-to-date') {
                    updatesElem.textContent = `Local version is the latest`;
                }
                else if (this.availableUpdates === 'none-found') {
                    updatesElem.textContent = `Update information not found`;
                }
                else {
                    for (const [channel, ver] of this.availableUpdates.entries()) {
                        const verElem = document.createElement('div');
                        verElem.textContent = `${channel} channel has newer version ${ver}`;
                        updatesElem.appendChild(verElem);
                    }
                }
            }
        }
    }
    addActionsDisplayTo(newElem) {
        const actions = newElem.querySelector(".app-actions");
        if (this.state !== 'installed') {
            const installBtn = document.createElement('button');
            const localVersion = this.latestVersionFromInfo();
            installBtn.textContent = `Install ${this.id} version ${localVersion}`;
            if (this.state === 'bundled') {
                installBtn.onclick = ev => this.installBundled(localVersion);
            }
            else if (this.state === 'downloaded') {
                installBtn.onclick = ev => this.installDownloaded(localVersion);
            }
            actions.appendChild(installBtn);
        }
        if (!this.availableUpdates) {
            const checkUpdatesBtn = document.createElement('button');
            checkUpdatesBtn.textContent = ((this.state === 'installed') ?
                `Check ${this.id} Updates` :
                `Check ${this.id} other version(s)`);
            checkUpdatesBtn.onclick = ev => this.checkForUpdate();
            actions.appendChild(checkUpdatesBtn);
        }
        else if (typeof this.availableUpdates === 'object') {
            for (const [channel, ver] of this.availableUpdates.entries()) {
                const updateBtn = document.createElement('button');
                updateBtn.textContent = ((this.channel === channel) ?
                    `Update to version ${ver}` :
                    `Update to version ${ver} in ${channel} channel`);
                updateBtn.onclick = ev => this.downloadAndInstallUpdate(channel, ver);
                actions.appendChild(updateBtn);
            }
        }
    }
    async setInfoAndRender(info) {
        this.info = info;
        this.state = appStateFromInfo(this.info);
        this.channel = await this.storage.getAppChannel(this.id);
        this.render();
    }
    async refreshInfoAndRender() {
        const refreshedInfo = await w3n.apps.opener.getAppVersions(this.id);
        this.setInfoAndRender(refreshedInfo);
    }
    async checkForUpdate() {
        if (this.actionInProcess) {
            return;
        }
        this.actionInProcess = new StaticTextProgressComponent(`Checking for ${this.id} update ...`);
        this.render();
        try {
            const currentVersion = this.latestVersionFromInfo();
            const availableVersions = new Map();
            if (this.channel) {
                try {
                    const ver = await w3n.apps.downloader.getLatestAppVersion(this.id, this.channel);
                    availableVersions.set(this.channel, ver);
                }
                catch (err) {
                    logInfo(`Can't get latest version in ${this.id} channel ${this.channel}`, err);
                }
            }
            else {
                await this.initDistributionChannelsInfo();
                const channels = (this.distChannels ?
                    Object.keys(this.distChannels.channels) : []);
                for (const channel of channels) {
                    try {
                        const ver = await w3n.apps.downloader.getLatestAppVersion(this.id, channel);
                        availableVersions.set(channel, ver);
                    }
                    catch (err) {
                        logInfo(`Can't get latest version in ${this.id} channel ${channel}`, err);
                    }
                }
            }
            if (availableVersions.size > 0) {
                const newVersions = new Map();
                const currentSymVer = toSymVer(currentVersion);
                for (const [channel, ver] of availableVersions.entries()) {
                    const symVer = toSymVer(ver);
                    if (symVer && (compareVersions(symVer, currentSymVer) > 0)) {
                        newVersions.set(channel, ver);
                    }
                }
                this.availableUpdates = ((newVersions.size > 0) ?
                    newVersions : 'up-to-date');
            }
            else if (!this.availableUpdates) {
                this.availableUpdates = 'none-found';
            }
        }
        finally {
            this.actionInProcess = undefined;
            this.refreshInfoAndRender();
        }
    }
    latestVersionFromInfo() {
        if (this.state === 'installed') {
            return this.info.current;
        }
        else if (this.state === 'bundled') {
            return this.info.bundled;
        }
        else if (this.state === 'downloaded') {
            return latestVersionIn(this.info.packs);
        }
        else {
            throw new Error(`This point in code shouldn't be reached`);
        }
    }
    async initDistributionChannelsInfo() {
        try {
            this.distChannels = await w3n.apps.downloader.getAppChannels(this.id);
            return true;
        }
        catch (err) {
            logInfo(`Can't fetch platform distribution channels`, err);
            return false;
        }
    }
    async downloadAndInstallUpdate(channel, version) {
        var _a;
        if (this.actionInProcess) {
            return;
        }
        const actionInProcess = new AppDownloadAndInstallProgress(version);
        this.actionInProcess = actionInProcess;
        this.render();
        try {
            const alreadyDownloaded = !!((_a = this.info.packs) === null || _a === void 0 ? void 0 : _a.includes(version));
            if (!alreadyDownloaded) {
                await (new Promise((complete, error) => w3n.apps.downloader.downloadWebApp(this.id, version, {
                    next: p => actionInProcess.update(p),
                    error,
                    complete
                })));
            }
            actionInProcess.switchToCompletion();
            await w3n.apps.installer.installApp(this.id, version);
            if (this.channel !== channel) {
                this.channel = channel;
                await this.storage.saveAppChannel(this.id, this.channel);
            }
        }
        finally {
            this.actionInProcess = undefined;
            this.refreshInfoAndRender();
        }
    }
    async installDownloaded(version) {
        if (this.actionInProcess) {
            return;
        }
        this.actionInProcess = new StaticTextProgressComponent(`Installing downloaded ...`);
        this.render();
        try {
            await w3n.apps.installer.installApp(this.id, version);
        }
        finally {
            this.actionInProcess = undefined;
            this.refreshInfoAndRender();
        }
    }
    async installBundled(version) {
        var _a;
        if (this.actionInProcess) {
            return;
        }
        const actionInProcess = new AppInstallBundledProgress();
        this.actionInProcess = actionInProcess;
        this.render();
        try {
            const alreadyUnpacked = !!((_a = this.info.packs) === null || _a === void 0 ? void 0 : _a.includes(version));
            if (!alreadyUnpacked) {
                await (new Promise((complete, error) => w3n.apps.installer.unpackBundledApp(this.id, {
                    next: p => actionInProcess.update(p),
                    error,
                    complete
                })));
            }
            actionInProcess.switchToCompletion();
            await w3n.apps.installer.installApp(this.id, version);
        }
        finally {
            this.actionInProcess = undefined;
            this.refreshInfoAndRender();
        }
    }
}
class ProgressComponent {
    constructor() {
        this.stage = 'start';
        this.progress = undefined;
        this.elem = undefined;
    }
    renderIn(newElem) {
        if (newElem) {
            this.elem = newElem;
            this.progressBar = undefined;
            this.progressTxt1 = undefined;
            this.progressTxt2 = undefined;
        }
        this.render();
    }
    render() {
        if (this.stage === 'start') {
            this.renderOnStart();
        }
        else if (this.progress && (this.stage === 'process')) {
            const percent = this.progressPercentage();
            this.elem.textContent = null;
            if (!this.progressBar) {
                this.progressBar = document.createElement('progress');
                this.progressBar.max = 100;
                this.progressBar.value = percent;
                this.elem.appendChild(this.progressBar);
            }
            if (!this.progressTxt1) {
                this.progressTxt1 = document.createElement('div');
                this.elem.appendChild(this.progressTxt1);
            }
            if (!this.progressTxt2) {
                this.progressTxt2 = document.createElement('div');
                this.elem.appendChild(this.progressTxt2);
            }
            this.progressBar.value = percent;
            this.renderOnProcess();
        }
        else if (this.stage === 'completion') {
            if (this.progressBar) {
                this.progressBar.hidden = true;
                this.progressBar = undefined;
            }
            if (this.progressTxt1) {
                this.progressTxt1.hidden = true;
                this.progressTxt1 = undefined;
            }
            if (this.progressTxt2) {
                this.progressTxt2.hidden = true;
                this.progressTxt2 = undefined;
            }
            this.renderOnCompletion();
        }
    }
    update(p) {
        this.stage = 'process';
        // DEBUG log
        console.log(p);
        this.progress = p;
        this.render();
    }
    switchToCompletion() {
        this.stage = 'completion';
        this.render();
    }
}
class AppInstallBundledProgress extends ProgressComponent {
    renderOnStart() {
        this.elem.textContent = `Installing bundled ...`;
    }
    renderOnCompletion() {
        this.elem.textContent = `Installing unpacked ...`;
    }
    progressPercentage() {
        return Math.floor(100 * this.progress.numOfProcessed / this.progress.numOfFiles);
    }
    renderOnProcess() {
        this.progressTxt1.textContent = `unpacked ${this.progress.numOfProcessed} of ${this.progress.numOfFiles} files`;
        this.progressTxt2.textContent = (this.progress.fileInProgress ?
            `unpacking ${this.progress.fileInProgress}` :
            null);
    }
}
class AppDownloadAndInstallProgress extends ProgressComponent {
    constructor(version) {
        super();
        this.version = version;
    }
    renderOnStart() {
        this.elem.textContent = `Downloading version ${this.version} ...`;
    }
    renderOnCompletion() {
        this.elem.textContent = `Installing downloaded ...`;
    }
    progressPercentage() {
        const bytesDownloaded = this.progress.totalBytes - this.progress.bytesLeft;
        return Math.floor(100 * bytesDownloaded / this.progress.totalBytes);
    }
    renderOnProcess() {
        const downloadedFiles = this.progress.totalFiles - this.progress.filesLeft;
        this.progressTxt1.textContent = `downloaded ${downloadedFiles} of ${this.progress.totalFiles} files`;
        this.progressTxt2.textContent = (this.progress.fileInProgress ?
            `downloading ${this.progress.currentFileSize} bytes of ${this.progress.fileInProgress}` :
            null);
    }
}
class PlatformDownloadAndApplyingProgress extends ProgressComponent {
    constructor(channel) {
        super();
        this.channel = channel;
    }
    renderOnStart() {
        this.elem.textContent = `Downloading from ${this.channel} channel ...`;
    }
    renderOnCompletion() { }
    progressPercentage() {
        return ((this.progress &&
            (this.progress.event === 'download-progress') &&
            (typeof this.progress.percent === 'number')) ?
            this.progress.percent : 0);
    }
    renderOnProcess() {
        if (this.progress.event === 'checking-for-update') {
            this.progressTxt1.textContent = `Connecting ...`;
        }
        else if (this.progress.event === 'update-available') {
            this.totalMBs = this.progress.totalSizeMBs;
            this.progressTxt1.textContent = `Update is ${this.totalMBs}MB`;
        }
        else if (this.progress.event === 'download-progress') {
            this.progressTxt1.textContent = `${this.progress.percent}% of ${this.totalMBs}MB downloaded`;
        }
    }
}
class AllApps {
    constructor(storage) {
        this.storage = storage;
        this.apps = [];
        this.elem = undefined;
    }
    async refreshAndRenderAll() {
        const infos = await w3n.apps.opener.listApps();
        const container = document.createElement('div');
        const installed = infos.filter(app => !!app.current);
        const onlyBundled = infos.filter(app => (!installed.includes(app) && !!app.bundled));
        const onlyDownloaded = infos.filter(app => (!installed.includes(app) && !onlyBundled.includes(app) && !!app.packs));
        this.apps = [];
        for (const info of installed.concat(onlyBundled, onlyDownloaded)) {
            const app = new AppUpdater(this.storage);
            await app.setInfoAndRender(info);
            container.appendChild(app.displayElem);
            this.apps.push(app);
        }
        this.setElem(container);
    }
    setElem(newElem) {
        if (!this.elem) {
            this.elem = document.getElementById("apps-display");
        }
        this.elem.replaceWith(newElem);
        this.elem = newElem;
    }
    async checkForAllUpdates() {
        for (const app of this.apps) {
            await app.checkForUpdate()
                .catch(err => logError(`Error in checking updates for ${app.id}`, err));
        }
    }
}
class AppsSearch {
    constructor(storage, knownApps) {
        this.storage = storage;
        this.knownApps = knownApps;
    }
    async findApp() {
        console.error(`findApp() is not implemeted, yet`);
    }
}
const storage = new InfoStorage();
const platform = new PlatformUpdater(storage);
const apps = new AllApps(storage);
const searches = new AppsSearch(storage, apps);
apps.refreshAndRenderAll();
platform.setupAndRender();
async function checkForAllUpdates() {
    await platform.checkForUpdate();
    await apps.checkForAllUpdates();
}
