/*
 *  Licensed to the Apache Software Foundation (ASF) under one or more
 *  contributor license agreements.  See the NOTICE file distributed with
 *  this work for additional information regarding copyright ownership.
 *  The ASF licenses this file to You 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
 *
 *      https://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 org.apache.tools.ant.taskdefs.optional.vss;

import java.io.File;
import java.io.IOException;
import java.text.DateFormat;
import java.text.ParseException;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;

import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.Task;
import org.apache.tools.ant.taskdefs.Execute;
import org.apache.tools.ant.taskdefs.LogStreamHandler;
import org.apache.tools.ant.types.Commandline;
import org.apache.tools.ant.types.EnumeratedAttribute;
import org.apache.tools.ant.util.FileUtils;

/**
 * A base class for creating tasks for executing commands on Visual SourceSafe.
 * <p>
 * The class extends the 'exec' task as it operates by executing the ss.exe program
 * supplied with SourceSafe. By default the task expects ss.exe to be in the path,
 * you can override this be specifying the ssdir attribute.
 * </p>
 * <p>
 * This class provides set and get methods for 'login' and 'vsspath' attributes. It
 * also contains constants for the flags that can be passed to SS.
 * </p>
 *
 */
public abstract class MSVSS extends Task implements MSVSSConstants {

    private String ssDir = null;
    private String vssLogin = null;
    private String vssPath = null;
    private String serverPath = null;

    /**  Version */
    private String version = null;
    /**  Date */
    private String date = null;
    /**  Label */
    private String label = null;
    /**  Auto response */
    private String autoResponse = null;
    /**  Local path */
    private String localPath = null;
    /**  Comment */
    private String comment = null;
    /**  From label */
    private String fromLabel = null;
    /**  To label */
    private String toLabel = null;
    /**  Output file name */
    private String outputFileName = null;
    /**  User */
    private String user = null;
    /**  From date */
    private String fromDate = null;
    /**  To date */
    private String toDate = null;
    /**  History style */
    private String style = null;
    /**  Quiet defaults to false */
    private boolean quiet = false;
    /**  Recursive defaults to false */
    private boolean recursive = false;
    /**  Writable defaults to false */
    private boolean writable = false;
    /**  Fail on error defaults to true */
    private boolean failOnError = true;
    /**  Get local copy for checkout defaults to true */
    private boolean getLocalCopy = true;
    /**  Number of days offset for History */
    private int numDays = Integer.MIN_VALUE;
    /**  Date format for History */
    private DateFormat dateFormat = DateFormat.getDateInstance(DateFormat.SHORT);
    /**  Timestamp for retrieved files */
    private CurrentModUpdated timestamp = null;
    /**  Behaviour for writable files */
    private WritableFiles writableFiles = null;

    /**
     * Each sub-class must implement this method and return the constructed
     * command line to be executed. It is up to the sub-task to determine the
     * required attributes and their order.
     * @return    The Constructed command line.
     */
    abstract Commandline buildCmdLine();

    /**
     * Directory where <code>ss.exe</code> resides.
     * By default the task expects it to be in the PATH.
     * @param  dir  The directory containing ss.exe.
     */
    public final void setSsdir(String dir) {
        this.ssDir = FileUtils.translatePath(dir);
    }

    /**
     * Login to use when accessing VSS, formatted as "username,password".
     * <p>
     * You can omit the password if your database is not password protected.
     * If you have a password and omit it, Ant will hang.
     * @param  vssLogin  The login string to use.
     */
    public final void setLogin(final String vssLogin) {
        this.vssLogin = vssLogin;
    }

    /**
     * SourceSafe path which specifies the project/file(s) you wish to perform
     * the action on.
     * <p>
     * A prefix of 'vss://' will be removed if specified.
     * @param  vssPath  The VSS project path.
     * @ant.attribute group="required"
     */
    public final void setVsspath(final String vssPath) {
        String projectPath;
        // CheckStyle:MagicNumber OFF
        if (vssPath.startsWith("vss://")) { //$NON-NLS-1$
            projectPath = vssPath.substring(5);
        } else {
            projectPath = vssPath;
        }
        // CheckStyle:MagicNumber ON

        if (projectPath.startsWith(PROJECT_PREFIX)) {
            this.vssPath = projectPath;
        } else {
            this.vssPath = PROJECT_PREFIX + projectPath;
        }
    }

    /**
     * Directory where <code>srssafe.ini</code> resides.
     * @param  serverPath  The path to the VSS server.
     */
    public final void setServerpath(final String serverPath) {
        this.serverPath = serverPath;
    }

