/*
 * IzPack - Copyright 2001-2008 Julien Ponge, All Rights Reserved.
 * 
 * http://izpack.org/
 * http://izpack.codehaus.org/
 * 
 * Copyright 2004 Elmar Klaus Bartz
 * 
 * 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.izforge.izpack.util;

import java.io.*;
import java.util.Properties;
import java.util.StringTokenizer;

/**
 * <p>
 * Class with some IO related helper.
 * </p>
 */
public class IoHelper
{

    // This class uses the same values for family and flavor as
    // TargetFactory. But this class should not depends on TargetFactory,
    // because it is possible that TargetFactory is not bound. Therefore
    // the definition here again.

    // ------------------------------------------------------------------------
    // Constant Definitions
    // ------------------------------------------------------------------------

    /**
     * Placeholder during translatePath computing
     */
    private static final String MASKED_SLASH_PLACEHOLDER = "~&_&~";

    private static Properties envVars = null;

    /**
     * Default constructor
     */
    private IoHelper()
    {
    }

    /**
     * Copies the contents of inFile into outFile.
     *
     * @param inFile  path of file which should be copied
     * @param outFile path of file to create and copy the contents of inFile into
     */
    public static void copyFile(String inFile, String outFile) throws IOException
    {
        copyFile(new File(inFile), new File(outFile));
    }

    /**
     * Creates an in- and output stream for the given File objects and copies all the data from the
     * specified input to the specified output.
     *
     * @param inFile  File object for input
     * @param outFile File object for output
     * @throws IOException if an I/O error occurs
     */
    public static void copyFile(File inFile, File outFile) throws IOException
    {
        copyFile(inFile, outFile, null, null);
    }

    /**
     * Creates an in- and output stream for the given File objects and copies all the data from the
     * specified input to the specified output. If permissions is not null, a chmod will be done on
     * the output file.
     *
     * @param inFile      File object for input
     * @param outFile     File object for output
     * @param permissions permissions for the output file
     * @throws IOException if an I/O error occurs
     */
    public static void copyFile(File inFile, File outFile, String permissions) throws IOException
    {
        copyFile(inFile, outFile, permissions, null);
    }

    /**
     * Creates an in- and output stream for the given File objects and copies all the data from the
     * specified input to the specified output. If the VariableSubstitutor is not null, a substition
     * will be done during copy.
     *
     * @param inFile  File object for input
     * @param outFile File object for output
     * @param vss     substitutor which is used during copying
     * @throws IOException if an I/O error occurs
     */
    public static void copyFile(File inFile, File outFile, VariableSubstitutor vss)
            throws IOException
    {
        copyFile(inFile, outFile, null, vss);
    }

    /**
     * Creates an in- and output stream for the given File objects and copies all the data from the
     * specified input to the specified output. If the VariableSubstitutor is not null, a substition
     * will be done during copy. If permissions is not null, a chmod will be done on the output
     * file.
     *
     * @param inFile      File object for input
     * @param outFile     File object for output
     * @param permissions permissions for the output file
     * @param vs          substitutor which is used during copying
     * @throws IOException if an I/O error occurs
     */
    public static void copyFile(File inFile, File outFile, String permissions,
                                VariableSubstitutor vs) throws IOException
    {
        copyFile(inFile, outFile, permissions, vs, null);
    }

    /**
     * Creates an in- and output stream for the given File objects and copies all the data from the
     * specified input to the specified output. If the VariableSubstitutor is not null, a substition
     * will be done during copy. If permissions is not null, a chmod will be done on the output
     * file. If type is not null, that type is used as file type at substitution.
     *
     * @param inFile      File object for input
     * @param outFile     File object for output
     * @param permissions permissions for the output file
     * @param vs          substitutor which is used during copying
     * @param type        file type for the substitutor
     * @throws IOException if an I/O error occurs
     */
    public static void copyFile(File inFile, File outFile, String permissions,
                                VariableSubstitutor vs, String type) throws IOException
    {
        FileOutputStream out = new FileOutputStream(outFile);
        FileInputStream in = new FileInputStream(inFile);
        copyStream(in, out, vs, type);
        if (permissions != null && IoHelper.supported("chmod"))
        {
            chmod(outFile.getAbsolutePath(), permissions);
        }
    }

