/* ###
 * IP: GHIDRA
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * 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.
 */
package ghidra.app.util.bin.format.dwarf4;

import java.io.IOException;
import java.util.*;

import ghidra.app.util.bin.BinaryReader;
import ghidra.app.util.bin.format.dwarf4.next.DWARFProgram;
import ghidra.util.Msg;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.TaskMonitor;

/**
 * A DWARF "CompilationUnit" is a contiguous block of {@link DebugInfoEntry DIE} records found
 * in a ".debug_info" section of an ELF program.  The compilation unit block starts with a
 * header that has a few important values and flags, and is followed by the DIE records.
 * <p>
 * The first DIE record must be a DW_TAG_compile_unit (see {@link DWARFCompileUnit},
 * and {@link #getCompileUnit()}).
 * <p>
 * DIE records are identified by their byte offset in the ".debug_info" section.
 * <p>
 */
public class DWARFCompilationUnit {

	public static final int DWARF_32 = 32;
	public static final int DWARF_64 = 64;

	/**
	 * Reference to the owning {@link DWARFProgram}.
	 */
	private final DWARFProgram dwarfProgram;

	/**
	 * Offset in the debug_info section of this compUnit's header
	 */
	private final long startOffset;

	/**
	 * Offset in the debug_info section of the end of this compUnit.  (right after
	 * the last DIE record)
	 */
	private final long endOffset;

	/**
	 * Length in bytes of this compUnit header and DIE records.
	 */
	private final long length;

	/**
	 * {@link #DWARF_32} or {@link #DWARF_64}
	 */
	private final int format;

	/**
	 * DWARF ver number, as read from the compunit structure, currently not used but being kept.
	 */
	@SuppressWarnings("unused")
	private final short version;

	/**
	 * Sequential number of this compUnit
	 */
	private final int compUnitNumber;

	/**
	 * Size of pointers that are held in DIEs in this compUnit.
	 */
	private final byte pointerSize;

	/**
	 * Offset in the abbr section of this compUnit's abbreviations.
	 */
	private final long abbreviationOffset;

	/**
	 * Offset in the debug_info section of the first DIE of this compUnit.
	 */
	private final long firstDIEOffset;

	/**
	 * Map of abbrevCode to {@link DWARFAbbreviation} instances.
	 */
	private final Map<Integer, DWARFAbbreviation> codeToAbbreviationMap;

	/**
	 * The contents of the first DIE (that must be a compile unit) in this compUnit.
	 */
	private DWARFCompileUnit compUnit;

