/*
 * This file is part of the LibreOffice project.
 *
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 *
 * This file incorporates work covered by the following license notice:
 *
 *   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 .
 */

package org.openoffice.test;

import com.sun.star.bridge.UnoUrlResolver;
import com.sun.star.bridge.XUnoUrlResolver;
import com.sun.star.comp.helper.Bootstrap;
import com.sun.star.connection.NoConnectException;
import com.sun.star.frame.XDesktop;
import com.sun.star.lang.DisposedException;
import com.sun.star.lang.XMultiComponentFactory;
import com.sun.star.uno.UnoRuntime;
import com.sun.star.uno.XComponentContext;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintStream;
import java.util.Map;
import java.util.UUID;
import static org.junit.Assert.*;

/** Start up and shut down an OOo instance.

    Details about the OOo instance are tunneled in via
    org.openoffice.test.arg.... system properties.
*/


public final class OfficeConnection {
    /** Start up an OOo instance.
    */
    public void setUp() throws Exception {
        String sofficeArg = Argument.get("soffice");
        if (sofficeArg.startsWith("path:")) {
            description = "pipe,name=oootest" + UUID.randomUUID();
            ProcessBuilder pb = new ProcessBuilder(
                sofficeArg.substring("path:".length()), "--quickstart=no",
                "--norestore", "--nologo", "--headless",
                "--accept=" + description + ";urp",
                "-env:UserInstallation=" + Argument.get("user"),
                "-env:UNO_JAVA_JFW_ENV_JREHOME=true");
            String workdirArg = Argument.get("workdir");
            if (workdirArg != null) {
                pb.directory(new File(workdirArg));
            }
            String envArg = Argument.get("env");
            if (envArg != null) {
                Map<String, String> env = pb.environment();
                int i = envArg.indexOf('=');
                if (i == -1) {
                    env.remove(envArg);
                } else {
                    env.put(envArg.substring(0, i), envArg.substring(i + 1));
                }
            }
            process = pb.start();
            outForward = new Forward(process.getInputStream(), System.out);
            outForward.start();
            errForward = new Forward(process.getErrorStream(), System.err);
            errForward.start();
        } else if (sofficeArg.startsWith("connect:")) {
            description = sofficeArg.substring("connect:".length());
        } else {
            fail(
                "\"soffice\" argument \"" + sofficeArg +
                " starts with neither \"path:\" nor \"connect:\"");
        }
        XUnoUrlResolver resolver = UnoUrlResolver.create(
            Bootstrap.createInitialComponentContext(null));
        for (;;) {
            try {
                context = UnoRuntime.queryInterface(
                    XComponentContext.class,
                    resolver.resolve(
                        "uno:" + description +
                        ";urp;StarOffice.ComponentContext"));
                break;
            } catch (NoConnectException e) {}
            if (process != null) {
                assertNull(waitForProcess(process, 1000)); // 1 sec
            }
        }
    }

