"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.queues = void 0;
const node_path_1 = require("node:path");
const types_1 = require("./types");
const Local_1 = require("./Local");
const URI_1 = require("./utils/URI");
const ramda_1 = require("ramda");
const rxjs_1 = require("rxjs");
const Connection_1 = __importDefault(require("./Connection"));
const log_1 = __importDefault(require("./log"));
class Queue {
    constructor(type, connId, src, dest, watcher, onState, onConflict, onError, onComplete) {
        this.type = type;
        this.connId = connId;
        this.watcher = watcher;
        this.onState = onState;
        this.onConflict = onConflict;
        this.onError = onError;
        this.onComplete = onComplete;
        this.queue = [];
        this.queue$ = new rxjs_1.Subject();
        this.totalCnt = 0;
        this.doneCnt = 0;
        this.totalSize = 0;
        this.doneSize = 0;
        this.pending = [];
        this.touched = new Set();
        if (type == types_1.QueueType.Download) {
            this.download(src, (0, URI_1.parseURI)(dest)['path']);
        }
        else if (type == types_1.QueueType.Upload) {
            this.upload(src.map(uri => (0, URI_1.parseURI)(uri)['path']), dest);
        }
        else {
            this.remove(src.map(uri => (0, URI_1.parseURI)(uri)['path']));
        }
    }
    resolve(action, forAll = false) {
        forAll && (this.action = action);
        const drained = !this.queue.length;
        this.queue.push(...this.pending.splice(0, forAll ? this.pending.length : 1).map(f => ({ from: f.src, to: f.dest.path, action })));
        drained && this.queue$.next(this.queue.shift());
        if (this.pending.length) {
            const { src, dest } = this.pending[0];
            this.onConflict(src, dest);
        }
    }
    close() {
        if (this.processing?.closed === false) {
            this.processing?.unsubscribe();
            this.onComplete();
            if (this.type == types_1.QueueType.Upload || this.type == types_1.QueueType.Remove) {
                this.touched.forEach(path => this.watcher.refresh(this.connId + path));
            }
        }
    }
    async download(src, dest) {
        await this.enqueue(src.map((0, ramda_1.pipe)(URI_1.parseURI, (0, ramda_1.prop)('path'))), dest, (0, ramda_1.memoizeWith)(ramda_1.identity, (path) => Connection_1.default.list(this.connId, path)));
        const transmit = async (fs, from, to) => {
            await (0, Local_1.touch)(to);
            log_1.default.log(`start downloading ${from.path} -> ${to}`);
            try {
                await fs.get(from.path, to);
            }
            catch (error) {
                this.onError(error);
            }
            log_1.default.log(`end downloading ${from.path}`);
            this.sendState(from.size);
        };
        this.processing = this.queue$.subscribe(async ({ from, to, action }) => {
            let a = action ?? this.action;
            const existing = (0, Local_1.stat)(to);
            if (existing) {
                if (a) {
                    if (a.type == types_1.QueueActionType.Skip) {
                        this.sendState(from.size);
                        return this.next();
                    }
                }
                else {
                    this.putOnHold(from, existing);
                    return this.next();
                }
            }
            const [fs, close] = await Connection_1.default.transmit(this.connId);
            existing ?
                this.applyAction(a, from, existing, transmit.bind(this, fs)).then(close) :
                transmit(fs, from, to).then(close);
            this.next();
        });
        this.next();
    }
    async upload(src, dest) {
        const conn = Connection_1.default.get(this.connId);
        const to = (0, URI_1.parseURI)(dest)['path'];
        await this.enqueue(src, to, path => Promise.resolve((0, Local_1.list)(path)));
        this.touched.add(to);
        const transmit = async (fs, from, to) => {
            if (!((0, Local_1.stat)(from.path))) {
                return;
            }
            log_1.default.log(`start uploading ${from.path} -> ${to}`);
            try {
                const dir = (0, node_path_1.dirname)(to);
                if (!this.touched.has(dir)) {
                    await fs.mkdir(dir);
                }
                await fs.put(from.path, to);
                this.touched.add(dir);
            }
            catch (error) {
                this.onError(error);
            }
            log_1.default.log(`end uploading ${from.path}`);
            this.sendState(from.size);
        };
        const ls = (0, ramda_1.memoizeWith)(ramda_1.identity, async (path) => {
            try {
                return await conn.ls(path);
            }
            catch (e) { }
            return [];
        });
        const exists = async (path) => (await ls((0, node_path_1.dirname)(path))).find((0, ramda_1.whereEq)({ path }));
        this.processing = this.queue$.subscribe(async ({ from, to, action }) => {
            let a = action ?? this.action;
            const existing = await exists(to);
            if (existing) {
                if (a) {
                    if (a.type == types_1.QueueActionType.Skip) {
                        this.sendState(from.size);
                        return this.next();
                    }
                }
                else {
                    this.putOnHold(from, existing);
                    return this.next();
                }
            }
            const [fs, close] = await Connection_1.default.transmit(this.connId);
            existing ?
                this.applyAction(a, from, existing, transmit.bind(this, fs)).then(close) :
                transmit(fs, from, to).then(close);
            this.next();
        });
        this.next();
    }
    async remove(paths) {
        this.totalCnt = paths.length;
        const touched = new Map();
        paths.forEach(async (path) => {
            const dir = (0, node_path_1.dirname)(path);
            const [conn, close] = await Connection_1.default.transmit(this.connId);
            if (!touched.has(dir)) {
                try {
                    touched.set(dir, await conn.ls(dir));
                }
                catch (e) {
                    touched.set(dir, null);
                }
            }
            const name = (0, node_path_1.basename)(path);
            const file = touched.get(dir)?.find(f => f.name == name);
            if (file) {
                try {
                    await conn.rm(path, file.dir);
                }
                catch (error) {
                    this.onError({
                        type: types_1.FailureType.RemoteError,
                        id: this.connId,
                        error
                    });
                }
            }
            close();
            this.sendState(0);
            if (this.doneCnt == this.totalCnt) {
                this.onComplete();
                touched.forEach((files, dir) => {
                    this.watcher.refresh((0, URI_1.createURI)(this.connId, dir));
                });
            }
        });
    }
    next() {
        if (this.queue.length) {
            this.queue$.next(this.queue.shift());
        }
        else if (!this.pending.length) {
            this.close();
        }
    }
    async enqueue(paths, dest, ls) {
        const add = async (path, to) => {
            const from = (await ls((0, node_path_1.dirname)(path))).find((0, ramda_1.whereEq)({ path }));
            from && (from.dir ?
                (await ls(path)).forEach(async (f) => await add(f.path, (0, node_path_1.join)(to, (0, node_path_1.basename)(path)))) :
                this.queue.push({ from, to: (0, node_path_1.join)(to, (0, node_path_1.basename)(path)) }));
        };
        for (const path of paths) {
            await add(path, dest);
        }
        this.queue.forEach(({ from: { size } }) => { this.totalCnt++; this.totalSize += size; });
    }
    applyAction(action, from, to, transmit) {
        switch (action.type) {
            case types_1.QueueActionType.Replace:
                return transmit(from, to.path);
        }
    }
    putOnHold(src, dest) {
        this.pending.push({ src, dest });
        if (this.pending.length == 1) {
            this.onConflict(src, dest);
        }
    }
    sendState(size) {
        this.doneCnt++;
        this.doneSize += size;
        this.onState({
            totalCnt: this.totalCnt,
            doneCnt: this.doneCnt,
            totalSize: this.totalSize,
            doneSize: this.doneSize,
            pending: this.pending.length
        });
    }
}
exports.default = Queue;
exports.queues = new Map();
