package com.shimizukenta.secs.hsmsss;

import java.io.IOException;
import java.nio.channels.AsynchronousSocketChannel;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Optional;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

import com.shimizukenta.secs.AbstractSecsCommunicator;
import com.shimizukenta.secs.SecsException;
import com.shimizukenta.secs.SecsLog;
import com.shimizukenta.secs.SecsMessage;
import com.shimizukenta.secs.SecsSendMessageException;
import com.shimizukenta.secs.SecsWaitReplyMessageException;
import com.shimizukenta.secs.secs2.Secs2;

public abstract class HsmsSsCommunicator extends AbstractSecsCommunicator {
	
	private final ExecutorService execServ = Executors.newCachedThreadPool(r -> {
		Thread th = new Thread(r);
		th.setDaemon(true);
		return th;
	});
	
	private final HsmsSsCommunicatorConfig hsmsSsConfig;
	protected final HsmsSsSendReplyManager sendReplyManager;
	private HsmsSsCommunicateState hsmsSsCommunicateState;
	
	protected HsmsSsCommunicator(HsmsSsCommunicatorConfig config) {
		super(config);
		
		this.hsmsSsConfig = config;
		this.sendReplyManager = new HsmsSsSendReplyManager(this);
		
		this.notifyHsmsSsCommunicateStateChange(HsmsSsCommunicateState.NOT_CONNECTED);
	}
	
	public static HsmsSsCommunicator newInstance(HsmsSsCommunicatorConfig config) {
		
		switch ( config.protocol() ) {
		case PASSIVE: {
			
			if ( config.rebindIfPassive().isPresent() ) {
				
				return new HsmsSsRebindPassiveCommunicator(config);
				
			} else {
				
				return new HsmsSsPassiveCommunicator(config);
			}
			/* break; */
		}
		case ACTIVE: {
			
			return new HsmsSsActiveCommunicator(config);
			/* break; */
		}
		default: {
			
			throw new IllegalStateException("undefined protocol: " + config.protocol());
		}
		}
	}
	
	public static HsmsSsCommunicator open(HsmsSsCommunicatorConfig config) throws IOException {
		
		final HsmsSsCommunicator inst = newInstance(config);
		
		try {
			inst.open();
		}
		catch ( IOException e ) {
			
			try {
				inst.close();
			}
			catch ( IOException giveup ) {
			}
			
			throw e;
		}
		
		return inst;
	}
	


	protected ExecutorService executorService() {
		return execServ;
	}
	
	protected HsmsSsCommunicatorConfig hsmsSsConfig() {
		return hsmsSsConfig;
	}
	
	@Override
	public void open() throws IOException {
		super.open();
		
		execServ.execute(taskRecvMsg);
		execServ.execute(taskNotifyLog);
		execServ.execute(taskTrySendMsgPassThroughQueue);
		execServ.execute(taskSendedMsgPassThroughQueue);
		execServ.execute(taskRecvMsgPassThroughQueue);
	}
	
	@Override
	public void close() throws IOException {
		
		IOException ioExcept = null;
		
		try {
			synchronized ( this ) {
				if ( isClosed() ) {
					return;
				}
				
				super.close();
			}
		}
		catch ( IOException e ) {
			ioExcept = e;
		}
		
		try {
			execServ.shutdown();
			if ( ! execServ.awaitTermination(10L, TimeUnit.MILLISECONDS) ) {
				execServ.shutdownNow();
				if ( ! execServ.awaitTermination(5L, TimeUnit.SECONDS) ) {
					ioExcept = new IOException("ExecutorService#shutdown failed");
				}
			}
		}
		catch ( InterruptedException giveup ) {
		}
		
		if ( ioExcept != null ) {
			throw ioExcept;
		}
	}
	
	
	/* Receive Message Queue */
	private final BlockingQueue<HsmsSsMessage> recvDataMsgQueue = new LinkedBlockingQueue<>();
	
	protected void putReceiveDataMessage(HsmsSsMessage msg) throws InterruptedException {
		recvDataMsgQueue.put(msg);
	}
	
