/*
* Licensed Materials - Property of IBM Corp.
* IBM UrbanCode Build
* (c) Copyright IBM Corporation 2012, 2014. All Rights Reserved.
*
* U.S. Government Users Restricted Rights - Use, duplication or disclosure restricted by
* GSA ADP Schedule Contract with IBM Corp.
*/
package com.urbancode.air.plugin.scm;

import com.urbancode.air.*

import java.io.File;
import java.text.DateFormat;
import java.util.Date;
import java.util.regex.Matcher;

abstract public class SCMStep {

    //**************************************************************************
    // CLASS
    //**************************************************************************

    static public final String REPO_TYPE = 'git'

    /*
     * Use of null (e.g. \0 OR %x00) breaks git log format string under msysgit,
     * so we will use the unit-separator character as an alternative to null when needed.
     */
    static protected final char UNIT_SEPARATOR = '\037'
    static protected final String UNIT_SEPARATOR_FMT = '%x1F'

    //**************************************************************************
    // INSTANCE
    //**************************************************************************

    def out = System.out;

    String scmCommand;
    File directory;

    String remoteName;
    String remoteBaseUrl;
    String remoteUrl;
    String remoteUser;
    String remotePass;
    String branch;
    String revision;
    String fetchRef; //for Gerrit preflight checkout

    /**
     * Initialize the workspace with data from the remote repository {@link #remoteUrl} for {@link #branch} up to {@link #revision}
     */
    protected void initWorkspace() {
        // create the repository directory if not present
        if (!directory.canonicalFile.mkdirs() && !directory.exists()) {
            throw new Exception("Could not create working directory ${directory}")
        }
        else if (directory.isFile()) {
            throw new Exception("Specified working directory is a file!! ${directory}")
        }

        final def cmdHelper = getCommandHelperBase();

        // Repository does not exist, initialize a new one
        if (!new File(directory, '.git').isDirectory()) {
            def initCommand = [getScmCommand(), '--no-pager', 'init']
            cmdHelper.runCommand('Git init', initCommand);
        }

        //
        // Add remote if absent
        //
        def repoUrl = calculateRepositoryUrl();
        def remoteList = getCommandText([getScmCommand(), '--no-pager', 'remote']).readLines()
        if (!remoteList.contains(getRemoteName())) {
            def remoteCommand = [getScmCommand(), '--no-pager', 'remote', 'add', getRemoteName(), repoUrl]
            def printCommand = remoteCommand.collect{it.replaceFirst(~"^(https?://.*):(.*)@", '$1' + ":****@")};
            cmdHelper.runCommand('message':"Git Remote Add",
                'command':remoteCommand,
                'printCommand': printCommand
                );
        }
        else {
            def remoteCommand = [getScmCommand(), '--no-pager', 'remote', 'set-url', getRemoteName(), repoUrl]
            def printCommand = remoteCommand.collect{it.replaceFirst(~"^(https?://.*):(.*)@", '$1' + ":****@")};
            cmdHelper.runCommand(
                'message':'Git Remote Set-Url',
                'command': remoteCommand,
                'printCommand': printCommand
                );
       }

        //
        // Fetch revisions from remote
        // WARN: if http prompts for authentication this will hang!
        //
        def fetchCommand = [getScmCommand(), '--no-pager', 'fetch', '--force', getRemoteName()]
        cmdHelper.runCommand('Git Remote Fetch Branch', fetchCommand);

        //
        // prune stale remote tracking branches
        //
        def pruneCommand = [getScmCommand(), '--no-pager', 'remote', 'prune', getRemoteName()]
        cmdHelper.runCommand('Git Remote Prune', pruneCommand)

        if (fetchRef) {
            def ref = getRemoteTrackingBranch()
            fetchCommand << "+$fetchRef:refs/remotes/$ref"
            cmdHelper.runCommand('Git Remote Fetch Specific Ref', fetchCommand)
        }

    }

    //
    // Utility Methods
    //