	/**
	 * Creates a new {@link DWARFCompilationUnit} by reading a compilationUnit's header data
	 * from the debug_info section and the debug_abbr section and its compileUnit DIE (ie.
	 * the first DIE right after the header).
	 * <p>
	 * Returns NULL if there was an ignorable error while reading the compilation unit (and
	 * leaves the input stream at the next compilation unit to read), otherwise throws
	 * an IOException if there was an unrecoverable error.
	 *
	 * @param dwarfProgram the dwarf program.
	 * @param debugInfoBR the debug info binary reader.
	 * @param debugAbbrBR the debug abbreviation binary reader
	 * @param cuNumber the compilation unit number
	 * @param monitor the current task monitor
	 * @return the read compilation unit.
	 * @throws DWARFException if an invalid or unsupported DWARF version is read.
	 * @throws IOException if the length of the compilation unit is invalid.
	 * @throws CancelledException if the task has been canceled.
	 */
	public static DWARFCompilationUnit readCompilationUnit(DWARFProgram dwarfProgram,
			BinaryReader debugInfoBR, BinaryReader debugAbbrBR, int cuNumber, TaskMonitor monitor)
			throws DWARFException, IOException, CancelledException {

		long startOffset = debugInfoBR.getPointerIndex();
		long length = debugInfoBR.readNextUnsignedInt();
		int format;

		if (length == 0xffffffffL) {
			// Length of 0xffffffff implies 64-bit DWARF format
			// Mostly untested as there is no easy way to force the compiler
			// to generate this
			length = debugInfoBR.readNextLong();
			format = DWARF_64;
		}
		else if (length >= 0xfffffff0L) {
			// Length of 0xfffffff0 or greater is reserved for DWARF
			throw new DWARFException("Reserved DWARF length value: " + Long.toHexString(length) +
				". Unknown extension.");
		}
		else if (length == 0) {
			throw new DWARFException("Invalid length 0 for DWARF Compilation Unit at 0x" +
				Long.toHexString(startOffset));
		}
		else {
			format = DWARF_32;
		}

		long endOffset = (debugInfoBR.getPointerIndex() + length);
		short version = debugInfoBR.readNextShort();
		long abbreviationOffset = DWARFUtil.readOffsetByDWARFformat(debugInfoBR, format);
		byte pointerSize = debugInfoBR.readNextByte();
		long firstDIEOffset = debugInfoBR.getPointerIndex();

		if (version < 2 || version > 4) {
			throw new DWARFException(
				"Only DWARF version 2, 3, or 4 information is currently supported.");
		}
		if (firstDIEOffset > endOffset) {
			throw new IOException("Invalid length " + (endOffset - startOffset) +
				" for DWARF Compilation Unit at 0x" + Long.toHexString(startOffset));
		}
		else if (firstDIEOffset == endOffset) {
			// silently skip this empty compunit
			return null;
		}

		debugAbbrBR.setPointerIndex(abbreviationOffset);
		Map<Integer, DWARFAbbreviation> abbrMap =
			DWARFAbbreviation.readAbbreviations(debugAbbrBR, dwarfProgram, monitor);

		DWARFCompilationUnit cu =
			new DWARFCompilationUnit(dwarfProgram, startOffset, endOffset, length, format, version,
				abbreviationOffset, pointerSize, cuNumber, firstDIEOffset, abbrMap);

		try {
			DebugInfoEntry compileUnitDIE =
				DebugInfoEntry.read(debugInfoBR, cu, dwarfProgram.getAttributeFactory());

			DWARFCompileUnit compUnit = DWARFCompileUnit.read(
				DIEAggregate.createSingle(compileUnitDIE), dwarfProgram.getDebugLine());
			cu.setCompileUnit(compUnit);
			return cu;
		}
		catch (IOException ioe) {
			Msg.error(null,
				"Failed to parse the DW_TAG_compile_unit DIE at the start of compilation unit " +
					cuNumber + " at offset " + startOffset + " (0x" +
					Long.toHexString(startOffset) + "), skipping entire compilation unit",
				ioe);
			debugInfoBR.setPointerIndex(cu.getEndOffset());
			return null;
		}
	}

	/**
	 * This ctor is public only for junit tests.  Do not use directly.
	 */
	public DWARFCompilationUnit(DWARFProgram dwarfProgram, long startOffset, long endOffset,
			long length, int format, short version, long abbreviationOffset, byte pointerSize,
			int compUnitNumber, long firstDIEOffset,
			Map<Integer, DWARFAbbreviation> codeToAbbreviationMap) {
		this.dwarfProgram = dwarfProgram;
		this.startOffset = startOffset;
		this.endOffset = endOffset;
		this.length = length;
		this.format = format;
		this.version = version;
		this.abbreviationOffset = abbreviationOffset;
		this.pointerSize = pointerSize;
		this.compUnitNumber = compUnitNumber;
		this.firstDIEOffset = firstDIEOffset;
		this.codeToAbbreviationMap =
			(codeToAbbreviationMap != null) ? codeToAbbreviationMap : new HashMap<>();
	}

	public DWARFCompileUnit getCompileUnit() {
		return compUnit;
	}

	protected void setCompileUnit(DWARFCompileUnit compUnit) {
		this.compUnit = compUnit;
	}

	public DWARFProgram getProgram() {
		return dwarfProgram;
	}

	/**
	 * An unsigned long (4 bytes in 32-bit or 8 bytes in 64-bit format) representing
	 * the length of the .debug_info contribution for that compilation unit,
	 * not including the length field itself.
	 * @return the length in bytes of the this compilation unit
	 */
	public long getLength() {
		return this.length;
	}

