/**
 * The reactor that will oversee the management of all network events. In contrast to other reactor
 * implementations, this one runs on the main game thread selecting and handling network events
 * either asynchronously or right on the underlying thread.
 *
 * @author lare96 <http://github.com/lare96>
 */
public final class ServerReactor {

  /** The logger that will print important information. */
  private final Logger logger = LoggerUtils.getLogger(ServerReactor.class);

  /** The selector that will select network events. */
  private final Selector selector;

  /** The socket channel that the server will listen on. */
  private final ServerSocketChannel channel;

  /**
   * Creates a new {@link ServerReactor}.
   *
   * @param selector the selector that will select network events.
   * @param channel the socket channel that the server will listen on.
   */
  public ServerReactor(Selector selector, ServerSocketChannel channel) {
    this.selector = selector;
    this.channel = channel;
  }

  /**
   * Loops through all of the selected network events and determines which ones are ready to be
   * handled, and executes them if so.
   */
  public void sequence() {
    try {
      selector.selectNow();
      Iterator<SelectionKey> $it = selector.selectedKeys().iterator();
      while ($it.hasNext()) {
        ServerSelectionKey key = new ServerSelectionKey($it.next(), selector, channel);
        Optional<ServerSelectionEvent> event = key.determineEvent();
        event.ifPresent(e -> e.execute(key));
        $it.remove();
      }
    } catch (Exception e) {
      logger.log(Level.SEVERE, "An error has occured while selecting network events!", e);
    }
  }
}
/**
 * The session handler dedicated to a player that will handle input and output operations.
 *
 * @author lare96 <http://github.com/lare96>
 * @author blakeman8192
 */
public final class PlayerIO {

  /** The logger that will print important information. */
  private static Logger logger = LoggerUtils.getLogger(PlayerIO.class);

  /** The byte buffer for reading data from the client. */
  private final ByteBuffer inData = ByteBuffer.allocateDirect(512);

  /** The byte buffer for writing data to the client. */
  private final ByteBuffer outData = ByteBuffer.allocateDirect(8192);

  /** The selection key registered to the selector. */
  private final SelectionKey key;

  /** The socket channel for sending and receiving raw data. */
  private final SocketChannel channel;

  /** The player I/O operations will be executed for. */
  private final Player player;

  /** The host address this session is bound to. */
  private final String host;

  /** The login protocol decoder chain of events. */
  private final LoginProtocolDecoderChain chain;

  /** The stopwatch that determines when this I/O session will timeout. */
  private final Stopwatch timeout = new Stopwatch();

  /** The amount of packets that have been decoded this sequence. */
  private final MutableNumber packetCount = new MutableNumber();

  /** The current state of this I/O session. */
  private IOState state = IOState.CONNECTED;

  /** The current login response for this session. */
  private LoginResponse response;

  /** The opcode of the packet currently being decoded. */
  private int packetOpcode = -1;

  /** The size of the packet currently being decoded. */
  private int packetSize = -1;

  /** The encryptor that will encode sent packets. */
  private ISAACCipher encryptor;

  /** The decryptor that will decode received packets. */
  private ISAACCipher decryptor;

  /** The flag that determines if the player disconnected while sending data. */
  private boolean packetDisconnect;

  /** The flag that determines if the player disconnected while in combat. */
  private boolean combatLogout;

  /**
   * Creates a new {@link PlayerIO}.
   *
   * @param key the selection key registered to the selector.
   * @param response the current login response for this session.
   */
  public PlayerIO(SelectionKey key, LoginResponse response) {
    this.key = key;
    this.response = response;
    this.channel = (SocketChannel) key.channel();
    this.host = channel.socket().getInetAddress().getHostAddress().toLowerCase();
    this.player = new Player(this);
    this.chain =
        new LoginProtocolDecoderChain(2)
            .append(new HandshakeLoginDecoder(this))
            .append(new PostHandshakeLoginDecoder(this));
  }

  @Override
  public String toString() {
    return "SESSION[host= " + host + ", state= " + state.name() + "]";
  }

  /**
   * Disconnects this session from the server by canceling the registered key and closing the socket
   * channel.
   *
   * @param forced if the session must be disconnected because of an IO issue.
   */
  public void disconnect(boolean forced) {
    try {
      if (!forced && player.getCombatBuilder().inCombat()) {
        combatLogout = true;
        key.attach(null);
        key.cancel();
        channel.close();
        World.submit(
            new Task(150, false) {
              @Override
              public void execute() {
                if (!player.getCombatBuilder().inCombat()) {
                  disconnect(true);
                  this.cancel();
                }
              }
            });
        return;
      }
      packetDisconnect = forced;
      if (state == IOState.LOGGED_IN) {
        if (player.getOpenShop() != null)
          Shop.SHOPS.get(player.getOpenShop()).getPlayers().remove(player);
        World.getTaskQueue().cancel(player.getCombatBuilder());
        World.getTaskQueue().cancel(player);
        player.setSkillAction(false);
        World.getPlayers().remove(player);
        MinigameHandler.execute(player, m -> m.onLogout(player));
        player.getTradeSession().reset(false);
        player.getPrivateMessage().updateOtherList(false);
        if (FightCavesHandler.remove(player)) player.move(new Position(2399, 5177));
        player.save();
      }
      key.attach(null);
      key.cancel();
      channel.close();
      ConnectionHandler.remove(host);
      logger.info(
          state == IOState.LOGGED_IN
              ? player + " has logged " + "out."
              : this + " has logged out.");
      state = IOState.LOGGED_OUT;
    } catch (Exception e) {
      e.printStackTrace();
    }
  }