    /** Shut down the OOo instance.
    */
    public void tearDown()
        throws InterruptedException, com.sun.star.uno.Exception
    {
        boolean cleanTermination = false;
        int code = 0;
        try {
            if (process != null) {
                if (context != null) {
                    XDesktop desktop = null;
                    try {
                        XMultiComponentFactory factory =
                            context.getServiceManager();
                        assertNotNull(factory);
                        desktop = UnoRuntime.queryInterface(XDesktop.class,
                            factory.createInstanceWithContext(
                                "com.sun.star.frame.Desktop", context));
                    } catch (DisposedException e) {
                        // it can happen that the Java bridge was disposed
                        // already, we want to ensure soffice.bin is killed
                        process.destroy();
                    }
                    context = null;
                    if (desktop != null) {
                        try {
                            boolean desktopTerminated = desktop.terminate();
                            if (!desktopTerminated) {
                                // in case terminate() fails we would wait
                                // forever for the process to die, so kill it
                                process.destroy();
                            }
                            assertTrue(desktopTerminated);
                        } catch (DisposedException e) {}
                        // it appears that DisposedExceptions can already happen
                        // while receiving the response of the terminate call
                    }
                    desktop = null;
                } else {
                    process.destroy();
                }
            }
            if (process != null) {
                code = process.waitFor();
            }
            boolean outTerminated = outForward == null
                || outForward.terminated();
            boolean errTerminated = errForward == null
                || errForward.terminated();
            assertEquals(0, code);
            cleanTermination = true;
            assertTrue(outTerminated);
            assertTrue(errTerminated);
        } finally {
            if (!cleanTermination) {
                try {
                    String sofficeArg = Argument.get("soffice");
                    String workdir = Argument.get("workdir");
                    String postprocesscommand = Argument.get(
                        "postprocesscommand");
                    if (sofficeArg.startsWith("path:") && workdir != null
                        && postprocesscommand != null)
                    {
                        ProcessBuilder pb = new ProcessBuilder(
                            postprocesscommand,
                            sofficeArg.substring("path:".length()) + ".bin",
                            workdir, String.valueOf(code));
                        Process postprocess = pb.start();
                        Forward ppoutForward = new Forward(
                            postprocess.getInputStream(), System.out);
                        ppoutForward.start();
                        Forward pperrForward = new Forward(
                            postprocess.getErrorStream(), System.err);
                        pperrForward.start();
                        code = postprocess.waitFor();
                        if (code != 0) {
                            throw new PostprocessFailedException(code);
                        }
                    }
                }
                catch (IOException e) {
                    throw new PostprocessFailedException(e);
                }
            }
        }
    }

    /** Obtain the component context of the running OOo instance.
    */
    public XComponentContext getComponentContext() {
        return context;
    }

    //TODO: get rid of this hack for legacy qa/unoapi tests
    public String getDescription() {
        return description;
    }

    private static Integer waitForProcess(Process process, final int millis)
        throws InterruptedException
    {
        final Thread t1 = Thread.currentThread();
        Thread t2 = new Thread("waitForProcess") {
                @Override
                public void run() {
                    util.utils.pause(millis);
                    t1.interrupt();
                }
            };
        boolean old = Thread.interrupted();
            // clear interrupted status, get old status
        t2.start();
        int n = 0;
        boolean done = false;
        try {
            n = process.waitFor();
            done = true;
        } catch (InterruptedException e) {}
        t2.interrupt();
        try {
            t2.join();
        } catch (InterruptedException e) {
            t2.join();
        }
        Thread.interrupted(); // clear interrupted status
        if (old) {
            t1.interrupt(); // reset old status
        }
        return done ? Integer.valueOf(n) : null;
    }

    private static final class Forward extends Thread {
        public Forward(InputStream in, PrintStream out) {
            super("process output forwarder");
            this.in = in;
            this.out = out;
        }

        @Override
        public void run() {
            for (;;) {
                byte[] buf = new byte[1024];
                int n;
                try {
                    n = in.read(buf);
                } catch (IOException e) {
                    throw new RuntimeException("wrapping", e);
                }
                if (n == -1) {
                    break;
                }
                out.write(buf, 0, n);
            }
            done = true;
        }

        public boolean terminated() throws InterruptedException {
            join();
            return done;
        }

        private final InputStream in;
        private final PrintStream out;
        private boolean done = false;
    }

    private static final class PostprocessFailedException
        extends RuntimeException
    {
        PostprocessFailedException(int exitCode) {
            super("postprocessing failed with exit code " + exitCode);
        }

        PostprocessFailedException(IOException cause) {
            super("postprocessing failed with IOException " + cause, cause);
        }
    }

    private String description;
    private Process process = null;
    private Forward outForward = null;
    private Forward errForward = null;
    private XComponentContext context = null;
}
// vim:set et sw=4 sts=4:
