import * as documents from "./_namespaces/documents.js";
import { Compiler } from "./_namespaces/Harness.js";
import * as ts from "./_namespaces/ts.js";
import * as Utils from "./_namespaces/Utils.js";

interface SourceMapSpanWithDecodeErrors {
    sourceMapSpan: ts.Mapping;
    decodeErrors: string[] | undefined;
}

namespace SourceMapDecoder {
    let sourceMapMappings: string;
    let decodingIndex: number;
    let mappings: ts.MappingsDecoder | undefined;

    export interface DecodedMapping {
        sourceMapSpan: ts.Mapping;
        error?: string;
    }

    export function initializeSourceMapDecoding(sourceMap: ts.RawSourceMap) {
        decodingIndex = 0;
        sourceMapMappings = sourceMap.mappings;
        mappings = ts.decodeMappings(sourceMap.mappings);
    }

    export function decodeNextEncodedSourceMapSpan(): DecodedMapping {
        if (!mappings) return ts.Debug.fail("not initialized");
        const result = mappings.next();
        if (result.done) return { error: mappings.error || "No encoded entry found", sourceMapSpan: mappings.state };
        return { sourceMapSpan: result.value };
    }

    export function hasCompletedDecoding() {
        if (!mappings) return ts.Debug.fail("not initialized");
        return mappings.pos === sourceMapMappings.length;
    }

    export function getRemainingDecodeString() {
        return sourceMapMappings.substr(decodingIndex);
    }
}

namespace SourceMapSpanWriter {
    let sourceMapRecorder: Compiler.WriterAggregator;
    let sourceMapSources: string[];
    let sourceMapNames: string[] | null | undefined; // eslint-disable-line no-restricted-syntax

    let jsFile: documents.TextDocument;
    let jsLineMap: readonly number[];
    let tsCode: string;
    let tsLineMap: number[];

    let spansOnSingleLine: SourceMapSpanWithDecodeErrors[];
    let prevWrittenSourcePos: number;
    let nextJsLineToWrite: number;
    let spanMarkerContinues: boolean;

    export function initializeSourceMapSpanWriter(sourceMapRecordWriter: Compiler.WriterAggregator, sourceMap: ts.RawSourceMap, currentJsFile: documents.TextDocument) {
        sourceMapRecorder = sourceMapRecordWriter;
        sourceMapSources = sourceMap.sources;
        sourceMapNames = sourceMap.names;

        jsFile = currentJsFile;
        jsLineMap = jsFile.lineStarts;

        spansOnSingleLine = [];
        prevWrittenSourcePos = 0;
        nextJsLineToWrite = 0;
        spanMarkerContinues = false;

        SourceMapDecoder.initializeSourceMapDecoding(sourceMap);
        sourceMapRecorder.WriteLine("===================================================================");
        sourceMapRecorder.WriteLine("JsFile: " + sourceMap.file);
        sourceMapRecorder.WriteLine("mapUrl: " + ts.tryGetSourceMappingURL(ts.getLineInfo(jsFile.text, jsLineMap)));
        sourceMapRecorder.WriteLine("sourceRoot: " + sourceMap.sourceRoot);
        sourceMapRecorder.WriteLine("sources: " + sourceMap.sources);
        if (sourceMap.sourcesContent) {
            sourceMapRecorder.WriteLine("sourcesContent: " + JSON.stringify(sourceMap.sourcesContent));
        }
        sourceMapRecorder.WriteLine("===================================================================");
    }

    function getSourceMapSpanString(mapEntry: ts.Mapping, getAbsentNameIndex?: boolean) {
        let mapString = "Emitted(" + (mapEntry.generatedLine + 1) + ", " + (mapEntry.generatedCharacter + 1) + ")";
        if (ts.isSourceMapping(mapEntry)) {
            mapString += " Source(" + (mapEntry.sourceLine + 1) + ", " + (mapEntry.sourceCharacter + 1) + ") + SourceIndex(" + mapEntry.sourceIndex + ")";
            if (mapEntry.nameIndex! >= 0 && mapEntry.nameIndex! < sourceMapNames!.length) {
                mapString += " name (" + sourceMapNames![mapEntry.nameIndex!] + ")";
            }
            else {
                if ((mapEntry.nameIndex && mapEntry.nameIndex !== -1) || getAbsentNameIndex) {
                    mapString += " nameIndex (" + mapEntry.nameIndex + ")";
                }
            }
        }

        return mapString;
    }

