/*
 * Copyright 2015 Julien Viet
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package io.termd.core.telnet;

import java.util.Arrays;
import java.util.concurrent.TimeUnit;

/**
* @author <a href="mailto:julien@julienviet.com">Julien Viet</a>
*/
public abstract class TelnetConnection {

  public static final byte BYTE_IAC = (byte)  0xFF;
  public static final byte BYTE_DONT = (byte) 0xFE;
  public static final byte BYTE_DO = (byte)   0xFD;
  public static final byte BYTE_WONT = (byte) 0xFC;
  public static final byte BYTE_WILL = (byte) 0xFB;
  public static final byte BYTE_SB = (byte)   0xFA;
  public static final byte BYTE_SE = (byte)   0xF0;

  private byte[] pendingBuffer = new byte[256];
  private int pendingLength = 0;
  Status status;
  Byte paramsOptionCode;
  byte[] paramsBuffer;
  int paramsLength;
  boolean paramsIac;
  boolean sendBinary;
  boolean receiveBinary;
  final TelnetHandler handler;

  public TelnetConnection(TelnetHandler handler) {
    this.status = Status.DATA;
    this.paramsOptionCode = null;
    this.paramsBuffer = null;
    this.paramsIac = false;
    this.sendBinary = false;
    this.receiveBinary = false;
    this.handler = handler;
  }

  private void appendToParams(byte b) {
    while (paramsLength >= paramsBuffer.length) {
      paramsBuffer = Arrays.copyOf(paramsBuffer, paramsBuffer.length + 100);
    }
    paramsBuffer[paramsLength++] = b;
  }

  public void onInit() {
    handler.onOpen(this);
  }

  public abstract void close();

  /**
   * Write a <i>do</i> option request to the client.
   *
   * @param option the option to send
   */
  public final void writeDoOption(Option option) {
    send(new byte[]{BYTE_IAC, BYTE_DO, option.code});
  }

  /**
   * Write a do <i>will</i> request to the client.
   *
   * @param option the option to send
   */
  public final void writeWillOption(Option option) {
    send(new byte[]{BYTE_IAC, BYTE_WILL, option.code});
  }

  private void rawWrite(byte[] data, int offset, int length) {
    if (length > 0) {
      if (offset == 0 && length == data.length) {
        send(data);
      } else {
        byte[] chunk = new byte[length];
        System.arraycopy(data, offset, chunk, 0, chunk.length);
        send(chunk);
      }
    }
  }

  protected abstract void execute(Runnable task);

  protected abstract void schedule(Runnable task, long delay, TimeUnit unit);

  protected abstract void send(byte[] data);

  public void receive(byte[] data) {
    for (byte b : data) {
      status.handle(this, b);
    }
    flushDataIfNecessary();
  }


  /**
   * Write data to the client, escaping data if necessary or truncating it. The original buffer can
   * be mutated if incorrect data is provided.
   *
   * @param data the data to write
   */
  public final void write(byte[] data) {
    if (sendBinary) {
      int prev = 0;
      for (int i = 0;i < data.length;i++) {
        if (data[i] == -1) {
          rawWrite(data, prev, i - prev);
          send(new byte[]{-1, -1});
          prev = i + 1;
        }
      }
      rawWrite(data, prev, data.length - prev);
    } else {
      for (int i = 0;i < data.length;i++) {
        data[i] = (byte)(data[i] & 0x7F);
      }
      send(data);
    }
  }

  protected void onClose() {
    handler.onClose();
  }

  /**
   * Handle option <code>WILL</code> call back. The implementation will try to find a matching option
   * via the {@code Option#values()} and invoke it's {@link Option#handleWill(TelnetConnection)} method
   * otherwise a <code>DON'T</code> will be sent to the client.<p>
   *
   * This method can be subclassed to handle an option.
   *
   * @param optionCode the option code
   */
  protected void onOptionWill(byte optionCode) {
    for (Option option : Option.values()) {
      if (option.code == optionCode) {
        option.handleWill(this);
        return;
      }
    }
    send(new byte[]{BYTE_IAC, BYTE_DONT, optionCode});
  }

  /**
   * Handle option <code>WON'T</code> call back. The implementation will try to find a matching option
   * via the {@code Option#values()} and invoke it's {@link Option#handleWont(TelnetConnection)} method.<p>
   *
   * This method can be subclassed to handle an option.
   *
   * @param optionCode the option code
   */
  protected void onOptionWont(byte optionCode) {
    for (Option option : Option.values()) {
      if (option.code == optionCode) {
        option.handleWont(this);
        return;
      }
    }
  }

  /**
   * Handle option <code>DO</code> call back. The implementation will try to find a matching option
   * via the {@code Option#values()} and invoke it's {@link Option#handleDo(TelnetConnection)} method
   * otherwise a <code>WON'T</code> will be sent to the client.<p>
   *
   * This method can be subclassed to handle an option.
   *
   * @param optionCode the option code
   */
  protected void onOptionDo(byte optionCode) {
    for (Option option : Option.values()) {
      if (option.code == optionCode) {
        option.handleDo(this);
        return;
      }
    }
    send(new byte[]{BYTE_IAC, BYTE_WONT, optionCode});
  }