	@Override
	protected void notifyReceiveMessage(SecsMessage msg) {
		throw new UnsupportedOperationException("use #putReceiveMessage");
	}
	
	private final Runnable taskRecvMsg = new Runnable() {

		@Override
		public void run() {
			
			try {
				for ( ;; ) {
					HsmsSsCommunicator.super.notifyReceiveMessage(recvDataMsgQueue.take());
				}
			}
			catch ( InterruptedException ignore ) {
			}
		}
	};
	
	
	/* Log Queue */
	private final BlockingQueue<SecsLog> logQueue = new LinkedBlockingQueue<>();
	
	@Override
	protected void notifyLog(SecsLog log) {
		logQueue.offer(log);
	}
	
	private final Runnable taskNotifyLog = new Runnable() {

		@Override
		public void run() {
			try {
				for ( ;; ) {
					HsmsSsCommunicator.super.notifyLog(logQueue.take());
				}
			}
			catch ( InterruptedException ignore ) {
			}
		}
	};
	
	@Override
	protected void notifyLog(CharSequence subject) {
		super.notifyLog(subject);
	}
	
	@Override
	protected void notifyLog(CharSequence subject, Object value) {
		super.notifyLog(subject, value);
	}
	
	@Override
	protected void notifyLog(Throwable t) {
		super.notifyLog(t);
	}
	
	
	/* trySendMsgPassThroughQueue */
	private final BlockingQueue<HsmsSsMessage> trySendMsgPassThroughQueue = new LinkedBlockingQueue<>();
	
	@Override
	protected void notifyTrySendMessagePassThrough(SecsMessage msg) {
		throw new UnsupportedOperationException("use #putTrySendMessagePassThrough");
	}
	
	protected void putTrySendMessagePassThrough(HsmsSsMessage msg) throws InterruptedException {
		trySendMsgPassThroughQueue.put(msg);
	}
	
	private final Runnable taskTrySendMsgPassThroughQueue = new Runnable() {

		@Override
		public void run() {
			try {
				for ( ;; ) {
					HsmsSsCommunicator.super.notifyTrySendMessagePassThrough(trySendMsgPassThroughQueue.take());
				}
			}
			catch ( InterruptedException ignore ) {
			}
		}
	};
	
	
	/* sendedPassThroughQueue */
	private final BlockingQueue<HsmsSsMessage> sendedMsgPassThroughQueue = new LinkedBlockingQueue<>();
	
	@Override
	protected void notifySendedMessagePassThrough(SecsMessage msg) {
		throw new UnsupportedOperationException("use #putSendedMessagePassThrough");
	}
	
	protected void putSendedMessagePassThrough(HsmsSsMessage msg) throws InterruptedException {
		sendedMsgPassThroughQueue.put(msg);
	}
	
	private final Runnable taskSendedMsgPassThroughQueue = new Runnable() {

		@Override
		public void run() {
			try {
				for ( ;; ) {
					HsmsSsCommunicator.super.notifySendedMessagePassThrough(sendedMsgPassThroughQueue.take());
				}
			}
			catch ( InterruptedException ignore ) {
			}
		}
	};

	
	/* sendedPassThroughQueue */
	private final BlockingQueue<HsmsSsMessage> recvMsgPassThroughQueue = new LinkedBlockingQueue<>();
	
	@Override
	protected void notifyReceiveMessagePassThrough(SecsMessage msg) {
		throw new UnsupportedOperationException("use #putReceiveMessagePassThrough");
	}
	
	protected void putReceiveMessagePassThrough(HsmsSsMessage msg) throws InterruptedException {
		recvMsgPassThroughQueue.put(msg);
	}
	
	private final Runnable taskRecvMsgPassThroughQueue = new Runnable() {

		@Override
		public void run() {
			try {
				for ( ;; ) {
					HsmsSsCommunicator.super.notifyReceiveMessagePassThrough(recvMsgPassThroughQueue.take());
				}
			}
			catch ( InterruptedException ignore ) {
			}
		}
	};
	
	
	/* channels */
	private final Collection<AsynchronousSocketChannel> channels = new ArrayList<>();
	
