import type { Ansis } from 'ansis';
import type { Merge } from 'type-fest';

import update from 'log-update';

import { bold, neonGreen, pink } from './colors';
import { Header, Prefix } from './write';

type SpinnerStyles = 'brielle' | 'arrows' | 'spinning'

export interface SpinnerOptions {
  /**
   * Whether or not a line prefix is applied
   *
   * @default true
   * @example  │ ⠋
   */
  line?: boolean;
  /**
   * An actionable spinner renders differently and will impose
   * an arrows spinner style.
   *
   * **Examples**
   *
   * ```bash
   * # when input param is passed
   * │ input → before ▹▹▹▹▹ after
   *
   * # when input param is omitted
   * │ before ▹▹▹▹▹ after
   *
   * ```
   *
   * @default null
   */
  action?: {
    /**
     * The arrows loading color, if `color` is passed it will inherit,
     * otherwise when set to defaults, it uses `neonGreen`
     *
     * @default 'neonGreen'
     */
    color?: Ansis;
    /**
     * The before label
     *
     * ```bash
     * before ▹▹▹▹▹
     * ```
     */
    before: string;
    /**
     * The after label
     *
     * ```bash
     * ▹▹▹▹▹ after
     * ```
     */
    after: string;
  };
  /**
   * The spinner color - If `action` is provided, this will have no effect.
   *
   * @default 'pink'
   */
  color?: Ansis;
  /**
   * The spinner color  - If `action` is provided, this will be set to `arrows`
   *
   * ```bash
   * ◓ # spinning
   * ⠋ # brielle
   * ▹ # arrows (only used for actionable spinner)
   * ```
   *
   * @default 'spinning'
   */
  style?: SpinnerStyles;

}

export interface CLISpinner {
  /**
   * Render Spinner
   *
   * Loads the spinner. Optionally pass in text to append.
   *
   * **Passing no parametes**
   *
   * ```
   * │ ⠋
   * ```
   *
   * **Passing text parameter**
   *
   * ```
   * │ ⠋ input
   * ```
   *
   * **Passing spinning style**
   *
   * ```
   * │ ◓ input
   * ```
   *
   * **Passing action options with input**
   *
   * ```
   * │ input → before ▹▹▹▹▹ after
   * ```
   *
   * **Passing action options without input**
   *
   * ```
   * │ before ▹▹▹▹▹ after
   * ```
   *
   * ---
   *
   * @param text
   * The text to append on the right side of the spinner
   *
   * @param options
   * Spinner options
   *
   */
  (input?: SpinnerOptions | string, options?: SpinnerOptions): void;
  /**
   * Updates the text of the loader
   *
   * @param message
   * The new message to apply
   */
  update: (message: string) => void;
  /**
   * Clears the interval and stops the spinner. Optionally
   * provide preserve text, if none passed, line is cleared.
   *
   * @param message
   * Optional text to append
   */
  stop: (message?: string) => void;
  /**
   * Whether or not the spinner is running
   */
  readonly active?: boolean;
}

export function Spinner () {

  /**
   * The interval instance
   */
  let interval: NodeJS.Timeout;

  /**
   * Whether or not the spinner is running
   */
  let active: boolean = false;

  /**
   * The log message
   */
  let message: string = '';

  /**
   * Whether or not a tree line should apply
   */
  let tline: boolean = true;

  const { loaders } = Spinner;
  const defaults: Merge<SpinnerOptions, { label: string }> = {
    label: '',
    line: true,
    color: null,
    style: 'spinning',
    action: null
  };

  /**
   * TUI Spinner
   *
   * Generates a log spinner.
   */
  const spin: CLISpinner = function spin (input, settings) {

    let options: Merge<SpinnerOptions, { label: string }> = { ...defaults };

    if (typeof input === 'object') {

      options = Object.assign(options, input);

    } else if (typeof input === 'string') {

      options.label = input;

      if (typeof settings === 'object') {

        options = Object.assign(options, settings);

      }
    }

    active = true;
    tline = options.line;

    let color: Ansis;
    let frame: number = 0;
    let frames: string[];
    let size: number = 0;

    if (options.action !== null) {
      options.style = 'arrows';
      color = 'color' in options.action ? options.action.color : neonGreen;
      frames = loaders.arrows.frames;
      size = frames.length;

    } else {

      color = typeof options.color === 'function' ? options.color : pink;
      message = options.label;
      frames = loaders[options.style].frames;
      size = frames.length;

    }

    update.done();

    interval = setInterval(() => {

      if (!active) return;

      let label: string;

      if (options.action !== null) {

        const string = bold(options.action.before) +
        ' ' + frames[frame = ++frame % size] +
        ' ' + options.action.after;

        label = color(message !== '' ? Prefix(message, string) : string);

      } else {

        label = color(frames[frame = ++frame % size] + ' ' + message);
      }

      update(options.line ? Header(label) : label);

    }, loaders[options.style].interval);

  };

  spin.update = function (input: string) {

    message = input;

  };

  spin.stop = function (input?: string) {

    if (active === false) return;

    active = false;

    if (input) {
      update(tline ? Header(input) : input);
      update.done();
    } else {
      update.clear();
    }

    clearInterval(interval);

    interval = undefined;
    message = '';

  };

  Object.defineProperty(spin, 'active', { get () { return active; } });

  return spin;
}

Spinner.loaders = {
  dots: {
    interval: 100,
    frames: [
      '.',
      '..',
      '...',
      '....'
    ]
  },
  arrows: {
    interval: 120,
    frames: [
      '▹▹▹▹',
      '▸▹▹▹',
      '▹▸▹▹',
      '▹▹▸▹',
      '▹▹▹▸'
    ]
  },
  brielle: {
    interval: 80,
    frames: [
      '⠋',
      '⠙',
      '⠹',
      '⠸',
      '⠼',
      '⠴',
      '⠦',
      '⠧',
      '⠇',
      '⠏'
    ]
  },
  spinning: {
    interval: 80,
    frames: [
      '◐',
      '◓',
      '◑',
      '◒'
    ]
  }
};
