/**
 * Copyright (c) 2019 The xterm.js authors. All rights reserved.
 * @license MIT
 */

import { EventEmitter } from 'common/EventEmitter';
import { Disposable } from 'common/Lifecycle';
import { isMac } from 'common/Platform';
import { CursorStyle, IDisposable } from 'common/Types';
import { FontWeight, IOptionsService, ITerminalOptions } from 'common/services/Services';

export const DEFAULT_OPTIONS: Readonly<Required<ITerminalOptions>> = {
  cols: 80,
  rows: 24,
  cursorBlink: false,
  cursorStyle: 'block',
  cursorWidth: 1,
  cursorInactiveStyle: 'outline',
  customGlyphs: true,
  drawBoldTextInBrightColors: true,
  fastScrollModifier: 'alt',
  fastScrollSensitivity: 5,
  fontFamily: 'courier-new, courier, monospace',
  fontSize: 15,
  fontWeight: 'normal',
  fontWeightBold: 'bold',
  ignoreBracketedPasteMode: false,
  lineHeight: 1.0,
  letterSpacing: 0,
  linkHandler: null,
  logLevel: 'info',
  logger: null,
  scrollback: 1000,
  scrollOnUserInput: true,
  scrollSensitivity: 1,
  screenReaderMode: false,
  smoothScrollDuration: 0,
  macOptionIsMeta: false,
  macOptionClickForcesSelection: false,
  minimumContrastRatio: 1,
  disableStdin: false,
  allowProposedApi: false,
  allowTransparency: false,
  tabStopWidth: 8,
  theme: {},
  rightClickSelectsWord: isMac,
  windowOptions: {},
  windowsMode: false,
  windowsPty: {},
  wordSeparator: ' ()[]{}\',"`',
  altClickMovesCursor: true,
  convertEol: false,
  termName: 'xterm',
  cancelEvents: false,
  overviewRulerWidth: 0
};

const FONT_WEIGHT_OPTIONS: Extract<FontWeight, string>[] = ['normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900'];

export class OptionsService extends Disposable implements IOptionsService {
  public serviceBrand: any;

  public readonly rawOptions: Required<ITerminalOptions>;
  public options: Required<ITerminalOptions>;

  private readonly _onOptionChange = this.register(new EventEmitter<keyof ITerminalOptions>());
  public readonly onOptionChange = this._onOptionChange.event;

  constructor(options: Partial<ITerminalOptions>) {
    super();
    // set the default value of each option
    const defaultOptions = { ...DEFAULT_OPTIONS };
    for (const key in options) {
      if (key in defaultOptions) {
        try {
          const newValue = options[key];
          defaultOptions[key] = this._sanitizeAndValidateOption(key, newValue);
        } catch (e) {
          console.error(e);
        }
      }
    }

    // set up getters and setters for each option
    this.rawOptions = defaultOptions;
    this.options = { ... defaultOptions };
    this._setupOptions();
  }

  // eslint-disable-next-line @typescript-eslint/naming-convention
  public onSpecificOptionChange<T extends keyof ITerminalOptions>(key: T, listener: (value: ITerminalOptions[T]) => any): IDisposable {
    return this.onOptionChange(eventKey => {
      if (eventKey === key) {
        listener(this.rawOptions[key]);
      }
    });
  }

  // eslint-disable-next-line @typescript-eslint/naming-convention
  public onMultipleOptionChange(keys: (keyof ITerminalOptions)[], listener: () => any): IDisposable {
    return this.onOptionChange(eventKey => {
      if (keys.indexOf(eventKey) !== -1) {
        listener();
      }
    });
  }

  private _setupOptions(): void {
    const getter = (propName: string): any => {
      if (!(propName in DEFAULT_OPTIONS)) {
        throw new Error(`No option with key "${propName}"`);
      }
      return this.rawOptions[propName];
    };

    const setter = (propName: string, value: any): void => {
      if (!(propName in DEFAULT_OPTIONS)) {
        throw new Error(`No option with key "${propName}"`);
      }

      value = this._sanitizeAndValidateOption(propName, value);
      // Don't fire an option change event if they didn't change
      if (this.rawOptions[propName] !== value) {
        this.rawOptions[propName] = value;
        this._onOptionChange.fire(propName);
      }
    };

    for (const propName in this.rawOptions) {
      const desc = {
        get: getter.bind(this, propName),
        set: setter.bind(this, propName)
      };
      Object.defineProperty(this.options, propName, desc);
    }
  }

  private _sanitizeAndValidateOption(key: string, value: any): any {
    switch (key) {
      case 'cursorStyle':
        if (!value) {
          value = DEFAULT_OPTIONS[key];
        }
        if (!isCursorStyle(value)) {
          throw new Error(`"${value}" is not a valid value for ${key}`);
        }
        break;
      case 'wordSeparator':
        if (!value) {
          value = DEFAULT_OPTIONS[key];
        }
        break;
      case 'fontWeight':
      case 'fontWeightBold':
        if (typeof value === 'number' && 1 <= value && value <= 1000) {
          // already valid numeric value
          break;
        }
        value = FONT_WEIGHT_OPTIONS.includes(value) ? value : DEFAULT_OPTIONS[key];
        break;
      case 'cursorWidth':
        value = Math.floor(value);
        // Fall through for bounds check
      case 'lineHeight':
      case 'tabStopWidth':
        if (value < 1) {
          throw new Error(`${key} cannot be less than 1, value: ${value}`);
        }
        break;
      case 'minimumContrastRatio':
        value = Math.max(1, Math.min(21, Math.round(value * 10) / 10));
        break;
      case 'scrollback':
        value = Math.min(value, 4294967295);
        if (value < 0) {
          throw new Error(`${key} cannot be less than 0, value: ${value}`);
        }
        break;
      case 'fastScrollSensitivity':
      case 'scrollSensitivity':
        if (value <= 0) {
          throw new Error(`${key} cannot be less than or equal to 0, value: ${value}`);
        }
        break;
      case 'rows':
      case 'cols':
        if (!value && value !== 0) {
          throw new Error(`${key} must be numeric, value: ${value}`);
        }
        break;
      case 'windowsPty':
        value = value ?? {};
        break;
    }
    return value;
  }
}

function isCursorStyle(value: unknown): value is CursorStyle {
  return value === 'block' || value === 'underline' || value === 'bar';
}