	protected boolean addChannel(AsynchronousSocketChannel ch) {
		synchronized ( channels ) {
			if ( channels.isEmpty() ) {
				return channels.add(ch);
			} else {
				return false;
			}
		}
	}
	
	protected boolean removeChannel(AsynchronousSocketChannel ch) {
		synchronized ( channels ) {
			return channels.remove(ch);
		}
	}
	
	protected Optional<AsynchronousSocketChannel> optionalChannel() {
		synchronized ( channels ) {
			return channels.stream().findAny();
		}
	}
	
	
	/* HSMS Communicate State */
	private final Object syncHsmsSsCommunicateState = new Object();
	
	protected HsmsSsCommunicateState hsmsSsCommunicateState() {
		synchronized ( syncHsmsSsCommunicateState ) {
			return hsmsSsCommunicateState;
		}
	}
	
	protected void notifyHsmsSsCommunicateStateChange(HsmsSsCommunicateState state) {
		
		synchronized ( syncHsmsSsCommunicateState ) {
			
			if ( this.hsmsSsCommunicateState != state ) {
				
				this.hsmsSsCommunicateState = state;
				
				notifyLog("HsmsSs-Connect-state-chenged: " + state.toString());
				notifyCommunicatableStateChange(state.communicatable());
			}
		}
	}
	
	
	/**
	 * Blocking-method<br />
	 * 
	 * @return true if success
	 * @throws InterruptedException
	 * @throws SecsException
	 */
	public boolean linktest() throws InterruptedException {
		try {
			return send(createLinktestRequest()).isPresent();
		}
		catch ( SecsException e ) {
			return false;
		}
	}
	
	
	private final AtomicInteger autoNumber = new AtomicInteger();
	
	private int autoNumber() {
		return autoNumber.incrementAndGet();
	}
	
	protected byte[] systemBytes() {
		
		byte[] bs = new byte[4];
		
		if ( hsmsSsConfig().isEquip() ) {
			
			int sessionid = hsmsSsConfig().sessionId();
			
			bs[0] = (byte)(sessionid >> 8);
			bs[1] = (byte)sessionid;
			
		} else {
			
			bs[0] = (byte)0;
			bs[1] = (byte)0;
		}
		
		int n = autoNumber();
		
		bs[2] = (byte)(n >> 8);
		bs[3] = (byte)n;
		
		return bs;
	}
	
	protected void send(AsynchronousSocketChannel channel, HsmsSsMessage msg)
			throws SecsSendMessageException, SecsException
			, InterruptedException {
		
		sendReplyManager.send(channel, msg);
	}
	
	public Optional<HsmsSsMessage> send(HsmsSsMessage msg)
			throws SecsSendMessageException, SecsWaitReplyMessageException, SecsException
			, InterruptedException {
		
		try {
			return sendReplyManager.send(msg);
		}
		catch ( SecsException e ) {
			notifyLog(e);
			throw e;
		}
	}
	
	@Override
	public Optional<SecsMessage> send(int strm, int func, boolean wbit, Secs2 secs2)
			throws SecsSendMessageException, SecsWaitReplyMessageException, SecsException
			, InterruptedException {
		
		HsmsSsMessageType mt = HsmsSsMessageType.DATA;
		int sessionid = hsmsSsConfig().sessionId();
		byte[] sysbytes = systemBytes();
		
		byte[] head = new byte[] {
				(byte)(sessionid >> 8)
				, (byte)(sessionid)
				, (byte)strm
				, (byte)func
				, mt.pType()
				, mt.sType()
				, sysbytes[0]
				, sysbytes[1]
				, sysbytes[2]
				, sysbytes[3]
		};
		
		if ( wbit ) {
			head[2] |= 0x80;
		}
		
		return send(new HsmsSsMessage(head, secs2)).map(msg -> (SecsMessage)msg);
	}

