/**
 * a multicast socket session implementation for FastFix @WARN CHANGES TO THIS CLASS SHOULD BE
 * CHECKED AGAINST NonBlockingFastFixSocketSession
 */
public abstract class FastFixSocketSession extends AbstractSocketSession {

  protected static final Logger _log = LoggerFactory.create(FastFixSocketSession.class);

  private static final int MIN_BYTES = 30;

  public static int getDataOffset(String name, boolean isInbound) {
    return AbstractSession.getLogHdrLen(name, isInbound);
  }

  public FastFixSocketSession(
      String name,
      MessageRouter inboundRouter,
      SocketConfig socketConfig,
      MessageDispatcher dispatcher,
      Encoder encoder,
      Decoder decoder,
      ThreadPriority receiverPriority) {

    super(
        name,
        inboundRouter,
        socketConfig,
        dispatcher,
        encoder,
        decoder,
        new DummyDecoder(),
        receiverPriority);
  }

  @Override
  public final void processNextInbound() throws Exception {

    int preBuffered = _inPreBuffered;
    Message msg;

    _inPreBuffered = 0; // reset the preBuffer .. incase of exception
    final int startPos = _inHdrLen + preBuffered;
    _inByteBuffer.limit(_inByteBuffer.capacity());
    _inByteBuffer.position(startPos);

    final int bytesRead = initialChannelRead(preBuffered, MIN_BYTES);
    if (bytesRead == 0) {
      _inPreBuffered = preBuffered;
      return;
    }

    // time starts from when we have read the full message off the socket
    if (_logStats) _decoder.setReceived(Utils.nanoTime());

    _inByteBuffer.position(_inHdrLen);

    final int maxIdx = _inHdrLen + bytesRead;
    _inByteBuffer.limit(maxIdx);

    if (_stopping) return;

    _inLogBuf.setLength(maxIdx);

    msg = decode(_inHdrLen, bytesRead);

    final int msgLen = _decoder.getLength();
    int extraBytes = bytesRead;

    if (msg != null) {

      extraBytes = bytesRead - msgLen;

      logInEvent(null);
      logInEventPojo(msg);

      // message decoded, shift left any extra bytes before invoke controller as that can throw
      // exception .. avoids extra try-finally
      invokeController(msg);
    } else {
      // no partial messages in UDP ... could also be duplicate in any case discard
      extraBytes = 0;
    }

    if (extraBytes > 0) {
      _inPreBuffered = extraBytes;
      final int nextMsgStart = _inHdrLen + msgLen;
      shiftInBufferLeft(extraBytes, nextMsgStart);
    }
  }

  @Override
  public final void handleForSync(Message msg) {
    //
  }

  @Override
  public final void persistLastInboundMesssage() {
    // N/A
  }

  @Override
  public final void logInboundError(Exception e) {
    super.logInboundError(e);
    ((FastFixDecoder) _decoder).logLastMsg();
  }

  protected abstract void invokeController(Message msg);

  protected abstract void dispatchMsgGap(int channelId, int lastSeqNum, int seqNum);

  @Override
  protected final synchronized Message recoveryDecode(
      byte[] buf, int offset, int len, boolean inBound) {
    return null;
  }

  @Override
  protected final Message recoveryDecodeWithContext(
      byte[] buf, int offset, int len, byte[] opt, int optOffset, int optLen, boolean inBound) {
    return null;
  }

  @Override
  protected final void sendNow(Message msg) throws IOException {
    _encoder.encode(msg);

    int length = _encoder.getLength();

    if (length > 0) {

      final int lastIdx = length + _encoder.getOffset();

      _outByteBuffer.clear();
      _outByteBuffer.limit(lastIdx);
      _outByteBuffer.position(_encoder.getOffset());

      blockingWriteSocket();

      logOutEvent(null);
      logOutEventPojo(msg);

      if (_stopping) return;
    }
  }

  @Override
  public final boolean discardOnDisconnect(Message msg) {
    return false;
  }

  @Override
  public final boolean canHandle() {
    return isConnected();
  }

