package bndtools.release;

import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.ui.PlatformUI;

import aQute.bnd.build.Project;
import aQute.bnd.build.ProjectBuilder;
import aQute.bnd.build.model.BndEditModel;
import aQute.bnd.differ.Baseline;
import aQute.bnd.differ.Baseline.Info;
import aQute.bnd.osgi.Builder;
import aQute.bnd.osgi.Constants;
import aQute.bnd.osgi.Jar;
import aQute.bnd.osgi.JarResource;
import aQute.bnd.properties.Document;
import aQute.bnd.service.RepositoryPlugin;
import aQute.bnd.version.Version;
import aQute.lib.io.IO;
import aQute.service.reporter.Reporter;
import bndtools.central.Central;
import bndtools.release.api.IReleaseParticipant;
import bndtools.release.api.IReleaseParticipant.Scope;
import bndtools.release.api.ReleaseContext;
import bndtools.release.api.ReleaseContext.Error;
import bndtools.release.api.ReleaseOption;
import bndtools.release.api.ReleaseUtils;
import bndtools.release.nl.Messages;

public class ReleaseHelper {

	public final static String	VERSION_WITH_MACRO_STRING	= "(\\d+)\\.(\\d+)\\.(\\d+)\\.(\\$\\{[-.\\w]+\\})";	//$NON-NLS-1$
	public final static Pattern	VERSION_WITH_MACRO			= Pattern.compile(VERSION_WITH_MACRO_STRING);

	public static void updateProject(ReleaseContext context) throws Exception {
		try (ProjectBuilder pb = context.getProject()
			.getBuilder(null)) {
			for (Builder builder : pb.getSubBuilders()) {

				Baseline current = getBaselineForBuilder(builder, context);
				if (current == null) {
					continue;
				}
				for (Info info : current.getPackageInfos()) {
					context.getProject()
						.setPackageInfo(info.packageName, info.suggestedVersion);
				}

				updateBundleVersion(context, current, builder);
			}
		}
	}

	private static void updateBundleVersion(ReleaseContext context, Baseline current, Builder builder)
		throws IOException, CoreException {

		Version bundleVersion = current.getSuggestedVersion();
		if (bundleVersion != null) {

			File file = builder.getPropertiesFile();
			Properties properties = builder.getProperties();
			if (file == null) {
				file = context.getProject()
					.getPropertiesFile();
				properties = context.getProject()
					.getProperties();
			}
			final IFile resource = (IFile) ReleaseUtils.toResource(file);

			final Document document;
			if (resource.exists()) {
				byte[] bytes = readFully(resource.getContents());
				document = new Document(new String(bytes, resource.getCharset()));
			} else {
				document = new Document(""); //$NON-NLS-1$
			}

			final BndEditModel model;
			BndEditModel model2;
			try {
				model2 = new BndEditModel(Central.getWorkspace());
			} catch (Exception e) {
				System.err.println("Unable to create BndEditModel with Workspace, defaulting to without Workspace");
				model2 = new BndEditModel();
			}
			model = model2;
			model.loadFrom(document);

			String currentVersion = model.getBundleVersionString();
			String templateVersion = updateTemplateVersion(currentVersion, bundleVersion);
			model.setBundleVersion(templateVersion);
			properties.setProperty(Constants.BUNDLE_VERSION, templateVersion);

			final Document finalDoc = document;
			Runnable run = () -> {
				model.saveChangesTo(finalDoc);

				try {
					writeFully(finalDoc.get(), resource, false);
					resource.refreshLocal(IResource.DEPTH_ZERO, null);
				} catch (CoreException e) {
					throw new RuntimeException(e);
				}
			};
			if (Display.getCurrent() == null) {
				Display.getDefault()
					.syncExec(run);
			} else
				run.run();
		}
	}

	private static Baseline getBaselineForBuilder(Builder builder, ReleaseContext context) {
		Baseline current = null;
		for (Baseline jd : context.getBaselines()) {
			if (jd.getBsn()
				.equals(builder.getBsn())) {
				current = jd;
				break;
			}
		}
		return current;
	}