    export function recordSourceMapSpan(sourceMapSpan: ts.Mapping) {
        // verify the decoded span is same as the new span
        const decodeResult = SourceMapDecoder.decodeNextEncodedSourceMapSpan();
        let decodeErrors: string[] | undefined;
        if (typeof decodeResult.error === "string" || !ts.sameMapping(decodeResult.sourceMapSpan, sourceMapSpan)) {
            if (decodeResult.error) {
                decodeErrors = ["!!^^ !!^^ There was decoding error in the sourcemap at this location: " + decodeResult.error];
            }
            else {
                decodeErrors = ["!!^^ !!^^ The decoded span from sourcemap's mapping entry does not match what was encoded for this span:"];
            }
            decodeErrors.push("!!^^ !!^^ Decoded span from sourcemap's mappings entry: " + getSourceMapSpanString(decodeResult.sourceMapSpan, /*getAbsentNameIndex*/ true) + " Span encoded by the emitter:" + getSourceMapSpanString(sourceMapSpan, /*getAbsentNameIndex*/ true));
        }

        if (spansOnSingleLine.length && spansOnSingleLine[0].sourceMapSpan.generatedLine !== sourceMapSpan.generatedLine) {
            // On different line from the one that we have been recording till now,
            writeRecordedSpans();
            spansOnSingleLine = [];
        }
        spansOnSingleLine.push({ sourceMapSpan, decodeErrors });
    }

    export function recordNewSourceFileSpan(sourceMapSpan: ts.Mapping, newSourceFileCode: string) {
        let continuesLine = false;
        if (spansOnSingleLine.length > 0 && spansOnSingleLine[0].sourceMapSpan.generatedCharacter === sourceMapSpan.generatedLine) {
            writeRecordedSpans();
            spansOnSingleLine = [];
            nextJsLineToWrite--; // walk back one line to reprint the line
            continuesLine = true;
        }

        recordSourceMapSpan(sourceMapSpan);

        assert.isTrue(spansOnSingleLine.length === 1);
        sourceMapRecorder.WriteLine("-------------------------------------------------------------------");
        sourceMapRecorder.WriteLine("emittedFile:" + jsFile.file + (continuesLine ? ` (${sourceMapSpan.generatedLine + 1}, ${sourceMapSpan.generatedCharacter + 1})` : ""));
        sourceMapRecorder.WriteLine("sourceFile:" + sourceMapSources[spansOnSingleLine[0].sourceMapSpan.sourceIndex!]);
        sourceMapRecorder.WriteLine("-------------------------------------------------------------------");

        tsLineMap = ts.computeLineStarts(newSourceFileCode);
        tsCode = newSourceFileCode;
        prevWrittenSourcePos = 0;
    }

    export function close() {
        // Write the lines pending on the single line
        writeRecordedSpans();

        if (!SourceMapDecoder.hasCompletedDecoding()) {
            sourceMapRecorder.WriteLine("!!!! **** There are more source map entries in the sourceMap's mapping than what was encoded");
            sourceMapRecorder.WriteLine("!!!! **** Remaining decoded string: " + SourceMapDecoder.getRemainingDecodeString());
        }

        // write remaining js lines
        writeJsFileLines(jsLineMap.length);
    }

    function getTextOfLine(line: number, lineMap: readonly number[], code: string) {
        const startPos = lineMap[line];
        const endPos = lineMap[line + 1];
        const text = code.substring(startPos, endPos);
        return line === 0 ? Utils.removeByteOrderMark(text) : text;
    }

    function writeJsFileLines(endJsLine: number) {
        for (; nextJsLineToWrite < endJsLine; nextJsLineToWrite++) {
            sourceMapRecorder.Write(">>>" + getTextOfLine(nextJsLineToWrite, jsLineMap, jsFile.text));
        }
    }

