// Copyright 2015-2020 Swim inc.
//
// 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.

import {Builder} from "@swim/util";
import {AnyOutputSettings, OutputSettings} from "./OutputSettings";
import {Format} from "./Format";
import {OutputException} from "./OutputException";

/**
 * Non-blocking token stream writer.  `Output` enables incremental,
 * interruptible writing of network protocols and data formats.
 *
 * ### Output tokens
 * Output tokens are modeled as primitive numbers, commonly representing
 * Unicode code points, or raw octets; each `Output` implementation specifies
 * the semantic type of its tokens.
 *
 * ### Output states
 * An `Output` writer is always in one of three states: _cont_​inue, _full_,
 * or _done_.  The _cont_ state indicates that the stream is ready to write a
 * single token; the _full_ state indicates that the stream is unable to write
 * additional tokens at this time, but that the stream may logically resume at
 * some point in the future; and the _done_ state indicates that the stream has
 * terminated, and that [[bind]] will return the output result.  [[isCont]]
 * returns `true` when in the _cont_ state; [[isFull]] returns `true` when in
 * the _full_ state; and [[isDone]] returns `true` when in the _done_ state.
 *
 * ### Output results
 * An `Output` writer yields a value of type `T`, obtained via the [[bind]]
 * method, representing some implementation defined result of writing the
 * output.  For example, an `Output<string>` implementation may–but is not
 * required to–yield a `string` containing all code points written to the
 * output.
 *
 * ### Non-blocking behavior
 * `Output` writers never block.  An `Output` writer that would otherwise block
 * writing additional output instead enters the _full_ state, signaling the
 * output generator to back off producing the output, but to remain prepared to
 * produce additional output in the future.  An `Output` writer enters the
 * _done_ state when it encounters the final end of its output, signaling to
 * the output generator to stop producing.
 *
 * ### Output settings
 * An output generator may alter the tokens it produces based on its `Output`
 * writer's [[settings]].  Uses include pretty printing and styling generated
 * output.  [[OutputSettings]] subclasses can provide additional parameters
 * understood by specialized output producers.
 *
 * ### Cloning
 * An `Output` writer may be [[clone cloned]] to branch the token stream in an
 * implementation specified manner.  Not all `Output` implementations support
 * cloning.
 *
 * @see [[OutputSettings]]
 * @see [[Writer]]
 */
export abstract class Output<T = unknown> implements Builder<number, T> {
  /**
   * Returns `true` when the next [[write write(number)]] will succeed.
   * i.e. this `Output` is in the _cont_ state.
   */
  abstract isCont(): boolean;

  /**
   * Returns `true` when an immediate `write` will fail, but writes may succeed
   * at some point in the future.  i.e. this `Output` is in the _full_ state.
   */
  abstract isFull(): boolean;

  /**
   * Returns `true` when no `write` will ever again suucced.
   * i.e. this `Output` is in the _done_ state.
   */
  abstract isDone(): boolean;

  /**
   * Returns `true` when an immediate `write` will fail due to an
   * error with the token stream. i.e. this `Output` is in the `error` state.
   * When `true`, `trap()` will return the output error.
   */
  abstract isError(): boolean;

  /**
   * Returns `true` if this is a partial `Output` that will enter
   * the `full` state when it is unable to write additional tokens.
   */
  abstract isPart(): boolean;

  /**
   * Returns a partial `Output` equivalent to this `Output`, if
   * `isPart` is `true`; returns a final `Output` equivalent
   * to this `Output` if `isPart` is `false`. The caller's reference
   * to `this` `Output` should be replaced by the returned `Output`.
   */
  abstract isPart(isPart: boolean): Output<T>;

  /**
   * Writes a single `token` to the stream, if this `Output` is in the
   * _cont_ state.
   *
   * @return `this`
   * @throws [[OutputException]] if this `Output` is not in the _cont_ state.
   */
  abstract write(token: number): Output<T>;

  /**
   * Writes the code points of the given `string`.  Assumes this is a Unicode
   * `Output` writer with sufficient capacity.
   *
   * @return `this`
   * @throws [[OutputException]] if this `Output` exits the _cont_ state before
   *         the full `string` has been written.
   */
  abstract write(string: string): Output<T>;

