"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 (mod) {
    if (mod && mod.__esModule) return mod;
    var result = {};
    if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
    __setModuleDefault(result, mod);
    return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.makeTunnelManager = void 0;
const tunnel_client_1 = require("@applitools/tunnel-client");
const utils = __importStar(require("@applitools/utils"));
async function makeTunnelManager({ settings, logger, }) {
    const client = (0, tunnel_client_1.makeTunnelClient)({ settings, logger });
    const pools = new Map();
    return { acquire, release, close: client.close };
    async function acquire(credentials) {
        const key = JSON.stringify(credentials);
        let pool = pools.get(key);
        if (!pool) {
            pool = makePool({
                create: () => { var _a; return Promise.all(Array.from({ length: (_a = settings === null || settings === void 0 ? void 0 : settings.groupSize) !== null && _a !== void 0 ? _a : 1 }, () => client.create(credentials))); },
                destroy: tunnels => Promise.all(tunnels.map(tunnel => client.destroy(tunnel.tunnelId))).then(() => undefined),
                ...settings === null || settings === void 0 ? void 0 : settings.pool,
            });
            pools.set(key, pool);
        }
        return pool.acquire(credentials);
    }
    async function release(tunnels) {
        const key = JSON.stringify(tunnels[0].credentials);
        const pool = pools.get(key);
        if (!pool)
            return;
        await pool.release(tunnels);
    }
}
exports.makeTunnelManager = makeTunnelManager;
function makePool(options) {
    const pool = new Map();
    const pending = new Set();
    return {
        acquire,
        add,
        get,
        use,
        release,
        destroy,
    };
    async function acquire(resourceOptions) {
        let resource = await get();
        if (!resource) {
            resource = await create(resourceOptions);
            await add(resource);
        }
        await use(resource);
        return resource;
    }
    async function create(resourceOptions) {
        const availableItem = [...pending].reduce((availableItem, item) => {
            return (!options.maxInuse || item.waiting < options.maxInuse) &&
                (!availableItem || availableItem.waiting > item.waiting)
                ? item
                : availableItem;
        }, null);
        if (availableItem) {
            availableItem.waiting += 1;
            return availableItem.resource;
        }
        const resource = options.create(resourceOptions);
        const item = { resource, waiting: 1 };
        resource.finally(() => pending.delete(item));
        pending.add(item);
        return resource;
    }
    async function add(resource) {
        var _a, _b;
        const item = {
            resource,
            destroyed: false,
            inuse: 0,
        };
        if ((_a = options.timeout) === null || _a === void 0 ? void 0 : _a.expiration) {
            (_b = item.timers) !== null && _b !== void 0 ? _b : (item.timers = {});
            item.timers.expiration = setTimeout(() => destroy(resource), options.timeout.expiration);
            item.expireAt = Date.now() + options.timeout.expiration;
        }
        pool.set(JSON.stringify(resource), item);
    }
    async function get() {
        var _a;
        const freeItem = [...pool.values()].reduce((freeItem, item) => {
            return !item.destroyed &&
                (!options.maxInuse || item.inuse < options.maxInuse) &&
                (!freeItem || freeItem.inuse > item.inuse)
                ? item
                : freeItem;
        }, null);
        return (_a = freeItem === null || freeItem === void 0 ? void 0 : freeItem.resource) !== null && _a !== void 0 ? _a : null;
    }
    async function use(resource) {
        var _a, _b;
        const item = pool.get(JSON.stringify(resource));
        if (!item)
            return false;
        if ((_a = item.timers) === null || _a === void 0 ? void 0 : _a.idle)
            clearTimeout((_b = item.timers) === null || _b === void 0 ? void 0 : _b.idle);
        item.inuse += 1;
        return true;
    }
    async function release(resource) {
        var _a, _b;
        const item = pool.get(JSON.stringify(resource));
        if (!item)
            return false;
        item.inuse -= 1;
        if (item.destroyed)
            await destroy(resource);
        else if (!utils.types.isNull((_a = options.timeout) === null || _a === void 0 ? void 0 : _a.idle) && item.inuse <= 0) {
            if (options.timeout.idle > 0) {
                (_b = item.timers) !== null && _b !== void 0 ? _b : (item.timers = {});
                item.timers.idle = setTimeout(() => destroy(resource), options.timeout.idle);
            }
            else {
                await destroy(resource);
            }
        }
        return true;
    }
    async function destroy(resource) {
        var _a, _b, _c, _d;
        const item = pool.get(JSON.stringify(resource));
        if (!item)
            return false;
        item.destroyed = true;
        if (item.inuse > 0)
            return false;
        if ((_a = item.timers) === null || _a === void 0 ? void 0 : _a.idle)
            clearTimeout((_b = item.timers) === null || _b === void 0 ? void 0 : _b.idle);
        if ((_c = item.timers) === null || _c === void 0 ? void 0 : _c.expiration)
            clearTimeout((_d = item.timers) === null || _d === void 0 ? void 0 : _d.expiration);
        await options.destroy(item.resource);
        return true;
    }
}