	public static boolean release(ReleaseContext context, List<Baseline> diffs) throws Exception {

		boolean ret = true;

		List<IReleaseParticipant> participants = Activator.getReleaseParticipants();

		switch (context.getReleaseOption()) {
			case UPDATE :
			default :
				if (!doUpdateVersions(context, participants)) {
					return false;
				}
				break;
			case RELEASE :
				ret = doRelease(context, diffs, participants);
				break;
			case UPDATE_RELEASE :
				if (!doUpdateVersions(context, participants)) {
					return false;
				}
				ret = doRelease(context, diffs, participants);
				break;
		}

		postRelease(context, participants, ret);
		return ret;
	}

	private static boolean doUpdateVersions(ReleaseContext context, List<IReleaseParticipant> participants)
		throws Exception {

		if (!preUpdateProjectVersions(context, participants)) {
			postRelease(context, participants, false);
			displayErrors(context);
			return false;
		}

		ReleaseHelper.updateProject(context);

		IProject proj = ReleaseUtils.getProject(context.getProject());
		proj.refreshLocal(IResource.DEPTH_INFINITE, context.getProgressMonitor());

		context.getProject()
			.clear();

		return true;
	}

	private static boolean doRelease(ReleaseContext context, List<Baseline> diffs,
		List<IReleaseParticipant> participants) throws Exception {
		boolean ret = true;
		if (!preRelease(context, participants)) {
			postRelease(context, participants, false);
			displayErrors(context);
			return false;
		}

		try (ProjectBuilder pb = context.getProject()
			.getBuilder(null)) {
			List<Builder> builders = pb.getSubBuilders();

			for (Baseline diff : diffs) {
				Builder builder = null;
				for (Builder b : builders) {
					if (b.getBsn()
						.equals(diff.getBsn())) {
						builder = b;
						break;
					}
				}
				if (builder != null) {
					if (!release(context, participants, builder)) {
						ret = false;
					}
				}
			}
		}
		return ret;
	}

	private static void handleBuildErrors(ReleaseContext context, Reporter reporter, Jar jar) {
		String symbName = null;
		String version = null;
		if (jar != null) {
			symbName = ReleaseUtils.getBundleSymbolicName(jar);
			version = ReleaseUtils.getBundleVersion(jar);
		}
		for (String message : reporter.getErrors()) {
			context.getErrorHandler()
				.error(symbName, version, message);
		}
	}

	private static void handleReleaseErrors(ReleaseContext context, Reporter reporter, String symbolicName,
		String version) {
		for (String message : reporter.getErrors()) {
			context.getErrorHandler()
				.error(symbolicName, version, message);
		}
	}

	private static void displayErrors(ReleaseContext context) {

		final String name = context.getProject()
			.getName();
		final List<Error> errors = new ArrayList<>(context.getErrorHandler()
			.getErrors());
		context.getErrorHandler()
			.clear();
		if (errors.size() > 0) {
			Runnable runnable = () -> {
				Shell shell = PlatformUI.getWorkbench()
					.getDisplay()
					.getActiveShell();
				ErrorDialog error = new ErrorDialog(shell, name, errors);
				error.open();
			};

			if (Display.getCurrent() == null) {
				Display.getDefault()
					.asyncExec(runnable);
			} else {
				runnable.run();
			}

		}

	}

