/*
 * 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.pop;

import davmail.AbstractConnection;
import davmail.BundleMessage;
import davmail.DavGateway;
import davmail.Settings;
import davmail.exchange.DoubleDotOutputStream;
import davmail.exchange.ExchangeSession;
import davmail.exchange.ExchangeSessionFactory;
import davmail.exchange.MessageLoadThread;
import davmail.ui.tray.DavGatewayTray;
import davmail.util.IOUtil;
import org.apache.log4j.Logger;

import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;
import java.net.SocketException;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.List;
import java.util.StringTokenizer;

/**
 * Dav Gateway pop connection implementation
 */
public class PopConnection extends AbstractConnection {
    private static final Logger LOGGER = Logger.getLogger(PopConnection.class);

    private List<ExchangeSession.Message> messages;

    /**
     * Initialize the streams and start the thread.
     *
     * @param clientSocket POP client socket
     */
    public PopConnection(Socket clientSocket) {
        super(PopConnection.class.getSimpleName(), clientSocket, null);
    }

    protected long getTotalMessagesLength() {
        int result = 0;
        for (ExchangeSession.Message message : messages) {
            result += message.size;
        }
        return result;
    }

    protected void printCapabilities() throws IOException {
        sendClient("TOP");
        sendClient("USER");
        sendClient("UIDL");
        sendClient(".");
    }

    protected void printList() throws IOException {
        int i = 1;
        for (ExchangeSession.Message message : messages) {
            sendClient(i++ + " " + message.size);
        }
        sendClient(".");
    }

    protected void printUidList() throws IOException {
        int i = 1;
        for (ExchangeSession.Message message : messages) {
            sendClient(i++ + " " + message.getUid());
        }
        sendClient(".");
    }


