
package org.kne.cloud.network;

import java.io.IOException;
import java.io.InterruptedIOException;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.VarHandle;
import java.net.InetAddress;
import java.net.ProtocolFamily;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.SocketException;
import java.net.SocketOptions;
import java.net.StandardProtocolFamily;
import java.net.UnknownHostException;

import org.kne.cloud.network.klalb.KLALBVirtualRawSocketImpl;


public class RawSocket {


  public static final ProtocolFamily PF_INET=StandardProtocolFamily.INET;


  public static final ProtocolFamily PF_INET6=StandardProtocolFamily.INET6;

  private static final VarHandle STATE ;
  static {
      try {
          MethodHandles.Lookup l = MethodHandles.lookup();
          STATE = l.findVarHandle(RawSocket.class, "state", int.class);
      } catch (Exception e) {
          throw new InternalError(e);
      }
  }
//the underlying SocketImpl, may be null, may be swapped when connecting
 private RawSocketImpl impl;

  // state bits
  private static final int SOCKET_CREATED = 1 << 0;   // impl.create(boolean) called
  private static final int BOUND          = 1 << 1;
  private static final int CONNECTED      = 1 << 2;
  private static final int CLOSED         = 1 << 3;
  private static final int SHUT_IN        = 1 << 9;
  private static final int SHUT_OUT       = 1 << 10;
  private volatile int state;

  // used to coordinate creating and closing underlying socket
  private final Object socketLock = new Object();
  

  InetAddress connectedAddress = null;

  private boolean explicitFilter = false;
  private int bytesLeftToFilter;
  /**
   * Atomically sets state to the result of a bitwise OR of the current value
   * and the given mask.
   * @return the previous state value
   */
  private int getAndBitwiseOrState(int mask) {
      return (int) STATE.getAndBitwiseOr(this, mask);
  }

  private static boolean isBound(int s) {
      return (s & BOUND) != 0;
  }

  private static boolean isConnected(int s) {
      return (s & CONNECTED) != 0;
  }

  private static boolean isClosed(int s) {
      return (s & CLOSED) != 0;
  }

  private static boolean isInputShutdown(int s) {
      return (s & SHUT_IN) != 0;
  }

  private static boolean isOutputShutdown(int s) {
      return (s & SHUT_OUT) != 0;
  }

   
 
  protected RawSocket(RawSocketImpl impl) {
	  this.impl=impl;
  }

  public RawSocket(RawSocketImpl impl, Inet6Address bindAddress) throws SocketException {
	  this.impl=impl;
	  bind(bindAddress);
}

public RawSocket(KLALBVirtualRawSocketImpl impl, Inet6Address bindAddress, int bindProtocol) throws SocketException {
	 this.impl=impl;
	  bind(bindAddress,bindProtocol);
}

public boolean isConnected() {
      return isConnected(state);
  }
  public boolean isBound() {
      return isBound(state);
  }
  public boolean isOpen() {
	  return !isClosed(state);
  }
  public boolean isClosed() {
      return isClosed(state);
  }
  private void checkAddress(InetAddress addr, String op) {
      if (addr == null) {
          return;
      }
      if (!(addr instanceof Inet4Address || addr instanceof Inet6Address)) {
          throw new IllegalArgumentException(op + ": invalid address type");
      }
  }
  
  
  
  
  
  
  private RawSocketImpl getImpl() throws SocketException {
      if ((state & SOCKET_CREATED) == 0) {
          synchronized (socketLock) {
              int s = state;   // re-read state
              if ((s & SOCKET_CREATED) == 0) {
                  if (isClosed(s)) {
                      throw new SocketException("Socket is closed");
                  }
                  RawSocketImpl impl = this.impl;
                  if (impl == null) {
                      this.impl = impl = createImpl();
                  }
                  try {
                      impl.create();
                  } catch (SocketException e) {
                      throw e;
                  } catch (IOException e) {
                      throw new SocketException(e.getMessage(), e);
                  }
                  getAndBitwiseOrState(SOCKET_CREATED);
              }
          }
      }
      return impl;
  }
  