    /**
     * Indicates if the build should fail if the Sourcesafe command does. Defaults to true.
     * @param failOnError True if task should fail on any error.
     */
    public final void setFailOnError(final boolean failOnError) {
        this.failOnError = failOnError;
    }

    /**
     * Executes the task.
     * <p>
     * Builds a command line to execute ss.exe and then calls Exec's run method
     * to execute the command line.
     * </p>
     *
     * @throws BuildException if the command cannot execute.
     */
    public void execute() throws BuildException {
        Commandline commandLine = buildCmdLine();
        int result = run(commandLine);
        if (Execute.isFailure(result) && getFailOnError()) {
            String msg = "Failed executing: " + formatCommandLine(commandLine)
                     + " With a return code of " + result;
            throw new BuildException(msg, getLocation());
        }
    }

    // Special setters for the sub-classes

    /**
     * Set the internal comment attribute.
     * @param comment the value to use.
     */
    protected void setInternalComment(final String comment) {
        this.comment = comment;
    }

    /**
     * Set the auto response attribute.
     * @param autoResponse the value to use.
     */
    protected void setInternalAutoResponse(final String autoResponse) {
        this.autoResponse = autoResponse;
    }

    /**
     * Set the date attribute.
     * @param date the value to use.
     */
    protected void setInternalDate(final String date) {
        this.date = date;
    }

    /**
     * Set the date format attribute.
     * @param dateFormat the value to use.
     */
    protected void setInternalDateFormat(final DateFormat dateFormat) {
        this.dateFormat = dateFormat;
    }

    /**
     * Set the failOnError attribute.
     * @param failOnError the value to use.
     */
    protected void setInternalFailOnError(final boolean failOnError) {
        this.failOnError = failOnError;
    }

    /**
     * Set the from date attribute.
     * @param fromDate the value to use.
     */
    protected void setInternalFromDate(final String fromDate) {
        this.fromDate = fromDate;
    }

    /**
     * Set the from label attribute.
     * @param fromLabel the value to use.
     */
    protected void setInternalFromLabel(final String fromLabel) {
        this.fromLabel = fromLabel;
    }

    /**
     * Set the label attribute.
     * @param label the value to use.
     */
    protected void setInternalLabel(final String label) {
        this.label = label;
    }

    /**
     * Set the local path comment attribute.
     * @param localPath the value to use.
     */
    protected void setInternalLocalPath(final String localPath) {
        this.localPath = localPath;
    }

    /**
     * Set the num days attribute.
     * @param numDays the value to use.
     */
    protected void setInternalNumDays(final int numDays) {
        this.numDays = numDays;
    }

    /**
     * Set the outputFileName comment attribute.
     * @param outputFileName the value to use.
     */
    protected void setInternalOutputFilename(final String outputFileName) {
        this.outputFileName = outputFileName;
    }

    /**
     * Set the quiet attribute.
     * @param quiet the value to use.
     */
    protected void setInternalQuiet(final boolean quiet) {
        this.quiet = quiet;
    }

    /**
     * Set the recursive attribute.
     * @param recursive the value to use.
     */
    protected void setInternalRecursive(final boolean recursive) {
        this.recursive = recursive;
    }

    /**
     * Set the style attribute.
     * @param style the value to use.
     */
    protected void setInternalStyle(final String style) {
        this.style = style;
    }

    /**
     * Set the to date attribute.
     * @param toDate the value to use.
     */
    protected void setInternalToDate(final String toDate) {
        this.toDate = toDate;
    }

    /**
     * Set the to label attribute.
     * @param toLabel the value to use.
     */
    protected void setInternalToLabel(final String toLabel) {
        this.toLabel = toLabel;
    }

    /**
     * Set the user attribute.
     * @param user the value to use.
     */
    protected void setInternalUser(final String user) {
        this.user = user;
    }

    /**
     * Set the version attribute.
     * @param version the value to use.
     */
    protected void setInternalVersion(final String version) {
        this.version = version;
    }

    /**
     * Set the writable attribute.
     * @param writable the value to use.
     */
    protected void setInternalWritable(final boolean writable) {
        this.writable = writable;
    }

    /**
     * Set the timestamp attribute.
     * @param timestamp the value to use.
     */
    protected void setInternalFileTimeStamp(final CurrentModUpdated timestamp) {
        this.timestamp = timestamp;
    }

    /**
     * Set the writableFiles attribute.
     * @param writableFiles the value to use.
     */
    protected void setInternalWritableFiles(final WritableFiles writableFiles) {
        this.writableFiles = writableFiles;
    }

