import { inspect } from 'util';
import { toRegexRange, IOptions as IOptionsToRegexRange } from '@bluelovers/to-regex-range2';

export interface IOptions<V = string | number> extends IOptionsToRegexRange
{
  /**
   * The increment to use for the range. Can be used with letters or numbers.
   * @example
   * // numbers
   * console.log(fill('1', '10', 2)); //=> [ '1', '3', '5', '7', '9' ]
   * console.log(fill('1', '10', 3)); //=> [ '1', '4', '7', '10' ]
   * console.log(fill('1', '10', 4)); //=> [ '1', '5', '9' ]
   *
   * // letters
   * console.log(fill('a', 'z', 5)); //=> [ 'a', 'f', 'k', 'p', 'u', 'z' ]
   * console.log(fill('a', 'z', 7)); //=> [ 'a', 'h', 'o', 'v' ]
   * console.log(fill('a', 'z', 9)); //=> [ 'a', 'j', 's' ]
   */
  step?: number,

  /**
   * By default, null is returned when an invalid range is passed. Enable this option to throw a RangeError on invalid ranges.
   */
  strictRanges?: boolean,

  /**
   * Cast all returned values to strings. By default, integers are returned as numbers.
   * @example
   * console.log(fill(1, 5));                    //=> [ 1, 2, 3, 4, 5 ]
   * console.log(fill(1, 5, { stringify: true })); //=> [ '1', '2', '3', '4', '5' ]
   *
   */
  stringify?: boolean,
  /**
   * Create a regex-compatible source string, instead of expanding values to an array.
   * @example
   * // alphabetical range
   * console.log(fill('a', 'e', { toRegex: true })); //=> '[a-e]'
   * // alphabetical with step
   * console.log(fill('a', 'z', 3, { toRegex: true })); //=> 'a|d|g|j|m|p|s|v|y'
   * // numerical range
   * console.log(fill('1', '100', { toRegex: true })); //=> '[1-9]|[1-9][0-9]|100'
   * // numerical range with zero padding
   * console.log(fill('000001', '100000', { toRegex: true }));
   * //=> '0{5}[1-9]|0{4}[1-9][0-9]|0{3}[1-9][0-9]{2}|0{2}[1-9][0-9]{3}|0[1-9][0-9]{4}|100000'
   */
  toRegex?: boolean,

  /**
   * Customize each value in the returned array (or string). (you can also pass this function as the last argument to fill()).
   * @example
   * // add zero padding
   * console.log(fill(1, 5, value => String(value).padStart(4, '0')));
   * //=> ['0001', '0002', '0003', '0004', '0005']
   */
  transform?(val: number, index?: number): V,

  /**
   * set limit size
   */
  limit?: number,
  /**
   * only allow start < stop
   */
  strictOrder?: boolean,
}

interface IParts
{
  negatives: number[],
  positives: number[],
}

const enum EnumNegative
{
  negative = '-',
  none = '',
}

function isObject(val: unknown): val is IOptions {
  return val !== null && typeof val === 'object' && !Array.isArray(val);
}

const transform = (toNumber: boolean) => {
  if (toNumber === true) return value => Number(value);
  return value => String(value);
};

const isValidValue = (value): value is number | string => {
  return typeof value === 'number' || (typeof value === 'string' && value !== '');
};

const isNumber = (num: unknown): num is number => Number.isInteger(+num);

const zeros = input => {
  let value = `${input}`;
  let index = -1;
  if (value[0] === '-') value = value.slice(1);
  if (value === '0') return false;
  while (value[++index] === '0');
  return index > 0;
};

const stringify = (start, end, options: IOptions) => {
  if (typeof start === 'string' || typeof end === 'string') {
    return true;
  }
  return options.stringify === true;
};

const pad = (input: any, maxLength: number, toNumber: boolean) => {
  if (maxLength > 0) {
    input = toMaxLen(input, maxLength);
  }
  if (toNumber === false) {
    return String(input);
  }
  return input;
};

const toMaxLen = (input: string, _maxLength: number) => {
  let { result, negative, maxLength } = _prefixNegative(input, _maxLength);
  return negative + result.padStart(maxLength, '0')
};

