/**
 *
 * Copyright (c) 2024 Analog Devices, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */
﻿/* eslint-disable @typescript-eslint/no-namespace */
import { Dwarf } from "./Dwarf.js";
import { DwarfData } from "./DwarfData.js";

export namespace DwarfLine {
	// The expected number of arguments for standard opcodes.  This is
	// used to check the opcode_lengths header field for compatibility.
	const opcodeLengths: number[] = [
		0,
		// DW_LNS.copy
		0, 1, 1, 1, 1,
		// DW_LNS.negate_stmt
		0, 0, 0, 1, 0,
		// DW_LNS.set_epilogue_begin
		0, 1,
	];

	/**
	 * A source file in a line table.
	 */
	export class LineTableFile {
		/**
		 * The absolute path of this source file.
		 */
		public path: string;

		/**
		 * The last modification time of this source file in an
		 * implementation-defined encoding or 0 if unknown.
		 */
		public mtime: number; // x64

		/**
		 * The size in bytes of this source file or 0 if unknown.
		 */
		public length: number; // x64

		/**
		 * Construct a source file object.
		 */
		constructor(path = "", mtime = 0, length = 0) {
			this.path = path;
			this.mtime = mtime;
			this.length = length;
		}
	}

	/**
	 * An entry in the line table.
	 */
	export class LineTableEntry {
		/**
		 * The program counter value corresponding to a machine
		 * instruction generated by the compiler.
		 */
		public address = 0;

		/**
		 * The index of an operation within a VLIW instruction. The
		 * index of the first operation is 0. For non-VLIW
		 * architectures, this will always be 0.
		 */
		public opIndex = 0; // uint32

		/**
		 * The source file containing this instruction.
		 */
		public file: LineTableFile = null;

		/**
		 * The index of the source file containing this instruction.
		 */
		public fileIndex = 0; // uint32

		/**
		 * The source line number of this instruction, starting at 1.
		 * This may be 0 if this instruction cannot be attributed to
		 * any source line.
		 */
		public line = 0; // uint32

		/**
		 * The column number within this source line, starting at 1.
		 * The value 0 indicates that a statement begins at the "left
		 * edge" of the line, whatever that means.
		 */
		public column = 0; // uint32

		/**
		 * True if this instruction is a recommended breakpoint
		 * location.  Typically this is the beginning of a statement.
		 */
		public isStmt = false;

		/**
		 * True if this instruction is the beginning of a basic block.
		 */
		public basicBlock = false;

		/**
		 * True if this address is the first byte after the end of a
		 * sequence of target machine instructions.  In this case, all
		 * other fields besides address are not meaningful.
		 */
		public endSequence = false;

		/**
		 * True if this address is one where execution should be
		 * suspended for an entry breakpoint of a function.
		 */
		public prologue_end = false;

		/**
		 * True if this address is one where execution should be
		 * suspended for an exit breakpoint of a function.
		 */
		public epilogueBegin = false;

		/**
		 * The instruction set architecture of this instruction.  The
		 * meaning of this field is generally defined by an
		 * architecture's ABI.
		 */
		public isa = 0; // uint32

		/**
		 * A number that identifies the block containing the current
		 * instruction if multiple blocks are associated with the same
		 * source file, line, and column.
		 */
		public discriminator = 0; // uint32

		/**
		 * Reset this line info object to the default initial values
		 * for all fields.  isStmt has no default value, so the
		 * caller must provide it.
		 */
		public reset(isStmt: boolean) {
			this.address = this.opIndex = 0;
			this.file = null;
			this.fileIndex = this.line = 1;
			this.column = 0;
			this.isStmt = isStmt;
			this.basicBlock =
				this.endSequence =
				this.prologue_end =
				this.epilogueBegin =
					false;
			this.isa = this.discriminator = 0;
		}

		/**
		 * Return a descriptive string of the form
		 * "filename[:line[:column]]".
		 */
		public getDescription(): string {
			let ret: string = this.file.path;
			if (this.line > 0) {
				ret += ":" + this.line;
				if (this.column > 0) ret += ":" + this.column;
			}
			return ret;
		}

		public clone(): LineTableEntry {
			return Object.assign({}, this);
		}
	}

	/**
	 * A DWARF line number table.  A line number table is a list of line
	 * table entries, broken up into "sequences".  Within a sequence,
	 * entries are in order of increasing program counter ("address") and
	 * an entry provides information for all program counters between the
	 * entry's address and the address of the next entry.  Each sequence
	 * is terminated by a special entry with its
	 * LineTable::entry::end_sequence flag set.  The line number table
	 * also records the set of source files for a given compilation unit,
	 * which can be referred to from other DIE attributes.
	 */
	export class LineTable {
		dv: DataView = null;
		sec: Dwarf.Section = null;

