package gamecubeloader.dol;

import java.io.FileNotFoundException;
import java.io.FileReader;
import java.math.BigInteger;

import docking.widgets.OptionDialog;
import docking.widgets.filechooser.GhidraFileChooser;
import gamecubeloader.common.SystemMemorySections;
import gamecubeloader.common.SymbolLoader;
import gamecubeloader.dol.DOLHeader;
import ghidra.app.util.MemoryBlockUtils;
import ghidra.app.util.bin.ByteProvider;
import ghidra.program.model.address.AddressSpace;
import ghidra.program.model.listing.ContextChangeException;
import ghidra.program.model.listing.Program;
import ghidra.util.Msg;
import ghidra.util.filechooser.ExtensionFileFilter;
import ghidra.util.task.TaskMonitor;

public final class DOLProgramBuilder {
	private DOLHeader dol;
	
	private long baseAddress;
	private AddressSpace addressSpace;
	private Program program;
	private boolean autoloadMaps;
	private String binaryName;
	
	public DOLProgramBuilder(DOLHeader dol, ByteProvider provider, Program program,
			TaskMonitor monitor, boolean autoloadMaps, boolean createDefaultMemSections) {
		this.dol = dol;
		this.program = program;
		this.autoloadMaps = autoloadMaps;
		this.binaryName = provider.getName();
		
		this.load(monitor, provider);
		if (createDefaultMemSections) {
			SystemMemorySections.Create(program);
		}
	}
	
	protected void load(TaskMonitor monitor, ByteProvider provider) {
		this.baseAddress = 0x80000000L;
		this.addressSpace = program.getAddressFactory().getDefaultAddressSpace();
		
		try {
			this.program.setImageBase(addressSpace.getAddress(this.baseAddress), true);
			dol.memoryEndAddress = 0;
			
			// Load the DOL file.
			for (int i = 0; i < 7; i++) {
				if (dol.textSectionSizes[i] > 0) {
					MemoryBlockUtils.createInitializedBlock(this.program, false, String.format("MAIN_.text%d", i), addressSpace.getAddress(dol.textSectionMemoryAddresses[i]),
						provider.getInputStream(dol.textSectionOffsets[i]), dol.textSectionSizes[i], "", null, true, true, true, null, monitor);
					
					if (dol.memoryEndAddress < dol.textSectionMemoryAddresses[i] + dol.textSectionSizes[i]) {
						dol.memoryEndAddress = dol.textSectionMemoryAddresses[i] + dol.textSectionSizes[i];
					}
				}
			}
			
			for (int i = 0; i < 11; i++) {
				if (dol.dataSectionSizes[i] > 0) {
					MemoryBlockUtils.createInitializedBlock(this.program, false, String.format("MAIN_.data%d", i), addressSpace.getAddress(dol.dataSectionMemoryAddresses[i]),
						provider.getInputStream(dol.dataSectionOffsets[i]), dol.dataSectionSizes[i], "", null, true, true, false, null, monitor);
					
					if (dol.memoryEndAddress < dol.dataSectionMemoryAddresses[i] + dol.dataSectionSizes[i]) {
						dol.memoryEndAddress = dol.dataSectionMemoryAddresses[i] + dol.dataSectionSizes[i];
					}
				}
			}
			
			// Add uninitialized sections.
			this.CreateUninitializedSections();
		}
		catch (Exception e) {
			e.printStackTrace();
		}
		
		// Mark the DOL's entry point.
		this.program.getSymbolTable().addExternalEntryPoint(this.addressSpace.getAddress(this.dol.entryPoint));
			
		// Ask if the user wants to load a symbol map file.
		SymbolLoader.LoadMapResult mapLoadedResult = null;
		if (this.autoloadMaps) {
			var name = provider.getName();
			if (name.contains(".")) {
				name = name.substring(0, name.lastIndexOf("."));
			}
			
			mapLoadedResult = SymbolLoader.TryLoadAssociatedMapFile(name, provider.getFile().getParentFile(), this.program, monitor, dol.textSectionMemoryAddresses[0],
					32, dol.bssMemoryAddress);
		}
		
		if (mapLoadedResult != null && mapLoadedResult.loaded == false) {
			if (OptionDialog.showOptionNoCancelDialog(null, "Load Symbols?", "Would you like to load a symbol map for this DOL executable?", "Yes", "No", null) == 1) {
				var fileChooser = new GhidraFileChooser(null);
				fileChooser.setCurrentDirectory(provider.getFile().getParentFile());
				fileChooser.addFileFilter(new ExtensionFileFilter("map", "Symbol Map Files"));
				var selectedFile = fileChooser.getSelectedFile(true);
				
				if (selectedFile != null) {
					FileReader reader = null;
					try {
						reader = new FileReader(selectedFile);
					} catch (FileNotFoundException e) {
						Msg.error(this, String.format("Failed to open the symbol map file!\nReason: %s", e.getMessage()));
					}
					
					if (reader != null) {
						SymbolLoader loader = new SymbolLoader(this.program, monitor, reader, dol.textSectionMemoryAddresses[0], 32, dol.bssMemoryAddress,
							this.binaryName);
						loader.ApplySymbols();
					}
				}
			}
		}
	}
	