  @Override
  protected final void sendChain(Message msg, boolean canRecycle) {
    if (_chainSession != null && _chainSession.isConnected()) {
      _chainSession.handle(msg);
    } else if (canRecycle) {
      outboundRecycle(msg);
    }
  }

  @Override
  protected final void logInEvent(ZString event) {
    if (_logEvents) {
      ((FastFixDecoder) _decoder).logLastMsg();
    }
  }

  @Override
  protected final void logOutEvent(ZString event) {
    if (_logEvents) {
      ((FastFixEncoder) _encoder).logLastMsg();
    }
  }
}
/**
 * a multi session threaded dispatcher for session outbound messages
 *
 * <p>does NOT own spinning control thread, that is shared and which round robins sessions
 *
 * <p>if a session cannot send a message eg socket blocked due to slow consumer then the
 * NonBlockingSession returns immediately that session will have to wait its turn to retry writing
 * remaining data to the socket
 *
 * <p>Currently max sockets is 32 but this could easily be changed to 64 with minimal delay impact
 * IF using OS bypass eg Solarflare OpenOnload
 */
public final class MultiSessionThreadedDispatcher
    implements MultiSessionDispatcher, ExecutableElement {

  private static final Logger _log = LoggerFactory.create(MultiSessionThreadedDispatcher.class);

  private static final ErrorCode MISSING_HANDLER =
      new ErrorCode("MST100", "No registered dispatcher for message to session ");
  static final ZString DISCONNECTED =
      new ViewString("Unable to dispatch as destination Session Disconnected");
  private static final ZString DROP_MSG =
      new ViewString("Dropping session message as not logged in, type=");

  private class SessionWrapper {
    final MessageQueue _queue;
    final MessageQueue _syncQueue;
    final MessageQueue _preQueue; // when disconnected flushed events that need to be kept go here
    final NonBlockingSession _session;
    boolean _connected = false;

    SessionWrapper(NonBlockingSession session, MessageQueue queue, MessageQueue syncQueue) {
      _queue = queue;
      _syncQueue = syncQueue;
      _session = session;
      _preQueue = new SimpleMessageQueue();
    }
  }

  private SessionWrapper[] _sessions = new SessionWrapper[0];

  private int _nextSession = 0;

  private final ReusableString _logMsg = new ReusableString(50);
  private boolean _allDisconnected =
      true; // not volatile as mem barrier already occurs when get msg off queue

  private final Object _disconnectLock = new Object();

  private final ControlThread _ctl;

  private Message _curMsg = null;
  private SessionWrapper _curSessW = null;

  private volatile boolean _fullyFlushed = false;

  private AtomicBoolean _stopping = new AtomicBoolean(false);

  private final String _id;

  // array index assigned to session to save map lookup

  public MultiSessionThreadedDispatcher(String id, ControlThread ctl) {
    _ctl = ctl;
    _id = id;

    ctl.register(this);
  }

  @Override
  public void threadedInit() {
    for (int idx = 0; idx < _sessions.length; ++idx) {
      _sessions[idx]._session.threadedInit();
    }
  }

  @Override
  public String getComponentId() {
    return _id;
  }

  @Override
  public void notReady() {
    disconnectedFlushAll();
  }

  @Override
  public void execute() throws Exception {

    _curSessW = _sessions[_nextSession];

    final MessageQueue queue = _curSessW._queue;
    final MessageQueue preQueue = _curSessW._preQueue;
    final NonBlockingSession sess = _curSessW._session;

    if (++_nextSession >= _sessions.length) _nextSession = 0;

    if (_curSessW._connected && sess.canHandle()) {
      if (sess.isLoggedIn()) {
        if (sess.isMsgPendingWrite()) {
          sess.retryCompleteWrite();
        } else if (preQueue.isEmpty()) {
          _curMsg = queue.poll(); // POLL = non blocking, causes MEM_READ barrier
          if (_curMsg != null && _curMsg.getReusableType() != CoreReusableType.NullMessage) {
            sess.handleNow(_curMsg);
          }
        } else { // QUEUED MESSAGES FROM PREVIOUS FLUSH CALLS
          _curMsg = preQueue.next();
          if (_curMsg.getReusableType() != CoreReusableType.NullMessage) {
            sess.handleNow(_curMsg);
          }
        }
      } else { // SYNC mode
        final MessageQueue syncQueue = _curSessW._syncQueue;
        if (sess.isMsgPendingWrite()) {
          sess.retryCompleteWrite();
        } else if (!syncQueue.isEmpty()) {
          _curMsg = syncQueue.next();
          if (_curMsg.getReusableType() != CoreReusableType.NullMessage) {
            sess.handleNow(_curMsg);
          }
        }
      }
    } else {
      flush(_curSessW);
    }
  }

  @Override
  public void handleExecutionException(Exception ex) {
    final NonBlockingSession sess = _curSessW._session;

    if (_curMsg != null && sess != null) {
      _log.warn(
          "SessionThreadedDispatcher "
              + getComponentId()
              + ", msgSeqNum="
              + _curMsg.getMsgSeqNum()
              + ", sess="
              + sess.getComponentId()
              + " exception "
              + ex.getMessage());
    }

    flush(_curSessW);

    // some problem, possibly disconnect, poke controller to wake up anything waiting on controller
    // passive lock
    _ctl.statusChange(); // Mem barrier
  }

  @Override
  public boolean checkReady() {
    return (_allDisconnected == false);
  }

  @Override
  public synchronized void start() {
    _ctl.start();
  }

  @Override
  public void setHandler(MessageHandler handler) {
    throw new SMTRuntimeException(
        "MultiSessionThreadedDispatcher MISCONFIGURATION : only for use with multi session and addSession()");
  }

  /**
   * @param session - a non blocking session ie an NIO one that wont block if socket cant read/write
   */
  @Override
  public synchronized void addSession(final NonBlockingSession session) {

    SessionWrapper[] newSessions = new SessionWrapper[_sessions.length + 1];

    int idx = 0;
    while (idx < _sessions.length) {
      newSessions[idx] = _sessions[idx];
      ++idx;
    }

    newSessions[idx] =
        new SessionWrapper(session, session.getSendQueue(), session.getSendSyncQueue());

    _sessions = newSessions;

    _fullyFlushed = false;
  }

  @Override
  public void dispatch(final Message msg) {
    if (msg != null) {
      final MessageHandler handler = msg.getMessageHandler();
      final NonBlockingSession session = (NonBlockingSession) handler;
      final MessageQueue queue = session.getSendQueue();
      if (queue != null) {
        queue.add(msg);
      } else {
        // should NEVER happen
        ReusableString s = TLC.instance().pop();
        s.copy(((Session) handler).getComponentId())
            .append(": Missing Queue, unable to dispatch : ");
        msg.dump(s);
        _log.error(MISSING_HANDLER, s);
        TLC.instance().pushback(s);
      }
    }
  }

  @Override
  public void setStopping() {

    // dont actually stop, but wake up to force flush

    final int numSessions = _sessions.length;

    for (int i = 0; i < numSessions; i++) {
      final SessionWrapper s = _sessions[i];

      MessageQueue q = s._queue;

      q.add(new NullMessage()); // wake up queue
    }

    _fullyFlushed = false;
  }

  @Override
  public void stop() {
    if (_stopping.compareAndSet(false, true)) {
      _ctl.setStopping(true);

      final int numSessions = _sessions.length;

      for (int i = 0; i < numSessions; i++) {
        final SessionWrapper s = _sessions[i];

        MessageQueue q = s._queue;

        q.add(new NullMessage()); // wake up queue
      }

      _fullyFlushed = false;
    }
  }

  @Override
  public void handlerStatusChange(MessageHandler handler, boolean connected) {
    final int numSessions = _sessions.length;

    boolean allDisconnected = true;

    for (int i = 0; i < numSessions; i++) {
      SessionWrapper sessW = _sessions[i];

      if (sessW._session == handler) {
        if (connected != sessW._connected) {
          final NonBlockingSession sess = sessW._session;

          _log.info(
              "MultiSession OutDispatcher "
                  + getComponentId()
                  + " : "
                  + ((connected) ? "CONNECTED" : "DISCONNECTED")
                  + " with "
                  + sess.getComponentId()
                  + ", canHandle="
                  + sess.canHandle()
                  + ", isLoggedIn="
                  + sess.isLoggedIn());

          sessW._connected = connected;
        }
      }

      if (sessW._connected) {
        allDisconnected = false;
      }
    }

    _fullyFlushed = false;

    synchronized (_disconnectLock) { // force mem barrier
      _allDisconnected = allDisconnected;
    }

    _ctl.statusChange();
  }

  @Override
  public boolean canQueue() {
    return true;
  }

  @Override
  public String info() {
    return "MultiSessionThreadedDispatcher( " + _id + " )";
  }

  @Override
  public void dispatchForSync(Message msg) {
    if (msg != null) {
      final MessageHandler handler = msg.getMessageHandler(); // cant be null
      NonBlockingSession session = (NonBlockingSession) handler;
      MessageQueue queue = session.getSendSyncQueue();
      if (queue != null) {
        queue.add(msg);
      } else {
        // @TODO add ReusableString write( ReusableString buf ) to Message and log details
        // should NEVER happen
        _log.error(MISSING_HANDLER, ((Session) handler).getComponentId());
      }
    }
  }

  private void disconnectedFlushAll() {
    if (!_fullyFlushed) {

      final int numSessions = _sessions.length;

      for (int i = 0; i < numSessions; i++) {
        flush(_sessions[i]);
      }

      _fullyFlushed = true;
    }
  }

  // @NOTE keep flush private the preQ is not threadsafe
  private void flush(SessionWrapper sessW) {
    // disconnected drop any session messages
    // optionally keep any other messages or reject back  upstream

    final Session session = sessW._session;
    final MessageQueue queue = sessW._queue;
    final MessageQueue preQ = sessW._preQueue;
    final MessageQueue syncQ = sessW._syncQueue;

    while (!syncQ.isEmpty()) {
      syncQ.next(); // DISCARD
    }

    Message head = null;
    Message tail = null;

    while (!queue.isEmpty()) {
      Message msg = queue.next();
      if (msg.getReusableType() == CoreReusableType.NullMessage) break;

      if (session.discardOnDisconnect(msg) == false) {
        if (session.rejectMessageUpstream(msg, DISCONNECTED)) {
          // message recycled by successful reject processing
        } else {
          if (tail == null) {
            head = msg;
            tail = msg;
          } else {
            tail.attachQueue(msg);
            tail = msg;
          }
        }
      } else {
        _logMsg.copy(DROP_MSG).append(msg.getReusableType().toString());
        _log.info(_logMsg);
        session.outboundRecycle(msg);
      }
    }

    // move remaining messages to the preQ

    if (head != null) {
      Message tmp = head;

      while (tmp != null) {
        Message next = tmp.getNextQueueEntry();
        tmp.detachQueue();
        preQ.add(tmp);
        tmp = next;
      }
    }
  }

  @Override
  public void init(SMTStartContext ctx) {
    // nothing
  }

  @Override
  public void prepare() {
    // nothing
  }

  @Override
  public void startWork() {
    start();
  }

  @Override
  public void stopWork() {
    stop();
  }
}
public class EmeaDmaValidator implements EventValidator {

