/*
 * ====================================================================
 * Copyright (c) 2004-2012 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.core.internal.io.fs;

import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.util.List;

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.SVNStreamGobbler;
import org.tmatesoft.svn.core.internal.wc.SVNErrorManager;
import org.tmatesoft.svn.core.internal.wc.SVNFileType;
import org.tmatesoft.svn.core.internal.wc.SVNFileUtil;
import org.tmatesoft.svn.util.SVNLogType;

/**
 * @version 1.3
 * @author  TMate Software Ltd.
 */
public class FSHooks {

    public static final String SVN_REPOS_HOOK_START_COMMIT = "start-commit";
    public static final String SVN_REPOS_HOOK_PRE_COMMIT = "pre-commit";
    public static final String SVN_REPOS_HOOK_POST_COMMIT = "post-commit";
    public static final String SVN_REPOS_HOOK_PRE_REVPROP_CHANGE = "pre-revprop-change";
    public static final String SVN_REPOS_HOOK_POST_REVPROP_CHANGE = "post-revprop-change";
    public static final String SVN_REPOS_HOOK_PRE_LOCK = "pre-lock";
    public static final String SVN_REPOS_HOOK_POST_LOCK = "post-lock";
    public static final String SVN_REPOS_HOOK_PRE_UNLOCK = "pre-unlock";
    public static final String SVN_REPOS_HOOK_POST_UNLOCK = "post-unlock";
    public static final String SVN_REPOS_HOOK_READ_SENTINEL = "read-sentinels";
    public static final String SVN_REPOS_HOOK_WRITE_SENTINEL = "write-sentinels";
    public static final String SVN_REPOS_HOOK_DESC_EXT = ".tmpl";
    public static final String SVN_REPOS_HOOKS_DIR = "hooks";
    public static final String REVPROP_DELETE = "D";
    public static final String REVPROP_ADD = "A";
    public static final String REVPROP_MODIFY = "M";
    private static final String[] winExtensions = {
            ".exe", ".bat", ".cmd"
    };
    
    private static Boolean ourIsHooksEnabled;
    
    public static void setHooksEnabled(boolean enabled) {
        ourIsHooksEnabled = enabled ? Boolean.TRUE : Boolean.FALSE;
    }
    
    public static boolean isHooksEnabled() {
        if (ourIsHooksEnabled == null) {
            ourIsHooksEnabled = Boolean.valueOf(System.getProperty("svnkit.hooksEnabled", System.getProperty("javasvn.hooksEnabled", "true")));
        }
        return ourIsHooksEnabled.booleanValue();
    }

    public static String runPreLockHook(File reposRootDir, String path, String username, String comment, boolean stealLock) throws SVNException {
        username = username == null ? "" : username;
        path = path == null ? "" : path;
        return runHook(reposRootDir, SVN_REPOS_HOOK_PRE_LOCK, new String[] {path, username, comment != null ? comment : "", stealLock ? "1" : "0"}, null);
    }

    public static void runPostLockHook(File reposRootDir, String[] paths, String username) throws SVNException {
        StringBuffer pathsStr = new StringBuffer();
        for (int i = 0; i < paths.length; i++) {
            pathsStr.append(paths[i]);
            pathsStr.append("\n");
        }
        runLockHook(reposRootDir, SVN_REPOS_HOOK_POST_LOCK, null, username, pathsStr.toString());
    }

    public static void runPreUnlockHook(File reposRootDir, String path, String username) throws SVNException {
        runLockHook(reposRootDir, SVN_REPOS_HOOK_PRE_UNLOCK, path, username, null);
    }

    public static void runPostUnlockHook(File reposRootDir, String[] paths, String username) throws SVNException {
        StringBuffer pathsStr = new StringBuffer();
        for (int i = 0; i < paths.length; i++) {
            pathsStr.append(paths[i]);
            pathsStr.append("\n");
        }
        runLockHook(reposRootDir, SVN_REPOS_HOOK_POST_UNLOCK, null, username, pathsStr.toString());
    }

    private static void runLockHook(File reposRootDir, String hookName, String path, String username, String paths) throws SVNException {
        username = username == null ? "" : username;
        path = path == null ? "" : path;
        byte[] bytes = null;
        try {
            bytes = paths == null ? null : paths.getBytes("UTF-8");
        } catch (UnsupportedEncodingException e) {
            bytes = paths.getBytes();
        }
        runHook(reposRootDir, hookName, new String[] {path, username}, bytes);
    }