	private static boolean release(ReleaseContext context, List<IReleaseParticipant> participants, Builder builder)
		throws Exception {

		Jar jar = null;
		try {
			if (context.getReleaseOption() == ReleaseOption.UPDATE_RELEASE) {
				jar = builder.build();
			} else {
				// No need to rebuild if release only
				File jarFile = new File(context.getProject()
					.getTarget(), builder.getBsn() + ".jar");
				if (jarFile.isFile()) {
					jar = new Jar(jarFile);
				} else {
					jar = builder.build();
				}
			}

			handleBuildErrors(context, builder, jar);

			String symbName = ReleaseUtils.getBundleSymbolicName(jar);
			String version = ReleaseUtils.getBundleVersion(jar);

			boolean proceed = preJarRelease(context, participants, jar);
			if (!proceed) {
				postRelease(context, participants, false);
				displayErrors(context);
				return false;
			}

			try (JarResource jr = new JarResource(jar);
				InputStream is = new BufferedInputStream(jr.openInputStream())) {
				context.getProject()
					.release(context.getReleaseRepository()
						.getName(), jar.getName(), is);

				if (!context.getProject()
					.isOk()) {
					handleBuildErrors(context, context.getProject(), jar);
					displayErrors(context);
					context.getProject()
						.clear();
					return false;
				}
			}

			File file = context.getReleaseRepository()
				.get(symbName, Version.parseVersion(version), null);
			Jar releasedJar = null;
			if (file != null && file.exists()) {
				IResource resource = ReleaseUtils.toResource(file);
				if (resource != null) {
					resource.refreshLocal(IResource.DEPTH_ZERO, null);
				}
				releasedJar = jar;
				jar = null;
			}
			if (releasedJar == null) {
				handleReleaseErrors(context, context.getProject(), symbName, version);

				postRelease(context, participants, false);
				displayErrors(context);
				return false;
			}
			context.addReleasedJar(releasedJar);

			postJarRelease(context, participants, releasedJar);
			return true;
		} finally {
			if (jar != null) {
				jar.close();
			}
		}
	}

	private static boolean preUpdateProjectVersions(ReleaseContext context, List<IReleaseParticipant> participants) {
		context.setCurrentScope(Scope.PRE_UPDATE_VERSIONS);
		for (IReleaseParticipant participant : participants) {
			if (!participant.preUpdateProjectVersions(context)) {
				return false;
			}
		}
		return true;
	}

	private static boolean preRelease(ReleaseContext context, List<IReleaseParticipant> participants) {
		context.setCurrentScope(Scope.PRE_RELEASE);
		for (IReleaseParticipant participant : participants) {
			if (!participant.preRelease(context)) {
				return false;
			}
		}
		return true;
	}

	private static boolean preJarRelease(ReleaseContext context, List<IReleaseParticipant> participants, Jar jar) {
		context.setCurrentScope(Scope.PRE_JAR_RELEASE);
		for (IReleaseParticipant participant : participants) {
			if (!participant.preJarRelease(context, jar)) {
				return false;
			}
		}
		return true;
	}

	private static void postJarRelease(ReleaseContext context, List<IReleaseParticipant> participants, Jar jar) {
		context.setCurrentScope(Scope.POST_JAR_RELEASE);
		for (IReleaseParticipant participant : participants) {
			participant.postJarRelease(context, jar);
		}
	}

	private static void postRelease(ReleaseContext context, List<IReleaseParticipant> participants, boolean success) {
		context.setCurrentScope(Scope.POST_RELEASE);
		for (IReleaseParticipant participant : participants) {
			participant.postRelease(context, success);
		}
		context.close();
	}

	public static String[] getReleaseRepositories() {
		List<RepositoryPlugin> repos = Activator.getRepositories();
		List<String> ret = new ArrayList<>();
		for (RepositoryPlugin repo : repos) {
			if (repo.canWrite()) {
				if (repo.getName() != null) {
					ret.add(repo.getName());
				} else {
					ret.add(repo.toString());
				}
			}
		}
		return ret.toArray(new String[0]);
	}

	public static RepositoryPlugin getReleaseRepo(Project project) {
		RepositoryPlugin repo = null;
		String repoName = project.getProperty(Constants.RELEASEREPO);

		if (repoName != null && Constants.NONE.equals(repoName))
			return null;

		List<RepositoryPlugin> repos = project.getPlugins(RepositoryPlugin.class);
		for (RepositoryPlugin r : repos) {
			if (r.canWrite()) {
				if (repoName == null || r.getName()
					.equals(repoName)) {
					repo = r;
					break;
				}
			}
		}
		return repo;
	}