  private static final Logger _log = LoggerFactory.create(EmeaDmaValidator.class);

  private static final ZString MISSING_CLORDID = new ViewString("Missing clOrdId ");
  private static final ZString UNSUPPORTED = new ViewString("Unsupported attribute value ");
  private static final ZString UNABLE_TO_CHANGE_CCY =
      new ViewString("Unable to change the currency ");
  private static final ZString UNABLE_TO_CHANGE_ORDTYPE =
      new ViewString("Unable to change the order type ");
  private static final ZString UNABLE_TO_CHANGE_SIDE = new ViewString("Unable to change the side ");
  private static final ZString UNABLE_TO_CHANGE_SYM =
      new ViewString("Unable to change the symbol ");
  private static final ZString CANNOT_AMEND_BELOW_CQTY =
      new ViewString("Cannot amend qty below cumQty, qty=");
  private static final ZString MAJOR_FIELDS_UNCHANGED =
      new ViewString("At least one of Qty/Price/TIF must change on an amend");
  private static final ZString REQ_TOO_OLD =
      new ViewString("Request is older than max allowed seconds ");
  private static final ZString EXDEST_MISMATCH =
      new ViewString("EXDEST doesnt match the instrument REC, received ");
  private static final ZString SECEX_MISMATCH =
      new ViewString("Security Exchange doesnt match the instrument REC, received ");
  private static final ZString INVALID_PRICE = new ViewString("Failed tick validation ");
  private static final ZString MISSING_TICK =
      new ViewString("Missing tick type for instrument, RIC=");
  private static final ZString RESTRICTED =
      new ViewString("Cant trade restricted stock, bookingType=");
  private static final ZString INSTRUMENT_DISABLED = new ViewString("Instrument is disabled, RIC=");

