"use strict";
var __await = (this && this.__await) || function (v) { return this instanceof __await ? (this.v = v, this) : new __await(v); }
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 __asyncDelegator = (this && this.__asyncDelegator) || function (o) {
    var i, p;
    return i = {}, verb("next"), verb("throw", function (e) { throw e; }), verb("return"), i[Symbol.iterator] = function () { return this; }, i;
    function verb(n, f) { i[n] = o[n] ? function (v) { return (p = !p) ? { value: __await(o[n](v)), done: n === "return" } : f ? f(v) : v; } : f; }
};
var __asyncGenerator = (this && this.__asyncGenerator) || function (thisArg, _arguments, generator) {
    if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined.");
    var g = generator.apply(thisArg, _arguments || []), i, q = [];
    return i = {}, verb("next"), verb("throw"), verb("return"), i[Symbol.asyncIterator] = function () { return this; }, i;
    function verb(n) { if (g[n]) i[n] = function (v) { return new Promise(function (a, b) { q.push([n, v, a, b]) > 1 || resume(n, v); }); }; }
    function resume(n, v) { try { step(g[n](v)); } catch (e) { settle(q[0][3], e); } }
    function step(r) { r.value instanceof __await ? Promise.resolve(r.value.v).then(fulfill, reject) : settle(q[0][2], r); }
    function fulfill(value) { resume("next", value); }
    function reject(value) { resume("throw", value); }
    function settle(f, v) { if (f(v), q.shift(), q.length) resume(q[0][0], q[0][1]); }
};
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const fs_1 = __importDefault(require("fs"));
const path_1 = __importDefault(require("path"));
const stream_1 = require("stream");
const ixixx_1 = require("ixixx");
const command_1 = require("@oclif/command");
// locals
const gff3Adapter_1 = require("../types/gff3Adapter");
const vcfAdapter_1 = require("../types/vcfAdapter");
const base_1 = __importDefault(require("../base"));
const common_1 = require("../types/common");
function readConf(path) {
    return JSON.parse(fs_1.default.readFileSync(path, 'utf8'));
}
function writeConf(obj, path) {
    fs_1.default.writeFileSync(path, JSON.stringify(obj, null, 2));
}
function getLoc(elt) {
    return elt.locationType === 'LocalPathLocation' ? elt.localPath : elt.uri;
}
class TextIndex extends base_1.default {
    async run() {
        const { flags } = this.parse(TextIndex);
        const { perTrack, file } = flags;
        if (file) {
            await this.indexFileList();
        }
        else if (perTrack) {
            await this.perTrackIndex();
        }
        else {
            await this.aggregateIndex();
        }
        this.log('Finished!');
    }
    async aggregateIndex() {
        var _a;
        const { flags } = this.parse(TextIndex);
        const { out, target, tracks, assemblies, attributes, quiet, force, exclude, dryrun, prefixSize, } = flags;
        const outFlag = target || out || '.';
        const isDir = fs_1.default.lstatSync(outFlag).isDirectory();
        const confPath = isDir ? path_1.default.join(outFlag, 'config.json') : outFlag;
        const outLocation = path_1.default.dirname(confPath);
        const config = readConf(confPath);
        const trixDir = path_1.default.join(outLocation, 'trix');
        if (!fs_1.default.existsSync(trixDir)) {
            fs_1.default.mkdirSync(trixDir);
        }
        const aggregateTextSearchAdapters = config.aggregateTextSearchAdapters || [];
        const asms = (assemblies === null || assemblies === void 0 ? void 0 : assemblies.split(',')) ||
            ((_a = config.assemblies) === null || _a === void 0 ? void 0 : _a.map(a => a.name)) ||
            (config.assembly ? [config.assembly.name] : []);
        if (!(asms === null || asms === void 0 ? void 0 : asms.length)) {
            throw new Error('No assemblies found');
        }
        for (const asm of asms) {
            const trackConfigs = await this.getTrackConfigs(confPath, tracks === null || tracks === void 0 ? void 0 : tracks.split(','), asm);
            if (!trackConfigs.length) {
                this.log('Indexing assembly ' + asm + '...(no tracks found)...');
                continue;
            }
            this.log('Indexing assembly ' + asm + '...');
            if (dryrun) {
                this.log(trackConfigs.map(e => `${e.trackId}\t${e.adapter.type}`).join('\n'));
            }
            else {
                const id = asm + '-index';
                const idx = aggregateTextSearchAdapters.findIndex(x => x.textSearchAdapterId === id);
                if (idx !== -1 && !force) {
                    this.log(`Note: ${asm} has already been indexed with this configuration, use --force to overwrite this assembly. Skipping for now`);
                    continue;
                }
                await this.indexDriver({
                    trackConfigs,
                    outLocation,
                    quiet,
                    name: asm,
                    attributes: attributes.split(','),
                    typesToExclude: exclude.split(','),
                    assemblyNames: [asm],
                    prefixSize,
                });
                const trixConf = {
                    type: 'TrixTextSearchAdapter',
                    textSearchAdapterId: id,
                    ixFilePath: {
                        uri: `trix/${asm}.ix`,
                        locationType: 'UriLocation',
                    },
                    ixxFilePath: {
                        uri: `trix/${asm}.ixx`,
                        locationType: 'UriLocation',
                    },
                    metaFilePath: {
                        uri: `trix/${asm}_meta.json`,
                        locationType: 'UriLocation',
                    },
                    assemblyNames: [asm],
                };
                if (idx === -1) {
                    aggregateTextSearchAdapters.push(trixConf);
                }
                else {
                    aggregateTextSearchAdapters[idx] = trixConf;
                }
            }
        }
        if (!dryrun) {
            writeConf(Object.assign(Object.assign({}, config), { aggregateTextSearchAdapters }), confPath);
        }
    }
    async perTrackIndex() {
        const { flags } = this.parse(TextIndex);
        const { out, target, tracks, assemblies, attributes, quiet, force, exclude, prefixSize, } = flags;
        const outFlag = target || out || '.';
        const isDir = fs_1.default.lstatSync(outFlag).isDirectory();
        const confFilePath = isDir ? path_1.default.join(outFlag, 'config.json') : outFlag;
        const outLocation = path_1.default.dirname(confFilePath);
        const config = readConf(confFilePath);
        const configTracks = config.tracks || [];
        const trixDir = path_1.default.join(outLocation, 'trix');
        if (!fs_1.default.existsSync(trixDir)) {
            fs_1.default.mkdirSync(trixDir);
        }
        if (assemblies) {
            throw new Error(`Can't specify assemblies when indexing per track, remove assemblies flag to continue.`);
        }
        const confs = await this.getTrackConfigs(confFilePath, tracks === null || tracks === void 0 ? void 0 : tracks.split(','));
        if (!confs.length) {
            throw new Error(`Tracks not found in config.json, please add track configurations before indexing.`);
        }
        for (const trackConfig of confs) {
            const { textSearching, trackId, assemblyNames } = trackConfig;
            if ((textSearching === null || textSearching === void 0 ? void 0 : textSearching.textSearchAdapter) && !force) {
                this.log(`Note: ${trackId} has already been indexed with this configuration, use --force to overwrite this track. Skipping for now`);
                continue;
            }
            this.log('Indexing track ' + trackId + '...');
            await this.indexDriver({
                trackConfigs: [trackConfig],
                attributes: attributes.split(','),
                outLocation,
                quiet,
                name: trackId,
                typesToExclude: exclude.split(','),
                assemblyNames,
                prefixSize,
            });
            if (!(textSearching === null || textSearching === void 0 ? void 0 : textSearching.textSearchAdapter)) {
                // modifies track with new text search adapter
                const index = configTracks.findIndex(track => trackId === track.trackId);
                if (index !== -1) {
                    configTracks[index] = Object.assign(Object.assign({}, trackConfig), { textSearching: Object.assign(Object.assign({}, textSearching), { textSearchAdapter: {
                                type: 'TrixTextSearchAdapter',
                                textSearchAdapterId: trackId + '-index',
                                ixFilePath: {
                                    uri: `trix/${trackId}.ix`,
                                    locationType: 'UriLocation',
                                },
                                ixxFilePath: {
                                    uri: `trix/${trackId}.ixx`,
                                    locationType: 'UriLocation',
                                },
                                metaFilePath: {
                                    uri: `trix/${trackId}_meta.json`,
                                    locationType: 'UriLocation',
                                },
                                assemblyNames: assemblyNames,
                            } }) });
                }
                else {
                    this.log("Error: can't find trackId");
                }
            }
            writeConf(Object.assign(Object.assign({}, config), { tracks: configTracks }), confFilePath);
        }
    }
    async indexFileList() {
        const { flags } = this.parse(TextIndex);
        const { out, target, fileId, file, attributes, quiet, exclude, prefixSize, } = flags;
        const outFlag = target || out || '.';
        const trixDir = path_1.default.join(outFlag, 'trix');
        if (!fs_1.default.existsSync(trixDir)) {
            fs_1.default.mkdirSync(trixDir);
        }
        const trackConfigs = file
            .map(file => (0, common_1.guessAdapterFromFileName)(file))
            .filter(fileConfig => (0, common_1.supported)(fileConfig.adapter.type));
        if (fileId === null || fileId === void 0 ? void 0 : fileId.length) {
            for (let i = 0; i < fileId.length; i++) {
                trackConfigs[i].trackId = fileId[i];
            }
        }
        await this.indexDriver({
            trackConfigs,
            outLocation: outFlag,
            name: trackConfigs.length > 1 ? 'aggregate' : path_1.default.basename(file[0]),
            quiet,
            attributes: attributes.split(','),
            typesToExclude: exclude.split(','),
            assemblyNames: [],
            prefixSize,
        });
        this.log('Successfully created index for these files. See https://jbrowse.org/storybook/lgv/main/?path=/story/text-searching--page for info about usage');
    }
    async indexDriver({ trackConfigs, attributes, outLocation, name, quiet, typesToExclude, assemblyNames, prefixSize, }) {
        const readStream = stream_1.Readable.from(this.indexFiles({
            trackConfigs,
            attributes,
            outLocation,
            quiet,
            typesToExclude,
        }));
        const ixIxxStream = await this.runIxIxx({
            readStream,
            outLocation,
            name,
            prefixSize,
        });
        await (0, common_1.generateMeta)({
            trackConfigs,
            attributes,
            outLocation,
            name,
            typesToExclude,
            assemblyNames,
        });
        return ixIxxStream;
    }
    indexFiles({ trackConfigs, attributes, outLocation, quiet, typesToExclude, }) {
        return __asyncGenerator(this, arguments, function* indexFiles_1() {
            for (const config of trackConfigs) {
                const { adapter, textSearching } = config;
                const { type } = adapter;
                const { indexingFeatureTypesToExclude = typesToExclude, indexingAttributes = attributes, } = textSearching || {};
                let loc;
                if (type === 'Gff3TabixAdapter') {
                    loc = adapter.gffGzLocation;
                }
                else if (type === 'Gff3Adapter') {
                    loc = adapter.gffLocation;
                }
                else if (type === 'VcfAdapter') {
                    loc = adapter.vcfLocation;
                }
                else if (type === 'VcfTabixAdapter') {
                    loc = adapter.vcfGzLocation;
                }
                if (!loc) {
                    return yield __await(void 0);
                }
                if (type === 'Gff3TabixAdapter' || type === 'Gff3Adapter') {
                    yield __await(yield* __asyncDelegator(__asyncValues((0, gff3Adapter_1.indexGff3)({
                        config,
                        attributesToIndex: indexingAttributes,
                        inLocation: getLoc(loc),
                        outLocation,
                        typesToExclude: indexingFeatureTypesToExclude,
                        quiet,
                    }))));
                }
                else if (type === 'VcfTabixAdapter' || type === 'VcfAdapter') {
                    yield __await(yield* __asyncDelegator(__asyncValues((0, vcfAdapter_1.indexVcf)({
                        config,
                        attributesToIndex: indexingAttributes,
                        inLocation: getLoc(loc),
                        outLocation,
                        typesToExclude: indexingFeatureTypesToExclude,
                        quiet,
                    }))));
                }
            }
        });
    }
    runIxIxx({ readStream, outLocation, name, prefixSize, }) {
        return (0, ixixx_1.ixIxxStream)(readStream, path_1.default.join(outLocation, 'trix', `${name}.ix`), path_1.default.join(outLocation, 'trix', `${name}.ixx`), prefixSize);
    }
    async getTrackConfigs(configPath, trackIds, assemblyName) {
        const { tracks } = readConf(configPath);
        if (!tracks) {
            return [];
        }
        const trackIdsToIndex = trackIds || (tracks === null || tracks === void 0 ? void 0 : tracks.map(track => track.trackId));
        return trackIdsToIndex
            .map(trackId => {
            const currentTrack = tracks.find(t => trackId === t.trackId);
            if (!currentTrack) {
                throw new Error(`Track not found in config.json for trackId ${trackId}, please add track configuration before indexing.`);
            }
            return currentTrack;
        })
            .filter(track => { var _a; return (0, common_1.supported)((_a = track.adapter) === null || _a === void 0 ? void 0 : _a.type); })
            .filter(track => assemblyName ? track.assemblyNames.includes(assemblyName) : true);
    }
}
exports.default = TextIndex;
TextIndex.description = 'Make a text-indexing file for any given track(s).';
TextIndex.examples = [
    "# indexes all tracks that it can find in the current directory's config.json",
    '$ jbrowse text-index',
    '',
    "# indexes specific trackIds that it can find in the current directory's config.json",
    '$ jbrowse text-index --tracks=track1,track2,track3',
    '',
    "# indexes all tracks in a directory's config.json or in a specific config file",
    '$ jbrowse text-index --out /path/to/jb2/',
    '',
    '# indexes only a specific assembly, and overwrite what was previously there using force (which is needed if a previous index already existed)',
    '$ jbrowse text-index -a hg19 --force',
    '',
    '# create index for some files for use in @jbrowse/react-linear-genome-view or similar',
    '$ jbrowse text-index --file myfile.gff3.gz --file myfile.vcfgz --out indexes',
];
TextIndex.flags = {
    help: command_1.flags.help({ char: 'h' }),
    tracks: command_1.flags.string({
        description: `Specific tracks to index, formatted as comma separated trackIds. If unspecified, indexes all available tracks`,
    }),
    target: command_1.flags.string({
        description: 'Path to config file in JB2 installation directory to read from.',
    }),
    out: command_1.flags.string({
        description: 'Synonym for target',
    }),
    attributes: command_1.flags.string({
        description: 'Comma separated list of attributes to index',
        default: 'Name,ID',
    }),
    assemblies: command_1.flags.string({
        char: 'a',
        description: 'Specify the assembl(ies) to create an index for. If unspecified, creates an index for each assembly in the config',
    }),
    force: command_1.flags.boolean({
        default: false,
        description: 'Overwrite previously existing indexes',
    }),
    quiet: command_1.flags.boolean({
        char: 'q',
        default: false,
        description: 'Hide the progress bars',
    }),
    perTrack: command_1.flags.boolean({
        default: false,
        description: 'If set, creates an index per track',
    }),
    exclude: command_1.flags.string({
        description: 'Adds gene type to list of excluded types',
        default: 'CDS,exon',
    }),
    prefixSize: command_1.flags.integer({
        description: 'Specify the prefix size for the ixx index. We attempt to automatically calculate this, but you can manually specify this too. If many genes have similar gene IDs e.g. Z000000001, Z000000002 the prefix size should be larger so that they get split into different bins',
    }),
    file: command_1.flags.string({
        description: 'File or files to index (can be used to create trix indexes for embedded component use cases not using a config.json for example)',
        multiple: true,
    }),
    fileId: command_1.flags.string({
        description: 'Set the trackId used for the indexes generated with the --file argument',
        multiple: true,
    }),
    dryrun: command_1.flags.boolean({
        description: 'Just print out tracks that will be indexed by the process, without doing any indexing',
    }),
};
