/* ###
 * 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.opinion;

import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.util.*;

import ghidra.app.plugin.core.analysis.AnalysisWorker;
import ghidra.app.plugin.core.analysis.AutoAnalysisManager;
import ghidra.app.util.Option;
import ghidra.app.util.bin.ByteProvider;
import ghidra.app.util.importer.MessageLog;
import ghidra.program.model.address.Address;
import ghidra.program.model.lang.*;
import ghidra.program.model.listing.Program;
import ghidra.util.Msg;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.TaskMonitor;

/**
 * Loader for handling XML files generated by the Decompiler on the central function that was in the
 * Decompiler pane when the Debug Function Decompilation feature in the Decompiler is used.
 * 
 * Reference @DecompileDebug.java for the generation of the XML file. 
 */
public class DecompileDebugXmlLoader extends AbstractProgramLoader {

	public static final String DECOMPILER_DEBUG_SRC_NAME = "Decompiler Debug XML";

	@Override
	public String getName() {
		return DECOMPILER_DEBUG_SRC_NAME;
	}

	@Override
	public LoaderTier getTier() {
		return LoaderTier.SPECIALIZED_TARGET_LOADER;
	}

	@Override
	public int getTierPriority() {
		return 50;
	}

	/*
	 * This is an override method that identifies the load specs for the program to populate 
	 * the initial upload dialog.
	 * 
	 * @param provider
	 * @return the list of possible load specs (though really only one should work)
	 */
	@Override
	public Collection<LoadSpec> findSupportedLoadSpecs(ByteProvider provider) throws IOException {

		List<LoadSpec> loadSpecs = new ArrayList<>();

		// the language service sets up the log, language map, and service
		getLanguageService();

		ParseGhidraDebugResult result = parse(provider);
		DecompileDebugProgramInfo info = result.lastInfo;

		if (info == null) {
			// indicates that a different loader should be used
			return loadSpecs;
		}
		LanguageID languageID = new LanguageID(info.specString());

		LanguageDescription languageDescription =
			getLanguageService().getLanguageDescription(languageID);

		for (CompilerSpecDescription csd : languageDescription
				.getCompatibleCompilerSpecDescriptions()) {
			LanguageCompilerSpecPair pair =
				new LanguageCompilerSpecPair(languageDescription.getLanguageID(),
					csd.getCompilerSpecID());

			if (info.compilerString().equals(csd.getCompilerSpecID().getIdAsString())) {
				loadSpecs.add(new LoadSpec(this, 0, pair, true));
			}
		}

		return loadSpecs;
	}

	/**
	 * After initial parsing of the XML file, load the details into a new program for 
	 * loading/viewing in Ghidra.
	 * <hr>
	 * {@inheritDoc}
	 */
	@Override
	protected List<Loaded<Program>> loadProgram(ImporterSettings settings)
			throws IOException, CancelledException {

		LanguageCompilerSpecPair pair = settings.loadSpec().getLanguageCompilerSpec();
		Language importerLanguage = getLanguageService().getLanguage(pair.languageID);
		ParseGhidraDebugResult parsedResult = parse(settings.provider());
		if (parsedResult.lastInfo == null) {
			return new ArrayList<Loaded<Program>>();
		}

		Address imageBase = null;
		if (parsedResult.lastInfo.offset() != null) {
			imageBase =
				importerLanguage.getAddressFactory().getAddress(parsedResult.lastInfo.offset());
		}

		Program prog = createProgram(imageBase, settings);
		List<Loaded<Program>> loadedList = List.of(new Loaded<>(prog, settings));

		int loadingId = prog.startTransaction("Loading debug XML file");

		try {
			doImport(parsedResult.debugXmlMgr, settings.options(), settings.log(), prog,
				settings.monitor(), false, settings.importName());
		}
		catch (

		IllegalArgumentException e) {
			throw new LoadException("Failed to load");
		}

		finally {
			prog.endTransaction(loadingId, true);
		}
		return loadedList;
	}