    /**
     * Set the getLocalCopy attribute.
     * @param getLocalCopy the value to use.
     */
    protected void setInternalGetLocalCopy(final boolean getLocalCopy) {
        this.getLocalCopy = getLocalCopy;
    }

    /**
     * Gets the sscommand string. "ss" or "c:\path\to\ss"
     * @return    The path to ss.exe or just ss if sscommand is not set.
     */
    protected String getSSCommand() {
        if (ssDir == null) {
            return SS_EXE;
        }
        return ssDir.endsWith(File.separator) ? ssDir + SS_EXE : ssDir
                 + File.separator + SS_EXE;
    }

    /**
     * Gets the vssserverpath string.
     * @return    null if vssserverpath is not set.
     */
    protected String getVsspath() {
        return vssPath;
    }

    /**
     * Gets the quiet string. -O-
     * @return An empty string if quiet is not set or is false.
     */
    protected String getQuiet() {
        return quiet ? FLAG_QUIET : "";
    }

    /**
     * Gets the recursive string. "-R"
     * @return An empty string if recursive is not set or is false.
     */
    protected String getRecursive() {
        return recursive ? FLAG_RECURSION : "";
    }

    /**
     * Gets the writable string. "-W"
     * @return An empty string if writable is not set or is false.
     */
    protected String getWritable() {
        return writable ? FLAG_WRITABLE : "";
    }

    /**
     * Gets the label string. "-Lbuild1"
     * Max label length is 32 chars
     * @return An empty string if label is not set.
     */
    protected String getLabel() {
        String shortLabel = "";
        if (label != null && !label.isEmpty()) {
                shortLabel = FLAG_LABEL + getShortLabel();
        }
        return shortLabel;
    }
    /**
     * Return at most the 30 first chars of the label,
     * logging a warning message about the truncation
     * @return at most the 30 first chars of the label
     */
    private String getShortLabel() {
        String shortLabel;
        // CheckStyle:MagicNumber OFF
        if (label !=  null && label.length() > 31) {
            shortLabel = this.label.substring(0, 30);
            log("Label is longer than 31 characters, truncated to: " + shortLabel,
                Project.MSG_WARN);
        } else {
            shortLabel = label;
        }
        // CheckStyle:MagicNumber ON
        return shortLabel;
    }
    /**
     * Gets the style string. "-Lbuild1"
     * @return An empty string if label is not set.
     */
    protected String getStyle() {
        return style != null ? style : "";
    }

    /**
     * Gets the version string. Returns the first specified of version "-V1.0",
     * date "-Vd01.01.01", label "-Vlbuild1".
     * @return An empty string if a version, date and label are not set.
     */
    protected String getVersionDateLabel() {
        String versionDateLabel = "";
        if (version != null) {
            versionDateLabel = FLAG_VERSION + version;
        } else if (date != null) {
            versionDateLabel = FLAG_VERSION_DATE + date;
        } else {
            // Use getShortLabel() so labels longer then 30 char are truncated
            // and the user is warned
            String shortLabel = getShortLabel();
            if (shortLabel != null && !shortLabel.isEmpty()) {
                versionDateLabel = FLAG_VERSION_LABEL + shortLabel;
            }
        }
        return versionDateLabel;
    }

    /**
     * Gets the version string.
     * @return An empty string if a version is not set.
     */
    protected String getVersion() {
        return version != null ? FLAG_VERSION + version : "";
    }

    /**
     * Gets the localpath string. "-GLc:\source" <p>
     * The localpath is created if it didn't exist.
     * @return An empty string if localpath is not set.
     */
    protected String getLocalpath() {
        String lclPath = ""; //set to empty str if no local path return
        if (localPath != null) {
            //make sure m_LocalDir exists, create it if it doesn't
            File dir = getProject().resolveFile(localPath);
            if (!dir.exists()) {
                boolean done = dir.mkdirs() || dir.exists();
                if (!done) {
                    String msg = "Directory " + localPath + " creation was not "
                            + "successful for an unknown reason";
                    throw new BuildException(msg, getLocation());
                }
                getProject().log("Created dir: " + dir.getAbsolutePath());
            }
            lclPath = FLAG_OVERRIDE_WORKING_DIR + localPath;
        }
        return lclPath;
    }

    /**
     * Gets the comment string. "-Ccomment text"
     * @return A comment of "-" if comment is not set.
     */
    protected String getComment() {
        return comment != null ? FLAG_COMMENT + comment : FLAG_COMMENT + "-";
    }