  private static RawSocketImpl createImpl() {
	  RawSocketImplFactory factory = RawSocket.factory;
      if (factory != null) {
          return factory.createRawSocketImpl();
      } else {
    	  return null;
    	//  RawSocketImpl delegate = RawSocketImpl.createPlatformSocketImpl(false);
        //  return new SocksSocketImpl(delegate);
      }
  }
  

  private static volatile RawSocketImplFactory factory;

  static RawSocketImplFactory socketImplFactory() {
      return factory;
  }

 
  @Deprecated(since = "17")
  public static synchronized void setSocketImplFactory(RawSocketImplFactory fac)
      throws IOException
  {
      if (factory != null) {
          throw new SocketException("factory already defined");
      }
      @SuppressWarnings("removal")
      SecurityManager security = System.getSecurityManager();
      if (security != null) {
          security.checkSetFactory();
      }
      factory = fac;
  }
  public void bind(InetAddress address) throws SocketException {
	  bind(address,-1);
  }
  public void bind(InetAddress address,int protocolNumber)
    throws SocketException
  {
	  int s = state;
      if (isClosed(s))
          throw new SocketException("Socket is closed");
      if (isBound(s))
          throw new SocketException("Already bound");

   
      
      if (address == null) {
    	  try {
			address = Inet6Address.getByName("::0");
		} catch (UnknownHostException e) {
			e.printStackTrace();
		}
      }
      checkAddress (address, "bind");
      getImpl().bind(address,protocolNumber);
      getAndBitwiseOrState(BOUND);

    
  }

  
  
  public void bindDevice(String device)
    throws UnsupportedOperationException, IllegalStateException, IOException
  {
    if(!isOpen())
      throw new IllegalStateException();

   throw new UnsupportedOperationException();
  }


  public InetAddress getLocalAddress() {
      if (isClosed())
          return null;
      InetAddress in;
      try {
          in = (InetAddress) getImpl().getOption(SocketOptions.SO_BINDADDR);
          if (in.isAnyLocalAddress()) {
              in = InetAddress.getByName("::0");
          }
         
      } catch (Exception e) {
          try {
			in = InetAddress.getByName("::0");
		} catch (UnknownHostException e1) {
			e1.printStackTrace();
			in=null;
		} 
      }
      return in;
  }

 
  public void close()  {
      synchronized (socketLock) {
          if ((state & CLOSED) == 0) {
              int s = getAndBitwiseOrState(CLOSED);
              if ((s & (SOCKET_CREATED | CLOSED)) == SOCKET_CREATED) {
                  // close underlying socket if created
                  impl.close();
              }
          }
      }
  }

  public void setIPHeaderInclude(boolean on) throws IOException {
	  impl.setIPHeaderInclude(on);
  }


  
  public boolean getIPHeaderInclude() throws IOException {
    return impl.getIPHeaderInclude();
  }
 
  public void setSendBufferSize(int size) throws SocketException {
      if (size <= 0)
          throw new IllegalArgumentException("negative send size");
      if (isClosed())
          throw new SocketException("Socket is closed");
      getImpl().setOption(SocketOptions.SO_SNDBUF, size);
  }

  public int getSendBufferSize() throws SocketException {
      if (isClosed())
          throw new SocketException("Socket is closed");
      int result = 0;
      Object o = getImpl().getOption(SocketOptions.SO_SNDBUF);
      if (o instanceof Integer i) {
          result = i.intValue();
      }
      return result;
  }

  public void setReceiveBufferSize(int size) throws SocketException {
      if (size <= 0)
          throw new IllegalArgumentException("invalid receive size");
      if (isClosed())
          throw new SocketException("Socket is closed");
      getImpl().setOption(SocketOptions.SO_RCVBUF, size);
  }