function _partsSort(part: number[])
{
  part.sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
}

function _partsCapturePrefix(options: IOptions)
{
  return options.capture ? '' as const : '?:' as const;
}

function _prefixNegative(input: string, maxLength: number)
{
  const negative = input[0] === EnumNegative.negative ? EnumNegative.negative : EnumNegative.none;

  if (negative === EnumNegative.negative)
  {
    input = input.slice(1);
    maxLength--;
  }

  return {
    result: input,
    negative,
    maxLength,
  }
}

function _join(part: (string|number)[])
{
  return part.join('|')
}

const toSequence = (parts: IParts, options: IOptions) => {
  _partsSort(parts.negatives);
  _partsSort(parts.positives);

  let prefix = _partsCapturePrefix(options);
  let positives = '';
  let negatives = '';
  let result: string;

  if (parts.positives.length) {
    positives = _join(parts.positives);
  }

  if (parts.negatives.length) {
    negatives = `-(${prefix}${_join(parts.negatives)})`;
  }

  if (positives && negatives) {
    result = `${positives}|${negatives}`;
  } else {
    result = positives || negatives;
  }

  if (options.wrap) {
    return `(${prefix}${result})`;
  }

  return result;
};

const toRange = (a, b, isNumbers, options: IOptions) => {
  if (isNumbers) {
    return toRegexRange(a, b, { wrap: false, ...options });
  }

  const start = String.fromCharCode(a);
  if (a === b) return start;

  const stop = String.fromCharCode(b);
  return `[${start}-${stop}]`;
};

const toRegex = (start, end, options: IOptions): string => {
  if (Array.isArray(start)) {
    const wrap = options.wrap === true;
    const prefix = _partsCapturePrefix(options);
    start = _join(start);
    return wrap ? `(${prefix}${start})` : start;
  }
  return toRegexRange(start, end, options);
};

const rangeError = (...args) => {
  // @ts-ignore
  return new RangeError('Invalid range arguments: ' + inspect(...args));
};

const invalidRange = (start, end, options: IOptions): string[] => {
  if (options.strictRanges === true) throw rangeError([start, end], options);
  return [];
};

const invalidStep = (step, options: IOptions): string[] => {
  if (options.strictRanges === true) {
    throw new TypeError(`Expected step "${step}" to be a number`);
  }
  return [];
};

function _handleLimit(options: IOptions)
{
  return options.limit > 0 ? options.limit! : Infinity;
}

function _handleStep(step: number)
{
  return Math.max(Math.abs(step), 1)
}

function _handleOptions(opts: IOptions, clone?: boolean)
{
  if (clone === true)
  {
    opts = { ...opts };
  }
  if (opts.capture === true) opts.wrap = true;
  return opts;
}

function _handleDescending(start: number, end: number, options: IOptions)
{
  const descending = start > end;

  if (descending === true && options.strictOrder)
  {
    throw rangeError([start, end], options);
  }

  return descending
}

const fillNumbers = (start, end, step = 1, options: IOptions = {}): string[] | string => {
  let a = Number(start);
  let b = Number(end);

  if (!Number.isInteger(a) || !Number.isInteger(b)) {
    if (options.strictRanges === true) throw rangeError([start, end], options);
    return [];
  }

  // fix negative zero
  if (a === 0) a = 0;
  if (b === 0) b = 0;

  const descending = _handleDescending(a, b, options);
  const startString = String(start);
  const endString = String(end);
  const stepString = String(step);
  step = _handleStep(step);

  const padded = zeros(startString) || zeros(endString) || zeros(stepString);
  const maxLen = padded ? Math.max(startString.length, endString.length, stepString.length) : 0;
  const toNumber = padded === false && stringify(start, end, options) === false;
  const format = options.transform || transform(toNumber);

  if (options.toRegex && step === 1) {
    return toRange(toMaxLen(String(start), maxLen), toMaxLen(String(end), maxLen), true, options);
  }

  const parts: IParts = { negatives: [], positives: [] };
  const push = num => parts[num < 0 ? 'negatives' : 'positives'].push(Math.abs(num));
  const range: any[] = [];
  let index = 0;
  const limit = _handleLimit(options);

  while (descending ? a >= b : a <= b) {
    if (options.toRegex === true && step > 1) {
      push(a);
    } else {
      range.push(pad(format(a, index), maxLen, toNumber));
    }
    a = descending ? a - step : a + step;
    index++;
    if (index >= limit) break;
  }

  if (options.toRegex === true) {
    return step > 1
      ? toSequence(parts, options)
      : toRegex(range, null, { wrap: false, ...options });
  }

  return range;
};