    function writeRecordedSpans() {
        const markerIds: string[] = [];

        function getMarkerId(markerIndex: number) {
            let markerId = "";
            if (spanMarkerContinues) {
                assert.isTrue(markerIndex === 0);
                markerId = "1->";
            }
            else {
                markerId = "" + (markerIndex + 1);
                if (markerId.length < 2) {
                    markerId = markerId + " ";
                }
                markerId += ">";
            }
            return markerId;
        }

        let prevEmittedCol!: number;
        function iterateSpans(fn: (currentSpan: SourceMapSpanWithDecodeErrors, index: number) => void) {
            prevEmittedCol = 0;
            for (let i = 0; i < spansOnSingleLine.length; i++) {
                fn(spansOnSingleLine[i], i);
                prevEmittedCol = spansOnSingleLine[i].sourceMapSpan.generatedCharacter;
            }
        }

        function writeSourceMapIndent(indentLength: number, indentPrefix: string) {
            sourceMapRecorder.Write(indentPrefix);
            for (let i = 0; i < indentLength; i++) {
                sourceMapRecorder.Write(" ");
            }
        }

        function writeSourceMapMarker(currentSpan: SourceMapSpanWithDecodeErrors, index: number, endColumn = currentSpan.sourceMapSpan.generatedCharacter, endContinues = false) {
            const markerId = getMarkerId(index);
            markerIds.push(markerId);

            writeSourceMapIndent(prevEmittedCol, markerId);

            for (let i = prevEmittedCol; i < endColumn; i++) {
                sourceMapRecorder.Write("^");
            }
            if (endContinues) {
                sourceMapRecorder.Write("->");
            }
            sourceMapRecorder.WriteLine("");
            spanMarkerContinues = endContinues;
        }

        function writeSourceMapSourceText(currentSpan: SourceMapSpanWithDecodeErrors, index: number) {
            const sourcePos = tsLineMap[currentSpan.sourceMapSpan.sourceLine!] + (currentSpan.sourceMapSpan.sourceCharacter!);
            let sourceText = "";
            if (prevWrittenSourcePos < sourcePos) {
                // Position that goes forward, get text
                sourceText = tsCode.substring(prevWrittenSourcePos, sourcePos);
            }

            if (currentSpan.decodeErrors) {
                // If there are decode errors, write
                for (const decodeError of currentSpan.decodeErrors) {
                    writeSourceMapIndent(prevEmittedCol, markerIds[index]);
                    sourceMapRecorder.WriteLine(decodeError);
                }
            }

            const tsCodeLineMap = ts.computeLineStarts(sourceText);
            for (let i = 0; i < tsCodeLineMap.length; i++) {
                writeSourceMapIndent(prevEmittedCol, i === 0 ? markerIds[index] : "  >");
                sourceMapRecorder.Write(getTextOfLine(i, tsCodeLineMap, sourceText));
                if (i === tsCodeLineMap.length - 1) {
                    sourceMapRecorder.WriteLine("");
                }
            }

            prevWrittenSourcePos = sourcePos;
        }

        function writeSpanDetails(currentSpan: SourceMapSpanWithDecodeErrors, index: number) {
            sourceMapRecorder.WriteLine(markerIds[index] + getSourceMapSpanString(currentSpan.sourceMapSpan));
        }

        if (spansOnSingleLine.length) {
            const currentJsLine = spansOnSingleLine[0].sourceMapSpan.generatedLine;

            // Write js line
            writeJsFileLines(currentJsLine + 1);

            // Emit markers
            iterateSpans(writeSourceMapMarker);

            const jsFileText = getTextOfLine(currentJsLine + 1, jsLineMap, jsFile.text);
            if (prevEmittedCol < jsFileText.length - 1) {
                // There is remaining text on this line that will be part of next source span so write marker that continues
                writeSourceMapMarker(/*currentSpan*/ undefined!, spansOnSingleLine.length, /*endColumn*/ jsFileText.length - 1, /*endContinues*/ true); // TODO: GH#18217
            }

            // Emit Source text
            iterateSpans(writeSourceMapSourceText);

            // Emit column number etc
            iterateSpans(writeSpanDetails);

            sourceMapRecorder.WriteLine("---");
        }
    }
}

export function getSourceMapRecord(sourceMapDataList: readonly ts.SourceMapEmitResult[], program: ts.Program, jsFiles: readonly documents.TextDocument[], declarationFiles: readonly documents.TextDocument[]): string {
    const sourceMapRecorder = new Compiler.WriterAggregator();

    for (let i = 0; i < sourceMapDataList.length; i++) {
        const sourceMapData = sourceMapDataList[i];
        let prevSourceFile: ts.SourceFile | undefined;
        let currentFile: documents.TextDocument;
        if (ts.isDeclarationFileName(sourceMapData.sourceMap.file)) {
            if (sourceMapDataList.length > jsFiles.length) {
                currentFile = declarationFiles[Math.floor(i / 2)]; // When both kinds of source map are present, they alternate js/dts
            }
            else {
                currentFile = declarationFiles[i];
            }
        }
        else {
            if (sourceMapDataList.length > jsFiles.length) {
                currentFile = jsFiles[Math.floor(i / 2)];
            }
            else {
                currentFile = jsFiles[i];
            }
        }

        SourceMapSpanWriter.initializeSourceMapSpanWriter(sourceMapRecorder, sourceMapData.sourceMap, currentFile);
        const mapper = ts.decodeMappings(sourceMapData.sourceMap.mappings);
        for (let iterResult = mapper.next(); !iterResult.done; iterResult = mapper.next()) {
            const decodedSourceMapping = iterResult.value;
            const currentSourceFile = ts.isSourceMapping(decodedSourceMapping)
                ? program.getSourceFile(sourceMapData.inputSourceFileNames[decodedSourceMapping.sourceIndex])
                : undefined;
            if (currentSourceFile !== prevSourceFile) {
                if (currentSourceFile) {
                    SourceMapSpanWriter.recordNewSourceFileSpan(decodedSourceMapping, currentSourceFile.text);
                }
                prevSourceFile = currentSourceFile;
            }
            else {
                SourceMapSpanWriter.recordSourceMapSpan(decodedSourceMapping);
            }
        }
        SourceMapSpanWriter.close(); // If the last spans werent emitted, emit them
    }
    sourceMapRecorder.Close();
    return sourceMapRecorder.lines.join("\r\n");
}

export function getSourceMapRecordWithSystem(sys: ts.System, sourceMapFile: string): string {
    const sourceMapRecorder = new Compiler.WriterAggregator();
    let prevSourceFile: documents.TextDocument | undefined;
    const files = new Map<string, documents.TextDocument>();
    const sourceMap = ts.tryParseRawSourceMap(sys.readFile(sourceMapFile, "utf8")!);
    if (sourceMap) {
        const mapDirectory = ts.getDirectoryPath(sourceMapFile);
        const sourceRoot = sourceMap.sourceRoot ? ts.getNormalizedAbsolutePath(sourceMap.sourceRoot, mapDirectory) : mapDirectory;
        const generatedAbsoluteFilePath = ts.getNormalizedAbsolutePath(sourceMap.file, mapDirectory);
        const sourceFileAbsolutePaths = sourceMap.sources.map(source => ts.getNormalizedAbsolutePath(source, sourceRoot));
        const currentFile = getFile(generatedAbsoluteFilePath);

        SourceMapSpanWriter.initializeSourceMapSpanWriter(sourceMapRecorder, sourceMap, currentFile);
        const mapper = ts.decodeMappings(sourceMap.mappings);
        for (let iterResult = mapper.next(); !iterResult.done; iterResult = mapper.next()) {
            const decodedSourceMapping = iterResult.value;
            const currentSourceFile = ts.isSourceMapping(decodedSourceMapping)
                ? getFile(sourceFileAbsolutePaths[decodedSourceMapping.sourceIndex])
                : undefined;
            if (currentSourceFile !== prevSourceFile) {
                if (currentSourceFile) {
                    SourceMapSpanWriter.recordNewSourceFileSpan(decodedSourceMapping, currentSourceFile.text);
                }
                prevSourceFile = currentSourceFile;
            }
            else {
                SourceMapSpanWriter.recordSourceMapSpan(decodedSourceMapping);
            }
        }
        SourceMapSpanWriter.close(); // If the last spans werent emitted, emit them
    }
    sourceMapRecorder.Close();
    return sourceMapRecorder.lines.join("\r\n");

    function getFile(path: string) {
        const existing = files.get(path);
        if (existing) return existing;
        const value = new documents.TextDocument(path, sys.readFile(path, "utf8")!);
        files.set(path, value);
        return value;
    }
}