  private final ReusableString _err = new ReusableString(256);
  private final int _maxAgeMS;

  public EmeaDmaValidator(int maxAge) {
    _maxAgeMS = maxAge;
  }

  /** dont do validation that the exchange validator is doing */
  @Override
  public boolean validate(NewOrderSingle msg, Order order) {
    reset();

    final Instrument inst = msg.getInstrument();
    final Exchange ex = inst.getExchange();

    if (!inst.isTestInstrument()) { // dont validate test instruments at ALL
      final long now = System.currentTimeMillis();

      commonValidation(ex, msg, order, now);

      final OMExchangeValidator exchangeValidator =
          (OMExchangeValidator) ex.getExchangeEventValidator();

      // qty validation done in exchangevalidator

      if (exchangeValidator != null) exchangeValidator.validate(msg, _err, now);
    }

    return _err.length() == 0;
  }

  @Override
  public boolean validate(CancelReplaceRequest newVersion, Order order) {
    reset();

    final OrderVersion lastAcc = order.getLastAckedVerion();
    final OrderRequest previous = (OrderRequest) lastAcc.getBaseOrderRequest();
    final int cumQty = order.getLastAckedVerion().getCumQty();

    final Instrument inst = newVersion.getInstrument();
    final Exchange ex = inst.getExchange();

    if (!inst.isTestInstrument()) { // dont validate test instruments at ALL
      final long now = System.currentTimeMillis();
      final int newOrdQty = newVersion.getOrderQty();

      commonValidation(ex, newVersion, order, now);

      if (newVersion.getCurrency() != previous.getCurrency()) { // CANT CHANGE CCY
        delim()
            .append(UNABLE_TO_CHANGE_CCY)
            .append(Strings.FROM)
            .append(previous.getCurrency().toString())
            .append(Strings.TO)
            .append(newVersion.getCurrency().toString());
      }

      if (newVersion.getOrdType() != previous.getOrdType()) {
        delim()
            .append(UNABLE_TO_CHANGE_ORDTYPE)
            .append(Strings.FROM)
            .append(previous.getOrdType().toString())
            .append(Strings.TO)
            .append(newVersion.getOrdType().toString());
      }

      if (newVersion.getSide() != previous.getSide()) {
        delim()
            .append(UNABLE_TO_CHANGE_SIDE)
            .append(Strings.FROM)
            .append(previous.getSide().toString())
            .append(Strings.TO)
            .append(newVersion.getSide().toString());
      }

      if (!newVersion.getSymbol().equals(previous.getSymbol())) {
        delim()
            .append(UNABLE_TO_CHANGE_SYM)
            .append(Strings.FROM)
            .append(previous.getSymbol())
            .append(Strings.TO)
            .append(newVersion.getSymbol());
      }

      if (newVersion.getPrice() == previous.getPrice()
          && newOrdQty == previous.getOrderQty()
          && newVersion.getTimeInForce() == previous.getTimeInForce()) {
        delim().append(MAJOR_FIELDS_UNCHANGED);
      }

      if (newOrdQty < cumQty) {
        delim()
            .append(CANNOT_AMEND_BELOW_CQTY)
            .append(newOrdQty)
            .append(Strings.CUMQTY)
            .append(cumQty);
      }

      final OMExchangeValidator exchangeValidator =
          (OMExchangeValidator) ex.getExchangeEventValidator();
      if (exchangeValidator != null) {
        exchangeValidator.validate(newVersion, _err, now);
      }
    }

    return _err.length() == 0;
  }

