/*
 * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
 * Copyright (C) 2009  Mickael Guessant
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program 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 for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 */
package davmail;

import davmail.exception.DavMailException;
import davmail.exchange.ExchangeSession;
import davmail.ui.tray.DavGatewayTray;
import org.apache.log4j.Logger;

import java.io.*;
import java.net.Socket;
import java.nio.charset.StandardCharsets;


/**
 * Generic connection common to pop3 and smtp implementations
 */
public abstract class AbstractConnection extends Thread implements Closeable {

    protected enum State {
        INITIAL, LOGIN, USER, PASSWORD, AUTHENTICATED, STARTMAIL, RECIPIENT, MAILDATA
    }

    protected static class LineReaderInputStream extends PushbackInputStream {
        final String encoding;

        protected LineReaderInputStream(InputStream in, String encoding) {
            super(in);
            if (encoding == null) {
                this.encoding = "ASCII";
            } else {
                this.encoding = encoding;
            }
        }

        public String readLine() throws IOException {
            ByteArrayOutputStream byteArrayOutputStream = null;
            int b;
            while ((b = read()) > -1) {
                // Handles line ending variations
                if (b == '\r') {
                    int next = read();
                    if (next != '\n') {
                        unread(next);
                    }
                    break;
                } else if (b == '\n') {
                    break;
                }
                if (byteArrayOutputStream == null) {
                    byteArrayOutputStream = new ByteArrayOutputStream();
                }
                byteArrayOutputStream.write(b);
            }
            if (byteArrayOutputStream != null) {
                return byteArrayOutputStream.toString(encoding);
            } else {
                return null;
            }
        }

        /**
         * Read byteSize bytes from inputStream, return content as String.
         *
         * @param byteSize content size
         * @return content
         * @throws IOException on error
         */
        public String readContentAsString(int byteSize) throws IOException {
            return new String(readContent(byteSize), encoding);
        }

        /**
         * Read byteSize bytes from inputStream, return content as a byte array.
         *
         * @param byteSize content size
         * @return content
         * @throws IOException on error
         */
        public byte[] readContent(int byteSize) throws IOException {
            byte[] buffer = new byte[byteSize];
            int startIndex = 0;
            int count = 0;
            while (count >= 0 && startIndex < byteSize) {
                count = read(buffer, startIndex, byteSize - startIndex);
                startIndex += count;
            }
            if (startIndex < byteSize) {
                throw new DavMailException("EXCEPTION_END_OF_STREAM");
            }

            return buffer;
        }
    }

    protected final Socket client;

    protected LineReaderInputStream in;
    protected OutputStream os;
    // username and password initialized through connection
    protected String userName;
    protected String password;
    // connection state
    protected State state = State.INITIAL;
    // Exchange session proxy
    protected ExchangeSession session;

    /**
     * Only set the thread name and socket
     *
     * @param name         thread type name
     * @param clientSocket client socket
     */
    public AbstractConnection(String name, Socket clientSocket) {
        super(name + '-' + clientSocket.getPort());
        this.client = clientSocket;
        setDaemon(true);
    }

    /**
     * Initialize the streams and set a thread name.
     *
     * @param name         thread type name
     * @param clientSocket client socket
     * @param encoding     socket stream encoding
     */
    public AbstractConnection(String name, Socket clientSocket, String encoding) {
        super(name + '-' + clientSocket.getPort());
        this.client = clientSocket;
        logConnection("CONNECT", "");
        try {
            in = new LineReaderInputStream(client.getInputStream(), encoding);
            os = new BufferedOutputStream(client.getOutputStream());
        } catch (IOException e) {
            close();
            DavGatewayTray.error(new BundleMessage("LOG_EXCEPTION_GETTING_SOCKET_STREAMS"), e);
        }
    }

    public void logConnection(String action, String userName) {
        Logger.getLogger("davmail.connection").info(action+" - "+client.getInetAddress().getHostAddress()+":"+client.getPort()+" " + userName);
    }

    /**
     * Send the provided message to the client followed by CRLF.
     *
     * @param message message
     * @throws IOException on error
     */
    public void sendClient(String message) throws IOException {
        sendClient(null, message);
    }

    /**
     * Send prefix and message to the client followed by CRLF.
     *
     * @param prefix  prefix
     * @param message message
     * @throws IOException on error
     */
    public void sendClient(String prefix, String message) throws IOException {
        if (prefix != null) {
            os.write(prefix.getBytes(StandardCharsets.UTF_8));
            DavGatewayTray.debug(new BundleMessage("LOG_SEND_CLIENT_PREFIX_MESSAGE", prefix, message));
        } else {
            DavGatewayTray.debug(new BundleMessage("LOG_SEND_CLIENT_MESSAGE", message));
        }
        os.write(message.getBytes(StandardCharsets.UTF_8));
        os.write((char) 13);
        os.write((char) 10);
        os.flush();
    }

    /**
     * Send all bytes to the client.
     *
     * @param messageBytes content
     * @throws IOException on error
     */
    public void sendClient(byte[] messageBytes) throws IOException {
        sendClient(messageBytes, 0, messageBytes.length);
    }

    /**
     * Send only bytes to the client.
     *
     * @param messageBytes content
     * @param offset       the start offset in the data.
     * @param length       the number of bytes to write.
     * @throws IOException on error
     */
    public void sendClient(byte[] messageBytes, int offset, int length) throws IOException {
        os.write(messageBytes, offset, length);
        os.flush();
    }

    /**
     * Read a line from the client connection.
     * Log message to logger
     *
     * @return command line or null
     * @throws IOException when unable to read the next line
     */
    public String readClient() throws IOException {
        String line = in.readLine();
        if (line != null) {
            if (line.startsWith("PASS")) {
                DavGatewayTray.debug(new BundleMessage("LOG_READ_CLIENT_PASS"));
                // SMTP LOGIN
            } else if (line.startsWith("AUTH LOGIN ")) {
                DavGatewayTray.debug(new BundleMessage("LOG_READ_CLIENT_AUTH_LOGIN"));
                // IMAP LOGIN
            } else if (state == State.INITIAL && line.indexOf(' ') >= 0 &&
                    line.substring(line.indexOf(' ') + 1).toUpperCase().startsWith("LOGIN")) {
                DavGatewayTray.debug(new BundleMessage("LOG_READ_CLIENT_LOGIN"));
            } else if (state == State.PASSWORD) {
                DavGatewayTray.debug(new BundleMessage("LOG_READ_CLIENT_PASSWORD"));
                // HTTP Basic Authentication
            } else if (line.startsWith("Authorization:")) {
                DavGatewayTray.debug(new BundleMessage("LOG_READ_CLIENT_AUTHORIZATION"));
            } else if (line.startsWith("AUTH PLAIN")) {
                DavGatewayTray.debug(new BundleMessage("LOG_READ_CLIENT_AUTH_PLAIN"));
            } else {
                DavGatewayTray.debug(new BundleMessage("LOG_READ_CLIENT_LINE", line));
            }
        }
        DavGatewayTray.switchIcon();
        return line;
    }

    /**
     * Close client connection, streams, and Exchange session.
     */
    public void close() {
        logConnection("DISCONNECT", "");
        if (in != null) {
            try {
                in.close();
            } catch (IOException e2) {
                // ignore
            }
        }
        if (os != null) {
            try {
                os.close();
            } catch (IOException e2) {
                // ignore
            }
        }
        try {
            client.close();
        } catch (IOException e2) {
            DavGatewayTray.debug(new BundleMessage("LOG_EXCEPTION_CLOSING_CLIENT_SOCKET"), e2);
        }
    }
}
