package org.ghidra.bridge.project;

import java.io.File;
import java.util.LinkedHashMap;
import java.util.Map;

import ghidra.app.plugin.core.analysis.AutoAnalysisManager;
import ghidra.app.util.importer.ProgramLoader;
import ghidra.app.util.opinion.LoadResults;
import ghidra.base.project.GhidraProject;
import ghidra.framework.model.DomainFile;
import ghidra.program.model.listing.Program;
import ghidra.program.util.GhidraProgramUtilities;
import ghidra.util.task.TaskMonitor;
import org.ghidra.bridge.runtime.TaskMonitors;
import org.ghidra.bridge.serialization.JsonWriter;
import org.ghidra.bridge.session.ProgramHandle;
import org.ghidra.bridge.session.ProjectHandle;

public final class ProjectService {
    private ProjectService() {
    }

    public static ProjectHandle openOrCreateProject(String projectLocation, String projectName) throws Exception {
        try {
            return new ProjectHandle(GhidraProject.openProject(projectLocation, projectName, true));
        } catch (Exception ignored) {
            new File(projectLocation).mkdirs();
            return new ProjectHandle(GhidraProject.createProject(projectLocation, projectName, false));
        }
    }

    public static ProgramHandle openProgram(ProjectHandle projectHandle, String folderPath, String programName)
            throws Exception {
        GhidraProject project = projectHandle.project();
        return new ProgramHandle(project.openProgram(folderPath, programName, false));
    }

    public static LoadedProgram openOrImportProgram(
        ProjectHandle projectHandle,
        String binaryPath,
        String folderPath,
        String programName,
        String loader,
        String language,
        String compiler,
        String[] loaderArgs,
        int monitorTimeoutSeconds
    ) throws Exception {
        GhidraProject project = projectHandle.project();
        String projectPath = projectPath(folderPath, programName);
        TaskMonitor monitor = TaskMonitors.create(monitorTimeoutSeconds);
        DomainFile file = project.getProjectData().getFile(projectPath);
        if (file != null) {
            Program program = project.openProgram(folderPath, programName, false);
            return new LoadedProgram(
                new ProgramHandle(program),
                loadInfo(program, true, folderPath, programName, loader, monitor, monitorTimeoutSeconds)
            );
        }

        ProgramLoader.Builder builder = ProgramLoader.builder()
            .source(new File(binaryPath))
            .project(project.getProject())
            .projectFolderPath(folderPath)
            .name(programName)
            .monitor(monitor);
        if (loader != null) {
            builder.loaders(loader);
        }
        if (language != null) {
            builder.language(language);
        }
        if (compiler != null) {
            builder.compiler(compiler);
        }
        if (loaderArgs.length % 2 != 0) {
            throw new IllegalArgumentException("loader arguments must be name/value pairs");
        }
        for (int i = 0; i < loaderArgs.length; i += 2) {
            builder.addLoaderArg(loaderArgs[i], loaderArgs[i + 1]);
        }

        LoadResults<Program> results = builder.load();
        try {
            results.save(monitor);
        } finally {
            results.close();
        }
        Program program = project.openProgram(folderPath, programName, false);
        return new LoadedProgram(
            new ProgramHandle(program),
            loadInfo(
                program,
                false,
                folderPath,
                programName,
                loader,
                monitor,
                monitorTimeoutSeconds
            )
        );
    }

    public static String analyze(
        ProgramHandle programHandle,
        String mode,
        int monitorTimeoutSeconds
    ) {
        Program program = programHandle.program();
        TaskMonitor monitor = TaskMonitors.create(monitorTimeoutSeconds);
        boolean alreadyAnalyzed = !GhidraProgramUtilities.shouldAskToAnalyze(program);
        boolean analyzed = false;
        if ("skip".equals(mode)) {
            return JsonWriter.json(analysisReport(
                mode,
                false,
                alreadyAnalyzed,
                "",
                monitor,
                monitorTimeoutSeconds
            ));
        }
        if ("force".equals(mode)) {
            GhidraProgramUtilities.resetAnalysisFlags(program);
            alreadyAnalyzed = false;
        } else if (!"if_needed".equals(mode)) {
            throw new IllegalArgumentException("invalid analysis mode: " + mode);
        }

        if ("force".equals(mode) || GhidraProgramUtilities.shouldAskToAnalyze(program)) {
            AutoAnalysisManager manager = AutoAnalysisManager.getAnalysisManager(program);
            manager.setDebug(true);
            manager.initializeOptions();
            manager.reAnalyzeAll(null);
            manager.startAnalysis(monitor);
            if (!monitor.isCancelled()) {
                GhidraProgramUtilities.markProgramAnalyzed(program);
                analyzed = true;
            }
        }
        AutoAnalysisManager manager = AutoAnalysisManager.getAnalysisManager(program);
        return JsonWriter.json(analysisReport(
            mode,
            analyzed,
            alreadyAnalyzed,
            manager.getMessageLog().toString(),
            monitor,
            monitorTimeoutSeconds
        ));
    }

    public static void save(ProjectHandle projectHandle, ProgramHandle programHandle) throws Exception {
        projectHandle.project().save(programHandle.program());
    }

    public static void closeProgram(ProjectHandle projectHandle, ProgramHandle programHandle) {
        projectHandle.project().close(programHandle.program());
    }

    public static void closeProject(ProjectHandle projectHandle) {
        projectHandle.project().close();
    }

    private static String projectPath(String folderPath, String programName) {
        if ("/".equals(folderPath)) {
            return "/" + programName;
        }
        return folderPath + "/" + programName;
    }

    private static Map<String, Object> loadInfo(
        Program program,
        boolean openedExisting,
        String folderPath,
        String programName,
        String loader,
        TaskMonitor monitor,
        int monitorTimeoutSeconds
    ) {
        Map<String, Object> info = new LinkedHashMap<>();
        info.put("opened_existing", openedExisting);
        info.put("project_path", projectPath(folderPath, programName));
        info.put("folder", folderPath);
        info.put("name", programName);
        info.put("loader", loader);
        info.put("language", program.getLanguageID().toString());
        info.put("compiler", program.getCompilerSpec().getCompilerSpecID().toString());
        info.put("monitor", TaskMonitors.report(monitor, monitorTimeoutSeconds));
        return info;
    }

    private static Map<String, Object> analysisReport(
        String mode,
        boolean analyzed,
        boolean alreadyAnalyzed,
        String messageLog,
        TaskMonitor monitor,
        int monitorTimeoutSeconds
    ) {
        Map<String, Object> report = new LinkedHashMap<>();
        report.put("mode", mode);
        report.put("analyzed", analyzed);
        report.put("already_analyzed", alreadyAnalyzed);
        report.put("message_log", messageLog);
        report.put("monitor", TaskMonitors.report(monitor, monitorTimeoutSeconds));
        return report;
    }

    public static final class LoadedProgram {
        private final ProgramHandle program;
        private final Map<String, Object> info;

        public LoadedProgram(ProgramHandle program, Map<String, Object> info) {
            this.program = program;
            this.info = info;
        }

        public ProgramHandle program() {
            return program;
        }

        public Map<String, Object> info() {
            return info;
        }
    }
}
