/*
 * 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.apache.logging.log4j.core.test.smtp;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import org.apache.logging.log4j.core.test.internal.annotation.SuppressFBWarnings;
import org.apache.logging.log4j.util.Strings;

/**
 * Dummy SMTP server for testing purposes.
 *
 * @todo constructor allowing user to pass preinitialized ServerSocket
 */
public class SimpleSmtpServer implements Runnable {
    /**
     * Stores all of the email received since this instance started up.
     */
    private final List<SmtpMessage> receivedMail;

    /**
     * Default SMTP port is 25.
     */
    public static final int DEFAULT_SMTP_PORT = 25;

    /**
     * Indicates whether this server is stopped or not.
     */
    private volatile boolean stopped = true;

    /**
     * Handle to the server socket this server listens to.
     */
    private ServerSocket serverSocket;

    /**
     * Port the server listens on - set to the default SMTP port initially.
     */
    private int port = DEFAULT_SMTP_PORT;

    /**
     * Timeout listening on server socket.
     */
    private static final int TIMEOUT = 500;

    /**
     * Constructor.
     *
     * @param port port number
     */
    public SimpleSmtpServer(final int port) {
        receivedMail = new ArrayList<>();
        this.port = port;
    }

    /**
     * Main loop of the SMTP server.
     */
    @Override
    @SuppressFBWarnings("UNENCRYPTED_SERVER_SOCKET")
    public void run() {
        stopped = false;
        try {
            try {
                serverSocket = new ServerSocket(port);
                serverSocket.setSoTimeout(TIMEOUT); // Block for maximum of 1.5 seconds
            } finally {
                synchronized (this) {
                    // Notify when server socket has been created
                    notifyAll();
                }
            }

            // Server: loop until stopped
            while (!isStopped()) {
                // Start server socket and listen for client connections
                Socket socket = null;
                try {
                    socket = serverSocket.accept();
                } catch (final Exception e) {
                    if (socket != null) {
                        socket.close();
                    }
                    continue; // Non-blocking socket timeout occurred: try accept() again
                }

                // Get the input and output streams
                final BufferedReader input = new BufferedReader(new InputStreamReader(socket.getInputStream()));
                final PrintWriter out = new PrintWriter(socket.getOutputStream());

                synchronized (this) {
                    /*
                     * We synchronize over the handle method and the list update because the client call completes
                     * inside the handle method and we have to prevent the client from reading the list until we've
                     * updated it. For higher concurrency, we could just change handle to return void and update the
                     * list inside the method to limit the duration that we hold the lock.
                     */
                    final List<SmtpMessage> msgs = handleTransaction(out, input);
                    receivedMail.addAll(msgs);
                }
                socket.close();
            }
        } catch (final Exception e) {
            /** @todo Should throw an appropriate exception here. */
            e.printStackTrace();
        } finally {
            if (serverSocket != null) {
                try {
                    serverSocket.close();
                } catch (final IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    /**
     * Check if the server has been placed in a stopped state. Allows another thread to
     * stop the server safely.
     *
     * @return true if the server has been sent a stop signal, false otherwise
     */
    public synchronized boolean isStopped() {
        return stopped;
    }

    /**
     * Stops the server. Server is shutdown after processing of the current request is complete.
     */
    public synchronized void stop() {
        // Mark us closed
        stopped = true;
        try {
            // Kick the server accept loop
            serverSocket.close();
        } catch (final IOException e) {
            // Ignore
        }
    }

    /**
     * Handle an SMTP transaction, i.e. all activity between initial connect and QUIT command.
     *
     * @param out   output stream
     * @param input input stream
     * @return List of SmtpMessage
     * @throws IOException
     */
    private List<SmtpMessage> handleTransaction(final PrintWriter out, final BufferedReader input) throws IOException {
        // Initialize the state machine
        SmtpState smtpState = SmtpState.CONNECT;
        final SmtpRequest smtpRequest = new SmtpRequest(SmtpActionType.CONNECT, Strings.EMPTY, smtpState);

        // Execute the connection request
        final SmtpResponse smtpResponse = smtpRequest.execute();

        // Send initial response
        sendResponse(out, smtpResponse);
        smtpState = smtpResponse.getNextState();

        final List<SmtpMessage> msgList = new ArrayList<>();
        SmtpMessage msg = new SmtpMessage();

        while (smtpState != SmtpState.CONNECT) {
            final String line = input.readLine();

            if (line == null) {
                break;
            }

            // Create request from client input and current state
            final SmtpRequest request = SmtpRequest.createRequest(line, smtpState);
            // Execute request and create response object
            final SmtpResponse response = request.execute();
            // Move to next internal state
            smtpState = response.getNextState();
            // Send response to client
            sendResponse(out, response);

            // Store input in message
            final String params = request.getParams();
            msg.store(response, params);

            // If message reception is complete save it
            if (smtpState == SmtpState.QUIT) {
                msgList.add(msg);
                msg = new SmtpMessage();
            }
        }

        return msgList;
    }

    /**
     * Send response to client.
     *
     * @param out          socket output stream
     * @param smtpResponse response object
     */
    private static void sendResponse(final PrintWriter out, final SmtpResponse smtpResponse) {
        if (smtpResponse.getCode() > 0) {
            final int code = smtpResponse.getCode();
            final String message = smtpResponse.getMessage();
            out.print(code + " " + message + "\r\n");
            out.flush();
        }
    }

    /**
     * Get email received by this instance since start up.
     *
     * @return List of String
     */
    public synchronized Iterator<SmtpMessage> getReceivedEmail() {
        return receivedMail.iterator();
    }

    /**
     * Get the number of messages received.
     *
     * @return size of received email list
     */
    public synchronized int getReceivedEmailSize() {
        return receivedMail.size();
    }

    /**
     * Creates an instance of SimpleSmtpServer and starts it. Will listen on the default port.
     *
     * @return a reference to the SMTP server
     */
    public static SimpleSmtpServer start() {
        return start(DEFAULT_SMTP_PORT);
    }

    /**
     * Creates an instance of SimpleSmtpServer and starts it.
     *
     * @param port port number the server should listen to
     * @return a reference to the SMTP server
     */
    public static SimpleSmtpServer start(final int port) {
        final SimpleSmtpServer server = new SimpleSmtpServer(port);
        final Thread t = new Thread(server);

        // Block until the server socket is created
        synchronized (server) {
            t.start();
            try {
                server.wait();
            } catch (final InterruptedException e) {
                // Ignore don't care.
            }
        }
        return server;
    }
}
