"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.queues = exports.lsRemote = void 0;
const node_path_1 = require("node:path");
const types_1 = require("../types");
const rxjs_1 = require("rxjs");
const ramda_1 = require("ramda");
const Connection_1 = __importDefault(require("../Connection"));
const Local_1 = require("../Local");
const filter_1 = require("../utils/filter");
const lsRemote = (connId) => {
    const cache = new Map();
    return async (path) => {
        if (cache.has(path)) {
            return cache.get(path);
        }
        const list = new Promise(async (resove) => {
            const [conn, close] = await Connection_1.default.transmit(connId);
            conn.ls(path)
                .then(resove)
                .catch(() => resove(null))
                .finally(() => close());
        });
        cache.set(path, list);
        return list;
    };
};
exports.lsRemote = lsRemote;
class TransmitQueue {
    constructor(from, to, src, dest, filter, onState, onConflict, onComplete) {
        this.from = from;
        this.to = to;
        this.src = src;
        this.dest = dest;
        this.filter = filter;
        this.onState = onState;
        this.onConflict = onConflict;
        this.onComplete = onComplete;
        this.queue$ = new rxjs_1.Subject();
        this.queue = [];
        this.pending = [];
        this.stopped = false;
        this.totalCnt = 0;
        this.doneCnt = 0;
        this.totalSize = 0;
        this.doneSize = 0;
        this.transmits = 0;
        this.closed = false;
    }
    async create() {
        await this.enqueue(this.src, this.dest);
        if (this.stopped) {
            return;
        }
        const stat = this.stat(this.to);
        this.processing = this.queue$.subscribe(async ({ from, dirs, to, action }) => {
            let a = action ?? this.action;
            const existing = await stat((0, node_path_1.join)(to, ...dirs, from.name));
            if (existing) {
                if (a) {
                    if (a.type == types_1.QueueActionType.Skip) {
                        this.sendState(from.size);
                        return this.next();
                    }
                }
                else {
                    this.putOnHold(from, dirs, to, existing);
                    return this.next();
                }
            }
            const [fs, close] = await Connection_1.default.transmit(this.from != types_1.LocalFileSystemID ? this.from : this.to);
            this.transmits++;
            (new Promise((resolve) => resolve(existing ?
                this.applyAction(a, from, dirs, to, existing, this.transmit.bind(this, fs)) :
                this.transmit(fs, from, dirs, to)))).then(() => {
                close();
                if (--this.transmits == 0 && this.closed) {
                    this.close();
                }
            });
            this.next();
        });
        this.next();
    }
    async enqueue(paths, dest) {
        const ls = this.ls(this.from);
        let matchFilter = (file) => true;
        if (this.filter) {
            const re = (0, filter_1.filterRegExp)(this.filter);
            matchFilter = (file) => {
                const found = re.exec(file.name);
                return this.filter?.invert ?? false ? found === null : found !== null;
            };
        }
        paths = paths.map(node_path_1.normalize).filter(path => !paths.find(ancestor => path.startsWith(ancestor + node_path_1.sep)));
        const add = async (path, to, dirs = []) => {
            if (this.stopped) {
                return;
            }
            const parent = (0, node_path_1.dirname)(path);
            const from = (await ls(parent))?.find((0, ramda_1.whereEq)({ path }));
            if (from) {
                if (from.dir) {
                    return Promise.all((await ls(from.path))?.map(child => add(child.path, to, [...dirs, (0, node_path_1.basename)(path)])));
                }
                else {
                    if (matchFilter(from)) {
                        this.queue.push({ from, to, dirs });
                        this.totalCnt++;
                        this.totalSize += from.size;
                    }
                }
            }
        };
        await Promise.all(paths.map(path => add(path, dest)));
    }
    stop() {
        if (!this.stopped) {
            this.stopped = true;
            this.pending = [];
            this.close();
        }
    }
    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, dirs: f.dirs, to: f.to, action })));
        drained && this.queue$.next(this.queue.shift());
        if (this.pending.length) {
            const { src, dest } = this.pending[0];
            this.onConflict(src, dest);
        }
    }
    next() {
        if (this.queue.length) {
            this.queue$.next(this.queue.shift());
        }
        else if (!this.pending.length) {
            this.close();
        }
    }
    async close() {
        if (this.processing?.closed === false) {
            this.processing?.unsubscribe();
        }
        this.closed = true;
        if (this.transmits == 0) {
            this.finalize();
            this.onComplete(this.stopped);
        }
    }
    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
        });
    }
    async applyAction(action, from, dirs, to, existing, transmit) {
        switch (action.type) {
            case types_1.QueueActionType.Replace: {
                await transmit(from, dirs, to);
                break;
            }
            case types_1.QueueActionType.Rename: {
                from.name = await this.rename(from.name, (0, node_path_1.join)(to, ...dirs));
                await transmit(from, dirs, to);
                break;
            }
        }
    }
    putOnHold(src, dirs, to, dest) {
        this.pending.push({ src, dirs, to, dest });
        if (this.pending.length == 1) {
            this.onConflict(src, dest);
        }
    }
    ls(connId) {
        return connId == types_1.LocalFileSystemID ?
            (dir) => Connection_1.default.get(connId).ls(dir) :
            (0, exports.lsRemote)(connId);
    }
    stat(connId) {
        if (connId == types_1.LocalFileSystemID) {
            return (path) => Promise.resolve((0, Local_1.stat)(path));
        }
        const ls = this.ls(connId);
        return async (path) => (await ls((0, node_path_1.dirname)(path)))?.find((0, ramda_1.whereEq)({ path }));
    }
    async rename(name, dir) {
        let { name: oldName, ext } = (0, node_path_1.parse)(name);
        const parts = oldName.match(/^(.+) copy ?(\d*)$/);
        oldName = (parts && parts.length > 2) ? parts[1] : oldName;
        let counter = (parts && parts.length > 2) ? (parts[2] ? parseInt(parts[2]) + 1 : 1) : 1;
        let newName = `${oldName} copy${counter > 1 ? ' ' + counter : ''}`;
        const files = await this.ls(this.to)(dir);
        while (files?.find((0, ramda_1.whereEq)({ name: newName + ext }))) {
            newName = `${oldName} copy ${++counter}`;
        }
        return newName + ext;
    }
    async finalize() { }
}
exports.default = TransmitQueue;
exports.queues = new Map();
