import { Transform } from 'stream';
import { StringDecoder as Decoder } from 'string_decoder';
import Parser from './parse';
import { formatItem, formatSequence, } from './util';
// call a callback on the next process tick if running in
// an environment that supports it
function _callback(callback) {
    if (process && process.nextTick)
        process.nextTick(callback);
    else
        callback();
}
// shared arg processing for the parse routines
function _processParseOptions(options) {
    const out = {
        encoding: 'utf8',
        parseFeatures: true,
        parseDirectives: false,
        parseSequences: true,
        parseComments: false,
        bufferSize: 1000,
        ...options,
    };
    if (options.parseAll) {
        out.parseFeatures = true;
        out.parseDirectives = true;
        out.parseComments = true;
        out.parseSequences = true;
    }
    return out;
}
class GFFTransform extends Transform {
    constructor(inputOptions = {}) {
        super({ objectMode: true });
        this.textBuffer = '';
        const options = _processParseOptions(inputOptions);
        this.encoding = inputOptions.encoding || 'utf8';
        this.decoder = new Decoder();
        const push = this.push.bind(this);
        this.parser = new Parser({
            featureCallback: options.parseFeatures ? push : undefined,
            directiveCallback: options.parseDirectives ? push : undefined,
            commentCallback: options.parseComments ? push : undefined,
            sequenceCallback: options.parseSequences ? push : undefined,
            errorCallback: (err) => this.emit('error', err),
            bufferSize: options.bufferSize,
        });
    }
    _addLine(data) {
        if (data) {
            this.parser.addLine(data);
        }
    }
    _nextText(buffer) {
        const pieces = (this.textBuffer + buffer).split(/\r?\n/);
        this.textBuffer = pieces.pop() || '';
        pieces.forEach((piece) => this._addLine(piece));
    }
    _transform(chunk, _encoding, callback) {
        this._nextText(this.decoder.write(chunk));
        _callback(callback);
    }
    _flush(callback) {
        if (this.decoder.end)
            this._nextText(this.decoder.end());
        if (this.textBuffer != null)
            this._addLine(this.textBuffer);
        this.parser.finish();
        _callback(callback);
    }
}
/**
 * Parse a stream of text data into a stream of feature, directive, comment,
 * an sequence objects.
 *
 * @param options - Parsing options
 * @returns stream (in objectMode) of parsed items
 */
export function parseStream(options = {}) {
    return new GFFTransform(options);
}
export function parseStringSync(str, inputOptions = {}) {
    if (!str)
        return [];
    const options = _processParseOptions(inputOptions);
    const items = [];
    const push = items.push.bind(items);
    const parser = new Parser({
        featureCallback: options.parseFeatures ? push : undefined,
        directiveCallback: options.parseDirectives ? push : undefined,
        commentCallback: options.parseComments ? push : undefined,
        sequenceCallback: options.parseSequences ? push : undefined,
        bufferSize: Infinity,
        errorCallback: (err) => {
            throw err;
        },
    });
    str.split(/\r?\n/).forEach(parser.addLine.bind(parser));
    parser.finish();
    return items;
}
/**
 * Format an array of GFF3 items (features,directives,comments) into string of
 * GFF3. Does not insert synchronization (###) marks.
 *
 * @param items - Array of features, directives, comments and/or sequences
 * @returns the formatted GFF3
 */
export function formatSync(items) {
    // sort items into seq and other
    const other = [];
    const sequences = [];
    items.forEach((i) => {
        if ('sequence' in i)
            sequences.push(i);
        else
            other.push(i);
    });
    let str = other.map(formatItem).join('');
    if (sequences.length) {
        str += '##FASTA\n';
        str += sequences.map(formatSequence).join('');
    }
    return str;
}
class FormattingTransform extends Transform {
    constructor(options = {}) {
        super(Object.assign(options, { objectMode: true }));
        this.linesSinceLastSyncMark = 0;
        this.haveWeEmittedData = false;
        this.fastaMode = false;
        this.minLinesBetweenSyncMarks = options.minSyncLines || 100;
        this.insertVersionDirective = options.insertVersionDirective || false;
    }
    _transform(chunk, _encoding, callback) {
        // if we have not emitted anything yet, and this first
        // chunk is not a gff-version directive, emit one
        let str;
        if (!this.haveWeEmittedData && this.insertVersionDirective) {
            const thisChunk = Array.isArray(chunk) ? chunk[0] : chunk;
            if ('directive' in thisChunk) {
                if (thisChunk.directive !== 'gff-version') {
                    this.push('##gff-version 3\n');
                }
            }
        }
        // if it's a sequence chunk coming down, emit a FASTA directive and
        // change to FASTA mode
        if ('sequence' in chunk && !this.fastaMode) {
            this.push('##FASTA\n');
            this.fastaMode = true;
        }
        if (Array.isArray(chunk))
            str = chunk.map(formatItem).join('');
        else
            str = formatItem(chunk);
        this.push(str);
        if (this.linesSinceLastSyncMark >= this.minLinesBetweenSyncMarks) {
            this.push('###\n');
            this.linesSinceLastSyncMark = 0;
        }
        else {
            // count the number of newlines in this chunk
            let count = 0;
            for (let i = 0; i < str.length; i += 1) {
                if (str[i] === '\n')
                    count += 1;
            }
            this.linesSinceLastSyncMark += count;
        }
        this.haveWeEmittedData = true;
        _callback(callback);
    }
}
/**
 * Format a stream of features, directives, comments and/or sequences into a
 * stream of GFF3 text.
 *
 * Inserts synchronization (###) marks automatically.
 *
 * @param options - parser options
 */
export function formatStream(options = {}) {
    return new FormattingTransform(options);
}
/**
 * Format a stream of features, directives, comments and/or sequences into a
 * GFF3 file and write it to the filesystem.

 * Inserts synchronization (###) marks and a ##gff-version
 * directive automatically (if one is not already present).
 *
 * @param stream - the stream to write to the file
 * @param filename - the file path to write to
 * @param options - parser options
 * @returns promise for null that resolves when the stream has been written
 */
export function formatFile(stream, writeStream, options = {}) {
    const newOptions = {
        insertVersionDirective: true,
        ...options,
    };
    return new Promise((resolve, reject) => {
        stream
            .pipe(new FormattingTransform(newOptions))
            .on('end', () => resolve(null))
            .on('error', reject)
            .pipe(writeStream);
    });
}
//# sourceMappingURL=api.js.map