  /**
   * Sends a packet of data to the client through {@code buffer}.
   *
   * @param buffer the packet of data to send.
   */
  public void send(ByteBuffer buffer) {
    if (!channel.isOpen() || packetDisconnect || combatLogout) return;
    buffer.flip();
    try {
      channel.write(buffer);
    } catch (Exception ex) {
      ex.printStackTrace();
      disconnect(true);
    }
  }

  /**
   * Sends a packet of data to the client through {@code buffer}.
   *
   * @param buffer the packet of data to send.
   */
  public void send(DataBuffer buffer) {
    send(buffer.buffer());
  }

  /**
   * Gets the byte buffer for reading data from the client.
   *
   * @return the buffer for reading.
   */
  public ByteBuffer getInData() {
    return inData;
  }

  /**
   * Gets the byte buffer for writing data to the client.
   *
   * @return the buffer for writing.
   */
  public ByteBuffer getOutData() {
    return outData;
  }

  /**
   * Gets the selection key registered to the selector.
   *
   * @return the selection key.
   */
  public SelectionKey getKey() {
    return key;
  }

  /**
   * Gets the socket channel for sending and receiving raw data.
   *
   * @return the socket channel.
   */
  public SocketChannel getChannel() {
    return channel;
  }

  /**
   * Gets the player I/O operations will be executed for.
   *
   * @return the player I/O operations.
   */
  public Player getPlayer() {
    return player;
  }

  /**
   * Gets the host address this session is bound to.
   *
   * @return the host address.
   */
  public String getHost() {
    return host;
  }

  /**
   * Gets the login protocol decoder chain of events.
   *
   * @return the chain of events.
   */
  public LoginProtocolDecoderChain getChain() {
    return chain;
  }

  /**
   * Gets the stopwatch that determines when this I/O session will timeout.
   *
   * @return the stopwatch for determining timeout.
   */
  public Stopwatch getTimeout() {
    return timeout;
  }

  /**
   * Gets the amount of packets that have been decoded this sequence.
   *
   * @return the amount of packets decoded.
   */
  public MutableNumber getPacketCount() {
    return packetCount;
  }

  /**
   * Gets the current state of this I/O session.
   *
   * @return the current state.
   */
  public IOState getState() {
    return state;
  }

  /**
   * Sets the value for {@link PlayerIO#state}.
   *
   * @param state the new value to set.
   */
  public void setState(IOState state) {
    this.state = state;
  }

  /**
   * Gets the current login response for this session.
   *
   * @return the current login response.
   */
  public LoginResponse getResponse() {
    return response;
  }

  /**
   * Sets the value for {@link PlayerIO#response}.
   *
   * @param response the new value to set.
   */
  public void setResponse(LoginResponse response) {
    this.response = response;
  }

  /**
   * Gets the opcode of the packet currently being decoded.
   *
   * @return the opcode of the packet.
   */
  public int getPacketOpcode() {
    return packetOpcode;
  }

  /**
   * Sets the value for {@link PlayerIO#packetOpcode}.
   *
   * @param packetOpcode the new value to set.
   */
  public void setPacketOpcode(int packetOpcode) {
    this.packetOpcode = packetOpcode;
  }

  /**
   * Gets the size of the packet currently being decoded.
   *
   * @return the size of the packet.
   */
  public int getPacketSize() {
    return packetSize;
  }

  /**
   * Sets the value for {@link PlayerIO#packetSize}.
   *
   * @param packetSize the new value to set.
   */
  public void setPacketSize(int packetSize) {
    this.packetSize = packetSize;
  }

  /**
   * Gets the encryptor that will encode sent packets.
   *
   * @return the encryptor.
   */
  public ISAACCipher getEncryptor() {
    return encryptor;
  }

  /**
   * Sets the value for {@link PlayerIO#encryptor}.
   *
   * @param encryptor the new value to set.
   */
  public void setEncryptor(ISAACCipher encryptor) {
    this.encryptor = encryptor;
  }

  /**
   * Gets the decryptor that will decode received packets.
   *
   * @return the decryptor.
   */
  public ISAACCipher getDecryptor() {
    return decryptor;
  }

  /**
   * Sets the value for {@link PlayerIO#decryptor}.
   *
   * @param decryptor the new value to set.
   */
  public void setDecryptor(ISAACCipher decryptor) {
    this.decryptor = decryptor;
  }

  /**
   * Determines if the player disconnected while in combat.
   *
   * @return {@code true} if the player disconnected while in combat, {@code false} otherwise.
   */
  public boolean isCombatLogout() {
    return combatLogout;
  }
}