/*
 * 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
 *
 *   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 org.netbeans.modules.nativeexecution;

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InterruptedIOException;
import java.io.OutputStreamWriter;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.logging.Level;
import org.netbeans.modules.nativeexecution.api.ExecutionEnvironmentFactory;
import org.netbeans.modules.nativeexecution.api.HostInfo.OSFamily;
import org.netbeans.modules.nativeexecution.api.util.CommonTasksSupport;
import org.netbeans.modules.nativeexecution.api.util.ExternalTerminal;
import org.netbeans.modules.nativeexecution.api.util.ProcessUtils;
import org.netbeans.modules.nativeexecution.api.util.Signal;
import org.netbeans.modules.nativeexecution.support.EnvWriter;
import org.netbeans.modules.nativeexecution.support.Logger;
import org.netbeans.modules.nativeexecution.api.util.MacroMap;
import org.netbeans.modules.nativeexecution.api.util.PathUtils;
import org.netbeans.modules.nativeexecution.api.util.WindowsSupport;
import org.netbeans.modules.nativeexecution.support.InstalledFileLocatorProvider;
import org.openide.modules.InstalledFileLocator;
import org.openide.util.NbBundle;
import org.openide.util.Utilities;

/**
 * A process to be run in *external* terminal
 * @author akrasny
 */
public final class TerminalLocalNativeProcess extends AbstractNativeProcess {

    private static final java.util.logging.Logger log = Logger.getInstance();
    private static final File dorunScript;
    private ExternalTerminal terminal;
    private File resultFile;
    private final OSFamily osFamily;

    static {
        InstalledFileLocator fl = InstalledFileLocatorProvider.getDefault();
        File dorunScriptFile = fl.locate("bin/nativeexecution/dorun.sh", "org.netbeans.modules.dlight.nativeexecution", false); // NOI18N

        if (dorunScriptFile == null) {
            log.severe("Unable to locate bin/nativeexecution/dorun.sh file!"); // NOI18N
        } else if (!Utilities.isWindows()) {
            CommonTasksSupport.chmod(ExecutionEnvironmentFactory.getLocal(),
                    dorunScriptFile.getAbsolutePath(), 0755, null);
        }

        dorunScript = dorunScriptFile;
    }

    public TerminalLocalNativeProcess(
            final NativeProcessInfo info, final ExternalTerminal terminal) {
        super(info);
        this.terminal = terminal;
        setInputStream(new ByteArrayInputStream(
                (loc("TerminalLocalNativeProcess.ProcessStarted.text") + '\n').getBytes())); // NOI18N

        osFamily = hostInfo == null ? OSFamily.UNKNOWN : hostInfo.getOSFamily();
    }

