// Copyright (c) 2018, Compiler Explorer Authors
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
//     * Redistributions of source code must retain the above copyright notice,
//       this list of conditions and the following disclaimer.
//     * Redistributions in binary form must reproduce the above copyright
//       notice, this list of conditions and the following disclaimer in the
//       documentation and/or other materials provided with the distribution.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
// POSSIBILITY OF SUCH DAMAGE.

import fs from 'fs';

import * as utils from '../utils.js';

type SegmentOffset = {
    segment: string;
    addressInt: number;
    address: string;
    segmentLength: number;
};

export type Segment = {
    segment: string;
    addressInt: number;
    address: string;
    addressWithoutOffset: number;
    segmentLength: number;
    unitName?: string | false;
    id?: number;
    displayName?: string;
};

type ReconstructedSegment = {
    addressInt: number;
    address: string;
    endAddress?: string;
    segmentLength: number;
    unitName?: string | false;
};

type EntryPoint = {
    segment: string;
    addressWithoutOffset: string;
};

type LineNumber = {
    addressInt: number;
    address: string;
    segment: string;
    addressWithoutOffset: number;
    lineNumber?: number;
};

type AddressRangeInformation = {
    startAddress: number;
    startAddressHex: string;
    endAddress: number;
    endAddressHex: string;
};

export class MapFileReader {
    preferredLoadAddress = 0x400000;
    segmentMultiplier = 0x1000;
    segmentOffsets: SegmentOffset[] = [];
    segments: Segment[] = [];
    isegments: Segment[] = [];
    namedAddresses: Segment[] = [];
    entryPoint: '' | EntryPoint = '';

    lineNumbers: LineNumber[] = [];
    reconstructedSegments: ReconstructedSegment[] = [];

    regexEntryPoint = /^\sentry point at\s*([\da-f]*):([\da-f]*)$/i;

    /**
     * constructor of MapFileReader
     *  Note that this is a base class and should be overriden. (see for example map-file-vs.js)
     *  Note that this base class retains and uses state,
     *   so when you want to read a new file you need to instantiate a new object.
     */
    constructor(protected readonly mapFilename: string) {}

    /**
     * The function to call to load a map file (not async)
     */
    run() {
        if (this.mapFilename) {
            this.loadMap();
        }
    }

    getLineInfoByAddress(segment: string | undefined, address: number): LineNumber | undefined {
        for (let idx = 0; idx < this.lineNumbers.length; idx++) {
            const lineInfo = this.lineNumbers[idx];
            if (!segment && lineInfo.addressInt === address) {
                return lineInfo;
            } else if (segment === lineInfo.segment && lineInfo.addressWithoutOffset === address) {
                return lineInfo;
            }
        }

        return undefined;
    }

    getSegmentOffset(segment: string): number {
        if (this.segmentOffsets.length > 0) {
            for (let idx = 0; idx < this.segmentOffsets.length; idx++) {
                const info = this.segmentOffsets[idx];
                if (info.segment === segment) {
                    return info.addressInt;
                }
            }
        }

        return this.preferredLoadAddress + parseInt(segment, 16) * this.segmentMultiplier;
    }

    setSegmentOffset(segment: string, address: number) {
        for (let idx = 0; idx < this.segments.length; idx++) {
            const info = this.segments[idx];
            if (info.segment === segment) {
                this.segments[idx].addressInt = address;
                this.segments[idx].address = address.toString(16);
            }
        }

        if (this.segmentOffsets.length > 0) {
            for (let idx = 0; idx < this.segmentOffsets.length; idx++) {
                const info = this.segmentOffsets[idx];
                if (info.segment === segment) {
                    this.segmentOffsets[idx].addressInt = address;
                    this.segmentOffsets[idx].address = address.toString(16);
                    return;
                }
            }
        }

        this.segmentOffsets.push({
            segment: segment,
            addressInt: address,
            address: address.toString(16),
            segmentLength: 0,
        });
    }

    getSegmentInfoByUnitName(unitName: string): Segment | undefined {
        for (let idx = 0; idx < this.segments.length; idx++) {
            const info = this.segments[idx];
            if (info.unitName === unitName) {
                return info;
            }
        }

        return undefined;
    }

