content: package io.github.anonymous123_code.quilt_bisect.plugin;
  
  import io.github.anonymous123_code.quilt_bisect.plugin.gui.BisectPluginUi;
  import io.github.anonymous123_code.quilt_bisect.shared_plugin.*;
  import org.jetbrains.annotations.NotNull;
  import org.jetbrains.annotations.Nullable;
  import org.quiltmc.loader.api.plugin.solver.ModLoadOption;
  
  import javax.swing.*;
  import java.io.IOException;
  import java.nio.file.Files;
  import java.nio.file.Path;
  import java.security.NoSuchAlgorithmException;
  import java.util.*;
  import java.util.stream.Stream;
  
  import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
  
  public class Bisect {
  	public static void parentBisect(@Nullable String crashLog, @Nullable String crashLogPath) throws IOException, NoSuchAlgorithmException, InvalidCommunicationFileStateException, StacktraceExtractionFailException {
  		var config_dir = AbstractBisectConfig.configDirectory;
  		var modset_path = config_dir.resolve("modSet.txt");
  		var sections_path = config_dir.resolve("sections.txt");
  		List<String> modSet = readModSet(modset_path);
  		List<Integer> sections = readSections(sections_path);
  
  		var active_bisect = ActiveBisectConfig.INSTANCE;
  		var lastRunModSet = createLastRunModSet(modSet, sections, crashLog, crashLogPath, active_bisect);
  		copyLatestLog(lastRunModSet.id);
  		active_bisect.safe();
  	}
  
  
  	public static ModSet createLastRunModSet(List<String> modIdSet, List<Integer> sections, @Nullable String crashLog, @Nullable String crashLogPath, ActiveBisectConfig activeBisectConfig) throws StacktraceExtractionFailException {
  		Optional<Integer> issue = getOrAddIssue(crashLog, activeBisectConfig);
  		ModSet modSet = issue.isPresent() ? new ModSet.Erroring(
  			new ArrayList<>(modIdSet),
  			issue.get(),
  			crashLogPath != null ? crashLogPath : "",
  			new ArrayList<>(sections)
  		) : new ModSet.Working(new ArrayList<>(modIdSet), new ArrayList<>(sections));
  		activeBisectConfig.addAndValidateModSet(modSet);
  		return modSet;
  	}
  
  
  	public static Optional<Integer> getOrAddIssue(@Nullable String crashLog, ActiveBisectConfig activeBisectConfig) throws StacktraceExtractionFailException {
  		var issuePath = AbstractBisectConfig.configDirectory.resolve("issue.txt");
  		if (Files.exists(issuePath)) {
  			String issueData;
  			try {
  				issueData = Files.readString(issuePath);
  				Files.delete(issuePath);
  			} catch (IOException e) {
  				throw new RuntimeException("Failed to read issue comm file", e);
  			}
  			return Optional.of(Integer.parseInt(issueData, 10));
  		} else if (crashLog != null) {
  			var stacktrace = removeStacktracePoison(BisectUtils.extractStackTrace(crashLog));
  			for (int issueIndex = 0; issueIndex < activeBisectConfig.issues.size(); issueIndex++) {
  				var issue = activeBisectConfig.issues.get(issueIndex);
  				if (issue instanceof Issue.CrashIssue crashIssue) {
  					for (var knownStacktrace : crashIssue.stacktraces) {
  						String cleanKnownStacktrace = removeStacktracePoison(knownStacktrace);
  						if (cleanKnownStacktrace.equals(stacktrace)) {
  							return Optional.of(issueIndex);
  						}
  					}
  				}
  			}
  			for (int issueIndex = 0; issueIndex < activeBisectConfig.issues.size(); issueIndex++) {
  				var issue = activeBisectConfig.issues.get(issueIndex);
  				if (issue instanceof Issue.CrashIssue crashIssue) {
  					for (var knownStacktrace : crashIssue.stacktraces) {
  						String cleanKnownStacktrace = removeStacktracePoison(knownStacktrace);
  						if (MainConfig.INSTANCE.showDiffWhenNoExactMatchFound == MainConfig.ShowDiffOptions.Always || (MainConfig.INSTANCE.showDiffWhenNoExactMatchFound == MainConfig.ShowDiffOptions.FirstLineMatch && cleanKnownStacktrace.split(
  							"\n")[0].equals(stacktrace.split("\n")[0]))) {
  							try {
  								if (BisectPluginUi.openDialog(cleanKnownStacktrace, stacktrace)) {
  									crashIssue.stacktraces.add(stacktrace);
  									return Optional.of(issueIndex);
  								}
  							} catch (UnsupportedLookAndFeelException | ClassNotFoundException | InstantiationException |
  									 IllegalAccessException e) {
  								throw new RuntimeException("Unable to open crash dialog", e);
  							}
  						}
  					}
  				}
  			}
  			activeBisectConfig.issues.add(new Issue.CrashIssue(stacktrace));
  			return Optional.of(activeBisectConfig.issues.size() - 1);
  		} else {
  			return Optional.empty();
  		}
  	}
  
  	public static String removeStacktracePoison(String oldStacktrace) {
  		return oldStacktrace.replaceAll(":\\d+\\)", ")") // Line numbers
  							.replaceAll("\\.handler\\$[0-9a-z]{6}\\$", ".fuzzyMixinHandler\\$"); // Mixin
  	}
  
  	private static List<Integer> readSections(Path sectionsPath) throws IOException, InvalidCommunicationFileStateException {
  		if (Files.exists(sectionsPath)) {
  			var resultUnprocessed = Files.readAllLines(sectionsPath);
  			Files.delete(sectionsPath);
  			var result = new ArrayList<Integer>(resultUnprocessed.size());
  			for (String modSectionUnprocessed : resultUnprocessed) {
  				result.add(Integer.parseInt(modSectionUnprocessed, 10));
  			}
  			return result;
  		} else {
  			throw new InvalidCommunicationFileStateException("sections file");
  		}
  	}
  
  	private static void copyLatestLog(String modSetHash) throws IOException {
  		var logFilePath = Path.of("logs", "latest.log");
  		if (Files.isRegularFile(logFilePath)) {
  			Files.createDirectories(Path.of(MainConfig.INSTANCE.bisectLogStorageDirectory));
  			Files.copy(
  				logFilePath,
  				Path.of(MainConfig.INSTANCE.bisectLogStorageDirectory, modSetHash + ".log"),
  				REPLACE_EXISTING
  			);
  		}
  	}
  
  	private static @NotNull List<String> readModSet(Path modSetPath) throws IOException, InvalidCommunicationFileStateException {
  		if (Files.exists(modSetPath)) {
  			var result = Files.readAllLines(modSetPath);
  			Files.delete(modSetPath);
  			return result;
  		} else {
  			throw new InvalidCommunicationFileStateException("mod set file");
  		}
  	}
  
  	public static SectionList childBisect(List<ModLoadOption> loadOptions) throws IOException {
  		var activeBisect = ActiveBisectConfig.INSTANCE;
  		activeBisect.updateLoadOptions(loadOptions);
  		if (activeBisect.bisectSettings != null && activeBisect.bisectSettings.disableWorldSaving()) {
  			Path worldStorageLocation = Path.of("saves");
  			Path backupStorageLocation = Path.of(MainConfig.INSTANCE.bisectSavesBackupDirectory);
  
  			if (Files.exists(backupStorageLocation)) {
  				if (Files.isDirectory(backupStorageLocation)) {
  					// For every dir in backup:
  					try (Stream<Path> path = Files.list(backupStorageLocation)) {
  						for (var file : path.toList()) {
  							// Delete the corresponding world
  							deleteDirectory(worldStorageLocation.resolve(file.getFileName()));
  						}
  					}
  					copyDirectory(backupStorageLocation, worldStorageLocation);
  				} else {
  					throw new RuntimeException("Save backup is not a directory. please delete or rename it.");
  				}
  			}
  			copyDirectory(worldStorageLocation, backupStorageLocation);
  		}
  		activeBisect.safe();
  		return calculateModSet(activeBisect);
  	}
  
  	public static void deleteDirectory(Path dir) throws IOException {
  		if (Files.exists(dir)) {
  			try (Stream<Path> pathStream = Files.walk(dir)) {
  				for (Path path : (Iterable<? extends Path>) pathStream.sorted(Comparator.reverseOrder())::iterator) {
  					Files.deleteIfExists(path);
  				}
  			}
  		}
  	}
  
  	public static void copyDirectory(Path from, Path to) throws IOException {
  		try (Stream<Path> paths = Files.walk(from)) {
  			for (Path source : (Iterable<? extends Path>) paths::iterator) {
  				Path destination = to.resolve(from.relativize(source));
  				if (!Files.exists(destination)) {
  					Files.copy(source, destination);
  				}
  			}
  		}
  	}
  
  	private static SectionList calculateModSet(ActiveBisectConfig activeBisect) {
  		Optional<ModSet> firstInvalidated = activeBisect.getFirstInvalidatedModSet();
  		if (firstInvalidated.isPresent()) {
  			return SectionList.fromSections(List.of(firstInvalidated.get().getFullSection()));
  		} else {
  			// find the smallest mod set where an unsolved issue is present -> this is the mod set we're trying to debug
  			var modSet = testFixes(activeBisect);
  			if (modSet.isPresent()) {
  				return modSet.get();
  			}
  
  			ModSet.Erroring smallestIssueModSet = activeBisect.findSmallestUnfixedModSet();
  			while (smallestIssueModSet != null) {
  				modSet = debugIssueModSet(smallestIssueModSet, activeBisect);
  				if (modSet.isPresent()) {
  					return modSet.get();
  				}
  
  				modSet = testFixes(activeBisect);
  				if (modSet.isPresent()) {
  					return modSet.get();
  				}
  
  				smallestIssueModSet = activeBisect.findSmallestUnfixedModSet();
  			}
  
  			var fixes = BisectUtils.calculateFixes(activeBisect.issues.stream().map(issue -> issue.fix).toList());
  			modSet = testAllFixes(fixes, activeBisect);
  			if (modSet.isPresent()) {
  				return modSet.get();
  			}
  
  			Set<String> smallestFix = fixes.stream().min(Comparator.comparingInt(Set::size)).orElse(Set.of());
  			ModSet largestModSet = activeBisect.modSets.stream()
  													   .max(Comparator.comparingInt(modSet1 -> modSet1.modSet.size()))
  													   .orElse(new ModSet.Working(
  														   new ArrayList<>(),
  														   new ArrayList<>()
  													   ));
  
  			activeBisect.bisectSettings = null;
  			return SectionList.from(largestModSet.modSet.stream().filter(it -> !smallestFix.contains(it)).toList());
  		}
  	}
  
  	private static Optional<SectionList> debugIssueModSet(ModSet.Erroring issueModSet, ActiveBisectConfig activeBisect) {
  		var result = new ArrayList<ModSet.Section>();
  		var sections = issueModSet.sections();
  		for (int i = 0; i < sections.size(); i++) {
  			ModSet.Section section = sections.get(i);
  			if (section.size() != 1) {
  				result.addAll(sections.subList(i + 1, sections.size()));
  				result.addAll(bisect(section, activeBisect, result.toArray(new ModSet.Section[]{})));
  				return Optional.of(SectionList.fromSections(result));
  			}
  			result.add(section);
  		}
  
  		var reproductionSet = new ArrayList<String>();
  		for (ModSet.Section section : result) {
  			reproductionSet.add(section.getListCopy().get(0));
  		}
  		reproductionSet.sort(null);
  
  		activeBisect.issues.get(issueModSet.issueId).fix.reproductions.add(reproductionSet);
  		return Optional.empty();
  	}
  
  	private static Optional<SectionList> testFixes(ActiveBisectConfig activeBisect) {
  		for (int fixedIssueId : activeBisect.findFixedIssues()) {
  			Issue testedIssue = activeBisect.issues.get(fixedIssueId);
  			ArrayList<ArrayList<String>> reproductions = testedIssue.fix.reproductions;
  
  			List<ModSet> issueModSets = activeBisect.modSets.stream()
  															.filter(it -> it instanceof ModSet.Erroring erroring && erroring.issueId == fixedIssueId)
  															.toList();
  			// All mod sets of the issue that are not contained by any other mod set with the issue
  			List<ModSet> largestIssueModSets = issueModSets.stream().filter(it -> issueModSets.stream()
  																							  .noneMatch(other -> other != it && other.modSet.containsAll(
  																								  it.modSet))).toList();
  			var fixes = BisectUtils.mergeReproductions(reproductions);
  			System.out.println(Arrays.toString(fixes.toArray()));
  			for (var largestIssueModSet : largestIssueModSets) {
  				for (var fix : fixes) {
  					var fixedMods = new ArrayList<>(largestIssueModSet.modSet.stream()
  																			 .filter(it -> !fix.contains(it))
  																			 .toList());
  					if (activeBisect.getModSet(fixedMods).isEmpty()) {
  						return Optional.of(SectionList.from(fixedMods));
  					}
  				}
  			}
  		}
  		return Optional.empty();
  	}
  
  	private static Optional<SectionList> testAllFixes(Set<Set<String>> fixes, ActiveBisectConfig activeBisectConfig) {
  		List<ModSet> largestModSets = activeBisectConfig.modSets.stream()
  																.filter(it -> activeBisectConfig.modSets.stream()
  																										.noneMatch(other -> other != it && other.modSet.containsAll(
  																											it.modSet)))
  																.toList();
  		for (ModSet largestModSet : largestModSets) {
  			for (Set<String> fix : fixes) {
  				var fixedMods = new ArrayList<>(largestModSet.modSet.stream().filter(it -> !fix.contains(it)).toList());
  				if (activeBisectConfig.getModSet(fixedMods).isEmpty()) {
  					return Optional.of(SectionList.from(fixedMods));
  				}
  			}
  		}
  		return Optional.empty();
  	}
  
  	private static ArrayList<ModSet.Section> bisect(ModSet.Section parent, ActiveBisectConfig activeBisect, ModSet.Section... givenModSetSections) {
  		// If the size is 1, we found one culprit, and can return early
  		if (parent.size() == 1) {
  			return new ArrayList<>(List.of(parent));
  		}
  		// Divide the mod set into halves
  		for (ModSet.Section[] half : parent.getPossibleArrayHalveCombinations()) {
  			Optional<ModSet> half0ModSet = activeBisect.getModSet(half[0], givenModSetSections);
  			Optional<ModSet> half1ModSet = activeBisect.getModSet(half[1], givenModSetSections);
  			if (modSetExistsAndIsWorkingOrFixed(half0ModSet, activeBisect) && modSetExistsAndIsWorkingOrFixed(
  				half1ModSet,
  				activeBisect
  			)) {
  				// If both have been tested (without any issue), two mods in the parent section must be required for the incompatibility. In this case treat both halves as different sections and bisect them.
  				if (half[0].size() > 1) {
  					var modSetSection = new ModSet.Section[givenModSetSections.length + 1];
  					System.arraycopy(givenModSetSections, 0, modSetSection, 1, givenModSetSections.length);
  					modSetSection[0] = half[1];
  					var result = bisect(half[0], activeBisect, modSetSection);
  					result.add(half[1]);
  					return result;
  				} else {
  					var modSetSection = new ModSet.Section[givenModSetSections.length + 1];
  					System.arraycopy(givenModSetSections, 0, modSetSection, 1, givenModSetSections.length);
  					modSetSection[0] = half[0];
  					var result = bisect(half[1], activeBisect, modSetSection);
  					result.add(half[0]);
  					return result;
  				}
  			} else if (modSetExistsAndIsWorkingOrFixed(half0ModSet, activeBisect)) {
  				// If the first half already has been tested, test the other half
  				return new ArrayList<>(Collections.singletonList(half[1]));
  			} else if (modSetExistsAndIsWorkingOrFixed(half1ModSet, activeBisect)) {
  				// If the second half already has been tested, test the other half
  				return new ArrayList<>(Collections.singletonList(half[0]));
  			}
  		}
  		// If no halves have been tested, test the first one
  		return new ArrayList<>(Collections.singletonList(parent.getPossibleArrayHalveCombinations()[0][0]));
  	}
  
  	private static boolean modSetExistsAndIsWorkingOrFixed(Optional<ModSet> modSet, ActiveBisectConfig activeBisect) {
  		return modSet.isPresent() && modSet.get().isWorkingOrFixed(activeBisect);
  	}
  }
  