		// Header information
		programOffset: number;
		minimumInstruction_length: number; // ubyte
		maximumOperationsPerInstruction: number; // ubyte
		defaultIsStmt: boolean;
		standardOpcodeLengths: number[] = []; // ubyte[]

		public opcodeBase: number; // ubyte
		public lineBase: number; // sbyte
		public lineRange: number; // ubyte
		public compDir: string = "";
		public includeDirectories: Array<string> = [];
		public fileNames = new Array<LineTableFile>();

		// Slow!
		//public entries: LineTableEntry[] = [];
		public addressToEntryMap = new Map<number, LineTableEntry>();

		// The offset in sec following the last read file name entry.
		// File name entries can appear both in the line table header
		// and in the line number program itself.  Since we can
		// iterate over the line number program repeatedly, this keeps
		// track of how far we've gotten so we don't add the same
		// entry twice.
		lastFileNameEnd: number;
		// If an iterator has traversed the entire program, then we
		// know we've gathered all file names.
		fileNamesComplete: boolean;

		// Iteration
		private pos: number = 0;
		private entry = new LineTableEntry();
		private regs = new LineTableEntry();

		/**
		 * \internal Construct a line number table whose header begins
		 * at the given offset in sec.  cu_addr_size is the address
		 * size of the associated compilation unit.  cu_comp_dir and
		 * cu_name give the DW_AT::comp_dir and DW_AT::name attributes
		 * of the associated compilation unit.
		 */
		constructor(
			dv: DataView,
			secLine: Dwarf.Section,
			cuOffset: number,
			cuAddrSize: number,
			cuCompDir: string,
			cuName: string,
		) {
			this.dv = dv;

			// XXX DWARF2 and 3 give a weird specification for DW_AT_comp_dir

			if (cuCompDir.length == 0 || cuCompDir.endsWith("/")) {
				this.compDir = cuCompDir;
			} else {
				this.compDir = cuCompDir + "/";
			}

			// Read the line table header (DWARF2 section 6.2.4, DWARF3
			// section 6.2.4, DWARF4 section 6.2.3)
			let cur = new Dwarf.Cursor(dv, secLine, cuOffset);
			this.sec = cur.subSection();
			cur = new Dwarf.Cursor(dv, this.sec);
			cur.skipInitialLength();
			this.sec.addrSize = cuAddrSize;

			// Basic header information
			const version = cur.fixedUInt16(); // uhalf
			if (version < 2 || version > 4)
				throw new Error("unknown line number table version " + version);
			const header_length = cur.offset();
			this.programOffset = cur.getSectionOffset() + header_length;
			this.minimumInstruction_length = cur.fixedUInt8(); // ubyte
			this.maximumOperationsPerInstruction = 1;
			if (version >= 4) this.maximumOperationsPerInstruction = cur.fixedUInt8(); // ubyte
			if (this.maximumOperationsPerInstruction == 0)
				throw new Error(
					"maximum_operations_per_instruction cannot be 0 in line number table",
				);
			this.defaultIsStmt = cur.fixedUInt8() != 0; // ubyte
			this.lineBase = cur.fixedInt8(); // sbyte
			this.lineRange = cur.fixedUInt8(); // ubyte
			if (this.lineRange == 0)
				throw new Error("line_range cannot be 0 in line number table");
			this.opcodeBase = cur.fixedUInt8(); // ubyte

			// Opcode length table
			this.standardOpcodeLengths = new Array<number>(this.opcodeBase); // has one additional elem for idx 0
			this.standardOpcodeLengths[0] = 0;
			for (let i = 1; i < this.opcodeBase; ++i) {
				const length = cur.fixedUInt8(); // ubyte
				if (length != opcodeLengths[i])
					// The spec never says what to do if the
					// opcode length of a standard opcode doesn't
					// match the header.  Do the safe thing.
					throw new Error(
						"expected " +
							opcodeLengths[i] +
							" arguments for line number opcode " +
							i +
							", got " +
							length,
					);
				this.standardOpcodeLengths[i] = length;
			}

			// Include directories list
			// Include directory 0 is implicitly the compilation unit
			// current directory
			this.includeDirectories.push(this.compDir);
			while (true) {
				let incdir = cur.str();
				if (incdir.length == 0) break;
				if (!incdir.endsWith("/")) incdir += "/";
				if (this.isAbsolutePath(incdir)) this.includeDirectories.push(incdir);
				else this.includeDirectories.push(this.compDir + incdir);
			}

			// File name list
			// File name 0 is implicitly the compilation unit file name.
			// cu_name can be relative to comp_dir or absolute.
			if (this.isAbsolutePath(cuName)) {
				this.fileNames.push(new LineTableFile(cuName));
			} else {
				this.fileNames.push(new LineTableFile(this.compDir + cuName));
			}
			while (this.readFileEntry(cur, true));

			// Fill entries
			this.entry.reset(this.defaultIsStmt);
			this.regs.reset(this.defaultIsStmt);
			this.pos = this.programOffset;
			while (this.pos < this.sec.size()) {
				const entry = this.parseNextEntry();

				if (!entry.endSequence) {
					//this.entries.push(entry);
					if (entry.isStmt) {
						this.addressToEntryMap.set(entry.address, entry);
					}
				}
			}
			//console.log('addressToEntryMap.size:' + this.addressToEntryMap.size);
		}