	private void CreateUninitializedSections() {
		var uninitializedSectionsSize = dol.bssSize;
		var uninitializedSectionAddress = dol.bssMemoryAddress;
		var uninitializedSectionIdx = 0;
		
		while (uninitializedSectionsSize > 0 && uninitializedSectionIdx < 3) {
			// Check for intersecting sections at the current address + size.
			var uninitializedSectionEndAddress = uninitializedSectionAddress + uninitializedSectionsSize;
			var wroteSection = false;
			
			for (var i = 0; i < this.dol.dataSectionMemoryAddresses.length; i++) {
				var sectionAddress = this.dol.dataSectionMemoryAddresses[i];
				var sectionSize = this.dol.dataSectionSizes[i];
				if (sectionAddress >= uninitializedSectionAddress && sectionAddress < uninitializedSectionEndAddress) {
					// Truncate the size and create a section.
					var thisSectionSize = sectionAddress - uninitializedSectionAddress;
					if (thisSectionSize > 0) {
						var createdSection = MemoryBlockUtils.createUninitializedBlock(this.program, false, String.format("MAIN_%s", "uninitialized" + uninitializedSectionIdx),
								addressSpace.getAddress(uninitializedSectionAddress), thisSectionSize, "", null, true, true, false, null);
						
						if (createdSection == null) {
							Msg.warn(this, "Failed to create uninitialized section: " + "uninitialized" + uninitializedSectionIdx);
						}
						
						if (this.dol.memoryEndAddress < uninitializedSectionAddress + thisSectionSize) {
							this.dol.memoryEndAddress = uninitializedSectionAddress + thisSectionSize;
						}
						
						// We also have to subtract any intersecting sections from the size.
						// NOTE: This may not be correct for sections which aren't .sdata & .sdata2 which intersect it.
						uninitializedSectionsSize -= sectionSize;
						
						uninitializedSectionsSize -= thisSectionSize;
						uninitializedSectionAddress = sectionAddress + sectionSize;
						uninitializedSectionIdx++;
						wroteSection = true;
						break;
					}
				}
			}
			
			// If we didn't create any uninitialized sections, we must be clear to write the rest of the size without intersections.
			if (wroteSection == false) {
				var createdSection = MemoryBlockUtils.createUninitializedBlock(this.program, false, String.format("MAIN_%s", "uninitialized" + uninitializedSectionIdx),
						addressSpace.getAddress(uninitializedSectionAddress), uninitializedSectionsSize, "", null, true, true, false, null);
				
				if (createdSection == null) {
					Msg.warn(this, "Failed to create uninitialized section: " + DOLHeader.DATA_NAMES[8 + uninitializedSectionIdx]);
				}
				
				if (this.dol.memoryEndAddress < uninitializedSectionAddress + uninitializedSectionsSize) {
					this.dol.memoryEndAddress = uninitializedSectionAddress + uninitializedSectionsSize;
				}
				
				break;
			}
		}
	}
}