	/**
	 * A 1-byte unsigned integer representing the size
	 * in bytes of an address on the target
	 * architecture. If the system uses segmented addressing, this
	 * value represents the size of the offset portion of an address.
	 * @return the size in bytes of pointers
	 */
	public byte getPointerSize() {
		return this.pointerSize;
	}

	/**
	 * Returns the byte offset to the start of this compilation unit.
	 * @return the byte offset to the start of this compilation unit
	 */
	public long getStartOffset() {
		return this.startOffset;
	}

	/**
	 * Returns the byte offset to the end of this compilation unit.
	 * @return the byte offset to the end of this compilation unit
	 */
	public long getEndOffset() {
		return this.endOffset;
	}

	/**
	 * Returns either DWARF_32 or DWARF_64 depending on the current compilation unit format
	 * @return DWARF_32 or DWARF_64 constant depending on the current compilation unit format
	 */
	public int getFormat() {
		return this.format;
	}

	/**
	 * Returns true if the {@code offset} value is within
	 * this compUnit's start and end position in the debug_info section.
	 * @param offset DIE offset
	 * @return true if within range of this compunit
	 */
	public boolean containsOffset(long offset) {
		return firstDIEOffset <= offset && offset < endOffset;
	}

	@Override
	public String toString() {
		StringBuilder buffer = new StringBuilder();
		buffer.append("Compilation Unit");
		buffer.append(" [Start:0x" + Long.toHexString(this.startOffset) + "]");
		buffer.append(" [Length:0x" + Long.toHexString(this.length) + "]");
		buffer.append(" [AbbreviationOffset:0x" + Long.toHexString(this.abbreviationOffset) + "]");
		buffer.append(
			" [CompileUnit: " + (compUnit != null ? compUnit.toString() : "not present") + "]");
		return buffer.toString();
	}

	public Map<Integer, DWARFAbbreviation> getCodeToAbbreviationMap() {
		return codeToAbbreviationMap;
	}

	public long getFirstDIEOffset() {
		return firstDIEOffset;
	}

	public int getCompUnitNumber() {
		return compUnitNumber;
	}

	/**
	 * Reads the {@link DebugInfoEntry} records for this compilation unit from the .debug_info
	 * section.
	 * <p>
	 * @param entries List of DIE records that is written to by this method.  This list should
	 * be empty if the caller only wants this CU's records (ie. normal mode), or the list
	 * can be used to accumulate all DIE records (preload all DIE mode).
	 * @param monitor {@link TaskMonitor} to watch for cancelation
	 * @throws IOException if error reading data
	 * @throws DWARFException if error in DWARF structure
	 * @throws CancelledException if user cancels.
	 */
	public void readDIEs(List<DebugInfoEntry> entries, TaskMonitor monitor)
			throws IOException, DWARFException, CancelledException {

		BinaryReader br = dwarfProgram.getDebugInfo();
		br.setPointerIndex(getFirstDIEOffset());

		Deque<DebugInfoEntry> parentStack = new ArrayDeque<>();

		DebugInfoEntry parent = null;
		DebugInfoEntry die;
		DebugInfoEntry unexpectedTerminator = null;
		while ((br.getPointerIndex() < getEndOffset()) &&
			(die = DebugInfoEntry.read(br, this, dwarfProgram.getAttributeFactory())) != null) {

			monitor.checkCanceled();

			if (die.isTerminator()) {
				if (parent == null && parentStack.isEmpty()) {
					unexpectedTerminator = die;
					continue;
				}
				parent = !parentStack.isEmpty() ? parentStack.pop() : null;
				continue;
			}

			if (unexpectedTerminator != null) {
				throw new DWARFException("Unexpected terminator entry at " +
					Long.toHexString(unexpectedTerminator.getOffset()));
			}
			entries.add(die);

			if (parent != null) {
				parent.addChild(die);
				die.setParent(parent);
			}
			else {
				if (die.getOffset() != getFirstDIEOffset()) {
					throw new DWARFException(
						"Unexpected root level DIE at " + Long.toHexString(die.getOffset()));
				}
			}

			if (die.getAbbreviation().hasChildren()) {
				if (parent != null) {
					parentStack.push(parent);
				}
				parent = die;
			}
		}
	}
}