    /**
     * Copies an input stream to an output stream.  Both streams must already
     * be open. If the VariableSubstitutor is not null, a substition
     * will be done during copy.
     *
     * @param in          stream object for input
     * @param out         stream object for output
     * @param vs          substitutor which is used during copying
     * @param type        file type for the substitutor
     * @throws IOException if an I/O error occurs
     */
    public static void copyStream(InputStream in, OutputStream out, VariableSubstitutor vs, String type) throws IOException
    {
        if (vs == null)
        {
            byte[] buffer = new byte[5120];
            long bytesCopied = 0;
            int bytesInBuffer;
            while ((bytesInBuffer = in.read(buffer)) != -1)
            {
                out.write(buffer, 0, bytesInBuffer);
                bytesCopied += bytesInBuffer;
            }
            in.close();
            out.close();
        }
        else
        {
            BufferedInputStream bin = new BufferedInputStream(in, 5120);
            BufferedOutputStream bout = new BufferedOutputStream(out, 5120);
            vs.substitute(bin, bout, type, null);
            bin.close();
            bout.close();
        }
    }

    /**
     * Creates a temp file with delete on exit rule. The extension is extracted from the template if
     * possible, else the default extension is used. The contents of template will be copied into
     * the temporary file.
     *
     * @param template         file to copy from and define file extension
     * @param defaultExtension file extension if no is contained in template
     * @return newly created and filled temporary file
     * @throws IOException
     */
    public static File copyToTempFile(File template, String defaultExtension) throws IOException
    {
        return copyToTempFile(template, defaultExtension, null);
    }

    /**
     * Creates a temp file with delete on exit rule. The contents of the input stream will be copied into
     * the temporary file. If the variable substitutor is not null, variables will be replaced
     * during copying.
     *
     * @param is               input stream to copy from
     * @param ext              file extension
     * @param vss              substitutor which is used during copying
     * @return newly created and filled temporary file
     * @throws IOException
     */
    public static File copyToTempFile(InputStream is, String ext,
                                      VariableSubstitutor vss) throws IOException
    {
        File tmpFile = File.createTempFile("izpack_io", ext);
        tmpFile.deleteOnExit();
        IoHelper.copyStream(is, new FileOutputStream(tmpFile), vss, null);
        return tmpFile;
    }

    /**
     * Creates a temp file with delete on exit rule. The extension is extracted from the template if
     * possible, else the default extension is used. The contents of template will be copied into
     * the temporary file. If the variable substitutor is not null, variables will be replaced
     * during copying.
     *
     * @param template         file to copy from and define file extension
     * @param defaultExtension file extension if no is contained in template
     * @param vss              substitutor which is used during copying
     * @return newly created and filled temporary file
     * @throws IOException
     */
    public static File copyToTempFile(File template, String defaultExtension,
                                      VariableSubstitutor vss) throws IOException
    {
        String path = template.getCanonicalPath();
        int pos = path.lastIndexOf('.');
        String ext = path.substring(pos);
        if (ext == null)
        {
            ext = defaultExtension;
        }
        File tmpFile = File.createTempFile("izpack_io", ext);
        tmpFile.deleteOnExit();
        IoHelper.copyFile(template, tmpFile, vss);
        return tmpFile;
    }

    /**
     * Creates a temp file with delete on exit rule. The extension is extracted from the template if
     * possible, else the default extension is used. The contents of template will be copied into
     * the temporary file.
     *
     * @param template         file to copy from and define file extension
     * @param defaultExtension file extension if no is contained in template
     * @return newly created and filled temporary file
     * @throws IOException
     */
    public static File copyToTempFile(String template, String defaultExtension) throws IOException
    {
        return copyToTempFile(new File(template), defaultExtension);
    }