    @Override
    protected void create() throws Throwable {
        File pidFileFile = null;
        File shFileFile = null;

        try {
            if (dorunScript == null) {
                throw new IOException(loc("TerminalLocalNativeProcess.dorunNotFound.text")); // NOI18N
            }

            if (osFamily == OSFamily.WINDOWS && hostInfo.getShell() == null) {
                throw new IOException(loc("NativeProcess.shellNotFound.text")); // NOI18N
            }

            final String commandLine = info.getCommandLineForShell();
            final String wDir = info.getWorkingDirectory(true);

            final File workingDirectory = (wDir == null) ? new File(".") : new File(wDir); // NOI18N

            pidFileFile = File.createTempFile("dlight", "termexec", hostInfo.getTempDirFile()).getAbsoluteFile(); // NOI18N
            shFileFile = new File(pidFileFile.getPath() + ".sh"); // NOI18N
            resultFile = new File(shFileFile.getPath() + ".res"); // NOI18N

            resultFile.deleteOnExit();

            String pidFile = (osFamily == OSFamily.WINDOWS) ? WindowsSupport.getInstance().convertToShellPath(pidFileFile.getPath()) : pidFileFile.getPath();
            String shFile = pidFile + ".sh"; // NOI18N

            FileOutputStream shfos = new FileOutputStream(shFileFile);

            Charset scriptCharset;

            if (info.getCharset() != null) {
                scriptCharset = info.getCharset();
            } else {
                if (osFamily == OSFamily.WINDOWS) {
                    scriptCharset = WindowsSupport.getInstance().getShellCharset();
                } else {
                    scriptCharset = Charset.defaultCharset();
                }
            }

            final ExternalTerminalAccessor terminalInfo =
                    ExternalTerminalAccessor.getDefault();

            if (terminalInfo.getTitle(terminal) == null) {
                String title = getExecutableName();

                if (title == null) {
                    title = NbBundle.getMessage(TerminalLocalNativeProcess.class, "TerminalLocalNativeProcess.terminalTitle.text"); // NOI18N
                }

                terminal = terminal.setTitle(title);
            }

            List<String> terminalArgs = new ArrayList<>();

            String shellScriptPath = dorunScript.getAbsolutePath();
            if (osFamily == OSFamily.WINDOWS) {
                shellScriptPath = WindowsSupport.getInstance().convertToShellPath(shellScriptPath);
            }
            
            terminal = terminal.setWorkdir(workingDirectory.toString());

            terminalArgs.addAll(Arrays.asList(
                    shellScriptPath,
                    "-p", terminalInfo.getPrompt(terminal), // NOI18N
                    "-x", shFile)); // NOI18N

            List<String> command = terminalInfo.wrapCommand(
                    info.getExecutionEnvironment(),
                    terminal,
                    terminalArgs);

            ProcessBuilder pb = new ProcessBuilder(command);

            if (!workingDirectory.exists()) {
                throw new FileNotFoundException(loc("NativeProcess.noSuchDirectoryError.text", workingDirectory.getAbsolutePath())); // NOI18N
            }
            
            pb.directory(workingDirectory);
            pb.redirectErrorStream(true);

            LOG.log(Level.FINEST, "External terminal command: {0}", command); // NOI18N

            final MacroMap env = info.getEnvironment().clone();

            // setup DISPLAY variable for MacOS...
            if (osFamily == OSFamily.MACOSX) {
                ProcessBuilder pb1 = new ProcessBuilder(hostInfo.getShell(), "-c", "/bin/echo $DISPLAY"); // NOI18N
                // this is an external terminal process - it's ok to use ProcessUtils.execute here
                ProcessUtils.ExitStatus res = ProcessUtils.execute(pb1);
                String display = null;

                if (res.isOK()) {
                    display = res.getOutputString();
                } 

                if (display == null || "".equals(display)) { // NOI18N
                    display = ":0.0"; // NOI18N
                }

                pb.environment().put("DISPLAY", display); // NOI18N
            }

            OutputStreamWriter shWriter = new OutputStreamWriter(shfos, scriptCharset);
            shWriter.write("echo $$ > \"" + pidFile + "\" || exit $?\n"); // NOI18N

            if (!env.isEmpty()) {
                // TODO: FIXME (?)
                // Do PATH normalization on Windows....
                // Problem here is that this is done for PATH env. variable only!

                if (osFamily == OSFamily.WINDOWS) {
                    // Make sure that path in upper case
                    // [for external terminal only]
                    String path = env.get("PATH"); // NOI18N
                    env.remove("PATH"); // NOI18N
                    env.put("PATH", WindowsSupport.getInstance().convertToAllShellPaths(path)); // NOI18N
                }

                EnvWriter ew = new EnvWriter(shWriter);
                ew.write(env);

                if (LOG.isLoggable(Level.FINEST)) {
                    env.dump(System.err);
                }
            }

            shWriter.write("exec " + commandLine + "\n"); // NOI18N
            shWriter.close();

            Process terminalProcess = ProcessUtils.ignoreProcessOutputAndError(pb.start());

            creation_ts = System.nanoTime();

            waitPID(terminalProcess, pidFileFile);

            if (isInterrupted()) {
                throw new IOException(loc("TerminalLocalNativeProcess.terminalRunCancelled.text")); // NOI18N
            }
        } catch (Throwable ex) {
            resultFile = null;
            throw ex;
        } finally {
            if (pidFileFile != null) {
                pidFileFile.delete();
            }
            if (shFileFile != null) {
                shFileFile.delete();
            }
        }
    }

    private int getPIDNoException() {
        int pid = -1;

        try {
            pid = getPID();
        } catch (Exception ex) {
        }

        return pid;
    }