	public static void initializeProjectDiffs(List<ProjectDiff> projects) {
		String[] repos = getReleaseRepositories();
		for (ProjectDiff projectDiff : projects) {

			RepositoryPlugin repoPlugin = ReleaseHelper.getReleaseRepo(projectDiff.getProject());
			String repo;
			if (repoPlugin != null) {
				repo = repoPlugin.getName();
			} else {
				repo = null;
			}

			if (repo == null) {
				if (repos.length > 0) {
					repo = repos[0];
				} else {
					repo = "";
				}
			}
			projectDiff.setReleaseRepository(repo);
			projectDiff.setDefaultReleaseRepository(repo);

			for (Baseline baseline : projectDiff.getBaselines()) {
				if (ReleaseUtils.needsRelease(baseline)) {
					projectDiff.setRelease(true);
					projectDiff.setReleaseRequired(true);
					if (!baseline.getNewerVersion()
						.equals(baseline.getSuggestedVersion())) {
						projectDiff.setVersionUpdateRequired(true);
						continue;
					}
					for (Info info : baseline.getPackageInfos()) {
						if (!info.newerVersion.equals(info.suggestedVersion)) {
							projectDiff.setVersionUpdateRequired(true);
							break;
						}
					}
				} else {
					baseline.setSuggestedVersion(baseline.getOlderVersion());
				}
			}
		}
	}

	private static byte[] readFully(InputStream stream) throws IOException {
		return IO.read(stream);
	}

	public static void writeFully(String text, IFile file, boolean createIfAbsent) throws CoreException {
		ByteArrayInputStream inputStream;
		try {
			inputStream = new ByteArrayInputStream(text.getBytes(file.getCharset(true)));
		} catch (UnsupportedEncodingException e) {
			return;
		}
		if (file.exists()) {
			file.setContents(inputStream, false, true, null);
		} else {
			if (createIfAbsent)
				file.create(inputStream, false, null);
			else
				throw new CoreException(new Status(IStatus.ERROR, Activator.PLUGIN_ID, 0,
					String.format(Messages.fileDoesNotExist, file.getFullPath()
						.toString()),
					null));
		}
	}

	static String updateTemplateVersion(String currentVersion, Version newVersion) {
		String version = newVersion.toString();
		if (currentVersion == null || currentVersion.startsWith("${")) {
			if (newVersion.getQualifier() == null) {
				version = newVersion.toString() + ".${tstamp}";
			}
			return version;
		}

		Matcher m = VERSION_WITH_MACRO.matcher(currentVersion);
		if (m.matches()) {
			return newVersion.getMajor() + "." + newVersion.getMinor() + "." + newVersion.getMicro() + "." + m.group(4);
		}
		return version;
	}

	public static List<MacroInfo> getBsnsWithBundleVersionMacro(List<ProjectDiff> projectDiffs) {
		List<MacroInfo> bsns = new ArrayList<>();
		for (ProjectDiff diff : projectDiffs) {
			if (diff.isRelease()) {
				for (Baseline baseline : diff.getBaselines()) {
					try {
						Builder builder = diff.getProject()
							.getSubBuilder(baseline.getBsn());
						String bundleVersion = builder.getUnprocessedProperty(Constants.BUNDLE_VERSION, "");
						if (bundleVersion.startsWith("${")) {
							MacroInfo info = new MacroInfo();
							info.projectDiff = diff;
							info.bsn = builder.getBsn();
							info.macro = bundleVersion;
							bsns.add(info);
						}
					} catch (Exception e) {}
				}
			}
		}
		return bsns;
	}

	public static class MacroInfo {
		public ProjectDiff	projectDiff;
		public String		bsn;
		public String		macro;
	}
}