    /**
     * Changes the permissions of the given file to the given POSIX permissions.
     *
     * @param file        the file for which the permissions should be changed
     * @param permissions POSIX permissions to be set
     * @throws IOException if an I/O error occurs
     */
    public static void chmod(File file, String permissions) throws IOException
    {
        chmod(file.getAbsolutePath(), permissions);
    }

    /**
     * Changes the permissions of the given file to the given POSIX permissions. This method will be
     * raised an exception, if the OS is not UNIX.
     *
     * @param path        the absolute path of the file for which the permissions should be changed
     * @param permissions POSIX permissions to be set
     * @throws IOException if an I/O error occurs
     */
    public static void chmod(String path, String permissions) throws IOException
    {
        // Perform UNIX
        if (OsVersion.IS_UNIX)
        {
            String[] params = {"chmod", permissions, path};
            String[] output = new String[2];
            FileExecutor fe = new FileExecutor();
            fe.executeCommand(params, output);
        }
        else
        {
            throw new IOException("Sorry, chmod not supported yet on " + OsVersion.OS_NAME + ".");
        }
    }

    /**
     * Returns the free (disk) space for the given path. If it is not ascertainable -1 returns.
     *
     * @param path path for which the free space should be detected
     * @return the free space for the given path
     */
    public static long getFreeSpace(String path)
    {
        long retval = -1;
        if (OsVersion.IS_WINDOWS)
        {
            String command = "cmd.exe";
            if (System.getProperty("os.name").toLowerCase().indexOf("windows 9") > -1)
            {
                return (-1);
            }
            String[] params = {command, "/C", "\"dir /D /-C \"" + path + "\"\""};
            String[] output = new String[2];
            FileExecutor fe = new FileExecutor();
            fe.executeCommand(params, output);
            retval = extractLong(output[0], -3, 3, "%");
        }
        else if (OsVersion.IS_SUNOS)
        {
            String[] params = {"df", "-k", path};
            String[] output = new String[2];
            FileExecutor fe = new FileExecutor();
            fe.executeCommand(params, output);
            retval = extractLong(output[0], -3, 3, "%") * 1024;
        }
        else if (OsVersion.IS_HPUX)
        {
            String[] params = {"bdf", path};
            String[] output = new String[2];
            FileExecutor fe = new FileExecutor();
            fe.executeCommand(params, output);
            retval = extractLong(output[0], -3, 3, "%") * 1024;
        }
        else if (OsVersion.IS_UNIX)
        {
            String[] params = {"df", "-Pk", path};
            String[] output = new String[2];
            FileExecutor fe = new FileExecutor();
            fe.executeCommand(params, output);
            retval = extractLong(output[0], -3, 3, "%") * 1024;
        }
        return retval;
    }

    /**
     * Returns whether the given method will be supported with the given environment. Some methods
     * of this class are not supported on all operation systems.
     *
     * @param method name of the method
     * @return true if the method will be supported with the current enivronment else false
     * @throws RuntimeException if the given method name does not exist
     */
    public static boolean supported(String method)
    {
        if ("getFreeSpace".equals(method))
        {
            if (OsVersion.IS_UNIX)
            {
                return true;
            }
            if (OsVersion.IS_WINDOWS)
            { // getFreeSpace do not work on Windows 98.
                if (System.getProperty("os.name").toLowerCase().indexOf("windows 9") > -1)
                {
                    return (false);
                }
                return (true);
            }
        }
        else if ("chmod".equals(method))
        {
            if (OsVersion.IS_UNIX)
            {
                return true;
            }
        }
        else if ("copyFile".equals(method))
        {
            return true;
        }
        else if ("getPrimaryGroup".equals(method))
        {
            if (OsVersion.IS_UNIX)
            {
                return true;
            }
        }
        else if ("getenv".equals(method))
        {
            return true;
        }
        else
        {
            throw new RuntimeException("method name " + method + "not supported by this method");
        }
        return false;

    }

    /**
     * Returns the first existing parent directory in a path
     *
     * @param path path which should be scanned
     * @return the first existing parent directory in a path
     */
    public static File existingParent(File path)
    {
        File result = path;
        while (!result.exists())
        {
            if (result.getParent() == null)
            {
                return result;
            }
            result = result.getParentFile();
        }
        return result;
    }

