/******************************************************************************
 * Copyright 2021 TypeFox GmbH
 * This program and the accompanying materials are made available under the
 * terms of the MIT License, which is available in the project root.
 ******************************************************************************/
import { CompositeGeneratorNode, IndentNode, NewLineNode } from './generator-node.js';
import { getSourceRegion } from './generator-tracing.js';
class Context {
    constructor(defaultIndent) {
        this.defaultIndentation = '    ';
        this.pendingIndent = true;
        this.currentIndents = [];
        this.recentNonImmediateIndents = [];
        this.traceData = [];
        this.lines = [[]];
        this.length = 0;
        if (typeof defaultIndent === 'string') {
            this.defaultIndentation = defaultIndent;
        }
        else if (typeof defaultIndent === 'number') {
            this.defaultIndentation = ''.padStart(defaultIndent);
        }
    }
    get content() {
        return this.lines.map(e => e.join('')).join('');
    }
    get contentLength() {
        return this.length;
    }
    get currentLineNumber() {
        return this.lines.length - 1;
    }
    get currentLineContent() {
        return this.lines[this.currentLineNumber].join('');
    }
    get currentPosition() {
        return {
            offset: this.contentLength,
            line: this.currentLineNumber,
            character: this.currentLineContent.length
        };
    }
    append(value, isIndent) {
        if (value.length > 0) {
            const beforePos = isIndent && this.currentPosition;
            this.lines[this.currentLineNumber].push(value);
            this.length += value.length;
            if (beforePos) {
                this.indentPendingTraceRegions(beforePos);
            }
        }
    }
    indentPendingTraceRegions(before) {
        for (let i = this.traceData.length - 1; i >= 0; i--) {
            const tr = this.traceData[i];
            if (tr.targetStart && tr.targetStart.offset === before.offset /* tr.targetStart.line == before.line && tr.targetStart.character === before.character*/)
                tr.targetStart = this.currentPosition;
        }
    }
    increaseIndent(node) {
        this.currentIndents.push(node);
        if (!node.indentImmediately) {
            this.recentNonImmediateIndents.push(node);
        }
    }
    decreaseIndent() {
        this.currentIndents.pop();
    }
    get relevantIndents() {
        return this.currentIndents.filter(i => !this.recentNonImmediateIndents.includes(i));
    }
    resetCurrentLine() {
        this.length -= this.lines[this.currentLineNumber].join('').length;
        this.lines[this.currentLineNumber] = [];
        this.pendingIndent = true;
        this.recentNonImmediateIndents.length = 0;
    }
    addNewLine() {
        this.lines.push([]);
        this.pendingIndent = true;
        this.recentNonImmediateIndents.length = 0;
    }
    pushTraceRegion(sourceRegion) {
        const region = createTraceRegion(sourceRegion, this.currentPosition, it => this.traceData[this.traceData.length - 1]?.children?.push(it));
        this.traceData.push(region);
        return region;
    }
    popTraceRegion(expected) {
        const traceRegion = this.traceData.pop();
        // the following assertion can be dropped once the tracing is considered stable
        this.assertTrue(traceRegion === expected, 'Trace region mismatch!');
        return traceRegion;
    }
    getParentTraceSourceFileURI() {
        for (let i = this.traceData.length - 1; i > -1; i--) {
            const fileUri = this.traceData[i].sourceRegion?.fileURI;
            if (fileUri)
                return fileUri;
        }
        return undefined;
    }
    assertTrue(condition, msg) {
        if (!condition) {
            throw new Error(msg);
        }
    }
}
function createTraceRegion(sourceRegion, targetStart, accept) {
    const result = {
        sourceRegion,
        targetRegion: undefined,
        children: [],
        targetStart,
        complete: (targetEnd) => {
            result.targetRegion = {
                offset: result.targetStart.offset,
                end: targetEnd.offset,
                length: targetEnd.offset - result.targetStart.offset,
                range: {
                    start: {
                        line: result.targetStart.line,
                        character: result.targetStart.character
                    },
                    end: {
                        line: targetEnd.line,
                        character: targetEnd.character
                    },
                }
            };
            delete result.targetStart;
            if (result.children?.length === 0) {
                delete result.children;
            }
            if (result.targetRegion?.length) {
                accept(result);
            }
            delete result.complete;
            return result;
        }
    };
    return result;
}
export function processGeneratorNode(node, defaultIndentation) {
    const context = new Context(defaultIndentation);
    const trace = context.pushTraceRegion(undefined);
    processNodeInternal(node, context);
    context.popTraceRegion(trace);
    trace.complete && trace.complete(context.currentPosition);
    const singleChild = trace.children && trace.children.length === 1 ? trace.children[0] : undefined;
    const singleChildTargetRegion = singleChild?.targetRegion;
    const rootTargetRegion = trace.targetRegion;
    if (singleChildTargetRegion && singleChild.sourceRegion
        && singleChildTargetRegion.offset === rootTargetRegion.offset
        && singleChildTargetRegion.length === rootTargetRegion.length) {
        // some optimization:
        // if (the root) `node` is traced (`singleChild.sourceRegion` !== undefined) and spans the entire `context.content`
        //  we skip the wrapping root trace object created above at the beginning of this method
        return { text: context.content, trace: singleChild };
    }
    else {
        return { text: context.content, trace };
    }
}
function processNodeInternal(node, context) {
    if (typeof (node) === 'string') {
        processStringNode(node, context);
    }
    else if (node instanceof IndentNode) {
        processIndentNode(node, context);
    }
    else if (node instanceof CompositeGeneratorNode) {
        processCompositeNode(node, context);
    }
    else if (node instanceof NewLineNode) {
        processNewLineNode(node, context);
    }
}
function hasContent(node, ctx) {
    if (typeof (node) === 'string') {
        return node.length !== 0; // cs: do not ignore ws only content here, enclosed within other nodes it will matter!
    }
    else if (node instanceof CompositeGeneratorNode) {
        return node.contents.some(e => hasContent(e, ctx));
    }
    else if (node instanceof NewLineNode) {
        return !(node.ifNotEmpty && ctx.currentLineContent.length === 0);
    }
    else {
        return false;
    }
}
function processStringNode(node, context) {
    if (node) {
        handlePendingIndent(context, false);
        context.append(node);
    }
}
function handlePendingIndent(ctx, endOfLine) {
    if (ctx.pendingIndent) {
        let indent = '';
        for (const indentNode of ctx.relevantIndents.filter(e => e.indentEmptyLines || !endOfLine)) {
            indent += indentNode.indentation ?? ctx.defaultIndentation;
        }
        ctx.append(indent, true);
        ctx.pendingIndent = false;
    }
}
function processCompositeNode(node, context) {
    let traceRegion = undefined;
    const sourceRegion = getSourceRegion(node.tracedSource);
    if (sourceRegion) {
        traceRegion = context.pushTraceRegion(sourceRegion);
    }
    for (const child of node.contents) {
        processNodeInternal(child, context);
    }
    if (traceRegion) {
        context.popTraceRegion(traceRegion);
        const parentsFileURI = context.getParentTraceSourceFileURI();
        if (parentsFileURI && sourceRegion?.fileURI === parentsFileURI) {
            // if some parent's sourceRegion refers to the same source file uri (and no other source file was referenced inbetween)
            // we can drop the file uri in order to reduce repeated strings
            delete sourceRegion.fileURI;
        }
        traceRegion.complete && traceRegion.complete(context.currentPosition);
    }
}
function processIndentNode(node, context) {
    if (hasContent(node, context)) {
        if (node.indentImmediately && !context.pendingIndent) {
            context.append(node.indentation ?? context.defaultIndentation, true);
        }
        try {
            context.increaseIndent(node);
            processCompositeNode(node, context);
        }
        finally {
            context.decreaseIndent();
        }
    }
}
function processNewLineNode(node, context) {
    if (node.ifNotEmpty && !hasNonWhitespace(context.currentLineContent)) {
        context.resetCurrentLine();
    }
    else {
        handlePendingIndent(context, true);
        let count = node.count;
        while (count-- > 0) {
            context.append(node.lineDelimiter);
            context.addNewLine();
        }
    }
}
function hasNonWhitespace(text) {
    return text.trimStart() !== '';
}
//# sourceMappingURL=node-processor.js.map