    /**
     * Gets the auto response string. This can be Y "-I-Y" or N "-I-N".
     * @return The default value "-I-" if autoresponse is not set.
     */
    protected String getAutoresponse() {
        if (autoResponse == null) {
            return FLAG_AUTORESPONSE_DEF;
        }
        if (autoResponse.equalsIgnoreCase("Y")) {
            return FLAG_AUTORESPONSE_YES;
        } else if (autoResponse.equalsIgnoreCase("N")) {
            return FLAG_AUTORESPONSE_NO;
        } else {
            return FLAG_AUTORESPONSE_DEF;
        }
    }

    /**
     * Gets the login string. This can be user and password, "-Yuser,password"
     * or just user "-Yuser".
     * @return An empty string if login is not set.
     */
    protected String getLogin() {
        return vssLogin != null ? FLAG_LOGIN + vssLogin : "";
    }

    /**
     * Gets the output file string. "-Ooutput.file"
     * @return An empty string if user is not set.
     */
    protected String getOutput() {
        return outputFileName != null ? FLAG_OUTPUT + outputFileName : "";
    }

    /**
     * Gets the user string. "-Uusername"
     * @return An empty string if user is not set.
     */
    protected String getUser() {
        return user != null ? FLAG_USER + user : "";
    }

    /**
     * Gets the version string. This can be to-from "-VLbuild2~Lbuild1", from
     * "~Lbuild1" or to "-VLbuild2".
     * @return An empty string if neither tolabel or fromlabel are set.
     */
    protected String getVersionLabel() {
        if (fromLabel == null && toLabel == null) {
            return "";
        }
        // CheckStyle:MagicNumber OFF
        if (fromLabel != null && toLabel != null) {
            if (fromLabel.length() > 31) {
                fromLabel = fromLabel.substring(0, 30);
                log("FromLabel is longer than 31 characters, truncated to: "
                    + fromLabel, Project.MSG_WARN);
            }
            if (toLabel.length() > 31) {
                toLabel = toLabel.substring(0, 30);
                log("ToLabel is longer than 31 characters, truncated to: "
                    + toLabel, Project.MSG_WARN);
            }
            return FLAG_VERSION_LABEL + toLabel + VALUE_FROMLABEL + fromLabel;
        } else if (fromLabel != null) {
            if (fromLabel.length() > 31) {
                fromLabel = fromLabel.substring(0, 30);
                log("FromLabel is longer than 31 characters, truncated to: "
                    + fromLabel, Project.MSG_WARN);
            }
            return FLAG_VERSION + VALUE_FROMLABEL + fromLabel;
        } else {
            if (toLabel.length() > 31) {
                toLabel = toLabel.substring(0, 30);
                log("ToLabel is longer than 31 characters, truncated to: "
                    + toLabel, Project.MSG_WARN);
            }
            return FLAG_VERSION_LABEL + toLabel;
        }
        // CheckStyle:MagicNumber ON
    }

    /**
     * Gets the Version date string.
     * @return An empty string if neither Todate or from date are set.
     * @throws BuildException if there is an error.
     */
    protected String getVersionDate() throws BuildException {
        if (fromDate == null && toDate == null
            && numDays == Integer.MIN_VALUE) {
            return "";
        }
        if (fromDate != null && toDate != null) {
            return FLAG_VERSION_DATE + toDate + VALUE_FROMDATE + fromDate;
        } else if (toDate != null && numDays != Integer.MIN_VALUE) {
            try {
                return FLAG_VERSION_DATE + toDate + VALUE_FROMDATE
                        + calcDate(toDate, numDays);
            } catch (ParseException ex) {
                String msg = "Error parsing date: " + toDate;
                throw new BuildException(msg, getLocation());
            }
        } else if (fromDate != null && numDays != Integer.MIN_VALUE) {
            try {
                return FLAG_VERSION_DATE + calcDate(fromDate, numDays)
                        + VALUE_FROMDATE + fromDate;
            } catch (ParseException ex) {
                String msg = "Error parsing date: " + fromDate;
                throw new BuildException(msg, getLocation());
            }
        } else {
            return fromDate != null ? FLAG_VERSION + VALUE_FROMDATE
                    + fromDate : FLAG_VERSION_DATE + toDate;
        }
    }

    /**
     * Builds and returns the -G- flag if required.
     * @return An empty string if get local copy is true.
     */
    protected String getGetLocalCopy() {
        return (!getLocalCopy) ? FLAG_NO_GET : "";
    }

    /**
     * Gets the value of the fail on error flag.
     * @return    True if the FailOnError flag has been set or if 'writablefiles=skip'.
     */
    private boolean getFailOnError() {
        return !getWritableFiles().equals(WRITABLE_SKIP) && failOnError;
    }


