import chalk from 'chalk';
import { convertHSVtoRGB, getColor, type CustomLoggerColor } from './internal.ts';
import { defaultSpinnerOptions } from './Spinner.ts';
import { isUnicodeSupported } from './console.ts';
import { Widget } from './Widget.ts';

const boxChars = [' ', '▏', '▎', '▍', '▌', '▋', '▊', '▉'];
const fullBox = '█';
const asciiChars = {
  start: '[',
  end: ']',
  empty: ' ',
  fill: '=',
};

/**
 * This function is derived from an old program I wrote back in 2020 called `f` which did ffmpeg
 * handling. It is probably one of the coolest progress bars ever imagined. The original had the HSL
 * colors baked in, but this one doesn't do that.
 *
 * For those interested:
 * https://github.com/paperdave/f/blob/fcc418f11c7abe979ec01d90d5fdf7e50fb6ec25/src/render.ts.
 */
function getUnicodeBar(progress: number, width: number) {
  if (progress >= 1) {
    return fullBox.repeat(width);
  }
  if (progress <= 0 || isNaN(progress)) {
    return ' '.repeat(width);
  }

  const wholeWidth = Math.floor(progress * width);
  const remainderWidth = (progress * width) % 1;
  const partWidth = Math.floor(remainderWidth * 8);
  let partChar = boxChars[partWidth];
  if (width - wholeWidth - 1 < 0) {
    partChar = '';
  }

  const fill = fullBox.repeat(wholeWidth);
  const empty = ' '.repeat(width - wholeWidth - 1);

  return `${fill}${partChar}${empty}`;
}

/** Get an ascii progress bar. Boring. */
function getAsciiBar(progress: number, width: number) {
  return [
    asciiChars.start,
    asciiChars.fill.repeat(Math.floor(progress * (width - 2))),
    asciiChars.empty.repeat(width - Math.ceil(progress * (width - 2))),
    asciiChars.end,
  ].join('');
}

/** A Progress Bar Style. Ascii is forced in non-unicode terminals. */
export type BarStyle = 'unicode' | 'ascii';

/** Options to be passed to `new Progress` */
export interface ProgressOptions<Props extends Record<string, unknown> = Record<never, unknown>> {
  /** Text displayed to the right of the bar. */
  text: string | ((props: ExtendedProps<Props>) => string);
  /** Text displayed to the left of the bar, if specified. */
  beforeText?: string | ((props: ExtendedProps<Props>) => string);
  /** Properties to be passed to `text` and `beforeText` formatting functions. */
  props?: Props;
  /** Width of the progress bar itself. Default: 35. */
  barWidth?: number;
  /** Progress bar style, default `BarStyle.Unicode` */
  barStyle?: BarStyle;
  /** Spinner settings. Set to `null` to disable the spinner. */
  spinner?: Partial<BarSpinnerOptions> | null;
  /** Starting value. Default: 0. */
  value?: number;
  /** Ending value. Default: 100. */
  total?: number;
}

export interface BarSpinnerOptions {
  /** Frames per second of the Spinner. */
  fps: number;
  /** Sequence of frames for the spinner. */
  frames: string[];
  /** Color of the spinner. If set to `match` it will match the bar. */
  color: CustomLoggerColor | 'match';
}

const defaultOptions = {
  beforeText: '',
  barWidth: 35,
  barColor: 'rgb',
  barStyle: 'unicode',
  spinner: {
    ...defaultSpinnerOptions,
    color: 'match',
  },
  value: 0,
  total: 100,
} as const;

type ExtendedProps<T> = T & {
  value: number;
  total: number;
  /** Number 0-1, inclusive. */
  progress: number;
};

export class Progress<Props extends Record<string, unknown> = Record<never, unknown>> extends Widget {
  #text: string | ((props: ExtendedProps<Props>) => string);
  #beforeText: string | ((props: ExtendedProps<Props>) => string);
  #barWidth: number;
  #barStyle: NonNullable<ProgressOptions['barStyle']>;
  #spinnerColor: NonNullable<BarSpinnerOptions['color']>;
  #spinnerFrames?: readonly string[];
  #props: Props;
  #spinnerFPS: number;
  #value: number;
  #total: number;
  fps: number;

