/**
 * Logback: the reliable, generic, fast and flexible logging framework.
 * Copyright (C) 1999-2013, QOS.ch. All rights reserved.
 *
 * This program and the accompanying materials are dual-licensed under
 * either the terms of the Eclipse Public License v1.0 as published by
 * the Eclipse Foundation
 *
 *   or (per the licensee's choosing)
 *
 * under the terms of the GNU Lesser General Public License version 2.1
 * as published by the Free Software Foundation.
 */
package ch.qos.logback.classic.net;

import java.io.EOFException;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.net.ConnectException;
import java.net.InetAddress;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.RejectedExecutionException;

import javax.net.SocketFactory;

import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.net.SocketAppenderBase;
import ch.qos.logback.core.net.SocketConnector;
import ch.qos.logback.core.net.SocketConnectorBase;
import ch.qos.logback.core.util.CloseUtil;

/**
 * A component that receives serialized {@link ILoggingEvent} objects from a
 * remote appender over a {@link Socket}.
 *
 * @author Carl Harris
 */
public class SocketReceiver extends ReceiverBase
        implements Runnable, SocketConnector.ExceptionHandler {

  private static final int DEFAULT_ACCEPT_CONNECTION_DELAY = 5000;

  private String remoteHost;
  private InetAddress address;
  private int port;
  private int reconnectionDelay;
  private int acceptConnectionTimeout = DEFAULT_ACCEPT_CONNECTION_DELAY;

  private String remoteId;
  private volatile Socket socket;

  /**
   * {@inheritDoc}
   */
  protected boolean shouldStart() {
    int errorCount = 0;
    if (port == 0) {
      errorCount++;
      addError("No port was configured for remote. "
              + "For more information, please visit http://logback.qos.ch/codes.html#receiver_no_port");
    }

    if (remoteHost == null) {
      errorCount++;
      addError("No host name or address was configured for remote. "
              + "For more information, please visit http://logback.qos.ch/codes.html#receiver_no_host");
    }

    if (reconnectionDelay == 0) {
      reconnectionDelay = SocketAppenderBase.DEFAULT_RECONNECTION_DELAY;
    }

    if (errorCount == 0) {
      try {
        address = InetAddress.getByName(remoteHost);
      } catch (UnknownHostException ex) {
        addError("unknown host: " + remoteHost);
        errorCount++;
      }
    }

    if (errorCount == 0) {
      remoteId = "remote " + remoteHost + ":" + port + ": ";
    }

    return errorCount == 0;
  }

  /**
   * {@inheritDoc}
   */
  protected void onStop() {
    if (socket != null) {
      CloseUtil.closeQuietly(socket);
    }
  }

  @Override
  protected Runnable getRunnableTask() {
    return this;
  }

  /**
   * {@inheritDoc}
   */
  public void run() {
    try {
      LoggerContext lc = (LoggerContext) getContext();
      SocketConnector connector = createConnector(address, port, 0,
              reconnectionDelay);
      while (!Thread.currentThread().isInterrupted()) {
        try {
          getExecutor().execute(connector);
        } catch (RejectedExecutionException ex) {
          // executor is shutting down... 
          continue;
        }
        socket = connector.awaitConnection();
        dispatchEvents(lc);
        connector = createConnector(address, port, reconnectionDelay);
      }
    } catch (InterruptedException ex) {
      assert true;    // ok... we'll exit now
    }
    addInfo("shutting down");
  }

  private void dispatchEvents(LoggerContext lc) {
    try {
      socket.setSoTimeout(acceptConnectionTimeout);
      ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());
      socket.setSoTimeout(0);
      addInfo(remoteId + "connection established");
      while (true) {
        ILoggingEvent event = (ILoggingEvent) ois.readObject();
        Logger remoteLogger = lc.getLogger(event.getLoggerName());
        if (remoteLogger.isEnabledFor(event.getLevel())) {
          remoteLogger.callAppenders(event);
        }
      }
    } catch (EOFException ex) {
      addInfo(remoteId + "end-of-stream detected");
    } catch (IOException ex) {
      addInfo(remoteId + "connection failed: " + ex);
    } catch (ClassNotFoundException ex) {
      addInfo(remoteId + "unknown event class: " + ex);
    } finally {
      CloseUtil.closeQuietly(socket);
      socket = null;
      addInfo(remoteId + "connection closed");
    }
  }

  /**
   * {@inheritDoc}
   */
  public void connectionFailed(SocketConnector connector, Exception ex) {
    if (ex instanceof InterruptedException) {
      addInfo("connector interrupted");
    } else if (ex instanceof ConnectException) {
      addInfo(remoteId + "connection refused");
    } else {
      addInfo(remoteId + ex);
    }
  }

  private SocketConnector createConnector(InetAddress address, int port,
                                          int delay) {
    return createConnector(address, port, delay, delay);
  }

  private SocketConnector createConnector(InetAddress address, int port,
                                          int initialDelay, int retryDelay) {
    SocketConnector connector = newConnector(address, port, initialDelay,
            retryDelay);
    connector.setExceptionHandler(this);
    connector.setSocketFactory(getSocketFactory());
    return connector;
  }

  protected SocketConnector newConnector(InetAddress address,
                                         int port, int initialDelay, int retryDelay) {
    return new SocketConnectorBase(address, port, initialDelay, retryDelay);
  }

  protected SocketFactory getSocketFactory() {
    return SocketFactory.getDefault();
  }

  protected ExecutorService createExecutorService() {
    return Executors.newCachedThreadPool();
  }

  public void setRemoteHost(String remoteHost) {
    this.remoteHost = remoteHost;
  }

  public void setPort(int port) {
    this.port = port;
  }

  public void setReconnectionDelay(int reconnectionDelay) {
    this.reconnectionDelay = reconnectionDelay;
  }

  public void setAcceptConnectionTimeout(int acceptConnectionTimeout) {
    this.acceptConnectionTimeout = acceptConnectionTimeout;
  }

}