  /**
   * Writes the code points of the given `string`, followed by the code points
   * of the `settings`' [[OutputSettings.lineSeparator line separator].
   * Assumes this is a Unicode `Output` writer with sufficient capacity.
   *
   * @return `this`
   * @throws [[OutputException]] if this `Output` exits the _cont_ state before
   *         the full `string` and line separator has been written.
   */
  writeln(string?: string): Output<T> {
    if (typeof string === "string") {
      this.write(string);
    }
    return this.write(this.settings().lineSeparator());
  }

  /**
   * Writes the code points of the human-readable [[Display]] string of the
   * given `object`.  Assumes this is a Unicode `Output` writer with sufficient
   * capacity.
   *
   * @return `this`
   * @throws [[OutputException]] if this `Output` exits the _cont_ state before
   *         the full display string has been written.
   */
  display(object: unknown): Output<T> {
    Format.display(object, this);
    return this;
  }

  /**
   * Writes the code points of the developer-readable [[Debug]] string of the
   * given `object`.  Assumes this is a Unicode `Output` writer with sufficient
   * capacity.
   *
   * @return `this`
   * @throws [[OutputException]] if this `Output` exits the _cont_ state before
   *         the full debug string has been written.
   */
  debug(object: unknown): Output<T> {
    Format.debug(object, this);
    return this;
  }

  /**
   * Writes any internally buffered state to the underlying output stream.
   */
  flush(): Output<T> {
    return this;
  }

  push(...tokens: number[]): void {
    const n = tokens.length;
    for (let i = 0; i < n; i += 1) {
      this.write(tokens[i]);
    }
  }

  /**
   * Returns the `OutputSettings` used to configure the behavior of output
   * producers that write to this `Output`.
   */
  abstract settings(): OutputSettings;

  /**
   * Updates the `settings` associated with this `Output`.
   *
   * @return `this`
   */
  abstract settings(settings: AnyOutputSettings): Output<T>;

  /**
   * Returns the implementation-defined result of writing the output.
   */
  abstract bind(): T;

  /**
   * Returns the output error. Only guaranteed to return an error when in the
   * _error_ state.
   *
   * @throws OutputException if this `Output` is not in the _error_ state.
   */
  trap(): Error {
    throw new OutputException();
  }

  /**
   * Returns an implementation-defined branch of the token stream.
   *
   * @throws `Error` if this `Output` writer cannot be cloned.
   */
  clone(): Output<T> {
    throw new Error();
  }

  private static _full?: Output<any>;

  private static _done?: Output<any>;

  /**
   * Return an `Output` in the _full_ state, that binds the given `value`,
   * with the given `settings`.
   */
  static full<T>(value: T | null = null, settings: OutputSettings = OutputSettings.standard()): Output<T> {
    if (value === null && settings === OutputSettings.standard()) {
      if (Output._full === void 0) {
        Output._full = new OutputFull(value, OutputSettings.standard());
      }
      return Output._full;
    }
    return new OutputFull<T>(value!, settings);

  }

  /**
   * Returns an `Output` in the _done_ state, that binds the given `value`,
   * with the given `settings`.
   */
  static done<T>(value: T | null = null, settings: OutputSettings = OutputSettings.standard()): Output<T> {
    if (value === null && settings === OutputSettings.standard()) {
      if (Output._done === void 0) {
        Output._done = new OutputDone(value, OutputSettings.standard());
      }
      return Output._done;
    }
    return new OutputDone<T>(value!, settings);
  }

  /**
   * Return an `Output` in the _error_ state, that binds the given `value`,
   * with the given `settings`.
   */
  static error<T>(error: Error, settings: OutputSettings = OutputSettings.standard()): Output<T> {
    return new OutputError<T>(error, settings);
  }
}

/** @hidden */
class OutputFull<T> extends Output<T> {
  /** @hidden */
  readonly _value: T;
  /** @hidden */
  readonly _settings: OutputSettings;

