/*
 * ====================================================================
 * Copyright (c) 2004-2011 TMate Software Ltd.  All rights reserved.
 *
 * This software is licensed as described in the file COPYING, which
 * you should have received as part of this distribution.  The terms
 * are also available at http://svnkit.com/license.html.
 * If newer versions of this license are posted there, you may use a
 * newer version instead, at your option.
 * ====================================================================
 */
package org.tmatesoft.svn.cli;

import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.text.MessageFormat;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Map;

import org.tmatesoft.svn.cli.svn.SVNCommandEnvironment;
import org.tmatesoft.svn.core.SVNErrorCode;
import org.tmatesoft.svn.core.SVNErrorMessage;
import org.tmatesoft.svn.core.SVNException;
import org.tmatesoft.svn.core.internal.util.SVNFormatUtil;
import org.tmatesoft.svn.core.internal.util.SVNPathUtil;
import org.tmatesoft.svn.core.internal.wc.ISVNReturnValueCallback;
import org.tmatesoft.svn.core.internal.wc.SVNErrorManager;
import org.tmatesoft.svn.core.internal.wc.SVNFileUtil;
import org.tmatesoft.svn.util.SVNLogType;
import org.tmatesoft.svn.util.Version;


/**
 * @version 1.3
 * @author  TMate Software Ltd.
 */
public class SVNCommandUtil {
    
    public static String getLocalPath(String path) {
        path = path.replace('/', File.separatorChar);
        if ("".equals(path)) {
            path = ".";
        }
        return path;
    }

    public static boolean isURL(String pathOrUrl){
        return SVNPathUtil.isURL(pathOrUrl);
    }
    