    @Override
    public void run() {
        String line;
        StringTokenizer tokens;

        try {
            ExchangeSessionFactory.checkConfig();
            sendOK("DavMail " + DavGateway.getCurrentVersion() + " POP ready at " + new Date());

            for (; ;) {
                line = readClient();
                // unable to read line, connection closed ?
                if (line == null) {
                    break;
                }

                tokens = new StringTokenizer(line);
                if (tokens.hasMoreTokens()) {
                    String command = tokens.nextToken();

                    if ("QUIT".equalsIgnoreCase(command)) {
                        // delete messages before quit
                        if (session != null) {
                            session.purgeOldestTrashAndSentMessages();
                        }
                        sendOK("Bye");
                        break;
                    } else if ("USER".equalsIgnoreCase(command)) {
                        userName = null;
                        password = null;
                        session = null;
                        if (tokens.hasMoreTokens()) {
                            userName = line.substring("USER ".length());
                            sendOK("USER : " + userName);
                            state = State.USER;
                        } else {
                            sendERR("invalid syntax");
                            state = State.INITIAL;
                        }
                    } else if ("PASS".equalsIgnoreCase(command)) {
                        if (state != State.USER) {
                            sendERR("invalid state");
                            state = State.INITIAL;
                        } else if (!tokens.hasMoreTokens()) {
                            sendERR("invalid syntax");
                        } else {
                            // bug 2194492 : allow space in password
                            password = line.substring("PASS".length() + 1);
                            try {
                                session = ExchangeSessionFactory.getInstance(userName, password);
                                logConnection("LOGON", userName);
                                sendOK("PASS");
                                state = State.AUTHENTICATED;
                            } catch (SocketException e) {
                                logConnection("FAILED", userName);
                                // can not send error to client after a socket exception
                                LOGGER.warn(BundleMessage.formatLog("LOG_CLIENT_CLOSED_CONNECTION"));
                            } catch (Exception e) {
                                DavGatewayTray.error(e);
                                sendERR(e);
                            }
                        }
                    } else if ("CAPA".equalsIgnoreCase(command)) {
                        sendOK("Capability list follows");
                        printCapabilities();
                    } else if (state != State.AUTHENTICATED) {
                        sendERR("Invalid state not authenticated");
                    } else {
                        // load messages (once)
                        if (messages == null) {
                            messages = session.getAllMessageUidAndSize("INBOX");
                        }
                        if ("STAT".equalsIgnoreCase(command)) {
                            sendOK(messages.size() + " " +
                                    getTotalMessagesLength());
                        } else if ("NOOP".equalsIgnoreCase(command)) {
                            sendOK("");
                        } else if ("LIST".equalsIgnoreCase(command)) {
                            if (tokens.hasMoreTokens()) {
                                String token = tokens.nextToken();
                                try {
                                    int messageNumber = Integer.parseInt(token);
                                    ExchangeSession.Message message = messages.get(messageNumber - 1);
                                    sendOK("" + messageNumber + ' ' + message.size);
                                } catch (NumberFormatException | IndexOutOfBoundsException e) {
                                    sendERR("Invalid message index: " + token);
                                }
                            } else {
                                sendOK(messages.size() +
                                        " messages (" + getTotalMessagesLength() +
                                        " octets)");
                                printList();
                            }
                        } else if ("UIDL".equalsIgnoreCase(command)) {
                            if (tokens.hasMoreTokens()) {
                                String token = tokens.nextToken();
                                try {
                                    int messageNumber = Integer.parseInt(token);
                                    sendOK(messageNumber + " " + messages.get(messageNumber - 1).getUid());
                                } catch (NumberFormatException | IndexOutOfBoundsException e) {
                                    sendERR("Invalid message index: " + token);
                                }
                            } else {
                                sendOK(messages.size() +
                                        " messages (" + getTotalMessagesLength() +
                                        " octets)");
                                printUidList();
                            }
                        } else if ("RETR".equalsIgnoreCase(command)) {
                            if (tokens.hasMoreTokens()) {
                                try {
                                    int messageNumber = Integer.parseInt(tokens.nextToken()) - 1;
                                    ExchangeSession.Message message = messages.get(messageNumber);

                                    // load big messages in a separate thread
                                    os.write("+OK ".getBytes(StandardCharsets.US_ASCII));
                                    os.flush();
                                    MessageLoadThread.loadMimeMessage(message, os);
                                    sendClient("");

                                    DoubleDotOutputStream doubleDotOutputStream = new DoubleDotOutputStream(os);
                                    IOUtil.write(message.getRawInputStream(), doubleDotOutputStream);
                                    doubleDotOutputStream.close();
                                    if (Settings.getBooleanProperty("davmail.popMarkReadOnRetr")) {
                                        message.markRead();
                                    }
                                } catch (SocketException e) {
                                    // can not send error to client after a socket exception
                                    LOGGER.warn(BundleMessage.formatLog("LOG_CLIENT_CLOSED_CONNECTION"));
                                } catch (Exception e) {
                                    DavGatewayTray.error(new BundleMessage("LOG_ERROR_RETRIEVING_MESSAGE"), e);
                                    sendERR("error retrieving message " + e + ' ' + e.getMessage());
                                }
                            } else {
                                sendERR("invalid message index");
                            }
                        } else if ("DELE".equalsIgnoreCase(command)) {
                            if (tokens.hasMoreTokens()) {
                                ExchangeSession.Message message;
                                try {
                                    int messageNumber = Integer.parseInt(tokens.nextToken()) - 1;
                                    message = messages.get(messageNumber);
                                    message.moveToTrash();
                                    sendOK("DELETE");
                                } catch (NumberFormatException | IndexOutOfBoundsException e) {
                                    sendERR("invalid message index");
                                }
                            } else {
                                sendERR("invalid message index");
                            }
                        } else if ("TOP".equalsIgnoreCase(command)) {
                            int message = 0;
                            try {
                                message = Integer.parseInt(tokens.nextToken());
                                int lines = Integer.parseInt(tokens.nextToken());
                                ExchangeSession.Message m = messages.get(message - 1);
                                sendOK("");
                                DoubleDotOutputStream doubleDotOutputStream = new DoubleDotOutputStream(os);
                                IOUtil.write(m.getRawInputStream(), new TopOutputStream(doubleDotOutputStream, lines));
                                doubleDotOutputStream.close();
                            } catch (NumberFormatException e) {
                                sendERR("invalid command");
                            } catch (IndexOutOfBoundsException e) {
                                sendERR("invalid message index: " + message);
                            } catch (Exception e) {
                                sendERR("error retrieving top of messages");
                                DavGatewayTray.error(e);
                            }
                        } else if ("RSET".equalsIgnoreCase(command)) {
                            sendOK("RSET");
                        } else {
                            sendERR("unknown command");
                        }
                    }

                } else {
                    sendERR("unknown command");
                }

                os.flush();
            }
        } catch (SocketException e) {
            DavGatewayTray.debug(new BundleMessage("LOG_CONNECTION_CLOSED"));
        } catch (Exception e) {
            DavGatewayTray.log(e);
            try {
                sendERR(e.getMessage());
            } catch (IOException e2) {
                DavGatewayTray.debug(new BundleMessage("LOG_EXCEPTION_SENDING_ERROR_TO_CLIENT"), e2);
            }
        } finally {
            close();
        }
        DavGatewayTray.resetIcon();
    }

    protected void sendOK(String message) throws IOException {
        sendClient("+OK ", message);
    }

    protected void sendERR(Exception e) throws IOException {
        String message = e.getMessage();
        if (message == null) {
            message = e.toString();
        }
        sendERR(message);
    }

    protected void sendERR(String message) throws IOException {
        sendClient("-ERR ", message.replaceAll("\\n", " "));
    }

    /**
     * Filter to limit output lines to max body lines after header
     */
    private static final class TopOutputStream extends FilterOutputStream {
        protected enum State {
            START, CR, CRLF, CRLFCR, BODY
        }

        private int maxLines;
        private State state = State.START;

        private TopOutputStream(OutputStream os, int maxLines) {
            super(os);
            this.maxLines = maxLines;
        }

        @Override
        public void write(int b) throws IOException {
            if (state != State.BODY || maxLines > 0) {
                super.write(b);
            }
            if (state == State.BODY) {
                if (b == '\n') {
                    maxLines--;
                }
            } else if (state == State.START) {
                if (b == '\r') {
                    state = State.CR;
                }
            } else if (state == State.CR) {
                if (b == '\n') {
                    state = State.CRLF;
                } else {
                    state = State.START;
                }
            } else if (state == State.CRLF) {
                if (b == '\r') {
                    state = State.CRLFCR;
                } else {
                    state = State.START;
                }
            } else if (state == State.CRLFCR) {
                if (b == '\n') {
                    state = State.BODY;
                } else {
                    state = State.START;
                }
            }
        }
    }

}
