package com.jscape.util.net.socket;

import com.jscape.util.Clock;
import com.jscape.util.IOTools;
import com.jscape.util.net.TransportAddress;
import com.jscape.util.net.connection.SyncRawConnection;

import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
import java.io.*;
import java.net.Socket;
import java.util.Map;
import java.util.Stack;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;

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

/**
 * @author Alex Usun
 */
public class SocketRawConnection
        implements SyncRawConnection,
                   SocketConnection {

    private final Socket socket;
    private final long creationTime;
    private final Stack<SSLSocket> sslSockets;
    private final AtomicBoolean closed;
    private OutputStream out;
    private InputStream in;
    private TransportAddress localAddress;
    private TransportAddress remoteAddress;
    private final Map<Object, Object> attributes;

    public SocketRawConnection(Socket socket)
            throws IOException {
        assertNotNull(socket);
        this.socket = socket;

        this.creationTime = Clock.now();
        this.sslSockets = new Stack<>();
        this.closed = new AtomicBoolean(false);
        this.localAddress = new TransportAddress(socket.getLocalAddress(), socket.getLocalPort());
        this.remoteAddress = new TransportAddress(socket.getInetAddress(), socket.getPort());

        this.attributes = new ConcurrentHashMap<>();

        initStreams(this.socket);
    }

    public long creationTime() {
        return this.creationTime;
    }

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

    public void localAddress(TransportAddress value) {
        assertNotNull(value);
        this.localAddress = value;
    }

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

    public void remoteAddress(TransportAddress value) {
        assertNotNull(value);
        this.remoteAddress = value;
    }

    @Override
    public Map<Object, Object> attributes() {
        return this.attributes;
    }

    public boolean closed() {
        return this.closed.get();
    }

    public void close() {
        if (this.closed.getAndSet(true)) {
            return;
        }

        IOTools.close(this.socket);
        this.sslSockets.clear();
    }

    public int read()
            throws ConnectionException {
        try {
            return this.in.read();
        } catch (InterruptedIOException e) {
            throw new ReadTimeoutException(e);
        } catch (java.io.EOFException e) {
            return -1;
        } catch (Exception e) {
            throw new ConnectionException(e);
        }
    }

    public int read(byte[] data, int offset, int length)
            throws ConnectionException {
        try {
            return this.in.read(data, offset, length);
        } catch (InterruptedIOException e) {
            throw new ReadTimeoutException(e);
        } catch (java.io.EOFException e) {
            return -1;
        } catch (Exception e) {
            throw new ConnectionException(e);
        }
    }

    @Override
    public int availableBytes()
            throws ConnectionException {
        try {
            return this.in.available();
        } catch (IOException e) {
            throw new ConnectionException(e);
        }
    }

    public void write(byte data)
            throws ConnectionException {
        try {
            this.out.write(data);
        } catch (Exception e) {
            throw new WriteException(e);
        }
    }

    public void write(byte[] data, int offset, int length)
            throws ConnectionException {
        try {
            this.out.write(data, offset, length);
        } catch (Exception e) {
            throw new WriteException(e);
        }
    }

    public void flush()
            throws ConnectionException {
        try {
            this.out.flush();
        } catch (Exception e) {
            throw new WriteException(e);
        }
    }

    public Socket socket() {
        return this.socket;
    }

    @Override
    public SSLSocket protect(SSLSocketFactory factory)
            throws IOException {
        SSLSocket sslSocket = (SSLSocket) factory.createSocket(this.sslSockets.isEmpty() ? this.socket : this.sslSockets.peek(),
                                                               this.remoteAddress.hostName(),
                                                               this.remoteAddress.getPort(),
                                                               false);
        initStreams(sslSocket);
        this.sslSockets.push(sslSocket);

        return sslSocket;
    }

    @Override
    public void unprotect(boolean shutdown)
            throws IOException {
        SSLSocket sslSocket = this.sslSockets.pop();

        if (sslSocket != null && shutdown) {
            IOTools.close(sslSocket);
        }

        initStreams(this.sslSockets.empty() ? this.socket : this.sslSockets.peek());
    }

    @Override
    public String toString() {
        return String.format("%s <-> %s", this.localAddress, this.remoteAddress);
    }

    private void initStreams(Socket socket)
            throws IOException {
        OutputStream out = socket.getOutputStream();
        InputStream in = socket.getInputStream();

        this.out = out;
        this.in = in;
    }

}