    /**
     * Extracts a long value from a string in a special manner. The string will be broken into
     * tokens with a standard StringTokenizer. Arround the assumed place (with the given half range)
     * the tokens are scaned reverse for a token which represents a long. if useNotIdentifier is not
     * null, tokens which are contains this string will be ignored. The first founded long returns.
     *
     * @param in               the string which should be parsed
     * @param assumedPlace     token number which should contain the value
     * @param halfRange        half range for detection range
     * @param useNotIdentifier string which determines tokens which should be ignored
     * @return founded long
     */
    private static long extractLong(String in, int assumedPlace, int halfRange,
                                    String useNotIdentifier)
    {
        long retval = -1;
        StringTokenizer st = new StringTokenizer(in);
        int length = st.countTokens();
        int i;
        int currentRange = 0;
        String[] interestedEntries = new String[halfRange + halfRange];
        for (i = 0; i < length - halfRange + assumedPlace; ++i)
        {
            st.nextToken(); // Forget this entries.
        }

        for (i = 0; i < halfRange + halfRange; ++i)
        { // Put the interesting Strings into an intermediaer array.
            if (st.hasMoreTokens())
            {
                interestedEntries[i] = st.nextToken();
                currentRange++;
            }
        }

        for (i = currentRange - 1; i >= 0; --i)
        {
            if (useNotIdentifier != null && interestedEntries[i].indexOf(useNotIdentifier) > -1)
            {
                continue;
            }
            try
            {
                retval = Long.parseLong(interestedEntries[i]);
            }
            catch (NumberFormatException nfe)
            {
                continue;
            }
            break;
        }
        return retval;
    }

    /**
     * Returns the primary group of the current user. This feature will be supported only on Unix.
     * On other systems null returns.
     *
     * @return the primary group of the current user
     */
    public static String getPrimaryGroup()
    {
        if (supported("getPrimaryGroup"))
        {
            if (OsVersion.IS_SUNOS)
            { // Standard id of SOLARIS do not support -gn.
                String[] params = {"id"};
                String[] output = new String[2];
                FileExecutor fe = new FileExecutor();
                fe.executeCommand(params, output);
                // No we have "uid=%u(%s) gid=%u(%s)"
                if (output[0] != null)
                {
                    StringTokenizer st = new StringTokenizer(output[0], "()");
                    int length = st.countTokens();
                    if (length >= 4)
                    {
                        for (int i = 0; i < 3; ++i)
                        {
                            st.nextToken();
                        }
                        return (st.nextToken());
                    }
                }
                return (null);
            }
            else
            {
                String[] params = {"id", "-gn"};
                String[] output = new String[2];
                FileExecutor fe = new FileExecutor();
                fe.executeCommand(params, output);
                return output[0];
            }
        }
        else
        {
            return null;
        }
    }

    /**
     * Returns a string resulting from replacing all occurrences of what in this string with with.
     * In opposite to the String.replaceAll method this method do not use regular expression or
     * other methods which are only available in JRE 1.4 and later. This method was special made to
     * mask masked slashes to avert a conversion during path translation.
     *
     * @param destination string for which the replacing should be performed
     * @param what        what string should be replaced
     * @param with        with what string what should be replaced
     * @return a new String object if what was found in the given string, else the given string self
     */
    public static String replaceString(String destination, String what, String with)
    {
        if (destination.indexOf(what) >= 0)
        { // what found, with (placeholder) not included in destination ->
            // perform changing.
            StringBuffer buf = new StringBuffer();
            int last = 0;
            int current = destination.indexOf(what);
            int whatLength = what.length();
            while (current >= 0)
            { // Do not use Methods from JRE 1.4 and higher ...
                if (current > 0)
                {
                    buf.append(destination.substring(last, current));
                }
                buf.append(with);
                last = current + whatLength;
                current = destination.indexOf(what, last);
            }
            if (destination.length() > last)
            {
                buf.append(destination.substring(last));
            }
            return buf.toString();
        }
        return destination;
    }