	/**
	 * Import the XML; first parse and load details into the generation of a new program based 
	 * around the "central" function that the Decompile Debug XML file was generated from. 
	 *
	 * @param debugXmlMgr Manages the parsing of the XML objects 
	 * @param options if there are any settings for importing work - these will always be empty
	 * @param log for writing to
	 * @param prog main program
	 * @param monitor TaskMonitor
	 * @param isAddToProgram whether we are importing the program into an existing program - this 
	 * 	will always be false.
	 * @param programName name of program
	 * 
	 * @return boolean success 
	 */
	private boolean doImport(DecompileDebugFormatManager debugXmlMgr, List<Option> options,
			MessageLog log, Program prog, TaskMonitor monitor, boolean isAddToProgram,
			String programName)
			throws IOException {

		// if we're adding to another program, an autoAnalysisManager will already exist
		if (!AutoAnalysisManager.hasAutoAnalysisManager(prog)) {
			// helps to manage the threads/task workers
			int txId = prog.startTransaction("Decompiler Debug XML Import");
			try {
				return doImportWork(debugXmlMgr, options, log, prog, monitor, isAddToProgram,
					programName);
			}
			finally {
				prog.endTransaction(txId, true);
			}
		}

		AutoAnalysisManager analysisMgr = AutoAnalysisManager.getAnalysisManager(prog);
		try {
			return analysisMgr.scheduleWorker(new AnalysisWorker() {

				@Override
				public String getWorkerName() {
					return "Decompiler Debug XML Importer";
				}

				@Override
				public boolean analysisWorkerCallback(Program program, Object workerContext,
						TaskMonitor taskMonitor) throws Exception, CancelledException {

					return doImportWork(debugXmlMgr, options, log, program, taskMonitor,
						isAddToProgram, programName);
				}
			}, null, false, monitor);
		}
		catch (CancelledException e) {
			return false;
		}
		catch (InvocationTargetException e) {
			Throwable cause = e.getCause();
			if (cause instanceof IOException) {
				throw (IOException) cause;
			}
			throw new RuntimeException(e);
		}
		catch (InterruptedException e) {
			throw new RuntimeException(e);
		}
	}

	/**
	 * Handles calling the parsing function in the DecompileDebugFormat manager.
	 *  
	 * @param debugXmlMgr DecompileDebugFormatManager object which handles the parsing of the XML
	 * @param options if there are any settings for importing work - these will always be empty
	 * @param log for writing to
	 * @param prog main program
	 * @param monitor TaskMonitor
	 * @param isAddToProgram whether we are importing the program into an existing program - this 
	 * 	will always be false.
	 * @param programName name of program 
	 * 
	 * @return success
	 */
	private boolean doImportWork(DecompileDebugFormatManager debugXmlMgr, List<Option> options,
			MessageLog log, Program prog, TaskMonitor monitor, boolean isAddToProgram,
			String programName)
			throws LoadException {

		MessageLog mgrLog = null;
		boolean success = false;

		try {
			mgrLog = debugXmlMgr.read(prog, monitor, programName);
			log.copyFrom(mgrLog);
			success = true;
		}
		catch (Exception e) {
			String message = "";
			if (mgrLog != null && !"".equals(mgrLog.toString())) {
				message = mgrLog.toString();
			}
			if (log != null && !"".equals(log.toString())) {
				message = log.toString();
			}
			Msg.warn(this, "XML import exception, log: " + message, e);
			throw new LoadException(e.getMessage());
		}
		return success;
	}

	/**
	 * Class to organize the XML format handler and the program info as extracted from the 
	 * uploaded decompiler debug XML file.
	 */
	private static class ParseGhidraDebugResult {
		final DecompileDebugFormatManager debugXmlMgr;
		final DecompileDebugProgramInfo lastInfo;

		ParseGhidraDebugResult(DecompileDebugFormatManager debugXmlMgr,
				DecompileDebugProgramInfo lastInfo) {
			this.debugXmlMgr = debugXmlMgr;
			this.lastInfo = lastInfo;
		}
	}

	/**
	 * Parse the XML file for the needed tags in order to populate a full program to load
	 * 
	 * @param provider ByteProvider from the XML 
	 * 
	 * @return result object populated with the program details from the XML file
	 */
	private ParseGhidraDebugResult parse(ByteProvider provider) {

		try {
			DecompileDebugFormatManager debugXmlMgr = new DecompileDebugFormatManager(provider);
			DecompileDebugProgramInfo decompileProgInfo = debugXmlMgr.getProgramInfo();

			return new ParseGhidraDebugResult(debugXmlMgr, decompileProgInfo);
		}
		catch (Throwable e) {
			Msg.trace(this, "Unable to parse the Ghidra Decompiler XML for " + provider.getName(),
				e);
			return new ParseGhidraDebugResult(null, null);
		}

	}

	@Override
	protected void loadProgramInto(Program program, ImporterSettings settings)
			throws IOException, LoadException, CancelledException {
		// since we will not ever be loading this debug program into an existing program, this 
		// should not be used. 
	}

	/**
	 * Hold program details from the spec string for loading. Object is then passed forward in the 
	 * .getProgramInfo() function implemented in @DecompileDebugFormatManager.java and populated by @DecompileDebugXmlLoader.java.
	 * @param offset String
	 * @param compilerString String 
	 * @param specString String
	 */
	static record DecompileDebugProgramInfo(String offset, String compilerString,
			String specString) {}

}