    @Override
    public int waitResult() throws InterruptedException {
        int pid = getPIDNoException();

        if (pid < 0) {
            // Process was not even started
            return -1;
        }

        if (osFamily == OSFamily.LINUX || osFamily == OSFamily.SUNOS) {
            File f = new File("/proc/" + pid); // NOI18N

            while (f.exists()) {
                Thread.sleep(300);
            }
        } else {
            int rc = 0;
            while (rc == 0) {
                try {
                    rc = CommonTasksSupport.sendSignal(info.getExecutionEnvironment(), pid, Signal.NULL, null).get();
                } catch (ExecutionException ex) {
                    log.log(Level.FINEST, "", ex); // NOI18N
                    rc = -1;
                }

                Thread.sleep(300);
            }
        }

        if (resultFile == null) {
            return -1;
        }

        int exitCode = -1;

        BufferedReader statusReader = null;

        try {
            int attempts = 10;
            while (attempts-- > 0) {
                if (resultFile.exists() && resultFile.length() > 0) {
                    statusReader = new BufferedReader(new FileReader(resultFile));
                    String exitCodeString = statusReader.readLine();
                    if (exitCodeString != null) {
                        exitCode = Integer.parseInt(exitCodeString.trim());
                    }
                    break;
                }

                Thread.sleep(500);
            }
        } catch (InterruptedIOException ex) {
            throw new InterruptedException();
        } catch (IOException ex) {
        } catch (NumberFormatException ex) {
        } finally {
            if (statusReader != null) {
                try {
                    statusReader.close();
                } catch (IOException ex) {
                }
            }
        }

        return exitCode;
    }

    private void waitPID(Process termProcess, File pidFile) throws IOException {
        while (!isInterrupted()) {
            /**
             * The following sleep appears after an attempt to support konsole
             * KDE4. This was done to give some time for external process to
             * write information about process' PID to the pidfile and not to
             * get to termProcess.exitValue() too eraly...
             * Currently there are no means on KDE4 to start konsole in
             * 'not-background' mode.
             * An attempt to use --nofork fails when start konsole from jvm
             * (see http://www.nabble.com/Can%27t-use---nofork-for-KUniqueApplications-from-another-kde-process-td21047022.html)
             * So termProcess exits immediately...
             *
             * Also this sleep is justifable because this doesn't make any sense
             * to check for a pid file too often.
             *
             */
            try {
                Thread.sleep(500);
            } catch (InterruptedException ex) {
                Thread.currentThread().interrupt();
                break;
            }

            if (pidFile.exists() && pidFile.length() > 0) {
                InputStream pidIS = null;
                try {
                    pidIS = new FileInputStream(pidFile);
                    readPID(pidIS);
                } finally {
                    if (pidIS != null) {
                        pidIS.close();
                    }
                }
                break;
            }

            try {
                int result = termProcess.exitValue();

                if (result != 0) {
                    String err = ProcessUtils.readProcessErrorLine(termProcess);
                    if (!err.isEmpty()) { // it is redirected! but just in case let's read both
                        err = ProcessUtils.readProcessOutputLine(termProcess);
                    }                    
                    log.info(loc("TerminalLocalNativeProcess.terminalFailed.text")); // NOI18N
                    log.info(err);
                    throw new IOException(err);
                }

                // No exception - means process is finished..
                break;
            } catch (IllegalThreadStateException ex) {
                // expected ... means that terminal process exists
            }
        }
    }

    private static String loc(String key, String... params) {
        return NbBundle.getMessage(TerminalLocalNativeProcess.class, key, params);
    }

    private String getExecutableName() {
        String exec = info.getExecutable();

        if (exec != null) {
            if (new File(exec).exists()) {
                return new File(exec).getName();
            }

            if (osFamily == OSFamily.WINDOWS && new File(exec + ".exe").exists()) { // NOI18N
                return new File(exec).getName();
            }
        }

        String[] params = Utilities.parseParameters(info.getCommandLineForShell());
        if (params != null && params.length > 0) {
            exec = params[0];
            if (exec != null && new File(exec).exists()) {
                return new File(exec).getName();
            }
        }

        return null;
    }
}
