// Sshwifty - A Web SSH client
//
// Copyright (C) 2019 Rui NI <nirui@gmx.com>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program.  If not, see <https://www.gnu.org/licenses/>.

import Exception from "./exception.js";
import * as header from "./header.js";
import * as reader from "./reader.js";
import * as sender from "./sender.js";

export class Sender {
  /**
   * constructor
   *
   * @param {number} id ID of the stream
   * @param {sender.Sender} sd The data sender
   *
   */
  constructor(id, sd) {
    this.id = id;
    this.sender = sd;
    this.closed = false;
  }

  /**
   * Sends data to remote
   *
   * @param {number} marker binary marker
   * @param {Uint8Array} data data to be sent
   *
   * @throws {Exception} When the sender already been closed
   *
   */
  send(marker, data) {
    if (this.closed) {
      throw new Exception(
        "Sender already been closed. No data can be send",
        false
      );
    }

    let reqHeader = new header.Header(header.STREAM),
      stHeader = new header.Stream(0, 0),
      d = new Uint8Array(data.length + 3);

    reqHeader.set(this.id);
    stHeader.set(marker, data.length);

    d[0] = reqHeader.value();
    d.set(stHeader.buffer(), 1);
    d.set(data, 3);

    return this.sender.send(d);
  }

  /**
   * Send stream signals
   *
   * @param {number} signal Signal value
   *
   * @throws {Exception} When the sender already been closed
   *
   */
  signal(signal) {
    if (this.closed) {
      throw new Exception(
        "Sender already been closed. No signal can be send",
        false
      );
    }

    let reqHeader = new header.Header(signal);

    reqHeader.set(this.id);

    return this.sender.send(new Uint8Array([reqHeader.value()]));
  }

  /**
   * Send close signal and close current sender
   *
   */
  close() {
    if (this.closed) {
      return;
    }

    let r = this.signal(header.CLOSE);

    this.closed = true;

    return r;
  }
}

export class InitialSender {
  /**
   * constructor
   *
   * @param {number} id ID of the stream
   * @param {number} commandID ID of the command
   * @param {sender.Sender} sd The data sender
   *
   */
  constructor(id, commandID, sd) {
    this.id = id;
    this.command = commandID;
    this.sender = sd;
  }

  /**
   * Return how large the data can be
   *
   * @returns {number} Max data size
   *
   */
  static maxDataLength() {
    return header.InitialStream.maxDataSize();
  }

  /**
   * Sends data to remote
   *
   * @param {Uint8Array} data data to be sent
   *
   */
  send(data) {
    let reqHeader = new header.Header(header.STREAM),
      stHeader = new header.InitialStream(0, 0),
      d = new Uint8Array(data.length + 3);

    reqHeader.set(this.id);
    stHeader.set(this.command, data.length, true);

    d[0] = reqHeader.value();
    d.set(stHeader.buffer(), 1);
    d.set(data, 3);

    return this.sender.send(d);
  }
}

export class Stream {
  /**
   * constructor
   *
   * @param {number} id ID of the stream
   *
   */
  constructor(id) {
    this.id = id;
    this.command = null;
    this.isInitializing = false;
    this.isShuttingDown = false;
  }

  /**
   * Returns whether or not current stream is running
   *
   * @returns {boolean} True when it's running, false otherwise
   *
   */
  running() {
    return this.command !== null;
  }

  /**
   * Returns whether or not current stream is initializing
   *
   * @returns {boolean} True when it's initializing, false otherwise
   *
   */
  initializing() {
    return this.isInitializing;
  }

  /**
   * Unsets current stream
   *
   */
  clear() {
    this.command = null;
    this.isInitializing = false;
    this.isShuttingDown = false;
  }

  /**
   * Request the stream for a new command
   *
   * @param {number} commandID Command ID
   * @param {function} commandBuilder Function that returns a command
   * @param {sender.Sender} sd Data sender
   *
   * @throws {Exception} when stream already running
   *
   */
  run(commandID, commandBuilder, sd) {
    if (this.running()) {
      throw new Exception(
        "Stream already running, cannot accept new commands",
        false
      );
    }

    this.isInitializing = true;
    this.command = commandBuilder(new Sender(this.id, sd));

    return this.command.run(new InitialSender(this.id, commandID, sd));
  }

  /**
   * Called when initialization respond has been received
   *
   * @param {header.InitialStream} streamInitialHeader Stream Initial header
   *
   * @throws {Exception} When the stream is not running, or been shutting down
   *
   */
  initialize(hd) {
    if (!this.running()) {
      throw new Exception(
        "Cannot initialize a stream that is not running",
        false
      );
    }

    if (this.isShuttingDown) {
      throw new Exception(
        "Cannot initialize a stream that is about to shutdown",
        false
      );
    }

    this.command.initialize(hd);

    if (!hd.success()) {
      this.clear();

      return;
    }

    this.isInitializing = false;
  }

  /**
   * Called when Stream data has been received
   *
   * @param {header.Stream} streamHeader Stream header
   * @param {reader.Limited} rd Data reader
   *
   * @throws {Exception} When the stream is not running, or shutting down
   *
   */
  tick(streamHeader, rd) {
    if (!this.running()) {
      throw new Exception("Cannot tick a stream that is not running", false);
    }

    if (this.isShuttingDown) {
      throw new Exception(
        "Cannot tick a stream that is about to shutdown",
        false
      );
    }

    return this.command.tick(streamHeader, rd);
  }

  /**
   * Called when stream close request has been received
   *
   * @throws {Exception} When the stream is not running, or shutting down
   *
   */
  close() {
    if (!this.running()) {
      throw new Exception("Cannot close a stream that is not running", false);
    }

    if (this.isShuttingDown) {
      throw new Exception(
        "Cannot close a stream that is about to shutdown",
        false
      );
    }

    this.isShuttingDown = true;
    this.command.close();
  }

  /**
   * Called when stream completed respond has been received
   *
   * @throws {Exception} When stream isn't running, or not shutting down
   *
   */
  completed() {
    if (!this.running()) {
      throw new Exception("Cannot close a stream that is not running", false);
    }

    if (!this.isShuttingDown) {
      throw new Exception(
        "Can't complete current stream because Close " +
          "signal is not received",
        false
      );
    }

    this.command.completed();
    this.clear();
  }
}
