/*
 * Copyright 2014-2020 Aleksandr Mashchenko.
 *
 * 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.
 */
package com.amashchenko.maven.plugin.gitflow;

import java.util.HashMap;
import java.util.Map;

import org.apache.maven.artifact.Artifact;
import org.apache.maven.artifact.ArtifactUtils;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.shared.release.versions.VersionParseException;
import org.codehaus.plexus.components.interactivity.PrompterException;
import org.codehaus.plexus.util.StringUtils;
import org.codehaus.plexus.util.cli.CommandLineException;

/**
 * The git flow release start mojo.
 * 
 */
@Mojo(name = "release-start", aggregator = true)
public class GitFlowReleaseStartMojo extends AbstractGitFlowMojo {

    /**
     * Whether to use the same name of the release branch for every release.
     * Default is <code>false</code>, i.e. project version will be added to
     * release branch prefix. <br/>
     * Will have no effect if the <code>branchName</code> parameter is set.
     * <br/>
     *
     * Note: By itself the default releaseBranchPrefix is not a valid branch
     * name. You must change it when setting sameBranchName to <code>true</code>
     * .
     * 
     * @since 1.2.0
     */
    @Parameter(property = "sameBranchName", defaultValue = "false")
    private boolean sameBranchName = false;

    /**
     * Whether to allow SNAPSHOT versions in dependencies.
     * 
     * @since 1.2.2
     */
    @Parameter(property = "allowSnapshots", defaultValue = "false")
    private boolean allowSnapshots = false;

    /**
     * Release version to use instead of the default next release version in non
     * interactive mode.
     * 
     * @since 1.3.1
     */
    @Parameter(property = "releaseVersion", defaultValue = "")
    private String releaseVersion = "";

    /**
     * Whether to push to the remote.
     *
     * @since 1.6.0
     */
    @Parameter(property = "pushRemote", defaultValue = "false")
    private boolean pushRemote;

    /**
     * Whether to commit development version when starting the release (vs when
     * finishing the release which is the default). Has effect only when there
     * are separate development and production branches.
     * 
     * @since 1.7.0
     */
    @Parameter(property = "commitDevelopmentVersionAtStart", defaultValue = "false")
    private boolean commitDevelopmentVersionAtStart;

    /**
     * Whether to remove qualifiers from the next development version.
     *
     * @since 1.7.0
     */
    @Parameter(property = "digitsOnlyDevVersion", defaultValue = "false")
    private boolean digitsOnlyDevVersion = false;

    /**
     * Development version to use instead of the default next development
     * version in non interactive mode.
     *
     * @since 1.7.0
     */
    @Parameter(property = "developmentVersion", defaultValue = "")
    private String developmentVersion = "";

    /**
     * Which digit to increment in the next development version. Starts from
     * zero.
     *
     * @since 1.7.0
     */
    @Parameter(property = "versionDigitToIncrement")
    private Integer versionDigitToIncrement;

    /**
     * Start a release branch from this commit (SHA).
     * 
     * @since 1.7.0
     */
    @Parameter(property = "fromCommit")
    private String fromCommit;

    /**
     * Whether to use snapshot in release.
     * 
     * @since 1.10.0
     */
    @Parameter(property = "useSnapshotInRelease", defaultValue = "false")
    private boolean useSnapshotInRelease;

    /**
     * Name of the created release branch.<br>
     * The effective branch name will be a composite of this branch name and the
     * <code>releaseBranchPrefix</code>.
     * 
     * @since 1.14.0
     */
    @Parameter(property = "branchName")
    private String branchName;