    public static void runPreRevPropChangeHook(File reposRootDir, String propName, byte[] propNewValue, String author, long revision, String action) throws SVNException {
        runChangeRevPropHook(reposRootDir, SVN_REPOS_HOOK_PRE_REVPROP_CHANGE, propName, propNewValue, author, revision, action, true);
    }

    public static void runPostRevPropChangeHook(File reposRootDir, String propName, byte[] propOldValue, String author, long revision, String action) throws SVNException {
        runChangeRevPropHook(reposRootDir, SVN_REPOS_HOOK_POST_REVPROP_CHANGE, propName, propOldValue, author, revision, action, false);
    }
    
    private static void runChangeRevPropHook(File reposRootDir, String hookName, String propName, byte[] propValue, String author, long revision, String action, boolean isPre) throws SVNException {
        File hookFile = getHookFile(reposRootDir, hookName);
        if (hookFile == null) {
            if (isPre) {
                SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.REPOS_DISABLED_FEATURE,
                        "Repository has not been enabled to accept revision propchanges;\nask the administrator to create a pre-revprop-change hook");
                SVNErrorManager.error(err, SVNLogType.FSFS);
            }
            return;
        } 
        author = author == null ? "" : author;
        runHook(reposRootDir, hookName, new String[] {String.valueOf(revision), author, propName, action}, propValue);
    }

    public static void runStartCommitHook(File reposRootDir, String author, List<?> capabilities) throws SVNException {
        author = author == null ? "" : author;
        String capsString = getCapabilitiesAsString(capabilities);
        String[] args = capsString == null ? new String[] { author } : new String[] { author, capsString }; 
        runHook(reposRootDir, SVN_REPOS_HOOK_START_COMMIT, args, null);
    }

    public static void runPreCommitHook(File reposRootDir, String txnName) throws SVNException {
        runHook(reposRootDir, SVN_REPOS_HOOK_PRE_COMMIT, new String[] {txnName}, null);
    }

    public static void runPostCommitHook(File reposRootDir, long committedRevision) throws SVNException {
        runHook(reposRootDir, SVN_REPOS_HOOK_POST_COMMIT, new String[] {String.valueOf(committedRevision)}, null);
    }

    private static String runHook(File reposRootDir, String hookName, String[] args, byte[] input) throws SVNException {
        File hookFile = getHookFile(reposRootDir, hookName);
        if (hookFile == null) {
            return null;
        }
        if (args == null) {
            args = new String[0];
        }
        Process hookProc = null;
        String reposPath = reposRootDir.getAbsolutePath().replace(File.separatorChar, '/');
        String executableName = hookFile.getName().toLowerCase();
        boolean useCmd = (executableName.endsWith(".bat") || executableName.endsWith(".cmd")) && SVNFileUtil.isWindows;
        String[] cmd = useCmd ? new String[4 + args.length] : new String[2 + args.length];
        
        if (useCmd) {
            cmd = new String[] {"cmd", "/C", ""};
            cmd[2] = "\"" + "\"" + hookFile.getAbsolutePath() + "\" \"" + reposPath + "\"";
            for (int i = 0; i < args.length; i++) {
                cmd[2] += " \"" + args[i] + "\"";
            }
            cmd[2] += "\"";
        } else {
            int i = 0;
            if (useCmd) {
                cmd[0] = "cmd";
                cmd[1] = "/C";
                i = 2;
            }
            cmd[i] = hookFile.getAbsolutePath();
            i++;
            cmd[i] = reposPath;
            i++;
            for(int j = 0; j < args.length; j++) {
                cmd[i + j] = args[j];
            }
        }
        try {
            hookProc = Runtime.getRuntime().exec(cmd);
        } catch (IOException ioe) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.REPOS_HOOK_FAILURE, "Failed to start ''{0}'' hook: {1}", new Object[] {
                    hookFile, ioe.getLocalizedMessage()
            });
            SVNErrorManager.error(err, ioe, SVNLogType.FSFS);
        }
        return feedHook(hookFile, hookName, hookProc, input);
    }


    private static String feedHook(File hook, String hookName, Process hookProcess, byte[] stdInValue) throws SVNException {
        if (hookProcess == null) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.REPOS_HOOK_FAILURE, "Failed to start ''{0}'' hook", hook);
            SVNErrorManager.error(err, SVNLogType.FSFS);
        }

        SVNStreamGobbler inputGobbler = new SVNStreamGobbler(hookProcess.getInputStream());
        SVNStreamGobbler errorGobbler = new SVNStreamGobbler(hookProcess.getErrorStream());
        inputGobbler.start();
        errorGobbler.start();

        if (stdInValue != null) {
            OutputStream osToStdIn = hookProcess.getOutputStream();
            try {
                for (int i = 0; i < stdInValue.length; i += 1024) {
                    osToStdIn.write(stdInValue, i, Math.min(1024, stdInValue.length - i));
                    osToStdIn.flush();
                }
            } catch (IOException ioe) {
                // 
            } finally {
                SVNFileUtil.closeFile(osToStdIn);
            }
        }

        int rc = -1;
        try {
            inputGobbler.waitFor();
            errorGobbler.waitFor();
            rc = hookProcess.waitFor();
        } catch (InterruptedException ie) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.REPOS_HOOK_FAILURE, "Failed to start ''{0}'' hook: {1}", new Object[] {
                    hook, ie.getLocalizedMessage()
            });
            SVNErrorManager.error(err, ie, SVNLogType.FSFS);
        } finally {
            errorGobbler.close();
            inputGobbler.close();
            hookProcess.destroy();
        }

        if (rc == 0 ) {
            if (errorGobbler.getError() != null) {
                SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.REPOS_HOOK_FAILURE, "''{0}'' hook succeeded, but error output could not be read", hookName);
                SVNErrorManager.error(err, errorGobbler.getError(), SVNLogType.FSFS);
            }
            return inputGobbler.getResult();
        } else {
            String actionName = null;
            if (SVN_REPOS_HOOK_START_COMMIT.equals(hookName) || SVN_REPOS_HOOK_PRE_COMMIT.equals(hookName)) {
                actionName = "Commit";
            } else if (SVN_REPOS_HOOK_PRE_REVPROP_CHANGE.equals(hookName)) {
                actionName = "Revprop change";
            } else if (SVN_REPOS_HOOK_PRE_LOCK.equals(hookName)) {
                actionName = "Lock";
            } else if (SVN_REPOS_HOOK_PRE_UNLOCK.equals(hookName)) {
                actionName = "Unlock";
            }
            String stdErrMessage = errorGobbler.getError() != null ? "[Error output could not be read.]" : errorGobbler.getResult();
            String errorMessage = actionName != null ? 
                    actionName + " blocked by {0} hook (exit code {1})" : "{0} hook failed (exit code {1})";
            if (stdErrMessage != null && stdErrMessage.length() > 0) {
                errorMessage += " with output:\n{2}";
                SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.REPOS_HOOK_FAILURE, errorMessage, new Object[] {hookName, rc, stdErrMessage});
                SVNErrorManager.error(err, SVNLogType.FSFS);
            } else {
                errorMessage += " with no output.";
            }
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.REPOS_HOOK_FAILURE, errorMessage, new Object[] {hookName, rc});
            SVNErrorManager.error(err, SVNLogType.FSFS);
        }
        return null;
    }

    private static File getHookFile(File reposRootDir, String hookName) throws SVNException {
        if (!isHooksEnabled()) {
            return null;
        }
        File hookFile = null;
        if (SVNFileUtil.isWindows) {
            for (int i = 0; i < winExtensions.length; i++) {
                hookFile = new File(getHooksDir(reposRootDir), hookName + winExtensions[i]);
                SVNFileType type = SVNFileType.getType(hookFile);
                if (type == SVNFileType.FILE) {
                    return hookFile;
                }
            }
        } else {
            hookFile = new File(getHooksDir(reposRootDir), hookName);
            SVNFileType type = SVNFileType.getType(hookFile);
            if (type == SVNFileType.FILE) {
                return hookFile;
            } else if (type == SVNFileType.SYMLINK) {
                // should first resolve the symlink and then decide if it's
                // broken and
                // throw an exception
                File realFile = SVNFileUtil.resolveSymlinkToFile(hookFile);
                if (realFile == null) {
                    SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.REPOS_HOOK_FAILURE, "Failed to run ''{0}'' hook; broken symlink", hookFile);
                    SVNErrorManager.error(err, SVNLogType.FSFS);
                }
                return hookFile;
            }
        }
        return null;
    }

    private static File getHooksDir(File reposRootDir) {
        return new File(reposRootDir, SVN_REPOS_HOOKS_DIR);
    }
    
    private static String getCapabilitiesAsString(List<?> capabilities) {
        if (capabilities == null || capabilities.isEmpty()) {
            return "";
        }
        StringBuffer buffer = new StringBuffer();
        for (int i = 0; i < capabilities.size(); i++) {
            Object cap = capabilities.get(i); 
            buffer.append(cap);
            if (i < capabilities.size() - 1) {
                buffer.append(":");
            }
        }
        return buffer.toString();
    }
}