  /**
   * Handle option <code>DON'T</code> call back. The implementation will try to find a matching option
   * via the {@code Option#values()} and invoke it's {@link Option#handleDont(TelnetConnection)} method.<p>
   *
   * This method can be subclassed to handle an option.
   *
   * @param optionCode the option code
   */
  protected void onOptionDont(byte optionCode) {
    for (Option option : Option.values()) {
      if (option.code == optionCode) {
        option.handleDont(this);
        return;
      }
    }
  }

  /**
   * Handle option parameters call back. The implementation will try to find a matching option
   * via the {@code Option#values()} and invoke it's {@link Option#handleParameters(TelnetConnection, byte[])} method.
   *
   * This method can be subclassed to handle an option.
   *
   * @param optionCode the option code
   */
  protected void onOptionParameters(byte optionCode, byte[] parameters) {
    for (Option option : Option.values()) {
      if (option.code == optionCode) {
        option.handleParameters(this, parameters);
        return;
      }
    }
  }

  /**
   * Append a byte in the {@link #pendingBuffer} buffer. When the {@link #pendingBuffer} buffer is full, data
   * is flushed.
   *
   * @param b the byte
   * @see #flushData()
   */
  private void appendData(byte b) {
    if (pendingLength >= pendingBuffer.length) {
      flushData();
    }
    pendingBuffer[pendingLength++] = b;
  }

  /**
   * Flush the {@link #pendingBuffer} buffer when it is not empty.
   *
   * @see #flushData()
   */
  private void flushDataIfNecessary() {
    if (pendingLength > 0) {
      flushData();
    }
  }

  /**
   * Flush the {@link #pendingBuffer} buffer to {@link TelnetHandler#onData(byte[])}.
   */
  private void flushData() {
    byte[] data = Arrays.copyOf(pendingBuffer, pendingLength);
    pendingLength = 0;
    handler.onData(data);
  }

  enum Status {

    DATA() {
      @Override
      void handle(TelnetConnection session, byte b) {
        if (b == BYTE_IAC) {
          if (session.receiveBinary) {
            session.status = ESC;
          } else {
            session.flushDataIfNecessary();
            session.status = IAC;
          }
        } else {
          session.appendData(b);
        }
      }
    },

    ESC() {
      @Override
      void handle(TelnetConnection session, byte b) {
        if (b == BYTE_IAC) {
          session.appendData((byte)-1);
        } else {
          session.flushDataIfNecessary();
          IAC.handle(session, b);
        }
      }
    },

    IAC() {
      @Override
      void handle(TelnetConnection session, byte b) {
        if (b == BYTE_DO) {
          session.status = DO;
        } else if (b == BYTE_DONT) {
          session.status = DONT;
        } else if (b == BYTE_WILL) {
          session.status = WILL;
        } else if (b == BYTE_WONT) {
          session.status = WONT;
        } else if (b == BYTE_SB) {
          session.paramsBuffer = new byte[100];
          session.paramsLength = 0;
          session.status = SB;
        } else {
          session.handler.onCommand(b);
          session.status = DATA;
        }
      }
    },

    SB() {
      @Override
      void handle(TelnetConnection session, byte b) {
        if (session.paramsOptionCode == null) {
          session.paramsOptionCode = b;
        } else {
          if (session.paramsIac) {
            session.paramsIac = false;
            if (b == BYTE_SE) {
              try {
                session.onOptionParameters(session.paramsOptionCode, Arrays.copyOf(session.paramsBuffer, session.paramsLength));
              } finally {
                session.paramsOptionCode = null;
                session.paramsBuffer = null;
                session.status = DATA;
              }
            } else if (b == BYTE_IAC) {
              session.appendToParams((byte) -1);
            }
          } else {
            if (b == BYTE_IAC) {
              session.paramsIac = true;
            } else {
              session.appendToParams(b);
            }
          }
        }
      }
    },

    DO() {
      @Override
      void handle(TelnetConnection session, byte b) {
        try {
          session.onOptionDo(b);
        } finally {
          session.status = DATA;
        }
      }
    },

    DONT() {
      @Override
      void handle(TelnetConnection session, byte b) {
        try {
          session.onOptionDont(b);
        } finally {
          session.status = DATA;
        }
      }
    },

    WILL() {
      @Override
      void handle(TelnetConnection session, byte b) {
        try {
          session.onOptionWill(b);
        } finally {
          session.status = DATA;
        }
      }
    },

    WONT() {
      @Override
      void handle(TelnetConnection session, byte b) {
        try {
          session.onOptionWont(b);
        } finally {
          session.status = DATA;
        }
      }
    },

    ;

    abstract void handle(TelnetConnection session, byte b);
  }
}