	@Override
	public Optional<SecsMessage> send(SecsMessage primary, int strm, int func, boolean wbit, Secs2 secs2)
			throws SecsSendMessageException, SecsWaitReplyMessageException, SecsException
			, InterruptedException {
		
		byte[] pri = primary.header10Bytes();
		
		HsmsSsMessageType mt = HsmsSsMessageType.DATA;
		int sessionid = hsmsSsConfig().sessionId();
		
		byte[] head = new byte[] {
				(byte)(sessionid >> 8)
				, (byte)(sessionid)
				, (byte)strm
				, (byte)func
				, mt.pType()
				, mt.sType()
				, pri[6]
				, pri[7]
				, pri[8]
				, pri[9]
		};
		
		if ( wbit ) {
			head[2] |= 0x80;
		}
		
		return send(createHsmsSsMessage(head, secs2)).map(msg -> (SecsMessage)msg);
	}
	
	public HsmsSsMessage createHsmsSsMessage(byte[] head) {
		return createHsmsSsMessage(head, Secs2.empty());
	}
	
	public HsmsSsMessage createHsmsSsMessage(byte[] head, Secs2 body) {
		return new HsmsSsMessage(head, body);
	}
	
	public HsmsSsMessage createSelectRequest() {
		return createHsmsSsControlPrimaryMessage(HsmsSsMessageType.SELECT_REQ);
	}
	
	public HsmsSsMessage createSelectResponse(HsmsSsMessage primary, HsmsSsMessageSelectStatus status) {
		
		HsmsSsMessageType mt = HsmsSsMessageType.SELECT_RSP;
		byte[] pri = primary.header10Bytes();
		
		return createHsmsSsMessage(new byte[] {
				pri[0]
				, pri[1]
				, (byte)0
				, status.statusCode()
				, mt.pType()
				, mt.sType()
				, pri[6]
				, pri[7]
				, pri[8]
				, pri[9]
		});
	}
	
	public HsmsSsMessage createLinktestRequest() {
		return createHsmsSsControlPrimaryMessage(HsmsSsMessageType.LINKTEST_REQ);
	}
	
	public HsmsSsMessage createLinktestResponse(HsmsSsMessage primary) {
		
		HsmsSsMessageType mt = HsmsSsMessageType.LINKTEST_RSP;
		byte[] pri = primary.header10Bytes();
		
		return createHsmsSsMessage(new byte[] {
				pri[0]
				, pri[1]
				, (byte)0
				, (byte)0
				, mt.pType()
				, mt.sType()
				, pri[6]
				, pri[7]
				, pri[8]
				, pri[9]
		});
	}
	
	public HsmsSsMessage createRejectRequest(HsmsSsMessage ref, HsmsSsMessageRejectReason reason) {
		
		HsmsSsMessageType mt = HsmsSsMessageType.REJECT_REQ;
		byte[] bs = ref.header10Bytes();
		byte b = reason == HsmsSsMessageRejectReason.NOT_SUPPORT_TYPE_P ? bs[4] : bs[5];
		
		return createHsmsSsMessage(new byte[] {
				bs[0]
				, bs[1]
				, b
				, reason.reasonCode()
				, mt.pType()
				, mt.sType()
				, bs[6]
				, bs[7]
				, bs[8]
				, bs[9]
		});
	}
	
	public HsmsSsMessage createSeparateRequest() {
		return createHsmsSsControlPrimaryMessage(HsmsSsMessageType.SEPARATE_REQ);
	}
	
	private HsmsSsMessage createHsmsSsControlPrimaryMessage(HsmsSsMessageType mt) {
		
		byte[] sysbytes = systemBytes();
		
		return createHsmsSsMessage(new byte[] {
				(byte)0xFF
				, (byte)0xFF
				, (byte)0
				, (byte)0
				, mt.pType()
				, mt.sType()
				, sysbytes[0]
				, sysbytes[1]
				, sysbytes[2]
				, sysbytes[3]
		});
	}
	
}