/*
 * Copyright 2016 ThoughtWorks, Inc.
 *
 * 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.thoughtworks.go.server.launcher;

import com.apple.eawt.Application;
import com.apple.eawt.ApplicationEvent;

import javax.swing.*;
import java.awt.*;
import java.io.File;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.logging.Logger;

import static java.util.Arrays.asList;

public class GoMacLauncher extends JFrame {
    private static final Logger LOG = Logger.getLogger(GoMacLauncher.class.getCanonicalName());
    protected MacAboutBox aboutBox;
    private Application application = Application.getApplication();
    private static final int MAX_SLEEP_SECONDS = 2400;
    private static final int SLEEP_MILLIS = 1000;
    private static final String APPLICATION_SUPPORT_PATHNAME =
            MessageFormat.format("{0}/Library/Application Support/{1}/", System.getProperty("user.home"), System.getProperty("go.application.name", "Go Server"));
    private static final String GO_CONFIG_DIRECTORY_PATH = APPLICATION_SUPPORT_PATHNAME + "config/";
    private static final String CRUISE_SERVER_URL = "http://localhost:8153/go/";

    public static void main(String[] args) {
        new File(GO_CONFIG_DIRECTORY_PATH).mkdirs();
        new GoMacLauncher().spawnProcessAndWait();
    }

    public GoMacLauncher() throws HeadlessException {
        application.addApplicationListener(new MyApplicationAdapter());

        application.setEnabledAboutMenu(true);
        application.setEnabledPreferencesMenu(false);

        setVisible(false);
        setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
        setResizable(false);
        setSize(getPreferredSize());
    }

    private int spawnProcessAndWait() {
        int exitValue = 0;

        String sep = System.getProperty("file.separator");
        String defaultJava = System.getProperty("java.home") + sep + "bin" + sep + "java";
        String java = System.getProperty("go.java.to.use", defaultJava);

        String startMem = System.getProperty("cruise.server.mem", "512");
        String maxMem = System.getProperty("cruise.server.maxmem", "1024");
        String maxPerm = System.getProperty("cruise.server.maxpermgen", "256");
        String lang = System.getProperty("cruise.server.lang", "en");
        String country = System.getProperty("cruise.server.country", "US");
        boolean dbDebugMode = System.getProperty("cruise.server.db_debug_mode") != null;

        final List<String> arguments = new ArrayList<>();
        arguments.add(java);
        arguments.add("-Xms" + startMem + "m");
        arguments.add("-Xmx" + maxMem + "m");
        arguments.add("-XX:MaxMetaspaceSize=" + maxPerm + "m");
        if (dbDebugMode) {
            arguments.add("-DDB_DEBUG_MODE=true");
        }
        arguments.add("-Dapple.awt.UIElement=true");
        arguments.add("-Duser.language=" + lang);
        arguments.add("-Duser.country=" + country);
        arguments.add("-Dcruise.config.file=" + GO_CONFIG_DIRECTORY_PATH + "cruise-config.xml");
        arguments.add("-Dcruise.config.dir=" + GO_CONFIG_DIRECTORY_PATH);
        arguments.add("-Dcruise.server.port=" + System.getProperty("cruise.server.port", "8153"));
        arguments.add("-Dcruise.server.ssl.port=" + System.getProperty("cruise.server.ssl.port", "8154"));
        addOtherArguments(arguments);
        arguments.add("-jar");
        arguments.add(new File(System.getProperty("go.server.mac.go.jar.dir", "."), "go.jar").getAbsolutePath());
        LOG.info("Running server as: " + arguments);

        String[] args = arguments.toArray(new String[arguments.size()]);

        try {
            setUpApplicationSupport();

            ProcessBuilder processBuilder = new ProcessBuilder(args);
            processBuilder.directory(new File(APPLICATION_SUPPORT_PATHNAME));
            processBuilder.redirectErrorStream(true);
            processBuilder.redirectOutput(ProcessBuilder.Redirect.appendTo(new File(APPLICATION_SUPPORT_PATHNAME, "go-server.log")));
            final Process server = processBuilder.start();
            server.getOutputStream().close();

            Thread shutdownHook = new Thread(new Shutdown(server));
            Runtime.getRuntime().addShutdownHook(shutdownHook);


            Thread progressThread = new Thread(new Runnable() {
                public void run() {
                    try {
                        displayLaunchingProgress(server);
                    } catch (InterruptedException e) {
                        // Don't care
                    } catch (IOException e) {
                        LOG.severe("Exception while executing command: " + arguments + " - " + e.toString());
                    }
                }
            });

            progressThread.start();

            try {
                exitValue = server.waitFor();
                if (exitValue != 0) {
                    LOG.severe(String.format(
                            "Server was terminated with exit code %d.  Please see %sgo-server.log for more details.",
                            exitValue, APPLICATION_SUPPORT_PATHNAME));
                    System.exit(1);
                }
            } catch (InterruptedException ie) {
                LOG.severe("Server was interrupted. Terminating. " + ie.toString());
                server.destroy();
            }

            Runtime.getRuntime().removeShutdownHook(shutdownHook);
        } catch (IOException e) {
            LOG.severe("Exception while executing command: " + asList(args) + " - " + e.toString());
        }
        return exitValue;
    }

    /* Any property prefixed by GO_, is passed along as a property, after stripping the GO_ from it. */
    private void addOtherArguments(List<String> currentArguments) {
        Properties properties = System.getProperties();
        for (Map.Entry<Object, Object> entry : properties.entrySet()) {
            String key = String.valueOf(entry.getKey());
            if (key.startsWith("GO_")) {
                String value = String.valueOf(entry.getValue());
                currentArguments.add("-D" + key.substring(3) + "=" + value);
            }
        }
    }

    private void displayLaunchingProgress(Process server)
            throws InterruptedException, IOException {
        //Create and set up the window.
        final JFrame frame = new JFrame("Go Server");
        frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);

        //Create and set up the content pane.
        final LaunchProgress progressPane = new LaunchProgress();
        progressPane.setOpaque(true); //content panes must be opaque
        frame.setContentPane(progressPane);

        //Display the window.
        frame.pack();
        frame.setSize(frame.getPreferredSize());
        frame.setVisible(true);

        // Wait for the server to start
        int slept = 0;
        progressPane.progressBar.setIndeterminate(true);
        boolean startedOK = false;

        // When this URL gets a 200 code back we know we're good to go
        URL url = new URL(CRUISE_SERVER_URL);
        while ((slept * SLEEP_MILLIS / 1000) <= MAX_SLEEP_SECONDS) {
            Thread.sleep(SLEEP_MILLIS);
            progressPane.progressBar.setValue(slept);
            slept++;
            try {
                HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();
                urlConnection.connect();
                if (urlConnection.getResponseCode() == 200) {
                    LOG.info("Server up, let's go");
                    startedOK = true;
                    break;
                }
            } catch (Exception e) {
                // Don't care
                LOG.info("Server not up yet, sleeping...");
            }
        }

        if (!startedOK) {
            String errormsg = "Server not up within " + MAX_SLEEP_SECONDS + " seconds.";
            LOG.severe(errormsg);
            server.destroy();
            progressPane.progressBar.setVisible(false);
            progressPane.label.setText(errormsg + " Please see /var/log/system.log for more info.");
            frame.pack();
            frame.setSize(frame.getPreferredSize());
            Thread.sleep(15000);
            System.exit(1);
        }

        frame.setVisible(false);
        frame.dispose();

        // Now launch a browser window pointing at it
        Runtime.getRuntime().exec("open " + CRUISE_SERVER_URL);
    }

    private void setUpApplicationSupport() throws IOException {
        File applicationSupport = new File(APPLICATION_SUPPORT_PATHNAME);
        applicationSupport.mkdirs();

        if (!applicationSupport.isDirectory()) {
            throw new IOException(
                    "Could not create folder " + APPLICATION_SUPPORT_PATHNAME +
                            ". Please check the permission settings for folder " + applicationSupport.getParentFile().getAbsolutePath());
        }

    }

    private class LaunchProgress extends JPanel {
        public JProgressBar progressBar;
        public JLabel label;

        private LaunchProgress() {
            super(new BorderLayout());

            progressBar = new JProgressBar();
            progressBar.setIndeterminate(false);
            progressBar.setStringPainted(true);

            JPanel panel = new JPanel();
            label = new JLabel("Starting up");
            label.setHorizontalAlignment(JLabel.CENTER);
            panel.add(label);
            panel.add(progressBar);

            add(panel, BorderLayout.PAGE_START);
            setBorder(BorderFactory.createEmptyBorder(20, 20, 20, 20));
        }
    }

    private static class Shutdown implements Runnable {
        private final Process server;

        public Shutdown(Process server) {
            this.server = server;
        }

        public void run() {
            server.destroy();
        }
    }

    private class MyApplicationAdapter extends com.apple.eawt.ApplicationAdapter {
        public void handleAbout(ApplicationEvent e) {
            if (aboutBox == null) {
                aboutBox = new MacAboutBox();
            }
            LOG.info("Got About describeChange");
            aboutBox.setResizable(false);
            aboutBox.setVisible(true);
            e.setHandled(true);
        }

        public void handleQuit(ApplicationEvent event) {
            LOG.info("Got the boot...");
            super.handleQuit(event);
            System.exit(0);
        }
    }
}