		private isAbsolutePath(path: string) : boolean {
			if (path.length > 0 && path[0] === '/') return true; // unix
			if (path.length > 1 && path[1] === ':') return true; // win: first letter is the drive
			return false;
		}

		public getFile(index: number): LineTableFile {
			return this.fileNames[index];
		}

		private readFileEntry(cur: Dwarf.Cursor, in_header: boolean): boolean {
			//assert(cur.sec == this.sec);

			const file_name = cur.str();
			if (in_header && file_name.length == 0) return false;
			const dirIndex = cur.uleb128();
			const mtime = cur.uleb128();
			const length = cur.uleb128();

			// Have we already processed this file entry?
			if (cur.getSectionOffset() <= this.lastFileNameEnd) return true;
			this.lastFileNameEnd = cur.getSectionOffset();

			if (this.isAbsolutePath(file_name))
				this.fileNames.push(new LineTableFile(file_name, mtime, length));
			else if (dirIndex < this.includeDirectories.length)
				this.fileNames.push(
					new LineTableFile(
						this.includeDirectories[dirIndex] + file_name,
						mtime,
						length,
					),
				);
			else
				throw new Error("file name directory index out of range: " + dirIndex);

			return true;
		}

		private parseNextEntry(): LineTableEntry {
			const cur = new Dwarf.Cursor(this.dv, this.sec, this.pos);

			// Execute opcodes until we reach the end of the stream or an
			// opcode emits a line table row
			let stepped = false,
				output = false;
			while (!cur.end() && !output) {
				output = this.step(cur);
				stepped = true;
			}
			if (stepped && !output) throw new Error("unexpected end of line table");
			if (stepped && cur.end()) {
				// Record that all file names must be known now
				this.fileNamesComplete = true;
			}
			if (output) {
				// Resolve file name of entry
				if (this.entry.fileIndex < this.fileNames.length)
					this.entry.file = this.fileNames[this.entry.fileIndex];
				else
					throw new Error(
						"bad file index " + this.entry.fileIndex + " in line table",
					);
			}

			this.pos = cur.getSectionOffset();
			return this.entry;
		}

		private stepAdvancePc(uarg: number) {
			this.regs.address +=
				this.minimumInstruction_length *
				Math.floor(
					(this.regs.opIndex + uarg) / this.maximumOperationsPerInstruction,
				);
			this.regs.opIndex =
				(this.regs.opIndex + uarg) % this.maximumOperationsPerInstruction;
		}

		private step(cur: Dwarf.Cursor): boolean {
			// Read the opcode (DWARF4 section 6.2.3)
			const opcode = cur.fixedUInt8(); // ubyte
			//console.log("process opcode " + opcode);
			if (opcode >= this.opcodeBase) {
				// Special opcode (DWARF4 section 6.2.5.1)
				const adjusted_opcode = (opcode - this.opcodeBase) % 255; // ubyte
				//console.log("Special opcode (DWARF4 section 6.2.5.1). op:" + adjusted_opcode,);
				const op_advance = Math.floor(adjusted_opcode / this.lineRange);
				const line_inc = this.lineBase + (adjusted_opcode % this.lineRange);

				this.regs.line += line_inc;
				this.regs.address +=
					this.minimumInstruction_length *
					Math.floor(
						(this.regs.opIndex + op_advance) /
							this.maximumOperationsPerInstruction,
					);
				this.regs.opIndex =
					(this.regs.opIndex + op_advance) %
					this.maximumOperationsPerInstruction;
				this.entry = this.regs.clone();

				this.regs.basicBlock =
					this.regs.prologue_end =
					this.regs.epilogueBegin =
						false;
				this.regs.discriminator = 0;

				return true;
			} else if (opcode != 0) {
				// Standard opcode (DWARF4 sections 6.2.3 and 6.2.5.2)
				//
				// According to the standard, any opcode between the
				// highest defined opcode for a given DWARF version
				// and opcode_base should be treated as a
				// vendor-specific opcode. However, the de facto
				// standard seems to be to process these as standard
				// opcodes even if they're from a later version of the
				// standard than the line table header claims.
				let uarg: number = 0;
				const lns = opcode as DwarfData.DW_LNS;
				switch (lns) {
					case DwarfData.DW_LNS.copy:
						this.entry = this.regs.clone();
						this.regs.basicBlock =
							this.regs.prologue_end =
							this.regs.epilogueBegin =
								false;
						this.regs.discriminator = 0;
						break;
					case DwarfData.DW_LNS.advance_pc:
						// Opcode advance (as for special opcodes)
						uarg = cur.uleb128();
						this.stepAdvancePc(uarg);
						break;
					case DwarfData.DW_LNS.advance_line:
						this.regs.line = this.regs.line + cur.sleb128();
						break;
					case DwarfData.DW_LNS.set_file:
						this.regs.fileIndex = cur.uleb128();
						break;
					case DwarfData.DW_LNS.set_column:
						this.regs.column = cur.uleb128();
						break;
					case DwarfData.DW_LNS.negate_stmt:
						this.regs.isStmt = !this.regs.isStmt;
						break;
					case DwarfData.DW_LNS.set_basic_block:
						this.regs.basicBlock = true;
						break;
					case DwarfData.DW_LNS.const_add_pc:
						uarg = Math.floor((255 - this.opcodeBase) / this.lineRange);
						this.stepAdvancePc(uarg);
						break;
					case DwarfData.DW_LNS.fixed_advance_pc:
						this.regs.address += cur.fixedUInt16(); // uhalf
						this.regs.opIndex = 0;
						break;
					case DwarfData.DW_LNS.set_prologue_end:
						this.regs.prologue_end = true;
						break;
					case DwarfData.DW_LNS.set_epilogue_begin:
						this.regs.epilogueBegin = true;
						break;
					case DwarfData.DW_LNS.set_isa:
						this.regs.isa = cur.uleb128();
						break;
					default:
						// XXX Vendor extensions
						throw new Error("unknown line number opcode " + lns);
				}
				return lns == DwarfData.DW_LNS.copy;
			} else {
				// opcode == 0
				// Extended opcode (DWARF4 sections 6.2.3 and 6.2.5.3)
				//assert(opcode == 0);
				const length = cur.uleb128();
				const end = cur.getSectionOffset() + length;
				const opcode1 = cur.fixedUInt8(); // ubyte
				const lne = opcode1 as DwarfData.DW_LNE;
				switch (lne) {
					case DwarfData.DW_LNE.end_sequence:
						this.regs.endSequence = true;
						this.entry = this.regs.clone();
						this.regs.reset(this.defaultIsStmt);
						break;
					case DwarfData.DW_LNE.set_address:
						this.regs.address = cur.address();
						//console.log(
						//	"DwarfData.DW_LNE.set_address: 0x" +
						//		this.regs.address.toString(16),
						//);
						this.regs.opIndex = 0;
						break;
					case DwarfData.DW_LNE.define_file:
						this.readFileEntry(cur, false);
						break;
					case DwarfData.DW_LNE.set_discriminator:
						// XXX Only DWARF4
						this.regs.discriminator = cur.uleb128();
						break;
					default:
						if (
							lne >= DwarfData.DW_LNE.lo_user &&
							lne <= DwarfData.DW_LNE.hi_user
						) {
							// XXX Vendor extensions
							throw new Error(
								"vendor line number opcode " + lne + " not implemented",
							);
						}
						// XXX Prior to DWARF4, any opcode number
						// could be a vendor extension
						throw new Error("unknown line number opcode " + lne);
				}

				if (cur.getSectionOffset() > end)
					throw new Error("extended line number opcode exceeded its size");
				cur.pos += end - cur.getSectionOffset();
				return lne == DwarfData.DW_LNE.end_sequence;
			}
		}
	}
} // namespace DwarfLine