  constructor(options: ProgressOptions<Props> | string) {
    super();

    if (typeof options === 'string') {
      options = { text: options };
    }

    this.#text = options.text;
    this.#beforeText = options.beforeText ?? defaultOptions.beforeText;
    this.#barWidth = options.barWidth ?? defaultOptions.barWidth;
    this.#barStyle = options.barStyle ?? defaultOptions.barStyle;
    this.#props = options.props ?? ({} as Props);
    this.#value = options.value ?? defaultOptions.value;
    this.#total = options.total ?? defaultOptions.total;

    // Undefined will trigger the "no spinner"
    // eslint-disable-next-line eqeqeq
    if (options.spinner !== null) {
      this.fps = 15;
      this.#spinnerFPS = options.spinner?.fps ?? defaultOptions.spinner.fps;
      this.#spinnerFrames = options.spinner?.frames ?? defaultOptions.spinner.frames;
      this.#spinnerColor = options.spinner?.color ?? defaultOptions.spinner.color;
    } else {
      this.fps = 0;
      this.#spinnerFPS = defaultOptions.spinner.fps;
      this.#spinnerFrames = undefined;
      this.#spinnerColor = defaultOptions.spinner.color;
    }
  }

  /** Properties to be passed to `text` and `beforeText` formatting functions. */
  set props(value: Partial<Props>) {
    this.#props = {
      ...this.#props,
      ...value,
    };
    this.redraw();
  }

  get props(): ExtendedProps<Props> {
    return {
      ...this.#props,
      value: this.#value,
      total: this.#total,
      progress: this.#total === 0 ? 1 : this.#value / this.#total,
    };
  }

  /** Text displayed to the right of the bar. */
  get text(): string {
    return typeof this.#text === 'function' ? this.#text(this.props) : this.#text;
  }

  set text(value: string | (() => string)) {
    this.#text = value;
    this.redraw();
  }

  /** Text displayed to the left of the bar, if specified. */
  get beforeText(): string {
    return typeof this.#beforeText === 'function' ? this.#beforeText(this.props) : this.#beforeText;
  }

  set beforeText(value: string | (() => string)) {
    this.#beforeText = value;
    this.redraw();
  }
  /** Current value of progress bar. */
  get value() {
    return this.#value;
  }

  set value(value: number) {
    this.#value = value;
    this.redraw();
  }

  /** Total value of progress bar. When value === total, the bar is full. */
  get total() {
    return this.#total;
  }

  set total(value: number) {
    this.#total = value;
    this.redraw();
  }

  /** Updates the progress bar with a new value and props. */
  update(value: number, props?: Partial<Props>) {
    this.#value = value;
    if (props) {
      this.#props = {
        ...this.props,
        ...props,
      };
    }
    this.redraw();
  }

  format(now: number): string {
    const progress = this.#total === 0 ? 1 : this.#value / this.#total;

    const hue = Math.min(Math.max(progress, 0), 1) / 3;
    const barColor = chalk
      .rgb(...convertHSVtoRGB(hue, 0.8, 1))
      .bgRgb(...convertHSVtoRGB(hue, 0.8, 0.5));

    let spinner;
    if (this.#spinnerFrames) {
      const frame = Math.floor(now / (1000 / this.#spinnerFPS)) % this.#spinnerFrames.length;
      spinner = this.#spinnerColor
        ? (this.#spinnerColor === 'match'
          ? chalk.rgb(...convertHSVtoRGB(hue, 0.8, 1))
          : getColor(this.#spinnerColor))(this.#spinnerFrames[frame])
        : this.#spinnerFrames[frame];
    }

    const getBar = isUnicodeSupported && this.#barStyle === 'unicode' ? getUnicodeBar : getAsciiBar;

    const beforeText = this.beforeText;

    return [
      spinner ? spinner + ' ' : '',
      beforeText ? beforeText + ' ' : '',
      barColor(getBar(progress, this.#barWidth)),
      ' ',
      this.text,
    ]
      .filter(Boolean)
      .join('');
  }

  success(message?: string): void {
    super.success(message ?? this.text);
  }

  error(message?: string | Error): void {
    super.error(message ?? this.text);
  }
}

export interface WithProgressOptions<Props extends Record<string, unknown>, T>
  extends ProgressOptions<Props> {
  /** Message to print on success. If a function, the result is passed. */
  successText?: string | ((result: T) => string);
  /** Message to print on fail. If a function, the error is passed. */
  failureText?: string | ((error: Error) => string);
}

/** Calls a function with a progress bar. */
export async function withProgress<Props extends Record<string, unknown>, T>(
  opts: WithProgressOptions<Props, T> | string,
  fn: (bar: Progress<Props>) => Promise<T>
): Promise<T>;
export async function withProgress(opts: any, fn: any) {
  const bar = new Progress(opts);

  try {
    const result = await fn(bar);
    bar.success(
      opts.successText
        ? typeof opts.successText === 'function'
          ? opts.successText(result)
          : opts.successText
        : opts.text
          ? typeof opts.text === 'function'
            ? opts.text(bar.props)
            : opts.text
          : 'Completed'
    );
  } catch (error: any) {
    bar.error(
      typeof opts.failureText === 'function' ? opts.failureText(error) : opts.failureText ?? error
    );
    throw error;
  }
}