    getICodeSegmentInfoByUnitName(unitName: string): Segment | undefined {
        for (let idx = 0; idx < this.isegments.length; idx++) {
            const info = this.isegments[idx];
            if (info.unitName === unitName) {
                return info;
            }
        }

        return undefined;
    }

    getSegmentIdByUnitName(unitName: string): number | undefined {
        const info = this.getSegmentInfoByUnitName(unitName);
        if (info) {
            return info.id;
        }

        return 0;
    }

    /**
     * Get Segment info for exact address
     */
    getSegmentInfoByStartingAddress(segment: string | undefined, address: number): Segment | undefined {
        for (let idx = 0; idx < this.segments.length; idx++) {
            const info = this.segments[idx];
            if (!segment && info.addressInt === address) {
                return info;
            } else if (info.segment === segment && info.addressWithoutOffset === address) {
                return info;
            }
        }

        return undefined;
    }

    /**
     * Get Segment info for the segment where the given address is in
     */
    getSegmentInfoAddressIsIn(segment: string | undefined, address: number): Segment | undefined {
        for (let idx = 0; idx < this.segments.length; idx++) {
            const info = this.segments[idx];
            if (!segment && address >= info.addressInt && address < info.addressInt + info.segmentLength) {
                return info;
            } else if (
                segment === info.segment &&
                address >= info.addressWithoutOffset &&
                address < info.addressWithoutOffset + info.segmentLength
            ) {
                return info;
            }
        }

        return undefined;
    }

    /**
     * Get Segment info for the segment where the given address is in
     */
    getSegmentInfoAddressWithoutOffsetIsIn(segment: string, address: number): Segment | undefined {
        for (let idx = 0; idx < this.segments.length; idx++) {
            const info = this.segments[idx];
            if (
                segment === info.segment &&
                address >= info.addressWithoutOffset &&
                address < info.addressWithoutOffset + info.segmentLength
            ) {
                return info;
            }
        }

        return undefined;
    }

    getSymbolAt(segment: string | undefined, address: number): Segment | undefined {
        for (let idx = 0; idx < this.namedAddresses.length; idx++) {
            const info = this.namedAddresses[idx];
            if (!segment && info.addressInt === address) {
                return info;
            } else if (segment === info.segment && info.addressWithoutOffset === address) {
                return info;
            }
        }

        return undefined;
    }

    getSymbolBefore(segment: string, address: number): Segment | undefined {
        let maxNamed: Segment | undefined;

        for (let idx = 0; idx < this.namedAddresses.length; idx++) {
            const info = this.namedAddresses[idx];
            if (!segment && info.addressInt <= address) {
                if (!maxNamed || info.addressInt > maxNamed.addressInt) {
                    maxNamed = info;
                }
            } else if (segment === info.segment && info.addressWithoutOffset <= address) {
                if (!maxNamed || info.addressInt > maxNamed.addressInt) {
                    maxNamed = info;
                }
            }
        }

        return maxNamed;
    }

    getSymbolInfoByName(name: string): Segment | undefined {
        for (let idx = 0; idx < this.namedAddresses.length; idx++) {
            const info = this.namedAddresses[idx];
            if (info.displayName === name) {
                return info;
            }
        }

        return undefined;
    }

    addressToObject(segment: string, address: string): LineNumber {
        const addressWithoutOffset = parseInt(address, 16);
        const addressWithOffset = this.getSegmentOffset(segment) + addressWithoutOffset;

        return {
            segment: segment,
            addressWithoutOffset: addressWithoutOffset,
            addressInt: addressWithOffset,
            address: addressWithOffset.toString(16),
        };
    }

    /**
     * Try to match information about the address where a symbol is
     */
    // eslint-disable-next-line no-unused-vars
    tryReadingNamedAddress(line: string) {}

    /**
     * Tries to match the given line to code segment information.
     *  Implementation specific, so this base function is empty
     */
    // eslint-disable-next-line no-unused-vars
    tryReadingCodeSegmentInfo(line: string) {}

    tryReadingEntryPoint(line: string) {
        const matches = line.match(this.regexEntryPoint);
        if (matches) {
            this.entryPoint = {
                segment: matches[1],
                addressWithoutOffset: matches[2],
            };
        }
    }