    /**
     * Translates a relative path to a local system path.
     *
     * @param destination The path to translate.
     * @return The translated path.
     */
    public static String translatePath(String destination, VariableSubstitutor vs)
    {
        // Parse for variables
        destination = vs.substitute(destination, null);

        // Convert the file separator characters

        // destination = destination.replace('/', File.separatorChar);
        // Undo the conversion if the slashes was masked with
        // a backslash

        // Not all occurencies of slashes are path separators. To differ
        // between it we allow to mask a slash with a backslash infront.
        // Unfortunately we cannot use String.replaceAll because it
        // handles backslashes in the replacement string in a special way
        // and the method exist only beginning with JRE 1.4.
        // Therefore the little bit crude way following ...
        if (destination.indexOf("\\/") >= 0 && destination.indexOf(MASKED_SLASH_PLACEHOLDER) < 0)
        { // Masked slash found, placeholder not included in destination ->
            // perform masking.
            destination = replaceString(destination, "\\/", MASKED_SLASH_PLACEHOLDER);
            // Masked slashes changed to MASKED_SLASH_PLACEHOLDER.
            // Replace unmasked slashes.
            destination = destination.replace('/', File.separatorChar);
            // Replace the MASKED_SLASH_PLACEHOLDER to slashes; masking
            // backslashes will
            // be removed.
            destination = replaceString(destination, MASKED_SLASH_PLACEHOLDER, "/");
        }
        else
        {
            destination = destination.replace('/', File.separatorChar);
        }
        return destination;
    }

    /**
     * Returns the value of the environment variable given by key. This method is a work around for
     * VM versions which do not support getenv in an other way. At the first call all environment
     * variables will be loaded via an exec. On Windows keys are not case sensitive.
     *
     * @param key variable name for which the value should be resolved
     * @return the value of the environment variable given by key
     */
    public static String getenv(String key)
    {
        if (envVars == null)
        {
            loadEnv();
        }
        if (envVars == null)
        {
            return (null);
        }
        if (OsVersion.IS_WINDOWS)
        {
            key = key.toUpperCase();
        }
        return (String) (envVars.get(key));
    }

    /**
     * Loads all environment variables via an exec.
     */
    private static void loadEnv()
    {
        String[] output = new String[2];
        String[] params;
        if (OsVersion.IS_WINDOWS)
        {
            String command = "cmd.exe";
            if (System.getProperty("os.name").toLowerCase().indexOf("windows 9") > -1)
            {
                command = "command.com";
            }
            String[] paramst = {command, "/C", "set"};
            params = paramst;
        }
        else
        {
            String[] paramst = {"env"};
            params = paramst;
        }
        FileExecutor fe = new FileExecutor();
        fe.executeCommand(params, output);
        if (output[0].length() <= 0)
        {
            return;
        }
        String lineSep = System.getProperty("line.separator");
        StringTokenizer st = new StringTokenizer(output[0], lineSep);
        envVars = new Properties();
        String var = null;
        while (st.hasMoreTokens())
        {
            String line = st.nextToken();
            if (line.indexOf('=') == -1)
            { // May be a env var with a new line in it.
                if (var == null)
                {
                    var = lineSep + line;
                }
                else
                {
                    var += lineSep + line;
                }
            }
            else
            { // New var, perform the previous one.
                setEnvVar(var);
                var = line;
            }
        }
        setEnvVar(var);
    }

    /**
     * Extracts key and value from the given string var. The key should be separated from the value
     * by a sign. On Windows all chars of the key are translated to upper case.
     *
     * @param var
     */
    private static void setEnvVar(String var)
    {
        if (var == null)
        {
            return;
        }
        int index = var.indexOf('=');
        if (index < 0)
        {
            return;
        }
        String key = var.substring(0, index);
        // On windows change all key chars to upper.
        if (OsVersion.IS_WINDOWS)
        {
            key = key.toUpperCase();
        }
        envVars.setProperty(key, var.substring(index + 1));

    }
}
