/* ###
 * 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.
 */
// Script upgrades all Program files within the current project.
// Since there should be no checkouts when an upgrade is performed,
// the script will optionally list any existing checkouts prior to starting
// the batch upgrade.
//
//@category Program
import java.io.IOException;

import ghidra.app.script.GhidraScript;
import ghidra.framework.client.ClientUtil;
import ghidra.framework.client.RepositoryAdapter;
import ghidra.framework.data.CheckinHandler;
import ghidra.framework.model.*;
import ghidra.framework.store.ItemCheckoutStatus;
import ghidra.program.database.ProgramContentHandler;
import ghidra.program.model.lang.LanguageNotFoundException;
import ghidra.util.exception.CancelledException;
import ghidra.util.exception.VersionException;

public class RepositoryFileUpgradeScript extends GhidraScript {

	boolean includePrivatePrograms;

	@Override
	protected void run() throws Exception {
		RepositoryAdapter repository = null;
		try {
			Project project = state.getProject();
			if (project == null) {
				popup("No active project found!");
				return;
			}

			if (currentProgram != null) {
				popup("All programs must be closed before running this script!");
				return;
			}

			repository = project.getRepository();
			if (repository != null && !repository.isConnected()) {
				if (askYesNo("Connect?",
					"You are not connected to the repository server!  Connect now?")) {
					repository.connect();
					if (!repository.isConnected()) {
						return;
					}
				}
				else {
					popup("Operation cancelled - unable to proceed without connected repository.");
					return;
				}
			}

			ProjectData projectData = project.getProjectData();
			if (askYesNo("Find Active Checkouts?",
				"Would you like to query the repository for active checkouts before attempting upgrade?")) {
				int count = listCheckouts(projectData.getRootFolder());
				if (count == 0) {
					if (!askYesNo("Continue?",
						"Would you like to continue with Program file upgrades?")) {
						return;
					}
				}
				else {
					if (!askYesNo("Checkouts Found! Continue?",
						count + " checkouts were found and have been listed in the script log.\n" +
							"Checked-out files will be skipped during batch upgrade.\n \n" +
							"Would you like to continue with Program file upgrades?")) {
						return;
					}
				}
			}

			includePrivatePrograms = askYesNo("Upgrade Option",
				"Would you like to include private local files in upgrade?");

			int count = performProgramUpgrades(projectData.getRootFolder());
			popup(count + " Program files were upgraded");
		}
		catch (CancelledException e) {
			popup(getClass().getSimpleName() + " execution cancelled.");
		}
		catch (Exception e) {
			String operation = getClass().getSimpleName() + " execution";
			ClientUtil.handleException(repository, e, operation, null);
		}
	}

	private int listCheckouts(DomainFolder folder) throws IOException, CancelledException {
		// Avoid following folder-links so we don't count the same file more than once.
		// Link-files will never be in a checked-out state.
		int count = 0;
		for (DomainFile df : folder.getFiles()) {
			monitor.checkCancelled();
			count += listCheckouts(df);
		}
		for (DomainFolder subfolder : folder.getFolders()) {
			monitor.checkCancelled();
			count += listCheckouts(subfolder);
		}
		return count;
	}

	private int listCheckouts(DomainFile df) throws IOException {
		if (!df.isVersioned() || df.isLink()) {
			return 0; // ignore non-versioned files and link-files
		}
		int count = 0;
		for (ItemCheckoutStatus checkout : df.getCheckouts()) {
			++count;
			String loc = checkout.getUser() + "@" + checkout.getProjectPath();
			println("Active checkout: " + df.getPathname() + " (" + loc + ")");
		}
		return count;
	}

	private int performProgramUpgrades(DomainFolder folder) throws IOException, CancelledException {
		int count = 0;
		for (DomainFile df : folder.getFiles()) {
			monitor.checkCancelled();
			if (performProgramUpgrade(df)) {
				++count;
			}
		}
		for (DomainFolder subfolder : folder.getFolders()) {
			monitor.checkCancelled();
			count += performProgramUpgrades(subfolder);
		}
		return count;
	}

	private boolean performProgramUpgrade(DomainFile df) throws IOException, CancelledException {
		// Do not follow folder-links or consider program links.  Using content type
		// to filter is best way to control this.
		if (!ProgramContentHandler.PROGRAM_CONTENT_TYPE.equals(df.getContentType())) {
			return false;
		}
		boolean versionedFile = df.isVersioned();
		if (!versionedFile && !includePrivatePrograms) {
			return false;
		}
		if (df.isReadOnly()) {
			printerr("Skipping read-only file: " + df.getPathname());
			return false;
		}
		if (df.isCheckedOut()) {
			printerr("Skipping locally checked-out file: " + df.getPathname());
			return false;
		}
		if (versionedFile && !df.checkout(true, monitor)) {
			printerr("Failed to get exclusive checkout for file: " + df.getPathname());
			return false;
		}

		boolean upgraded = false;
		try {
			upgraded = upgradeProgram(df);
		}
		catch (LanguageNotFoundException e) {
			printerr("Skipping file (" + e.getMessage() + "): " + df.getPathname());
			return false;
		}
		catch (VersionException e) {
			printerr("Program created with newer/unknown Ghidra version: " + df.getPathname());
			return false;
		}
		finally {
			if (versionedFile) {
				df.undoCheckout(false);
			}
		}
		return upgraded;
	}

	private boolean upgradeProgram(DomainFile df)
			throws VersionException, CancelledException, IOException {

		try {
			// Check if upgrade is needed and is possible
			DomainObject dobj = df.getDomainObject(this, false, false, monitor);
			dobj.release(this);
			return false; // no upgrade needed
		}
		catch (VersionException e) {
			if (!e.isUpgradable()) {
				throw e; // upgrade not possible
			}
		}

		boolean upgraded = false;
		DomainObject dobj = df.getDomainObject(this, true, false, monitor);
		try {
			if (dobj.isChanged()) {
				dobj.save("Batch Upgrade", monitor);
				dobj.release(this);
				dobj = null;
				if (df.isVersioned()) {
					df.checkin(checkinHandler, monitor);
					println("Repository file upgraded: " + df.getPathname());
				}
				else {
					println("Local file upgraded: " + df.getPathname());
				}
				upgraded = true;
			}
		}
		finally {
			if (dobj != null) {
				dobj.release(this);
			}
		}
		return upgraded;
	}

	CheckinHandler checkinHandler = new CheckinHandler() {

		@Override
		public boolean keepCheckedOut() throws CancelledException {
			return true;
		}

		@Override
		public String getComment() throws CancelledException {
			return "Batch Upgrade";
		}

		@Override
		public boolean createKeepFile() throws CancelledException {
			return false;
		}
	};

}
