"use strict";
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
    if (mod && mod.__esModule) return mod;
    var result = {};
    if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
    __setModuleDefault(result, mod);
    return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.SourceMapper = void 0;
const path = __importStar(require("path"));
const source_map_1 = require("source-map");
const node_core_library_1 = require("@rushstack/node-core-library");
class SourceMapper {
    constructor() {
        // Map from .d.ts file path --> ISourceMap if a source map was found, or null if not found
        this._sourceMapByFilePath = new Map();
        // Cache the FileSystem.exists() result for mapped .ts files
        this._originalFileInfoByPath = new Map();
    }
    /**
     * Given a `.d.ts` source file and a specific position within the file, return the corresponding
     * `ISourceLocation`.
     */
    getSourceLocation(options) {
        const lineAndCharacter = options.sourceFile.getLineAndCharacterOfPosition(options.pos);
        const sourceLocation = {
            sourceFilePath: options.sourceFile.fileName,
            sourceFileLine: lineAndCharacter.line + 1,
            sourceFileColumn: lineAndCharacter.character + 1
        };
        if (options.useDtsLocation) {
            return sourceLocation;
        }
        const mappedSourceLocation = this._getMappedSourceLocation(sourceLocation);
        return mappedSourceLocation || sourceLocation;
    }
    _getMappedSourceLocation(sourceLocation) {
        const { sourceFilePath, sourceFileLine, sourceFileColumn } = sourceLocation;
        if (!node_core_library_1.FileSystem.exists(sourceFilePath)) {
            // Sanity check
            throw new node_core_library_1.InternalError('The referenced path was not found: ' + sourceFilePath);
        }
        const sourceMap = this._getSourceMap(sourceFilePath);
        if (!sourceMap)
            return;
        const nearestMappingItem = SourceMapper._findNearestMappingItem(sourceMap.mappingItems, {
            line: sourceFileLine,
            column: sourceFileColumn
        });
        if (!nearestMappingItem)
            return;
        const mappedFilePath = path.resolve(path.dirname(sourceFilePath), nearestMappingItem.source);
        // Does the mapped filename exist?  Use a cache to remember the answer.
        let originalFileInfo = this._originalFileInfoByPath.get(mappedFilePath);
        if (originalFileInfo === undefined) {
            originalFileInfo = {
                fileExists: node_core_library_1.FileSystem.exists(mappedFilePath),
                maxColumnForLine: []
            };
            if (originalFileInfo.fileExists) {
                // Read the file and measure the length of each line
                originalFileInfo.maxColumnForLine = node_core_library_1.FileSystem.readFile(mappedFilePath, {
                    convertLineEndings: node_core_library_1.NewlineKind.Lf
                })
                    .split('\n')
                    .map((x) => x.length + 1); // +1 since columns are 1-based
                originalFileInfo.maxColumnForLine.unshift(0); // Extra item since lines are 1-based
            }
            this._originalFileInfoByPath.set(mappedFilePath, originalFileInfo);
        }
        // Don't translate coordinates to a file that doesn't exist
        if (!originalFileInfo.fileExists)
            return;
        // The nearestMappingItem anchor may be above/left of the real position, due to gaps in the mapping.  Calculate
        // the delta and apply it to the original position.
        const guessedPosition = {
            line: nearestMappingItem.originalLine + sourceFileLine - nearestMappingItem.generatedLine,
            column: nearestMappingItem.originalColumn + sourceFileColumn - nearestMappingItem.generatedColumn
        };
        // Verify that the result is not out of bounds, in cause our heuristic failed
        if (guessedPosition.line >= 1 &&
            guessedPosition.line < originalFileInfo.maxColumnForLine.length &&
            guessedPosition.column >= 1 &&
            guessedPosition.column <= originalFileInfo.maxColumnForLine[guessedPosition.line]) {
            return {
                sourceFilePath: mappedFilePath,
                sourceFileLine: guessedPosition.line,
                sourceFileColumn: guessedPosition.column
            };
        }
        else {
            // The guessed position was out of bounds, so use the nearestMappingItem position instead.
            return {
                sourceFilePath: mappedFilePath,
                sourceFileLine: nearestMappingItem.originalLine,
                sourceFileColumn: nearestMappingItem.originalColumn
            };
        }
    }
    _getSourceMap(sourceFilePath) {
        let sourceMap = this._sourceMapByFilePath.get(sourceFilePath);
        if (sourceMap === undefined) {
            // Normalize the path and redo the lookup
            const normalizedPath = node_core_library_1.FileSystem.getRealPath(sourceFilePath);
            sourceMap = this._sourceMapByFilePath.get(normalizedPath);
            if (sourceMap !== undefined) {
                // Copy the result from the normalized to the non-normalized key
                this._sourceMapByFilePath.set(sourceFilePath, sourceMap);
            }
            else {
                // Given "folder/file.d.ts", check for a corresponding "folder/file.d.ts.map"
                const sourceMapPath = normalizedPath + '.map';
                if (node_core_library_1.FileSystem.exists(sourceMapPath)) {
                    // Load up the source map
                    const rawSourceMap = node_core_library_1.JsonFile.load(sourceMapPath);
                    const sourceMapConsumer = new source_map_1.SourceMapConsumer(rawSourceMap);
                    const mappingItems = [];
                    // Extract the list of mapping items
                    sourceMapConsumer.eachMapping((mappingItem) => {
                        mappingItems.push(Object.assign(Object.assign({}, mappingItem), { 
                            // The "source-map" package inexplicably uses 1-based line numbers but 0-based column numbers.
                            // Fix that up proactively so we don't have to deal with it later.
                            generatedColumn: mappingItem.generatedColumn + 1, originalColumn: mappingItem.originalColumn + 1 }));
                    }, this, source_map_1.SourceMapConsumer.GENERATED_ORDER);
                    sourceMap = { sourceMapConsumer, mappingItems };
                }
                else {
                    // No source map for this filename
                    sourceMap = null;
                }
                this._sourceMapByFilePath.set(normalizedPath, sourceMap);
                if (sourceFilePath !== normalizedPath) {
                    // Add both keys to the map
                    this._sourceMapByFilePath.set(sourceFilePath, sourceMap);
                }
            }
        }
        return sourceMap;
    }
    // The `mappingItems` array is sorted by generatedLine/generatedColumn (GENERATED_ORDER).
    // The _findNearestMappingItem() lookup is a simple binary search that returns the previous item
    // if there is no exact match.
    static _findNearestMappingItem(mappingItems, position) {
        if (mappingItems.length === 0) {
            return undefined;
        }
        let startIndex = 0;
        let endIndex = mappingItems.length - 1;
        while (startIndex <= endIndex) {
            const middleIndex = startIndex + Math.floor((endIndex - startIndex) / 2);
            const diff = SourceMapper._compareMappingItem(mappingItems[middleIndex], position);
            if (diff < 0) {
                startIndex = middleIndex + 1;
            }
            else if (diff > 0) {
                endIndex = middleIndex - 1;
            }
            else {
                // Exact match
                return mappingItems[middleIndex];
            }
        }
        // If we didn't find an exact match, then endIndex < startIndex.
        // Take endIndex because it's the smaller value.
        return mappingItems[endIndex];
    }
    static _compareMappingItem(mappingItem, position) {
        const diff = mappingItem.generatedLine - position.line;
        if (diff !== 0) {
            return diff;
        }
        return mappingItem.generatedColumn - position.column;
    }
}
exports.SourceMapper = SourceMapper;
//# sourceMappingURL=SourceMapper.js.map