/*
 * Copyright 2020 ETSI
 * 
 * Redistribution and use in source and binary forms, with or without 
 * modification, are permitted provided that the following conditions are met:
 * 1. Redistributions of source code must retain the above copyright notice, 
 *    this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright notice, 
 *    this list of conditions and the following disclaimer in the documentation 
 *    and/or other materials provided with the distribution.
 * 3. Neither the name of the copyright holder nor the names of its contributors 
 *    may be used to endorse or promote products derived from this software without 
 *    specific prior written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 
 * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 
 * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 
 * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE 
 * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 
 * OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package fr.mines_stetienne.ci.saref.managers;

import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.jena.vocabulary.OWL2;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.ListBranchCommand.ListMode;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.lib.Ref;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import fr.mines_stetienne.ci.saref.SAREF;
import fr.mines_stetienne.ci.saref.SAREFErrorLogger;
import fr.mines_stetienne.ci.saref.SAREFPipelineException;
import fr.mines_stetienne.ci.saref.SAREFPipeline.Mode;
import fr.mines_stetienne.ci.saref.entities.SAREFCore;
import fr.mines_stetienne.ci.saref.entities.SAREFExtension;
import fr.mines_stetienne.ci.saref.entities.SAREFNamedGraph;
import fr.mines_stetienne.ci.saref.entities.SAREFProject;
import fr.mines_stetienne.ci.saref.entities.SAREFRepository;
import fr.mines_stetienne.ci.saref.entities.SAREFVersion;
import fr.mines_stetienne.ci.saref.entities.SAREFVersionName;
import fr.mines_stetienne.ci.saref.utils.Languages;

/**
 * Reads the repository and all the branches
 * 
 * @return
 * @throws SAREFPipelineException
 */
public class RepositoryFactory extends SAREFErrorLogger {

	static final Logger LOG = LoggerFactory.getLogger(RepositoryFactory.class);
	private final static Pattern PATTERN = Pattern.compile(SAREF.REGEX_NAME_PROJECT);

	private static enum MESSAGE {
		target_name, develop_branch, not_clean, release_branch, release_git, source_name;
	}

	public static final String REGEX_VERSION_BRANCH_VAR = "type";
	public static final String REGEX_VERSION_BRANCH = String.format(
			"^(refs/heads/)?(?<%s>(develop|prerelease|release))-%s$", REGEX_VERSION_BRANCH_VAR,
			SAREF.REGEX_VERSION_NUMBER);
	public static final String REGEX_ORIGIN_VERSION_BRANCH = String.format(
			"^refs/remotes/origin/(?<%s>(develop|prerelease|release))-%s$", REGEX_VERSION_BRANCH_VAR,
			SAREF.REGEX_VERSION_NUMBER);

	private final boolean isTarget;

	/**
	 * @param isTarget if true, then local branches are read. Else, remote branches
	 *                 are read.
	 */
	public RepositoryFactory(SourcesManager sourcesManager, Logger errorLogger, boolean isTarget) {
		super(sourcesManager.getPipeline(), errorLogger);
		this.isTarget = isTarget;
	}

	public SAREFRepository create(File directory) throws SAREFPipelineException {
		final SAREFProject project;

		final String name = directory.getName();
		LOG.trace("name " + name);
		final Matcher matcher = PATTERN.matcher(name);
		if (!matcher.find()) {
			if (isTarget) {
				String msg = getMessage(MESSAGE.target_name, SAREF.REGEX_NAME_PROJECT, pipeline.directory.getName());
				log(msg, Mode.DEVELOP, Mode.RELEASE);
				throw new SAREFPipelineException(msg);
			} else {
				logError(getMessage(MESSAGE.source_name, SAREF.REGEX_NAME_PROJECT, name));
				return null;

			}
		}
		final String acronym = matcher.group(SAREF.REGEX_ACRONYM_VAR);
		LOG.trace("acronym " + acronym);
		if (acronym == null) {
			project = SAREFCore.INSTANCE;
		} else {
			project = new SAREFExtension(acronym);
		}

		try (Git git = Git.open(directory)) {
			final boolean isClean = git.status().call().isClean();
			LOG.trace("isClean " + isClean);
			if (!isClean) {
				final String msg = getMessage(MESSAGE.not_clean, project.getName());
				errorLogger.warn(msg);
			}
			String originalBranch = git.getRepository().getBranch();
			LOG.trace("originalBranch " + originalBranch);

			if (isTarget && System.getenv("CI_COMMIT_REF_NAME") != null) {
				originalBranch = System.getenv("CI_COMMIT_REF_NAME");
				LOG.trace("CI_COMMIT_REF_NAME is set to " + originalBranch);
			}

			final SAREFVersionName originalVersionName;
			Pattern pattern = Pattern.compile(REGEX_VERSION_BRANCH);
			Matcher m = pattern.matcher(originalBranch);
			if (!m.find()) {
				if (isTarget && pipeline.mode == Mode.RELEASE) {
					final String msg = String.format(getMessage(MESSAGE.release_branch, originalBranch));
					errorLogger.error(msg);
					throw new SAREFPipelineException(msg);
				} 
				originalVersionName = guessVersion(project, directory);
				if (isTarget && originalVersionName.equals(SAREFVersionName.DEFAULT)) {
					final String msg = getMessage(MESSAGE.develop_branch, originalBranch);
					errorLogger.warn(msg);
				}
			} else {
				int major = Integer.parseInt(m.group(SAREF.REGEX_VERSION_MAJOR_VAR));
				int minor = Integer.parseInt(m.group(SAREF.REGEX_VERSION_MINOR_VAR));
				int patch = Integer.parseInt(m.group(SAREF.REGEX_VERSION_PATCH_VAR));
				originalVersionName = new SAREFVersionName(major, minor, patch);
			}
			LOG.trace("originalVersionName " + originalVersionName);

			SAREFRepository repository = new SAREFRepository(directory, project, isClean, originalVersionName,
					originalBranch);
			if(isTarget) {
				addOriginalVersion(repository);
			}
			readVersions(git, repository);
			return repository;
		} catch (IOException ex) {
			LOG.trace("IOException " + ex, ex);
			if (pipeline.mode == Mode.RELEASE) {
				throw new SAREFPipelineException(getMessage(MESSAGE.release_git), ex);
			}
			final SAREFVersionName versionName = guessVersion(project, directory);
			final SAREFRepository repository = new SAREFRepository(directory, project, false, versionName, null);
			SAREFVersion version = new SAREFVersion(repository, versionName, null, null, null);
			repository.getVersions().put(versionName, version);
			return repository;
		} catch (GitAPIException ex) {
			LOG.trace("GitAPIException " + ex, ex);
			throw new SAREFPipelineException("Unexpected Git APIException", ex);
		}
	}

