"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
var __asyncValues = (this && this.__asyncValues) || function (o) {
    if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined.");
    var m = o[Symbol.asyncIterator], i;
    return m ? m.call(o) : (o = typeof __values === "function" ? __values(o) : o[Symbol.iterator](), i = {}, verb("next"), verb("throw"), verb("return"), i[Symbol.asyncIterator] = function () { return this; }, i);
    function verb(n) { i[n] = o[n] && function (v) { return new Promise(function (resolve, reject) { v = o[n](v), settle(resolve, reject, v.done, v.value); }); }; }
    function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); }
};
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ixIxxStream = exports.ixIxx = exports.makeIxx = exports.optimizePrefixSize = void 0;
const util_1 = require("util");
const stream_1 = require("stream");
const events_1 = require("events");
const split2_1 = __importDefault(require("split2"));
const fs_1 = __importDefault(require("fs"));
const readline_1 = __importDefault(require("readline"));
const tmp_1 = __importDefault(require("tmp"));
const external_sorting_1 = __importDefault(require("external-sorting"));
tmp_1.default.setGracefulCleanup();
const streamFinished = (0, util_1.promisify)(stream_1.finished); // (A)
// this file (index.ts) is a translation of ixIxx.c from ucscGenomeBrowser/kent
// the license of that file is reproduced below
/*
 * MIT License
 *
 * Copyright (C) 2001 UC Regents
 *
 * Permission is hereby granted, free of charge, to any person or non-commercial
 * entity obtaining a copy of this software and associated documentation files (the
 * "Software"), to deal in the Software without restriction, including without
 * limitation the rights to use, copy, modify, merge, publish, distribute,
 * sublicense, and/or sell copies of the Software, and to permit persons to whom
 * the Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
 * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
 * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
 * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */
let binSize = 64 * 1024; //64kb
// Characters that may be part of a word
const wordMiddleChars = [];
const wordBeginChars = [];
function isalpha(c) {
    return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z');
}
function isdigit(c) {
    return c >= '0' && c <= '9';
}
function isalnum(c) {
    return isalpha(c) || isdigit(c);
}
function initCharTables() {
    for (let c = 0; c < 256; ++c) {
        if (isalnum(String.fromCharCode(c))) {
            wordBeginChars[c] = true;
            wordMiddleChars[c] = true;
        }
    }
    wordBeginChars['_'.charCodeAt(0)] = wordMiddleChars['_'.charCodeAt(0)] = true;
    wordMiddleChars['.'.charCodeAt(0)] = true;
    wordMiddleChars['-'.charCodeAt(0)] = true;
}
class TrixInputTransform extends stream_1.Transform {
    _transform(chunk, _encoding, done) {
        const [id, ...words] = chunk.toString().split(/\s+/);
        this.push(words.map(word => `${word.toLowerCase()} ${id}\n`).join(''));
        done();
    }
}
function elt(buff, current) {
    return `${current} ${buff.map((elt, idx) => `${elt},${idx + 1}`).join(' ')}\n`;
}
class TrixOutputTransform extends stream_1.Transform {
    constructor() {
        super(...arguments);
        this.buff = [];
        this.current = '';
    }
    _transform(chunk, _encoding, done) {
        // weird: need to strip nulls from string, xref
        // https://github.com/GMOD/jbrowse-components/pull/2451
        let [id, data] = chunk.toString().replace(/\0/g, '').split(' ');
        if (this.current !== id) {
            if (this.buff.length) {
                this.push(elt(this.buff, this.current));
                this.buff = [];
            }
            this.current = id;
        }
        this.buff.push(data);
        done();
    }
    _flush(done) {
        if (this.buff.length) {
            this.push(elt(this.buff, this.current));
        }
        done();
    }
}
function makeIxStream(fileStream, outIxFilename) {
    return __awaiter(this, void 0, void 0, function* () {
        return new Promise((resolve, reject) => __awaiter(this, void 0, void 0, function* () {
            initCharTables();
            const dir = tmp_1.default.dirSync({
                prefix: 'jbrowse-trix-sort',
            });
            const tempDir = dir.name;
            const out = fs_1.default.createWriteStream(outIxFilename);
            // see https://stackoverflow.com/questions/68835344/ for explainer of
            // writer
            const input = (0, stream_1.pipeline)(fileStream, (0, split2_1.default)(), new TrixInputTransform(), function (err) {
                if (err) {
                    reject(err);
                }
            });
            const output = (0, split2_1.default)();
            (0, stream_1.pipeline)(output, new TrixOutputTransform(), out, function (err) {
                if (err) {
                    reject(err);
                }
            });
            yield (0, external_sorting_1.default)({
                input,
                output,
                tempDir,
            }).asc();
            resolve(true);
        }));
    });
}
function makeIx(inFile, outIndex) {
    return __awaiter(this, void 0, void 0, function* () {
        return makeIxStream(fs_1.default.createReadStream(inFile), outIndex);
    });
}
function getPrefix(word, prefixSize) {
    return word.slice(0, prefixSize).padEnd(prefixSize, ' ');
}
function optimizePrefixSize(inIx) {
    var _a, e_1, _b, _c;
    return __awaiter(this, void 0, void 0, function* () {
        let binSizeTotal = 0;
        let binCount = 0;
        let prefixSize = 5;
        for (; prefixSize < 40; prefixSize++) {
            const fileStream = fs_1.default.createReadStream(inIx);
            const rl = readline_1.default.createInterface({
                input: fileStream,
            });
            let lastPrefix;
            let writtenPrefix;
            let writtenPos = -binSize;
            let startPrefixPos = 0;
            let bytes = 0;
            let lastBin = 0;
            let maxBinSize = 0;
            try {
                for (var _d = true, rl_1 = (e_1 = void 0, __asyncValues(rl)), rl_1_1; rl_1_1 = yield rl_1.next(), _a = rl_1_1.done, !_a;) {
                    _c = rl_1_1.value;
                    _d = false;
                    try {
                        const line = _c;
                        const [word] = line.split(/\s/);
                        const curPrefix = getPrefix(word, prefixSize);
                        if (curPrefix !== lastPrefix) {
                            startPrefixPos = bytes;
                        }
                        if (bytes - writtenPos >= binSize && curPrefix !== writtenPrefix) {
                            const binSize = startPrefixPos - lastBin;
                            binSizeTotal += binSize;
                            maxBinSize = Math.max(binSize, maxBinSize);
                            binCount++;
                            lastBin = startPrefixPos;
                            writtenPos = bytes;
                            writtenPrefix = curPrefix;
                        }
                        lastPrefix = curPrefix;
                        bytes += line.length + 1;
                    }
                    finally {
                        _d = true;
                    }
                }
            }
            catch (e_1_1) { e_1 = { error: e_1_1 }; }
            finally {
                try {
                    if (!_d && !_a && (_b = rl_1.return)) yield _b.call(rl_1);
                }
                finally { if (e_1) throw e_1.error; }
            }
            const avgBinSize = binSizeTotal / binCount;
            // some heuristics. note: binSizeTotal===0 means everything was lumped into
            // one bin
            if ((binSizeTotal === 0 && bytes > binSize) ||
                avgBinSize > 3 * binSize ||
                maxBinSize > 10 * binSize) {
                continue;
            }
            else {
                break;
            }
        }
        return prefixSize;
    });
}
exports.optimizePrefixSize = optimizePrefixSize;
function makeIxx(inIx, outIxx, prefixSizeParam) {
    var _a, e_2, _b, _c;
    return __awaiter(this, void 0, void 0, function* () {
        const out = fs_1.default.createWriteStream(outIxx);
        const prefixSize = prefixSizeParam !== null && prefixSizeParam !== void 0 ? prefixSizeParam : (yield optimizePrefixSize(inIx));
        try {
            const fileStream = fs_1.default.createReadStream(inIx);
            const rl = readline_1.default.createInterface({
                input: fileStream,
            });
            let lastPrefix;
            let writtenPrefix;
            let writtenPos = -binSize;
            let startPrefixPos = 0;
            let bytes = 0;
            let lastBin = 0;
            try {
                for (var _d = true, rl_2 = __asyncValues(rl), rl_2_1; rl_2_1 = yield rl_2.next(), _a = rl_2_1.done, !_a;) {
                    _c = rl_2_1.value;
                    _d = false;
                    try {
                        const line = _c;
                        const [word] = line.split(/\s/);
                        const curPrefix = getPrefix(word, prefixSize);
                        if (curPrefix !== lastPrefix) {
                            startPrefixPos = bytes;
                        }
                        if (bytes - writtenPos >= binSize && curPrefix !== writtenPrefix) {
                            const res = out.write(`${curPrefix}${startPrefixPos
                                .toString(16)
                                .toUpperCase()
                                .padStart(10, '0')}\n`);
                            lastBin = startPrefixPos;
                            // Handle backpressure
                            // ref https://nodesource.com/blog/understanding-streams-in-nodejs/
                            if (!res) {
                                yield (0, events_1.once)(out, 'drain');
                            }
                            writtenPos = bytes;
                            writtenPrefix = curPrefix;
                        }
                        lastPrefix = curPrefix;
                        bytes += line.length + 1;
                    }
                    finally {
                        _d = true;
                    }
                }
            }
            catch (e_2_1) { e_2 = { error: e_2_1 }; }
            finally {
                try {
                    if (!_d && !_a && (_b = rl_2.return)) yield _b.call(rl_2);
                }
                finally { if (e_2) throw e_2.error; }
            }
        }
        finally {
            out.end();
            yield streamFinished(out);
        }
    });
}
exports.makeIxx = makeIxx;
function ixIxx(inText, outIx, outIxx, prefixSize) {
    return __awaiter(this, void 0, void 0, function* () {
        yield makeIx(inText, outIx);
        yield makeIxx(outIx, outIxx, prefixSize);
    });
}
exports.ixIxx = ixIxx;
function ixIxxStream(stream, outIx, outIxx, prefixSize) {
    return __awaiter(this, void 0, void 0, function* () {
        yield makeIxStream(stream, outIx);
        yield makeIxx(outIx, outIxx, prefixSize);
    });
}
exports.ixIxxStream = ixIxxStream;
//# sourceMappingURL=index.js.map