/*
 * Copyright (c) 2002-2010 JSCAPE
 * 1147 S. 53rd Pl., Mesa, Arizona, 85206, U.S.A.
 * All rights reserved.
 *
 * This software is the confidential and proprietary information of
 * JSCAPE. ("Confidential Information").  You shall not disclose such
 * Confidential Information and shall use it only in accordance with
 *  the terms of the license agreement you entered into with JSCAPE.
 */

package com.jscape.util.net.socket;

import com.jscape.util.IOTools;
import com.jscape.util.Time;
import com.jscape.util.net.TransportAddress;
import com.jscape.util.net.connection.AsyncConnector;
import com.jscape.util.net.connection.SyncRawConnection;
import com.jscape.util.service.AbstractService;

import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;

import static com.jscape.util.Parameters.assertNotNull;

/**
 * @author Alex Usun
 */
public class SocketAcceptor
        extends AbstractService<AsyncConnector.Listener<SyncRawConnection>>
        implements AsyncConnector<SyncRawConnection> {

    private final TransportAddress address;
    private final int backlog;
    private final Time timeout;
    private final SocketParameters socketParameters;
    private final ExecutorService executorService;
    private ServerSocket acceptor;
    private TransportAddress boundAddress;

    public SocketAcceptor(TransportAddress address,
                          int backlog,
                          Time timeout,
                          SocketParameters socketParameters,
                          ExecutorService executorService) {
        assertNotNull(address);
        this.address = address;

        this.backlog = backlog;
        this.timeout = timeout;

        assertNotNull(socketParameters);
        this.socketParameters = socketParameters;

        assertNotNull(executorService);
        this.executorService = executorService;
    }

    public TransportAddress boundAddress() {
        return this.boundAddress;
    }

    @Override
    public String toString() {
        return "SocketAcceptor {"
               + "address=" + this.address
               + ", backlog=" + this.backlog
               + ", timeout=" + this.timeout
               + ", socketParameters=" + this.socketParameters
               + ", executorService=" + this.executorService
               + ", acceptor=" + this.acceptor
               + ", boundAddress=" + this.boundAddress
               + '}';
    }

    @Override
    protected void actualStart()
            throws Exception {
        initAcceptor();
        this.boundAddress = new TransportAddress(this.acceptor.getInetAddress(), this.acceptor.getLocalPort());

        this.executorService.submit(this::run);
    }

    @Override
    protected void actualStop() {
        IOTools.close(this.acceptor);
    }

    protected void initAcceptor()
            throws Exception {
        this.acceptor = new ServerSocket(this.address.getPort(), this.backlog, this.address.inetAddress());
        this.acceptor.setReuseAddress(true);
    }

    protected void run() {
        try {
            while (isStarted()) {
                Socket socket = this.acceptor.accept();
                postConnection(socket);
            }
        } catch (Exception e) {
            processError(e);
        }
    }

    protected void postConnection(Socket socket) {
        this.executorService.submit(() -> processConnection(socket));
    }

    protected void processConnection(Socket socket) {
        try {
            initSocket(socket);
            SocketRawConnection connection = new SocketRawConnection(socket);
            this.context.onConnection(this, connection);
        } catch (Exception e) {
            IOTools.close(socket);
            this.context.onError(this, e);
        }
    }

    protected void initSocket(Socket socket)
            throws Exception {
        if (this.timeout != null) {
            socket.setSoTimeout((int) this.timeout.toMillis());
        }

        this.socketParameters.applyTo(socket);
    }

    protected void processError(Exception e) {
        if (isStarted()) {
            this.context.onError(this, e);
            stop();
        }
    }

}