    /**
     * Calculate the repository's complete URL from the source config url and the remoteBaseUrl
     * @return
     */
    protected String calculateRepositoryUrl() {
        def result = new URI(remoteUrl);
        if (remoteBaseUrl) {
            def prefixURI = new URI(remoteBaseUrl+"/");
            result = prefixURI.resolve(remoteUrl);
        }
        result = result.normalize();
        String repoUrl = result.toString();

        // Embed username and password credentials on remoteUrl
        if (repoUrl.startsWith('http')) {
            def userInfo = result.getRawUserInfo();
            if (userInfo != null && userInfo.size() > 0) {
                def firstColonIndex = userInfo.indexOf(':');
                def userInfoUsername = "";
                def userInfoPassword = "";
                if (firstColonIndex == -1) {
                    // userInfo string did not have a ':' in it, the whole string should be treated
                    // as username
                    userInfoUsername = userInfo;
                }
                else {
                    // there is a ':' in userInfo string, which means it contains both a username and
                    // a password. Due to the fact that getRawUserInfo is used to get the userInfo,
                    // we are sure that if either the username or password contain reserved characters,
                    // it is still in percent-encoded format so we can safely check for ':' as a separator
                    userInfoUsername = userInfo.substring(0, firstColonIndex);
                    userInfoPassword = userInfo.substring(firstColonIndex + 1);
                }
                if (userInfoPassword == null || userInfoPassword.size() == 0) {
                    if (remotePass) {
                        userInfo += ":" + encode(remotePass);
                    }
                    else {
                        println("warning: a password has not been specified in base url and no password has been " +
                                "specified in the repository configuration either. This may cause plugin to hang " +
                                "on interactive input.");
                    }
                }
            }
            else {
                userInfo = "";
                if (remoteUser == null || remoteUser.size() == 0) {
                    println("warning: base url does not contain a username and none is provided in repository configuration");
                }
                else {
                    userInfo = encode(remoteUser);
                }
                if (remotePass == null || remotePass.size() == 0) {
                    println("warning: base url does not contain a password and none is provided in repository configuration");
                }
                else if (userInfo != null && userInfo.size() > 0) {
                    userInfo += ":" + encode(remotePass);
                }
                else {
                    println("warning: ignoring the provided password because no username has been specified");
                }
            }
            if (userInfo != null && userInfo.size() > 0) {
                repoUrl = repoUrl.replaceFirst(~'^(https?://)(.*@)?', '$1' + Matcher.quoteReplacement(userInfo) + '@');
            }
        }
        else {
            println "warning: can not use username/password with non-http(s) url";
        }
        return repoUrl;
    }

    /**
     * Print a message to the output stream of choice.
     * @param msg
     */
    protected void println(String msg) {
        out.println(msg);
    }

    protected String getCommandText(def command) {
        final def cmdHelper = getCommandHelperBase();
        def outputText = null;
        cmdHelper.runCommand(null, command){ Process proc ->
            // close stdin
            proc.out.close()
            Thread t = proc.consumeProcessErrorStream(out)
            outputText = proc.in.text.trim()
            t.join(10000L)
        };

        // sanitize the raw output slightly and print
        def entrySepPattern = '\0|'+UNIT_SEPARATOR;
        println(outputText.replaceAll(entrySepPattern, '\n'));

        return outputText;
    }

    protected DateFormat getDateArgumentFormatter() {
        DateFormat gitDateFormat = new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss.S Z")
        gitDateFormat.timeZone = TimeZone.getTimeZone("GMT")
        return gitDateFormat;
    }

    protected CommandHelper getCommandHelperBase() {
        return new CommandHelper(directory);
    }

    // Convert date into revision
    protected String lookupRevision(Date date) {
        if (date) {
            String dateString = getDateArgumentFormatter().format(date);

            def remoteBranch = this.getRemoteName()+"/"+this.getBranch();
            def revListCommand = [this.getScmCommand(), '--no-pager', 'rev-list', '--max-count=1', "--before=$dateString", remoteBranch];
            return getCommandText(revListCommand);
        }
        else {
            return null;
        }
    }


    //
    // Default Getters
    //

    /**
     * Get the branch to operate upon. Default to "master"
     * @return
     */
    public String getBranch() {
        return this.branch ?: 'master'
    }

    /**
     * Get the remote name to use.  Default to "origin"
     * @return
     */
    public String getRemoteName() {
        return this.remoteName ?: 'origin';
    }

    /**
     * Get the scm command to use. Default to "git"
     * @return
     */
    public String getScmCommand() {
        return this.scmCommand ?: 'git';
    }

    public String getRemoteTrackingBranch() {
        def ref = fetchRef ?: this.getBranch()
        def shortRef = ref.replaceFirst('^refs/(?:heads/|tags/|remotes/[^/]+/)?', '')
        return this.getRemoteName() + "/" + shortRef
    }

    /**
     * Percent encode reserved characters of a string
     * @param value String
     * @return
     */
    private String encode(String value) {
        value = value.replaceAll(~/[^-_.~A-Za-z0-9]/, { c ->
            return ('%' + c.getBytes('UTF8').encodeHex()).toUpperCase();
        });
        return value;
    }
}