    /**
     * Gets the value set for the FileTimeStamp.
     * if it equals "current" then we return -GTC
     * if it equals "modified" then we return -GTM
     * if it equals "updated" then we return -GTU
     * otherwise we return -GTC
     *
     * @return The default file time flag, if not set.
     */
    public String getFileTimeStamp() {
        if (timestamp == null) {
            return "";
        } else if (timestamp.getValue().equals(TIME_MODIFIED)) {
            return FLAG_FILETIME_MODIFIED;
        } else if (timestamp.getValue().equals(TIME_UPDATED)) {
            return FLAG_FILETIME_UPDATED;
        } else {
            return FLAG_FILETIME_DEF;
        }
    }


    /**
     * Gets the value to determine the behaviour when encountering writable files.
     * @return An empty String, if not set.
     */
    public String getWritableFiles() {
        if (writableFiles == null) {
            return "";
        } else if (writableFiles.getValue().equals(WRITABLE_REPLACE)) {
            return FLAG_REPLACE_WRITABLE;
        } else if (writableFiles.getValue().equals(WRITABLE_SKIP)) {
            // ss.exe exits with '100', when files have been skipped
            // so we have to ignore the failure
            failOnError = false;
            return FLAG_SKIP_WRITABLE;
        } else {
            return "";
        }
    }

    /**
     *  Sets up the required environment and executes the command line.
     *
     * @param  cmd  The command line to execute.
     * @return      The return code from the exec'd process.
     */
    private int run(Commandline cmd) {
        try {
            Execute exe = new Execute(new LogStreamHandler(this,
                    Project.MSG_INFO,
                    Project.MSG_WARN));

            // If location of ss.ini is specified we need to set the
            // environment-variable SSDIR to this value
            if (serverPath != null) {
                String[] env = exe.getEnvironment();
                if (env == null) {
                    env = new String[0];
                }
                String[] newEnv = new String[env.length + 1];
                System.arraycopy(env, 0, newEnv, 0, env.length);
                newEnv[env.length] = "SSDIR=" + serverPath;

                exe.setEnvironment(newEnv);
            }

            exe.setAntRun(getProject());
            exe.setWorkingDirectory(getProject().getBaseDir());
            exe.setCommandline(cmd.getCommandline());
            // Use the OS launcher so we get environment variables
            exe.setVMLauncher(false);
            return exe.execute();
        } catch (IOException e) {
            throw new BuildException(e, getLocation());
        }
    }

     /**
     * Calculates the start date for version comparison.
     * <p>
     * Calculates the date numDay days earlier than startdate.
     * @param   startDate    The start date.
     * @param   daysToAdd     The number of days to add.
     * @return The calculated date.
     * @throws ParseException
     */
    private String calcDate(String startDate, int daysToAdd) throws ParseException {
        Calendar calendar = new GregorianCalendar();
        Date currentDate = dateFormat.parse(startDate);
        calendar.setTime(currentDate);
        calendar.add(Calendar.DATE, daysToAdd);
        return dateFormat.format(calendar.getTime());
    }

    /**
     * Changes the password to '***' so it isn't displayed on screen if the build fails
     *
     * @param cmd   The command line to clean
     * @return The command line as a string with out the password
     */
    private String formatCommandLine(Commandline cmd) {
        StringBuffer sBuff = new StringBuffer(cmd.toString());
        int indexUser = sBuff.substring(0).indexOf(FLAG_LOGIN);
        if (indexUser > 0) {
            int indexPass = sBuff.substring(0).indexOf(",", indexUser);
            int indexAfterPass = sBuff.substring(0).indexOf(" ", indexPass);

            for (int i = indexPass + 1; i < indexAfterPass; i++) {
                sBuff.setCharAt(i, '*');
            }
        }
        return sBuff.toString();
    }

    /**
     * Extension of EnumeratedAttribute to hold the values for file time stamp.
     */
    public static class CurrentModUpdated extends EnumeratedAttribute {
        /**
         * Gets the list of allowable values.
         * @return The values.
         */
        public String[] getValues() {
            return new String[] {TIME_CURRENT, TIME_MODIFIED, TIME_UPDATED};
        }
    }

    /**
     * Extension of EnumeratedAttribute to hold the values for writable filess.
     */
    public static class WritableFiles extends EnumeratedAttribute {
        /**
         * Gets the list of allowable values.
         * @return The values.
         */
        public String[] getValues() {
            return new String[] {WRITABLE_REPLACE, WRITABLE_SKIP, WRITABLE_FAIL};
        }
    }
}
