"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const ReferenceCountMap_1 = __importDefault(require("./utils/ReferenceCountMap"));
const types_1 = require("./types");
const URI_1 = require("./utils/URI");
const Password_1 = __importDefault(require("./Password"));
const uniqid_1 = __importDefault(require("./utils/uniqid"));
const log_1 = __importStar(require("./log"));
const Local_1 = __importDefault(require("./fs/Local"));
const SFtp_1 = __importStar(require("./fs/SFtp"));
const Ftp_1 = __importStar(require("./fs/Ftp"));
const options_1 = __importDefault(require("./options"));
class default_1 {
    static initialize() {
        this.shared.set(types_1.LocalFileSystemID, new Local_1.default);
    }
    static async open(scheme, user, host, port) {
        const id = (0, URI_1.connectionID)(scheme, user, host, port);
        const attrs = (scheme == 'sftp') ? SFtp_1.ATTRIBUTES : Ftp_1.ATTRIBUTES;
        if (this.shared.inc(id)) {
            return attrs;
        }
        const conn = await this.create(id, scheme, user, host, port);
        await conn.open();
        this.shared.set(id, conn);
        return attrs;
    }
    static get(id) {
        return this.shared.get(id);
    }
    static close(id) {
        this.shared.dec(id)?.close();
    }
    static async list(id, path) {
        const files = await this.get(id)?.ls(path) || [];
        return files.map(f => ({ ...f, URI: (0, URI_1.createURI)(id, f.path) }));
    }
    static async transmit(id) {
        if (id == types_1.LocalFileSystemID) {
            return [new Local_1.default, () => { }];
        }
        const conn = await this.hold(id);
        if (conn) {
            return [conn[0], () => this.release(id, conn[1])];
        }
        return new Promise((resolve) => {
            const onRelease = (poolId) => {
                const { fs } = this.pools.get(id).get(poolId);
                resolve([fs, () => this.release(id, poolId)]);
            };
            id in this.pending ? this.pending[id].push(onRelease) : (this.pending[id] = [onRelease]);
        });
    }
    static async hold(id) {
        !this.pools.has(id) && this.pools.set(id, new Map());
        const pool = this.pools.get(id);
        const conn = this.getIdle(id);
        if (conn) {
            return conn;
        }
        if (pool.size < this.getLimit(id)) {
            return new Promise((resolve) => {
                const connect = async () => {
                    const conn = this.getIdle(id);
                    if (conn) {
                        resolve(conn);
                        return;
                    }
                    const poolId = (0, uniqid_1.default)();
                    try {
                        const onClose = () => this.pools.get(id)?.delete(poolId);
                        const fs = await this.createFromId(id, onClose);
                        await fs.open();
                        this.pools.get(id).set(poolId, { fs, idle: false });
                        resolve([fs, poolId]);
                    }
                    catch (e) {
                        const conn = this.getIdle(id);
                        if (conn) {
                            resolve(conn);
                            return;
                        }
                    }
                    resolve(null);
                };
                this.queue.push(connect);
                if (this.queue.length == 1) {
                    this.connectNext();
                }
            });
        }
        return null;
    }
    static getIdle(id) {
        const conn = Array.from(this.pools.get(id)?.entries() || []).find(([, { idle }]) => idle !== false);
        if (conn) {
            conn[1].idle !== false && clearTimeout(conn[1].idle);
            conn[1].idle = false;
            return [conn[1].fs, conn[0]];
        }
        return null;
    }
    static async connectNext() {
        this.queue
            .splice(0, Math.max(this.maxStartups - this.numOfStartups, 0))
            .map(connect => async () => { await connect(); this.numOfStartups--; this.connectNext(); })
            .forEach(f => { this.numOfStartups++; f(); });
    }
    static release(id, poolId) {
        const conn = this.pools.get(id)?.get(poolId);
        if (conn) {
            if (this.pending[id]?.length) {
                this.pending[id].shift()(poolId);
            }
            else {
                conn.idle = setTimeout(() => {
                    conn.fs.close();
                    this.pools.get(id)?.delete(poolId);
                }, 2000);
            }
        }
    }
    static getLimit(id) {
        return this.limits.has(id) ? this.limits.get(id) : 1024; // for vsftpd max_per_ip in /etc/vsftpd.conf
    }
    static async create(id, scheme, user, host, port, onClose = () => { }) {
        if (options_1.default.log) {
            return new log_1.LogFS(id, await this.createFS(scheme, user, host, port, await Password_1.default.get(id), onClose));
        }
        return this.createFS(scheme, user, host, port, await Password_1.default.get(id), onClose);
    }
    static async createFS(scheme, user, host, port, password, onClose) {
        switch (scheme) {
            case 'sftp': {
                return new SFtp_1.default(host, user, password, port, error => log_1.default.error('SFTP error:', error), onClose);
            }
            case 'ftp': {
                return new Ftp_1.default(host, user, password, port, error => log_1.default.error('FTP error:', error), onClose);
            }
            default:
                throw new Error(`Unsupported scheme ${scheme}`);
        }
    }
    static async createFromId(id, onClose = () => { }) {
        const { scheme, user, host, port } = (0, URI_1.parseURI)(id);
        return this.create(id, scheme, user, host, port, onClose);
    }
}
default_1.numOfStartups = 0;
default_1.maxStartups = 7; // for SFTP see MaxStartups in /etc/ssh/sshd_config
default_1.shared = new ReferenceCountMap_1.default;
// private static pools: Record<string, Record<string, {fs: FileSystem, idle: false|ReturnType<typeof setTimeout>}>> = {}
default_1.pools = new Map();
default_1.queue = [];
default_1.pending = {};
default_1.limits = new Map();
exports.default = default_1;
