/*
 * Copyright (c) 2011, 2022, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

package javacserver.server;

import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
import java.nio.file.Path;
import java.util.Optional;
import java.util.Random;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.spi.ToolProvider;
import javacserver.shared.PortFile;
import javacserver.shared.Protocol;
import javacserver.shared.Result;
import javacserver.util.LazyInitFileLog;
import javacserver.util.Log;
import javacserver.util.LoggingOutputStream;
import javacserver.util.Util;

/**
 * Start a new server main thread, that will listen to incoming connection requests from the client,
 * and dispatch these on to worker threads in a thread pool, running javac.
 */
public class Server {
    private ServerSocket serverSocket;
    private PortFile portFile;
    private PortFileMonitor portFileMonitor;
    private IdleMonitor idleMonitor;
    private CompilerThreadPool compilerThreadPool;

    // Set to false break accept loop
    final AtomicBoolean keepAcceptingRequests = new AtomicBoolean();

    // For logging server internal (non request specific) errors.
    private static LazyInitFileLog errorLog;

    public static void main(String... args) {
        initLogging();

        try {
            PortFile portFile = getPortFileFromArguments(args);
            if (portFile == null) {
                System.exit(Result.CMDERR.exitCode);
                return;
            }

            Server server = new Server(portFile);
            if (!server.start()) {
                System.exit(Result.ERROR.exitCode);
            } else {
                System.exit(Result.OK.exitCode);
            }
        } catch (IOException | InterruptedException ex) {
            ex.printStackTrace();
            System.exit(Result.ERROR.exitCode);
        }
    }

    private static void initLogging() {
        // Under normal operation, all logging messages generated server-side
        // are due to compilation requests. These logging messages should
        // be relayed back to the requesting client rather than written to the
        // server log. The only messages that should be written to the server
        // log (in production mode) should be errors,
        errorLog = new LazyInitFileLog("server.log");
        Log.setLogForCurrentThread(errorLog);
        Log.setLogLevel(Log.Level.ERROR); // should be set to ERROR.

        // Make sure no exceptions go under the radar
        Thread.setDefaultUncaughtExceptionHandler((t, e) -> {
            restoreServerErrorLog();
            Log.error(e);
        });

        // Inevitably someone will try to print messages using System.{out,err}.
        // Make sure this output also ends up in the log.
        System.setOut(new PrintStream(new LoggingOutputStream(System.out, Log.Level.INFO, "[stdout] ")));
        System.setErr(new PrintStream(new LoggingOutputStream(System.err, Log.Level.ERROR, "[stderr] ")));
    }

    private static PortFile getPortFileFromArguments(String[] args) {
        if (args.length != 1) {
            Log.error("javacserver daemon incorrectly called");
            return null;
        }
        String portfilename = args[0];
        PortFile portFile = new PortFile(portfilename);
        return portFile;
    }

    public Server(PortFile portFile) throws FileNotFoundException {
        this.portFile = portFile;
    }