  @Override
  public CxlRejReason getReplaceRejectReason() {
    return CxlRejReason.Other;
  }

  @Override
  public OrdRejReason getOrdRejectReason() {
    return OrdRejReason.UnsupOrdCharacteristic;
  }

  @Override
  public ViewString getRejectReason() {
    return _err;
  }

  private ReusableString delim() {
    if (_err.length() > 0) {
      _err.append(Strings.DELIM);
    }

    return _err;
  }

  private void addErrorUnsupported(Enum<?> val) {

    delim()
        .append(UNSUPPORTED)
        .append(val.toString())
        .append(Strings.TYPE)
        .append(val.getClass().getSimpleName());
  }

  private final void reset() {
    _err.reset();
  }

  private void validateHandlingInstruction(HandlInst handlInst) {
    if (handlInst == HandlInst.ManualBestExec) {
      addErrorUnsupported(handlInst);
    }
  }

  private void validateAge(int transactTime, long now) {
    if ((TimeZoneCalculator.instance().getTimeUTC(now) - transactTime) > _maxAgeMS) {
      delim().append(REQ_TOO_OLD).append(_maxAgeMS / 1000);
    }
  }

  private void commonValidation(Exchange ex, OrderRequest req, Order order, long now) {
    if (req.getClOrdId().length() == 0) addError(MISSING_CLORDID);

    validateHandlingInstruction(req.getHandlInst());
    validateAge(req.getTransactTime(), now);

    Instrument inst = req.getInstrument();

    final ViewString exDest = req.getExDest();
    final ViewString secEx = req.getSecurityExchange();
    final double price = order.getPendingVersion().getMarketPrice();

    if (exDest.length() > 0 && !ex.getRecCode().equals(exDest)) {
      delim()
          .append(EXDEST_MISMATCH)
          .append(exDest)
          .append(Strings.EXPECTED)
          .append(ex.getRecCode());
    }

    if (secEx.length() > 0 && !ex.getRecCode().equals(secEx)) {
      delim().append(SECEX_MISMATCH).append(secEx).append(Strings.EXPECTED).append(ex.getRecCode());
    }

    validateTicksize(inst, price);

    if (inst.isRestricted()
        && !canTradeRestricted(req.getClient(), req.getBookingType(), req.getOrderCapacity())) {
      delim()
          .append(RESTRICTED)
          .append(req.getBookingType())
          .append(Strings.ORDCAP)
          .append(req.getOrderCapacity());
    }

    if (!inst.isEnabled()) {
      delim().append(INSTRUMENT_DISABLED).append(inst.getRIC());
    }

    TradingRange band = inst.getValidTradingRange();

    band.valid(price, req.getSide().getIsBuySide(), _err);
  }

  private boolean canTradeRestricted(
      ClientProfile client, BookingType bookingType, OrderCapacity orderCapacity) {
    // TODO  THIS will be client specific

    return false;
  }

  private void addError(ZString msg) {
    delim().append(msg);
  }

  private void validateTicksize(Instrument instrument, double price) {
    TickType ts = instrument.getTickscale();

    if (ts.canVerifyPrice()) {
      if (!ts.isValid(price)) {

        delim().append(INVALID_PRICE);

        ts.writeError(price, _err);
      }
    } else {
      ReusableString msg = TLC.instance().pop();
      msg.append(MISSING_TICK).append(instrument.getRIC());
      _log.warn(msg);
      TLC.instance().pushback(msg);
    }
  }
}