  constructor(value: T, settings: OutputSettings) {
    super();
    this._value = value;
    this._settings = settings;
  }

  isCont(): boolean {
    return false;
  }

  isFull(): boolean {
    return true;
  }

  isDone(): boolean {
    return false;
  }

  isError(): boolean {
    return false;
  }

  isPart(): boolean;
  isPart(isPart: boolean): Output<T>;
  isPart(isPart?: boolean): boolean | Output<T> {
    if (isPart === void 0) {
      return true;
    } else if (isPart) {
      return Output.done(this._value, this._settings);
    } else {
      return this;
    }
  }

  write(token: number): Output<T>;
  write(string: string): Output<T>;
  write(tokenOrString: number | string): Output<T> {
    return Output.error(new OutputException("full"), this._settings);
  }

  writeln(string?: string): Output<T> {
    return Output.error(new OutputException("full"), this._settings);
  }

  bind(): T {
    return this._value;
  }

  settings(): OutputSettings;
  settings(settings: OutputSettings): Output<T>;
  settings(settings?: OutputSettings): OutputSettings | Output<T> {
    if (settings === void 0) {
      return this._settings;
    } else {
      return Output.full(this._value, settings);
    }
  }

  clone(): Output<T> {
    return this;
  }
}

/** @hidden */
class OutputDone<T> extends Output<T> {
  /** @hidden */
  readonly _value: T;
  /** @hidden */
  readonly _settings: OutputSettings;

  constructor(value: T, settings: OutputSettings) {
    super();
    this._value = value;
    this._settings = settings;
  }

  isCont(): boolean {
    return false;
  }

  isFull(): boolean {
    return false;
  }

  isDone(): boolean {
    return true;
  }

  isError(): boolean {
    return false;
  }

  isPart(): boolean;
  isPart(isPart: boolean): Output<T>;
  isPart(isPart?: boolean): boolean | Output<T> {
    if (isPart === void 0) {
      return false;
    } else if (isPart) {
      return this;
    } else {
      return Output.full(this._value, this._settings);
    }
  }

  write(token: number): Output<T>;
  write(string: string): Output<T>;
  write(tokenOrString: number | string): Output<T> {
    return Output.error(new OutputException("done"), this._settings);
  }

  writeln(string?: string): Output<T> {
    return Output.error(new OutputException("done"), this._settings);
  }

  bind(): T {
    return this._value;
  }

  settings(): OutputSettings;
  settings(settings: OutputSettings): Output<T>;
  settings(settings?: OutputSettings): OutputSettings | Output<T> {
    if (settings === void 0) {
      return this._settings;
    } else {
      return Output.done(this._value, settings);
    }
  }

  clone(): Output<T> {
    return this;
  }
}

/** @hidden */
class OutputError<T> extends Output<T> {
  /** @hidden */
  readonly _error: Error;
  /** @hidden */
  readonly _settings: OutputSettings;

  constructor(error: Error, settings: OutputSettings) {
    super();
    this._error = error;
    this._settings = settings;
  }

  isCont(): boolean {
    return false;
  }

  isFull(): boolean {
    return false;
  }

  isDone(): boolean {
    return false;
  }

  isError(): boolean {
    return true;
  }

  isPart(): boolean;
  isPart(isPart: boolean): Output<T>;
  isPart(isPart?: boolean): boolean | Output<T> {
    if (isPart === void 0) {
      return false;
    } else {
      return this;
    }
  }

  write(token: number): Output<T>;
  write(string: string): Output<T>;
  write(tokenOrString: number | string): Output<T> {
    return this;
  }

  writeln(string?: string): Output<T> {
    return this;
  }

  bind(): T {
    throw new OutputException();
  }

  trap(): Error {
    return this._error;
  }

  settings(): OutputSettings;
  settings(settings: OutputSettings): Output<T>;
  settings(settings?: OutputSettings): OutputSettings | Output<T> {
    if (settings === void 0) {
      return this._settings;
    } else {
      return Output.error(this._error, settings);
    }
  }

  clone(): Output<T> {
    return this;
  }
}