	private SAREFVersionName guessVersion(SAREFProject project, File directory) {
		try {
			File ontologyDirectory = new File(directory, "ontology");
			File ontologyFile = new File(ontologyDirectory, project.getOntologyFileName(Languages.TEXT_TURTLE));
			SAREFNamedGraph namedGraph = new SAREFNamedGraph("null");
			SAREF.loadModel(namedGraph, ontologyFile, errorLogger);
			String uri = namedGraph.getModel().listObjectsOfProperty(OWL2.versionIRI).next().asResource().getURI();
			SAREFVersionName versionName = SAREF.extractVersionName(uri);
			if(versionName == null) {
				return SAREFVersionName.DEFAULT;
			}
			return versionName;
		} catch (Exception ex) {
			return SAREFVersionName.DEFAULT;
		}
	}

	private void readVersions(Git git, SAREFRepository repository) throws GitAPIException {
		final List<Ref> allBranches;
		final Pattern pattern;
		if (isTarget) {
			allBranches = git.branchList().call();
			pattern = Pattern.compile(REGEX_VERSION_BRANCH);
		} else {
			allBranches = git.branchList().setListMode(ListMode.REMOTE).call();
			pattern = Pattern.compile(REGEX_ORIGIN_VERSION_BRANCH);
		}
		Map<SAREFVersionName, VersionBuilder> versionBuilders = new HashMap<>();
		for (Ref ref : allBranches) {
			Matcher m = pattern.matcher(ref.getName());
			if (m.find()) {
				String branchType = m.group(REGEX_VERSION_BRANCH_VAR);

				// only consider release branches when running in RELEASE_PORTAL mode
				if(pipeline.mode == Mode.RELEASE_PORTAL && !branchType.equals("release")) {
					continue;
				}

				// only consider prerelease and release branches when running in PRERELEASE_PORTAL mode
				if(pipeline.mode == Mode.PRERELEASE_PORTAL && branchType.equals("develop")) {
					continue;
				}

				int major = Integer.parseInt(m.group(SAREF.REGEX_VERSION_MAJOR_VAR));
				int minor = Integer.parseInt(m.group(SAREF.REGEX_VERSION_MINOR_VAR));
				int patch = Integer.parseInt(m.group(SAREF.REGEX_VERSION_PATCH_VAR));
				SAREFVersionName versionName = new SAREFVersionName(major, minor, patch);

				VersionBuilder versionBuilder = versionBuilders.get(versionName);
				if (versionBuilder == null) {
					versionBuilder = new VersionBuilder(repository, versionName);
					versionBuilders.put(versionName, versionBuilder);
				}
				switch (branchType) {
				case "develop":
					versionBuilder.developRef = ref;
					break;
				case "prerelease":
					versionBuilder.prereleaseRef = ref;
					break;
				case "release":
					versionBuilder.releaseRef = ref;
					break;
				default:
					throw new NullPointerException("Should not reach this point");
				}
			}
		}
		versionBuilders.forEach((versionName, versionBuilder) -> {
			repository.getVersions().put(versionName, versionBuilder.build());
		});
	}

	private void addOriginalVersion(SAREFRepository repository) {
		SAREFVersionName versionName = repository.getOriginalVersion();
		SAREFVersion version = new SAREFVersion(repository, versionName, null, null, null);
		repository.getVersions().put(versionName, version);
	}

	private static class VersionBuilder {

		private final SAREFRepository repository;
		private final SAREFVersionName versionName;
		private Ref developRef;
		private Ref prereleaseRef;
		private Ref releaseRef;

		private VersionBuilder(SAREFRepository repository, SAREFVersionName versionName) {
			this.repository = repository;
			this.versionName = versionName;
		}

		private SAREFVersion build() {
			return new SAREFVersion(repository, versionName, developRef, prereleaseRef, releaseRef);
		}
	}

}
