/*
 * Copyright (C) 2012 The Android Open Source Project
 *
 * 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.android.utils;

import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.google.common.io.Closeables;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;

public class GrabProcessOutput {

    public enum Wait {
        /**
         * Doesn't wait for the exec to complete.
         * This still monitors the output but does not wait for the process to finish.
         * In this mode the process return code is unknown and always 0.
         */
        ASYNC,
        /**
         * This waits for the process to finish.
         * In this mode, {@link GrabProcessOutput#grabProcessOutput} returns the
         * error code from the process.
         * <p>
         * In some rare cases and depending on the OS, the process might not have
         * finished dumping data into stdout/stderr.
         * </p>
         * Use this when you don't particularly care for the output but instead
         * care for the return code of the executed process.
         */
        WAIT_FOR_PROCESS,
        /**
         * <p>
         * This waits for the process to finish <em>and</em> for the stdout/stderr
         * threads to complete.
         * In this mode, {@link GrabProcessOutput#grabProcessOutput} returns the
         * error code from the process.
         * </p>
         * Use this one when capturing all the output from the process is important.
         */
        WAIT_FOR_READERS,
    }

    public interface IProcessOutput {
        /**
         * Processes an stdout message line.
         * @param line The stdout message line. Null when the reader reached the end of stdout.
         */
        void out(@Nullable String line);
        /**
         * Processes an stderr message line.
         * @param line The stderr message line. Null when the reader reached the end of stderr.
         */
        void err(@Nullable String line);
    }

    /**
     * Get the stderr/stdout outputs of a process and return when the process is done.
     * Both <b>must</b> be read or the process will block on windows.
     *
     * @param process The process to get the output from.
     * @param output Optional object to capture stdout/stderr.
     *      Note that on Windows capturing the output is not optional. If output is null
     *      the stdout/stderr will be captured and discarded.
     * @param waitMode Whether to wait for the process and/or the readers to finish.
     * @return the process return code.
     * @throws InterruptedException if {@link Process#waitFor()} was interrupted.
     */
    public static int grabProcessOutput(
            @NonNull final Process process,
            Wait waitMode,
            @Nullable final IProcessOutput output) throws InterruptedException {
        // read the lines as they come. if null is returned, it's
        // because the process finished
        Thread threadErr = new Thread("stderr") {
            @Override
            public void run() {
                // create a buffer to read the stderr output
                InputStream is = process.getErrorStream();
                InputStreamReader isr = new InputStreamReader(is);
                BufferedReader errReader = new BufferedReader(isr);

                try {
                    while (true) {
                        String line = errReader.readLine();
                        if (output != null) {
                            output.err(line);
                        }
                        if (line == null) {
                            break;
                        }
                    }
                } catch (IOException e) {
                    // do nothing.
                } finally {
                    try {
                        Closeables.close(is, true /* swallowIOException */);
                    } catch (IOException e) {
                        // cannot happen
                    }
                    try {
                        Closeables.close(isr, true /* swallowIOException */);
                    } catch (IOException e) {
                        // cannot happen
                    }
                    try {
                        Closeables.close(errReader, true /* swallowIOException */);
                    } catch (IOException e) {
                        // cannot happen
                    }
                }
            }
        };

        Thread threadOut = new Thread("stdout") {
            @Override
            public void run() {
                InputStream is = process.getInputStream();
                InputStreamReader isr = new InputStreamReader(is);
                BufferedReader outReader = new BufferedReader(isr);

                try {
                    while (true) {
                        String line = outReader.readLine();
                        if (output != null) {
                            output.out(line);
                        }
                        if (line == null) {
                            break;
                        }
                    }
                } catch (IOException e) {
                    // do nothing.
                } finally {
                    try {
                        Closeables.close(is, true /* swallowIOException */);
                    } catch (IOException e) {
                        // cannot happen
                    }
                    try {
                        Closeables.close(isr, true /* swallowIOException */);
                    } catch (IOException e) {
                        // cannot happen
                    }
                    try {
                        Closeables.close(outReader, true /* swallowIOException */);
                    } catch (IOException e) {
                        // cannot happen
                    }
                }
            }
        };

        threadErr.start();
        threadOut.start();

        if (waitMode == Wait.ASYNC) {
            return 0;
        }

        // it looks like on windows process#waitFor() can return
        // before the thread have filled the arrays, so we wait for both threads and the
        // process itself.
        if (waitMode == Wait.WAIT_FOR_READERS) {
            try {
                threadErr.join();
            } catch (InterruptedException e) {
            }
            try {
                threadOut.join();
            } catch (InterruptedException e) {
            }
        }

        // get the return code from the process
        return process.waitFor();
    }
}