  public int getReceiveBufferSize() throws SocketException {
      if (isClosed())
          throw new SocketException("Socket is closed");
      int result = 0;
      Object o = getImpl().getOption(SocketOptions.SO_RCVBUF);
      if (o instanceof Integer i) {
          result = i.intValue();
      }
      return result;
  }
  

  
  public void setUseSelectTimeout(boolean useSelect) throws IOException {
	  impl.setUseSelectTimeout(useSelect);
  }


  
  public boolean getUseSelectTimeout() throws IOException {
   return impl.getUseSelectTimeout();
  }

  
 /* public void setSendTimeout(int timeout) throws SocketException {
	  impl.setSendTimeout( timeout);
  }

  
  public int getSendTimeout() throws SocketException {
	  return impl.getSendTimeout();
  }*/

  
  public void setSoTimeout(int timeout) throws SocketException {
      if (isClosed())
          throw new SocketException("Socket is closed");
      if (timeout < 0)
          throw new IllegalArgumentException("timeout can't be negative");
      getImpl().setOption(SocketOptions.SO_TIMEOUT, timeout);
  }

 
  public int getSoTimeout() throws SocketException {
      if (isClosed())
          throw new SocketException("Socket is closed");
      Object o = getImpl().getOption(SocketOptions.SO_TIMEOUT);
      /* extra type safety */
      if (o instanceof Integer i) {
          return i.intValue();
      } else {
          return 0;
      }
  }

  
  public void send(DatagramPacket p) throws IOException {
      synchronized (p) {
          if (isClosed())
              throw new SocketException("Socket is closed");
          InetAddress packetAddress = p.getAddress();
          checkAddress(packetAddress, "send");
          if (isConnected()) {
              // we're connected
              if (packetAddress == null) {
                  p.setAddress(connectedAddress);
              } else if ((!packetAddress.equals(connectedAddress)) ) {
                  throw new IllegalArgumentException("connected address " +
                          "and packet address" +
                          " differ");
              }
           
          } else {
              if (packetAddress == null) {
                  throw new IllegalArgumentException("Address not set");
              }
          }
          // Check whether the socket is bound
          if (!isBound())
              bind(InetAddress.getByName("::0"));
          // call the  method to send
          getImpl().send(p);
      }
  }

  public synchronized void receive(DatagramPacket p) throws IOException {
      synchronized (p) {
          if (!isBound())
              bind(InetAddress.getByName("::0"));
          DatagramPacket tmp = null;
          if (explicitFilter) {
              // We have to do the filtering the old fashioned way since
              // the native impl doesn't support connect or the connect
              // via the impl failed, or .. "explicitFilter" may be set when
              // a socket is connected via the impl, for a period of time
              // when packets from other sources might be queued on socket.
              boolean stop = false;
              while (!stop) {
                  // peek at the packet to see who it is from.
                  DatagramPacket peekPacket = new DatagramPacket(new byte[1], 1);
                  getImpl().peekData(peekPacket);
                  InetAddress peekAddress = peekPacket.getAddress();
                  if ((!connectedAddress.equals(peekAddress)) ) {
                      // throw the packet away and silently continue
                      tmp = new DatagramPacket(
                              new byte[1024], 1024);
                      getImpl().receive(tmp);
                      if (explicitFilter) {
                          if (checkFiltering(tmp)) {
                              stop = true;
                          }
                      }
                  } else {
                      stop = true;
                  }
              }
          }
          // If the security check succeeds, or the datagram is
          // connected then receive the packet
          getImpl().receive(p);
          if (explicitFilter && tmp == null) {
              // packet was not filtered, account for it here
              checkFiltering(p);
          }
      }
  }
  private boolean checkFiltering(DatagramPacket p) throws SocketException {
      bytesLeftToFilter -= p.getLength();
      if (bytesLeftToFilter <= 0 || getImpl().dataAvailable() <= 0) {
          explicitFilter = false;
          return true;
      }
      return false;
  }

}