    /** {@inheritDoc} */
    @Override
    public void execute() throws MojoExecutionException, MojoFailureException {
        validateConfiguration();

        try {
            // set git flow configuration
            initGitFlowConfig();

            // check uncommitted changes
            checkUncommittedChanges();

            // git for-each-ref --count=1 refs/heads/release/*
            final String releaseBranch = gitFindBranches(
                    gitFlowConfig.getReleaseBranchPrefix(), true);

            if (StringUtils.isNotBlank(releaseBranch)) {
                throw new MojoFailureException(
                        "Release branch already exists. Cannot start release.");
            }

            if (fetchRemote) {
                // checkout from remote if doesn't exist
                gitFetchRemoteAndCreate(gitFlowConfig.getDevelopmentBranch());

                // fetch and check remote
                gitFetchRemoteAndCompare(gitFlowConfig.getDevelopmentBranch());
            }

            final String startPoint;
            if (StringUtils.isNotBlank(fromCommit) && notSameProdDevName()) {
                startPoint = fromCommit;
            } else {
                startPoint = gitFlowConfig.getDevelopmentBranch();
            }

            // need to be in develop to check snapshots and to get
            // correct project version
            gitCheckout(startPoint);

            // check snapshots dependencies
            if (!allowSnapshots) {
                checkSnapshotDependencies();
            }

            if (commitDevelopmentVersionAtStart && !notSameProdDevName()) {
                getLog().warn(
                        "The commitDevelopmentVersionAtStart will not have effect. It can be enabled only when there are separate branches for development and production.");
                commitDevelopmentVersionAtStart = false;
            }

            // get release version
            final String releaseVersion = getReleaseVersion();

            // get release branch
            String fullBranchName = gitFlowConfig.getReleaseBranchPrefix();
            if (StringUtils.isNotBlank(branchName)) {
                fullBranchName += branchName;
            } else if (!sameBranchName) {
                fullBranchName += releaseVersion;
            }

            String projectVersion = releaseVersion;
            if (useSnapshotInRelease && !ArtifactUtils.isSnapshot(projectVersion)) {
                projectVersion = projectVersion + "-" + Artifact.SNAPSHOT_VERSION;
            }

            if (useSnapshotInRelease && mavenSession.getUserProperties().get("useSnapshotInRelease") != null) {
                getLog().warn(
                        "The useSnapshotInRelease parameter is set from the command line. Don't forget to use it in the finish goal as well."
                                + " It is better to define it in the project's pom file.");
            }

            if (commitDevelopmentVersionAtStart) {
                // mvn versions:set ...
                // git commit -a -m ...
                commitProjectVersion(projectVersion,
                        commitMessages.getReleaseStartMessage()); 

                // git branch release/... develop
                gitCreateBranch(fullBranchName, startPoint);

                final String nextSnapshotVersion =
                        getNextSnapshotVersion(releaseVersion);

                // mvn versions:set ...
                // git commit -a -m ...
                commitProjectVersion(nextSnapshotVersion, commitMessages.getReleaseVersionUpdateMessage());

                // git checkout release/...
                gitCheckout(fullBranchName);
            } else {
                // git checkout -b release/... develop
                gitCreateAndCheckout(fullBranchName, startPoint);

                // mvn versions:set ...
                // git commit -a -m ...
                commitProjectVersion(projectVersion,
                        commitMessages.getReleaseStartMessage());
            }

            if (installProject) {
                // mvn clean install
                mvnCleanInstall();
            }

            if (pushRemote) {
                if (commitDevelopmentVersionAtStart) {
                    gitPush(gitFlowConfig.getDevelopmentBranch(), false);
                }

                gitPush(fullBranchName, false);
            }
        } catch (CommandLineException e) {
            throw new MojoFailureException("release-start", e);
        } catch (VersionParseException e) {
            throw new MojoFailureException("release-start", e);
        }
    }

    private String getNextSnapshotVersion(String currentVersion) throws MojoFailureException, VersionParseException {
        // get next snapshot version
        final String nextSnapshotVersion;
        if (!settings.isInteractiveMode()
                && StringUtils.isNotBlank(developmentVersion)) {
            nextSnapshotVersion = developmentVersion;
        } else {
            GitFlowVersionInfo versionInfo = new GitFlowVersionInfo(
                    currentVersion);
            if (digitsOnlyDevVersion) {
                versionInfo = versionInfo.digitsVersionInfo();
            }

            nextSnapshotVersion = versionInfo
                    .nextSnapshotVersion(versionDigitToIncrement);
        }

        if (StringUtils.isBlank(nextSnapshotVersion)) {
            throw new MojoFailureException(
                    "Next snapshot version is blank.");
        }
        return nextSnapshotVersion;
    }

    private String getReleaseVersion() throws MojoFailureException, VersionParseException, CommandLineException {
        // get current project version from pom
        final String currentVersion = getCurrentProjectVersion();

        String defaultVersion = null;
        if (tychoBuild) {
            defaultVersion = currentVersion;
        } else {
            // get default release version
            defaultVersion = new GitFlowVersionInfo(currentVersion)
                    .getReleaseVersionString();
        }

        if (defaultVersion == null) {
            throw new MojoFailureException(
                    "Cannot get default project version.");
        }

        String version = null;
        if (settings.isInteractiveMode()) {
            try {
                while (version == null) {
                    version = prompter.prompt("What is release version? ["
                            + defaultVersion + "]");

                    if (!"".equals(version)
                            && (!GitFlowVersionInfo.isValidVersion(version) || !validBranchName(version))) {
                        getLog().info("The version is not valid.");
                        version = null;
                    }
                }
            } catch (PrompterException e) {
                throw new MojoFailureException("release-start", e);
            }
        } else {
            version = releaseVersion;
        }

        if (StringUtils.isBlank(version)) {
            getLog().info("Version is blank. Using default version.");
            version = defaultVersion;
        }

        return version;
    }

    private void commitProjectVersion(String version, String commitMessage)
            throws CommandLineException, MojoFailureException {
        // execute if version changed
        String currentVersion = getCurrentProjectVersion();
        if (!version.equals(currentVersion)) {
            // mvn versions:set -DnewVersion=... -DgenerateBackupPoms=false
            mvnSetVersions(version);

            Map<String, String> properties = new HashMap<String, String>();
            properties.put("version", version);

            // git commit -a -m commitMessage
            gitCommit(commitMessage, properties);
        }
    }
}