package com.eactive.eai.adapter.socket.service;

import com.eactive.eai.adapter.socket.common.CommonLib;
import com.eactive.eai.common.util.Logger;

import java.io.IOException;
import java.net.Socket;

import java.nio.ByteBuffer;
import java.nio.BufferOverflowException;

import java.nio.channels.ByteChannel;
import java.nio.channels.SocketChannel;
import java.nio.channels.Selector;
import java.nio.channels.SelectionKey;

import java.util.Iterator;

public class NBChannel implements ByteChannel {

    SocketChannel channel;
    Selector      selector;
    Selector      writeSelector;
    ByteBuffer    pendingMessage;
    Logger        logger ;
    long          timeout;

    public NBChannel(SocketChannel channel, long timeout) throws IOException {

        this.channel  = channel;
        this.selector = Selector.open();
        this.writeSelector = Selector.open();
        this.channel.register( this.selector, SelectionKey.OP_READ );
        this.channel.register( this.writeSelector, SelectionKey.OP_WRITE );
        this.timeout  = timeout;

        pendingMessage  = ByteBuffer.allocate( 4096 );
        pendingMessage.clear();
        logger        = SocketLogManager.getInstance().getLogger();
    }

    public int read(ByteBuffer dst) throws IOException {
        if ( dst.capacity() == 0 || !dst.hasRemaining() ) return 0;
        int     y = 0;
        boolean isDone = false;

        if ( pendingMessage.position() > 0 ) {
            if ( pendingMessage.position() <= dst.remaining() ) {
                y += pendingMessage.position();
                byte[] data = new byte[ pendingMessage.position() ];
                pendingMessage.flip();
                pendingMessage.get( data );
                dst.put( data, 0, data.length );
                pendingMessage.clear();
            } else {
                int shiftCount = dst.remaining();
                y += shiftCount;
                byte[] data = new byte[ shiftCount ];
                pendingMessage.flip();
                pendingMessage.get( data );
                pendingMessage.position( pendingMessage.limit() );

                dst.put( data, 0, data.length );
                shiftBuffer( pendingMessage, shiftCount );
                isDone = true;
            }
        }

        if ( isDone ) return y;
        selector.select( timeout );
        y += this.channel.read( dst );
        return  y;
    }

    private void shiftBuffer(ByteBuffer buffer, int shiftBytes) {
        int pos = buffer.position();
        if ( pos <= shiftBytes ) {
            buffer.clear();
            return;
        }
        byte[] tmpBuff = new byte[ pos ];
        buffer.flip();
        buffer.get( tmpBuff );

        buffer.clear();
        buffer.put( tmpBuff, shiftBytes, tmpBuff.length - shiftBytes );
    }

    public int write(ByteBuffer src) throws IOException {

        try {
            writeSelector.select( timeout );
        } catch(Exception e) {
            throw new IOException("Write Select Error");
        }

        Iterator<SelectionKey> it = writeSelector.selectedKeys().iterator();
        int          writtenBytes = 0;
        SelectionKey key = null;

        while ( it.hasNext() ) {
            key    = (SelectionKey)it.next();
            it.remove();
            if ( key.isWritable() ) {
                writtenBytes = this.channel.write( src );
            }
        }

        return  writtenBytes;
    }

    public boolean isConnected() throws IOException {

        if ( !isOpen() ) {
            logger.info( CommonLib.getMessage("BICEAIASO003", new String[]{"", getSocketInfo()} ));
            return false;
        }

        boolean flag = false;

        if ( this.channel.isConnected() && this.channel.socket().isBound() ) {
            ByteBuffer tmpBuffer = ByteBuffer.allocate( 1 );
            tmpBuffer.clear();
            int i = channel.read( tmpBuffer );
            if ( i > 0 ) {
                try {
                    pendingMessage.put( tmpBuffer.array() );
                } catch ( BufferOverflowException e ) {
                    throw new IOException("Socket 어플리케이션 프로토콜 에러 발생 : " + e.getMessage());
                }
            }
            flag = true;
            if ( i == -1 ) {
                flag = false;
            }
        } else {
            flag = false;
        }

        return flag;
    }

    public void wakeup() {
        try {
            if ( selector != null && selector.isOpen() ) {
                selector.wakeup();
                writeSelector.wakeup();
            }
        } catch (Throwable e) {}
    }

    public void close() {
        if ( this.channel == null || !this.channel.isOpen() ) return;

//        try {
//            if ( selector != null && selector.isOpen() ) {
//                selector.wakeup();
//                writeSelector.close();
//            }
//            this.channel.socket().close();
//            this.channel.close();
//            this.selector.close();
//            this.writeSelector.close();
//        } catch (Throwable e) {}
        
        try {// 20250910 npe 
            if (selector != null) {
                if (selector.isOpen()) {
                    selector.wakeup();
                }
                this.selector.close();
            }

            if (writeSelector != null) {
                writeSelector.close();
            }

            if (this.channel != null) {
                if (this.channel.socket() != null) {
                	this.channel.socket().close();
                }
                this.channel.close();
            }
        } catch (IOException e) {
            e.printStackTrace(); // 최소한 로그는 남기는 것이 좋음
        }
    }

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

    public boolean isOpen() {
        if ( this.channel == null || !this.channel.isOpen() ) return false;

        return this.channel.socket().isConnected();
    }

    private String  getSocketInfo() {
        StringBuffer connInfo = new StringBuffer();

        connInfo.append("Local Address=");
        connInfo.append( getSocket().getLocalAddress().getHostAddress()).append(":").append(getSocket().getLocalPort()).append(", ");
        connInfo.append("Remote Address=");
        connInfo.append( getSocket().getInetAddress().getHostAddress()).append(":").append(getSocket().getPort());

        return connInfo.toString();
    }
}