import * as Moo from 'moo';
import { CalendarEvent } from '../models/calendar-event';
import * as Luxon from 'luxon';
import { Logger } from '@nestjs/common';

export const DEFAULT_PARAMS: Moo.Rules = {
  WS: /[ \t]+/,
  delimiter: /(?: - |: )/,
  delimiter2: /(?:^\* |^- |(?!\d)T(?!>\d))/,
  date: /\b(?:(?:\d{2}\.\d{2}\.(?:\d{2}|\d{4}))|(?:\d{4}-\d{2}-\d{2}))\b/,
  time: /\b(?:\d{2}\b:\d{2}:\d{2}|\d{2}:\d{2})\b/,
  word: /[\wА-Яа-я]+/,
  other: { match: /./, lineBreaks: true },
  NL: { match: /\n/, lineBreaks: true },
};

export const DEFAULT_DATE_FORMATS = ['yyyy-MM-dd', 'dd.MM.yyyy', 'dd.MM.yy'];

export const DEFAULT_TIME_FORMATS = ['HH:mm:ss', 'HH:mm'];

export const DEFAULT_EVENT_DURATION = 60 * 60 * 1000; // 1 hour in millis

const logger = new Logger('string-with-dates-parser');

export function parse(str: string, params?: Moo.Rules): Moo.Token[] {
  if (!params) params = DEFAULT_PARAMS;
  try {
    const lexer = Moo.compile(params);
    lexer.reset(str);
    const res: Moo.Token[] = [];
    let token = lexer.next();
    while (token) {
      res.push(token);
      token = lexer.next();
    }
    return res;
  } catch (ex) {
    logger.error(
      `Error at parse str=${str} with params=${params}, error message - ${ex}`,
    );
    return [];
  }
}

export type ParserOpts = {
  rules?: Moo.Rules;
  dateFormats?: string[];
  timeFormats?: string[];
};

export const DEFAULT_PARSER_OPTS: ParserOpts = {
  rules: DEFAULT_PARAMS,
  dateFormats: DEFAULT_DATE_FORMATS,
  timeFormats: DEFAULT_TIME_FORMATS,
};

export function parseDate(
  str: string,
  formats?: string[],
): Luxon.DateTime | null {
  if (!str) return null;
  if (!formats) formats = DEFAULT_DATE_FORMATS;
  let res: Luxon.DateTime;
  for (let i = 0; i < formats.length; i++) {
    const format = formats[i];
    res = Luxon.DateTime.fromFormat(str, format);
    if (res.isValid) return res;
  }
  return null;
}

export function parseTime(
  str: string,
  formats?: string[],
): Luxon.Duration | null {
  if (!str) return null;
  if (!formats) formats = DEFAULT_TIME_FORMATS;
  let res: Luxon.DateTime;
  for (let i = 0; i < formats.length; i++) {
    const format = formats[i];
    res = Luxon.DateTime.fromFormat(str, format);
    if (res.isValid) {
      const startOfDay = res.set({
        hour: 0,
        minute: 0,
        second: 0,
        millisecond: 0,
      });
      return res.diff(startOfDay).shiftToAll();
    }
  }
  return null;
}

export function parseToCalendarEvent(
  str: string,
  opts?: ParserOpts,
): CalendarEvent | null {
  if (!opts) opts = DEFAULT_PARSER_OPTS;
  const tokens = parse(str, opts?.rules);

  const words = tokens.filter((i) => i.type === 'word');

  const dates = tokens.filter((i) => i.type === 'date');
  if (dates.length < 0) return null;
  const date1 = parseDate(dates[0]?.value, opts?.dateFormats);
  const date2 = parseDate(dates[1]?.value, opts?.dateFormats);

  const times = tokens.filter((i) => i.type === 'time');
  const time1 = parseTime(times[0]?.value, opts?.timeFormats);
  const time2 = parseTime(times[1]?.value, opts?.timeFormats);

  let from: Luxon.DateTime;
  let to: Luxon.DateTime;
  let fullDay: boolean;

  if (date1 && time1) {
    from = date1.set({
      hour: time1.hours,
      minute: time1.minutes,
      second: time1.seconds,
    });
    fullDay = false;
  } else if (date1) {
    from = date1;
    fullDay = true;
  } else {
    return null;
  }

  if (date2 && time2) {
    to = date2.set({
      hour: time2.hours,
      minute: time2.minutes,
      second: time2.seconds,
    });
  } else if (date2) {
    to = date2.plus({ day: 1 });
  } else if (fullDay) {
    to = from.plus({ day: 1 });
  } else if (time2) {
    to = date1.set({
      hour: time2.hours,
      minute: time2.minutes,
      second: time2.seconds,
    });
  } else {
    to = from.plus(Luxon.Duration.fromMillis(DEFAULT_EVENT_DURATION));
  }

  return {
    from: from.toISO(),
    fromTimestamp: from.toMillis(),
    to: to.toISO(),
    toTimestamp: to.toMillis(),
    description: words.join(' '),
    fullDay: fullDay,
  };
}