    public static void mergeFileExternally(AbstractSVNCommandEnvironment env, String basePath, String repositoryPath, 
            String localPath, String mergeResultPath, String wcPath, final boolean[] remainsInConflict) throws SVNException {
        String[] testEnvironment = SVNFileUtil.getTestEnvironment();
        String mergeToolCommand = testEnvironment[1];
        if (testEnvironment[1] == null) {
            mergeToolCommand = SVNFileUtil.getEnvironmentVariable("SVN_MERGE");
            if (mergeToolCommand == null) {
                mergeToolCommand = env.getOptions().getMergeTool();
            }
            testEnvironment = null;
        } else {
            mergeToolCommand = testEnvironment[1];
            testEnvironment = new String[] {"SVNTEST_EDITOR_FUNC=" + (testEnvironment[2] == null ? "" : testEnvironment[2])};
        }

        if (mergeToolCommand != null) {
            mergeToolCommand = mergeToolCommand.trim();
            if (mergeToolCommand.length() == 0) {
                SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.CL_NO_EXTERNAL_MERGE_TOOL, 
                        "The SVN_MERGE environment variable is empty or consists solely of whitespace. Expected a shell command.");
                SVNErrorManager.error(err, SVNLogType.CLIENT);
            }
        } else {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.CL_NO_EXTERNAL_MERGE_TOOL, 
                    "The environment variable SVN_MERGE and the merge-tool-cmd run-time configuration option were not set.");
            SVNErrorManager.error(err, SVNLogType.CLIENT);
        }
        
        String merger = mergeToolCommand;
        if (SVNFileUtil.isWindows) {
            merger = mergeToolCommand.toLowerCase();
        }

        ISVNReturnValueCallback runCallback = new ISVNReturnValueCallback() {
            public void handleChar(char ch) throws SVNException {
            }

            public void handleReturnValue(int returnValue) throws SVNException {
                if (returnValue != 0 && returnValue != 1) {
                    SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.EXTERNAL_PROGRAM, 
                            "The external merge tool exited with exit code {0}", String.valueOf(returnValue));
                    SVNErrorManager.error(err, SVNLogType.CLIENT);
                }
                if (remainsInConflict != null && remainsInConflict.length > 0) {
                    remainsInConflict[0] = returnValue == 1; 
                }
            }

            public boolean isHandleProgramOutput() {
                return false;
            }
        };
        
        runEditor(merger, new String[] { basePath, repositoryPath, localPath, mergeResultPath, wcPath }, testEnvironment, runCallback);
        
    }

    public static void editFileExternally(AbstractSVNCommandEnvironment env, String editorCommand, String path) throws SVNException {
        editorCommand = getEditorCommand(env, editorCommand);
        String testEnv[] = SVNFileUtil.getTestEnvironment();
        if (testEnv[0] != null) {
            testEnv = new String[] {"SVNTEST_EDITOR_FUNC=" + (testEnv[2] != null ? testEnv[2] : "")};
        }
        
        if (testEnv != null) {
            LinkedList environment = new LinkedList();
            for (int i = 0; i < testEnv.length; i++) {
                if (testEnv[i] != null) {
                    environment.add(testEnv[i]);
                }
            }
            if (!environment.isEmpty()) {
                testEnv = (String[]) environment.toArray(new String[environment.size()]);                
            } else {
                testEnv = null;
            }
        }
        
        final int[] exitCode = { -1 };
        ISVNReturnValueCallback procCallback = new ISVNReturnValueCallback() {

            public void handleChar(char ch) throws SVNException {
            }

            public void handleReturnValue(int returnValue) throws SVNException {
                exitCode[0] = returnValue;
            }

            public boolean isHandleProgramOutput() {
                return false;
            }
            
        };
        
        String result = runEditor(editorCommand, new String[] {path}, testEnv, procCallback);
        if (result == null) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.EXTERNAL_PROGRAM, "system(''{0}'') returned {1}", 
                    new Object[] { editorCommand + " " + path, String.valueOf(exitCode[0]) });
            //SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.EXTERNAL_PROGRAM, "Editor command '" + 
            //        editorCommand + " " + path + "' failed.");
            SVNErrorManager.error(err, SVNLogType.CLIENT);
        }
    }
    
    public static byte[] runEditor(AbstractSVNCommandEnvironment env, String editorCommand, byte[] existingValue, String prefix) throws SVNException {
        File tmpDir = new File(System.getProperty("java.io.tmpdir"));
        File tmpFile = SVNFileUtil.createUniqueFile(tmpDir, prefix, ".tmp", false);
        OutputStream os = null;
        try {
            os = SVNFileUtil.openFileForWriting(tmpFile);
            os.write(existingValue);
        } catch (IOException e) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.IO_ERROR, e.getMessage());
            SVNErrorManager.error(err, SVNLogType.CLIENT);
        } finally {
            SVNFileUtil.closeFile(os);
        }
        tmpFile.setLastModified(System.currentTimeMillis() - 2000);
        long timestamp = tmpFile.lastModified();
        editorCommand = getEditorCommand(env, editorCommand);
        String[] testEnv = SVNFileUtil.getTestEnvironment();
        if (testEnv[0] != null) {
            testEnv = new String[] {"SVNTEST_EDITOR_FUNC=" + (testEnv[2] != null ? testEnv[2] : "")};
        } else {
            testEnv = null;
        }
        try {
            String result = runEditor(editorCommand, new String[] {tmpFile.getAbsolutePath()}, testEnv, null);
            if (result == null) {
                SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.IO_ERROR, "Editor command '" + editorCommand + " " + tmpFile.getAbsolutePath() + "' failed.");
                SVNErrorManager.error(err, SVNLogType.CLIENT);
            }
            // now read from file.
            if (timestamp == tmpFile.lastModified()) {
                return null;
            }
            InputStream is = null;
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            byte[] buffer = new byte[2048];
            try {
                is = SVNFileUtil.openFileForReading(tmpFile);
                while(true) {
                    int read = is.read(buffer);
                    if (read < 0) {
                        break;
                    }
                    bos.write(buffer, 0, read);
                }
            } catch (IOException e) {
                SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.IO_ERROR, e.getMessage());
                SVNErrorManager.error(err, SVNLogType.CLIENT);
            } finally {
                SVNFileUtil.closeFile(is);
            }
            return bos.toByteArray();
        } finally {
            SVNFileUtil.deleteFile(tmpFile);
        }
    }

    private static String runEditor(String editorCommand, String[] args, String[] env, ISVNReturnValueCallback callback) throws SVNException {
        String result = null;
        if (SVNFileUtil.isWindows || SVNFileUtil.isOS2) {
            String editor = editorCommand.trim().toLowerCase();
            if (!(editor.endsWith(".exe") || editor.endsWith(".bat") || editor.endsWith(".cmd"))) {
                String[] command = new String[3 + args.length];
                command[0] = "cmd.exe";
                command[1] = "/C";
                command[2] = editorCommand;
                for (int i = 0; i < args.length; i++) {
                    command[3 + i] = args[i];
                }
                result = SVNFileUtil.execCommand(command, env, false, callback);
            } else {
                String[] command = new String[1 + args.length];
                command[0] = editorCommand;
                for (int i = 0; i < args.length; i++) {
                    command[1 + i] = args[i];
                }
                result = SVNFileUtil.execCommand(command, env, false, callback);
            }
        } else if (SVNFileUtil.isLinux || SVNFileUtil.isBSD || SVNFileUtil.isOSX || SVNFileUtil.isSolaris){
            if (env == null) {
                String shellCommand = SVNFileUtil.getEnvironmentVariable("SHELL");
                if (shellCommand == null || "".equals(shellCommand.trim())) {
                    shellCommand = "/bin/sh";
                }
                String[] command = new String[3];
                command[0] = shellCommand;
                command[1] = "-c";
                command[2] = editorCommand;
                for (int i = 0; i < args.length; i++) {
                    command[2] += " " + args[i];
                }
                command[2] += " < /dev/tty > /dev/tty";
                result = SVNFileUtil.execCommand(command, env, false, callback);
            } else {
                // test mode, do not use bash and redirection.
                String[] command = new String[1 + args.length];
                command[0] = editorCommand;
                for (int i = 0; i < args.length; i++) {
                    command[1 + i] = args[i];
                }
                result = SVNFileUtil.execCommand(command, env, false, callback);
            }
        } else if (SVNFileUtil.isOpenVMS) {
            String[] command = new String[1 + args.length];
            command[0] = editorCommand;
            for (int i = 0; i < args.length; i++) {
                command[1 + i] = args[i];
            }
            result = SVNFileUtil.execCommand(command, env, false, callback);
        } 
        return result;
    }
    
    public static String prompt(String promptMessage, SVNCommandEnvironment env) throws SVNException {
        System.err.print(promptMessage);
        System.err.flush();
        String input = null;
        InputReader reader = new InputReader(System.in);
        Thread readerThread = new Thread(reader);
        readerThread.setDaemon(true);
        readerThread.start();
        while (true) {
            env.checkCancelled();
            if (reader.myIsFinished) {
                input = reader.getReadInput();
                break;
            }
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
            }
        }
        if (reader.getError() != null) {
            SVNErrorManager.error(reader.getError(), SVNLogType.CLIENT);
        }
        if (input == null) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.IO_ERROR, 
                        "Can't read stdin: End of file found");
                SVNErrorManager.error(err, SVNLogType.CLIENT);
        } 
        return input;
    }

    private static String getEditorCommand(AbstractSVNCommandEnvironment env, String editorCommand) throws SVNException {
        if (editorCommand != null) {
            return editorCommand;
        } 
        String[] testEnvironment = SVNFileUtil.getTestEnvironment();
        String command = testEnvironment[0];
        if (command == null) {
            command = SVNFileUtil.getEnvironmentVariable("SVN_EDITOR");
            if (command == null) {
                command = env.getOptions().getEditor();
            }
            if (command == null) {
                command = SVNFileUtil.getEnvironmentVariable("VISUAL");
            }
            if (command == null) {
                command = SVNFileUtil.getEnvironmentVariable("EDITOR");
            }
        }
        String errorMessage = null;
        if (command == null) {
            errorMessage = 
                "None of the environment variables SVN_EDITOR, VISUAL or EDITOR is " +
                "set, and no 'editor-cmd' run-time configuration option was found"; 
        } else if ("".equals(command.trim())) {
            errorMessage = 
                "The EDITOR, SVN_EDITOR or VISUAL environment variable or " +
                "'editor-cmd' run-time configuration option is empty or " +
                "consists solely of whitespace. Expected a shell command.";
        }
        if (errorMessage != null) {
            SVNErrorManager.error(SVNErrorMessage.create(SVNErrorCode.CL_NO_EXTERNAL_EDITOR, errorMessage), SVNLogType.CLIENT);
        }
        return command;
    }

    public static int getLinesCount(String str) {
        if ("".equals(str)) {
            return 1;
        }
        int count = 1;
        for (int i = 0; i < str.length(); i++) {
            if (str.charAt(i) == '\r') {
                count++;
                if (i < str.length() - 1 && str.charAt(i + 1) == '\n') {
                    i++;
                }
            } else if (str.charAt(i) == '\n') {
                count++;
            }
        }
        if (count == 0) {
            count++;
        }
        return count;
    }

    public static String[] breakToLines(String str) {
        if (str == null) {
            return null;
        }
        
        if ("".equals(str)) {
            return new String[] { "" };
        }
        
        LinkedList list = new LinkedList();
        int i = 0;
        int start = 0;
        for (; i < str.length(); i++) {
            char ch = str.charAt(i);
            if (ch == '\r' || ch == '\n') {
                if (i < str.length() - 1) {
                    char nextCh = str.charAt(i + 1);
                    if ((ch == '\r' && nextCh == '\n') || (ch == '\n' && nextCh == '\r')) {
                        i++;
                    }
                    list.add(str.substring(start, i + 1));
                } else {
                    list.add(str.substring(start, i + 1));
                }
                start = i + 1;
            }
        }
        if (start != i) {
            list.add(str.substring(start, i));
        }
        return (String[]) list.toArray(new String[list.size()]); 
    }

    public static String getCommandHelp(AbstractSVNCommand command, String programName, boolean printOptionAlias) {
        StringBuffer help = new StringBuffer();
        help.append(command.getName());
        if (command.getAliases().length > 0) {
            help.append(" (");
            for (int i = 0; i < command.getAliases().length; i++) {
                help.append(command.getAliases()[i]);
                if (i + 1 < command.getAliases().length) {
                    help.append(", ");
                }
            }
            help.append(")");
        }
        if (!"".equals(command.getName())) {
            help.append(": ");
        }
        help.append(command.getDescription());
        if (!command.getSupportedOptions().isEmpty()) {
            help.append("\n");            
            if (!command.getValidOptions().isEmpty()) {
                help.append("\nValid options:\n");
                for (Iterator options = command.getValidOptions().iterator(); options.hasNext();) {
                    AbstractSVNOption option = (AbstractSVNOption) options.next();
                    help.append("  ");
                    String optionDesc = null;
                    if (option.getAlias() != null && printOptionAlias) {
                        optionDesc = "-" + option.getAlias() + " [--" + option.getName() + "]";
                    } else {
                        optionDesc = "--" + option.getName();
                    }
                    
                    if (!option.isUnary()) {
                        optionDesc += " ARG";
                    }

                    int chars = optionDesc.length() < 24 ? 24 : optionDesc.length();
                    help.append(SVNFormatUtil.formatString(optionDesc, chars, true));
                    help.append(" : ");
                    help.append(option.getDescription(command, programName));
                    help.append("\n");
                }
            }

            if (!command.getGlobalOptions().isEmpty()) {
                help.append("\nGlobal options:\n");
                for (Iterator options = command.getGlobalOptions().iterator(); options.hasNext();) {
                    AbstractSVNOption option = (AbstractSVNOption) options.next();
                    help.append("  ");
                    String optionDesc = null;
                    if (option.getAlias() != null) {
                        optionDesc = "-" + option.getAlias() + " [--" + option.getName() + "]";
                    } else {
                        optionDesc = "--" + option.getName();
                    }
                    
                    if (!option.isUnary()) {
                        optionDesc += " ARG";
                    }
                
                    help.append(SVNFormatUtil.formatString(optionDesc, 24, true));
                    help.append(" : ");
                    help.append(option.getDescription(command, programName));
                    help.append("\n");
                }
            }
        }
        return help.toString();
    }

    public static String getVersion(AbstractSVNCommandEnvironment env, boolean quiet) {
        String version = Version.getMajorVersion() + "." + Version.getMinorVersion() + "." + Version.getMicroVersion();
        String revNumber = Version.getRevisionNumber() < 0 ? "SNAPSHOT" : Long.toString(Version.getRevisionNumber());
        String message = MessageFormat.format(env.getProgramName() + ", version {0}\n", new Object[] {version + " (r" + revNumber + ")"});
        if (quiet) {
            message = version;
        }
        if (!quiet) {
            message += 
                "\nCopyright (c) 2004-2011 TMate Software.\n" +
                "SVNKit is an Open Source software, see http://svnkit.com/ for more information.\n" +
                "SVNKit is a pure Java (TM) version of Subversion, see http://subversion.tigris.org/";
        }
        return message;
    
    }

    public static String getGenericHelp(String programName, String header, String footer, Comparator commandComparator) {
        StringBuffer help = new StringBuffer();
        if (header != null) {
            String version = Version.getMajorVersion() + "." + Version.getMinorVersion() + "." + Version.getMicroVersion();
            header = MessageFormat.format(header, new Object[] {programName, version});
            help.append(header);
        }

        for (Iterator commands = AbstractSVNCommand.availableCommands(commandComparator); commands.hasNext();) {
            AbstractSVNCommand command = (AbstractSVNCommand) commands.next();
            help.append("\n   ");
            help.append(command.getName());
            if (command.getAliases().length > 0) {
                help.append(" (");
                for (int i = 0; i < command.getAliases().length; i++) {
                    help.append(command.getAliases()[i]);
                    if (i + 1 < command.getAliases().length) {
                        help.append(", ");
                    }
                }
                help.append(")");
            }
        }

        help.append("\n\n");
        if (footer != null) {
            help.append(footer);            
        }
        return help.toString();
    }
 
    public static void parseConfigOption(String optionArg, Map configOptions, Map serversOptions) throws SVNException {
        if (optionArg != null) {
            int firstColonInd = optionArg.indexOf(':');
            if (firstColonInd != -1 && firstColonInd != optionArg.length() - 1) {
                int secondColonInd = optionArg.indexOf(':', firstColonInd + 1);
                if (secondColonInd != -1 && secondColonInd != firstColonInd + 1) {
                    int equalsSignInd = optionArg.indexOf('=', secondColonInd + 1);
                    if (equalsSignInd != -1 && equalsSignInd != secondColonInd + 1) {
                        String fileName = optionArg.substring(0, firstColonInd);
                        String section = optionArg.substring(firstColonInd + 1, secondColonInd);
                        String option = optionArg.substring(secondColonInd + 1, equalsSignInd);
                        if (option.indexOf(':') == -1) {
                            String value = optionArg.substring(equalsSignInd + 1);
                            
                            Map options = null;
                            if ("servers".equals(fileName)) {
                                options = serversOptions;
                            } else if ("config".equals(fileName)) {
                                options = configOptions;
                            }
                            
                            if (options != null) {
                                Map values = (Map) options.get(section);
                                if (values == null) {
                                    values = new HashMap();
                                    options.put(section, values);
                                }
                                values.put(option, value);
                                return;
                            }
                        }
                    }
                }
            }
        }
        
        SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.CL_ARG_PARSING_ERROR, "Invalid syntax of argument of --config-option");
        SVNErrorManager.error(err, SVNLogType.CLIENT);
    }
    
    private static class InputReader implements Runnable {
        private BufferedReader myReader;
        private String myReadInput;
        private SVNErrorMessage myError;
        volatile boolean myIsFinished;
        
        public InputReader(InputStream is) {
            myReader = new BufferedReader(new InputStreamReader(is));
        }
        
        public void run() {
            myIsFinished = false;
            try {
                myReadInput = myReader.readLine();
            } catch (IOException e) {
                myError = SVNErrorMessage.create(SVNErrorCode.IO_ERROR, "Can''t read stdin: {0}", 
                        e.getLocalizedMessage());
            }
            myIsFinished = true;
        }
        
        public String getReadInput() {
            return myReadInput;
        }
        
        public SVNErrorMessage getError() {
            return myError;
        }
    }
}