    /**
     * Start the daemon, unless another one is already running, in which it returns
     * false and exits immediately.
     */
    private boolean start() throws IOException, InterruptedException {
        // The port file is locked and the server port and cookie is written into it.
        portFile.lock();
        portFile.getValues();
        if (portFile.containsPortInfo()) {
            Log.debug("javacserver daemon not started because portfile exists!");
            portFile.unlock();
            return false;
        }

        serverSocket = new ServerSocket();
        InetAddress localhost = InetAddress.getByName(null);
        serverSocket.bind(new InetSocketAddress(localhost, 0));

        // At this point the server accepts connections, so it is  now safe
        // to publish the port / cookie information

        // The secret cookie shared between server and client through the port file.
        // Used to prevent clients from believing that they are communicating with
        // an old server when a new server has started and reused the same port as
        // an old server.
        long myCookie = new Random().nextLong();
        portFile.setValues(serverSocket.getLocalPort(), myCookie);
        portFile.unlock();

        portFileMonitor = new PortFileMonitor(portFile, this::shutdownServer);
        portFileMonitor.start();
        compilerThreadPool = new CompilerThreadPool();
        idleMonitor = new IdleMonitor(this::shutdownServer);

        Log.debug("javacserver daemon started. Accepting connections...");
        Log.debug("    port: " + serverSocket.getLocalPort());
        Log.debug("    time: " + new java.util.Date());
        Log.debug("    poolsize: " + compilerThreadPool.poolSize());

        keepAcceptingRequests.set(true);
        do {
            try {
                Socket socket = serverSocket.accept();
                 // Handle each incoming request in a separate thread. This is just for socket communication,
                 // the actual compilation will be done by the threadpool.
                Thread requestHandler = new Thread(() -> handleRequest(socket));
                requestHandler.start();
            } catch (SocketException se) {
                // Caused by serverSocket.close() and indicates shutdown
            }
        } while (keepAcceptingRequests.get());

        Log.debug("Shutting down.");

        // No more connections accepted. If any client managed to connect after
        // the accept() was interrupted but before the server socket is closed
        // here, any attempt to read or write to the socket will result in an
        // IOException on the client side.

        // Shut down
        idleMonitor.shutdown();
        compilerThreadPool.shutdown();

        return true;
    }

    private void handleRequest(Socket socket) {
        try (BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
             PrintWriter out = new PrintWriter(socket.getOutputStream(), true)) {
            try {
                idleMonitor.startCall();

                // Set up logging for this thread. Stream back logging messages to
                // client on the format "level:msg".
                Log.setLogForCurrentThread(new Protocol.ProtocolLog(out));

                String[] args = Protocol.readCommand(in);

                // If there has been any internal errors, notify client
                checkInternalErrorLog();

                // Perform compilation. This will call runCompiler() on a
                // thread in the thread pool
                int exitCode = compilerThreadPool.dispatchCompilation(args);
                Protocol.sendExitCode(out, exitCode);

                // Check for internal errors again.
                checkInternalErrorLog();
            } finally {
                idleMonitor.endCall();
            }
        } catch (Exception ex) {
            // Not much to be done at this point. The client side request
            // code will most likely throw an IOException and the
            // compilation will fail.
            Log.error(ex);
        } finally {
            Log.setLogForCurrentThread(null);
        }
    }

    public static int runCompiler(Log log, String[] args) {
        Log.setLogForCurrentThread(log);

        // Direct logging to our byte array stream.
        StringWriter strWriter = new StringWriter();
        PrintWriter printWriter = new PrintWriter(strWriter);

        // Compile
        Optional<ToolProvider> tool = ToolProvider.findFirst("javac");
        if (tool.isEmpty()) {
            Log.error("Can't find tool javac");
            return Result.ERROR.exitCode;
        }
        int exitcode = tool.get().run(printWriter, printWriter, args);

        // Process compiler output (which is always errors)
        printWriter.flush();
        Util.getLines(strWriter.toString()).forEach(Log::error);

        return exitcode;
    }

    private void checkInternalErrorLog() {
        Path errorLogPath = errorLog.getLogDestination();
        if (errorLogPath != null) {
            Log.error("Server has encountered an internal error. See " + errorLogPath.toAbsolutePath()
                    + " for details.");
        }
    }

    public static void restoreServerErrorLog() {
        Log.setLogForCurrentThread(errorLog);
    }

    public void shutdownServer(String quitMsg) {
        if (!keepAcceptingRequests.compareAndSet(true, false)) {
            // Already stopped, no need to shut down again
            return;
        }

        Log.debug("Quitting: " + quitMsg);

        portFileMonitor.shutdown(); // No longer any need to monitor port file

        // Unpublish port before shutting down socket to minimize the number of
        // failed connection attempts
        try {
            portFile.delete();
        } catch (IOException | InterruptedException e) {
            Log.error(e);
        }
        try {
            serverSocket.close();
        } catch (IOException e) {
            Log.error(e);
        }
    }
}