    // eslint-disable-next-line no-unused-vars
    tryReadingPreferredAddress(line: string) {}

    // eslint-disable-next-line no-unused-vars
    tryReadingLineNumbers(line: string): boolean {
        return false;
    }

    // eslint-disable-next-line no-unused-vars
    isStartOfLineNumbers(line: string) {
        return false;
    }

    /**
     * Tries to reconstruct segments information from contiguous named addresses
     */
    reconstructSegmentsFromNamedAddresses() {
        let currentUnit: false | string | undefined = false;
        let addressStart = 0;
        for (let idxSymbol = 0; idxSymbol < this.namedAddresses.length; ++idxSymbol) {
            const symbolObject = this.namedAddresses[idxSymbol];

            if (symbolObject.addressInt < this.preferredLoadAddress) continue;

            if (!currentUnit) {
                addressStart = symbolObject.addressInt;
                currentUnit = symbolObject.unitName;
            } else if (symbolObject.unitName !== currentUnit) {
                const segmentLen = symbolObject.addressInt - addressStart;

                this.reconstructedSegments.push({
                    addressInt: addressStart,
                    address: addressStart.toString(16),
                    endAddress: (addressStart + segmentLen).toString(16),
                    segmentLength: segmentLen,
                    unitName: currentUnit,
                });

                addressStart = symbolObject.addressInt;
                currentUnit = symbolObject.unitName;
            }

            if (idxSymbol === this.namedAddresses.length - 1) {
                this.reconstructedSegments.push({
                    addressInt: addressStart,
                    address: addressStart.toString(16),
                    segmentLength: -1,
                    unitName: symbolObject.unitName,
                });
            }
        }
    }

    /**
     * Returns an array of objects with address range information for a given unit (filename)
     */
    getReconstructedUnitAddressSpace(unitName: string): AddressRangeInformation[] {
        const addressSpace: AddressRangeInformation[] = [];

        for (let idxSegment = 0; idxSegment < this.reconstructedSegments.length; ++idxSegment) {
            const segment = this.reconstructedSegments[idxSegment];
            if (segment.unitName === unitName) {
                addressSpace.push({
                    startAddress: segment.addressInt,
                    startAddressHex: segment.addressInt.toString(16),
                    endAddress: segment.addressInt + segment.segmentLength,
                    endAddressHex: (segment.addressInt + segment.segmentLength).toString(16),
                });
            }
        }

        return addressSpace;
    }

    /**
     * Returns true if the address (and every address until address+size) is within
     *  one of the given spaces. Spaces should be of format returned by getReconstructedUnitAddressSpace()
     */
    isWithinAddressSpace(spaces: AddressRangeInformation[], address: number, size: number) {
        for (const spaceAddress of spaces) {
            if (
                (address >= spaceAddress.startAddress && address < spaceAddress.endAddress) ||
                (address < spaceAddress.startAddress && address + size >= spaceAddress.startAddress)
            ) {
                return true;
            }
        }

        return false;
    }

    /**
     * Retreives information from Map file contents
     */
    loadMapFromString(contents: string) {
        const mapLines = utils.splitLines(contents);

        let readLineNumbersMode = false;

        let lineIdx = 0;
        while (lineIdx < mapLines.length) {
            const line = mapLines[lineIdx];

            if (readLineNumbersMode) {
                if (!this.tryReadingLineNumbers(line)) {
                    readLineNumbersMode = false;
                }
            } else {
                this.tryReadingPreferredAddress(line);
                this.tryReadingEntryPoint(line);
                this.tryReadingCodeSegmentInfo(line);
                this.tryReadingNamedAddress(line);

                if (this.isStartOfLineNumbers(line)) {
                    readLineNumbersMode = true;
                    lineIdx++;
                }
            }

            lineIdx++;
        }

        this.reconstructSegmentsFromNamedAddresses();
    }

    /**
     * Reads the actual mapfile from disk synchronously and load it into this class
     */
    loadMap() {
        const data = fs.readFileSync(this.mapFilename);

        this.loadMapFromString(data.toString());
    }
}