function fillLetters(start, end, step: number, options: IOptions & {
  toRegex: true,
}): string
function fillLetters<V>(start, end, step?: number, options?: IOptions<V>): V[] | string
function fillLetters(start, end, step = 1, options: IOptions = {}): any[] | string
{
  if ((!isNumber(start) && start.length > 1) || (!isNumber(end) && end.length > 1))
  {
    return invalidRange(start, end, options) as any;
  }

  const format = options.transform || (val => String.fromCharCode(val));
  let a = `${start}`.charCodeAt(0);
  let b = `${end}`.charCodeAt(0);

  const descending = _handleDescending(a, b, options);
  const min = Math.min(a, b);
  const max = Math.max(a, b);

  if (options.toRegex === true && step === 1)
  {
    return toRange(min, max, false, options);
  }

  const range: any[] = [];
  let index = 0;
  const limit = _handleLimit(options);

  while (descending ? a >= b : a <= b)
  {
    range.push(format(a, index));
    a = descending ? a - step : a + step;
    index++;
    if (index >= limit) break;
  }

  if (options.toRegex === true)
  {
    return toRegex(range, null, { wrap: false, ...options });
  }

  return range;
}

export function fill<V = number | string>(start: number | string,
  end: number | string,
  step: IOptions<V> & {
    toRegex?: false,
  },
  options?: never
): V[]
export function fill<V = number | string>(start: number | string,
  end: number | string,
  step: number,
  options?: IOptions<V> & {
    toRegex?: false,
  }
): V[]
export function fill<V = number | string>(start: number | string,
  end: number | string,
  step: IOptions<V>["transform"],
  options?: never
): V[]
export function fill(start: number | string,
  end: number | string,
  step: IOptions & {
  toRegex: true,
  },
  options?: IOptions
): string
export function fill(start: number | string,
  end: number | string,
  step: number | IOptions["transform"],
  options: IOptions & {
    toRegex: true,
  }
): string
export function fill<R extends any[] | string = string[] | string>(start: number | string,
  end?: number | string,
  step?: number | IOptions["transform"] | IOptions,
  options?: IOptions
): R
export function fill(start: number | string, end?: number | string, step?: number | IOptions["transform"] | IOptions, options: IOptions = {}): any[] | string
{
  const _s = isValidValue(start);
  if ((typeof end === 'undefined' || end === null) && _s)
  {
    return [start] as any;
  }

  if (!_s || !isValidValue(end))
  {
    return invalidRange(start, end, options);
  }

  if (typeof step === 'function')
  {
    //return fill(start, end, 1, { transform: step });
    [step, options] = [1, { transform: step }];
  }

  if (isObject(step))
  {
    //return fill(start, end, 0, step);
    [step, options] = [0, step];
  }

  let opts: IOptions = options;
  step = step || opts.step || 1;

  if (!isNumber(step))
  {
    if (step != null && !isObject(step)) return invalidStep(step, opts);
    //return fill(start, end, 1, step as IOptions);
    [step, opts] = [1, opts];
  }

  opts = _handleOptions(opts, true);

  if (isNumber(start) && isNumber(end))
  {
    return fillNumbers(start, end, step, opts);
  }

  return fillLetters(start, end, _handleStep(step), opts);
}

Object.defineProperty(fill, '__esModule', { value: true });
Object.defineProperty(fill, 'fill', { value: fill });
Object.defineProperty(fill, 'default', { value: fill });

export default fill;
