import {
  TEST_PM_TIME,
  formatMilliseconds,
  parseMillisecondsIntoInteger,
  parseDigitsIntoInteger,
  getAmString,
  getPmString,
  getSeparator,
  searchAmOrPmToken
} from './helpers.js';
import { parseISOTime } from '@vaadin/time-picker/src/vaadin-time-picker-helper.js';

// Execute callback when predicate returns true.
// Try again later if predicate returns false.
function when(predicate, callback, timeout = 0) {
  if (predicate()) {
    callback();
  } else {
    setTimeout(() => when(predicate, callback, 200), timeout);
  }
}

function parseISO(text) {
  // The default i18n parser of the web component is ISO 8601 compliant.
  const timeObject = parseISOTime(text);

  // The web component returns an object with string values
  // while the connector expects number values.
  return {
    hours: parseInt(timeObject.hours || 0),
    minutes: parseInt(timeObject.minutes || 0),
    seconds: parseInt(timeObject.seconds || 0),
    milliseconds: parseInt(timeObject.milliseconds || 0)
  };
}

window.Vaadin.Flow.timepickerConnector = {};
window.Vaadin.Flow.timepickerConnector.initLazy = (timepicker) => {
  // Check whether the connector was already initialized for the timepicker
  if (timepicker.$connector) {
    return;
  }

  timepicker.$connector = {};

  timepicker.$connector.setLocale = (locale) => {
    // capture previous value if any
    let previousValueObject;
    if (timepicker.value && timepicker.value !== '') {
      previousValueObject = parseISO(timepicker.value);
    }

    try {
      // Check whether the locale is supported by the browser or not
      TEST_PM_TIME.toLocaleTimeString(locale);
    } catch (e) {
      locale = 'en-US';
      // FIXME should do a callback for server to throw an exception ?
      throw new Error(
        'vaadin-time-picker: The locale ' + locale + ' is not supported, falling back to default locale setting(en-US).'
      );
    }

    // 1. 24 or 12 hour clock, if latter then what are the am/pm strings ?
    const pmString = getPmString(locale);
    const amString = getAmString(locale);

    // 2. What is the separator ?
    const separator = getSeparator(locale);

    const includeSeconds = function () {
      return timepicker.step && timepicker.step < 60;
    };

    const includeMilliSeconds = function () {
      return timepicker.step && timepicker.step < 1;
    };

    let cachedTimeString;
    let cachedTimeObject;

    timepicker.i18n = {
      formatTime(timeObject) {
        if (!timeObject) return;

        const timeToBeFormatted = new Date();
        timeToBeFormatted.setHours(timeObject.hours);
        timeToBeFormatted.setMinutes(timeObject.minutes);
        timeToBeFormatted.setSeconds(timeObject.seconds !== undefined ? timeObject.seconds : 0);

        // the web component expects the correct granularity used for the time string,
        // thus need to format the time object in correct granularity by passing the format options
        let localeTimeString = timeToBeFormatted.toLocaleTimeString(locale, {
          hour: 'numeric',
          minute: 'numeric',
          second: includeSeconds() ? 'numeric' : undefined
        });

        // milliseconds not part of the time format API
        if (includeMilliSeconds()) {
          localeTimeString = formatMilliseconds(localeTimeString, timeObject.milliseconds, amString, pmString);
        }

        return localeTimeString;
      },

      parseTime(timeString) {
        if (timeString && timeString === cachedTimeString && cachedTimeObject) {
          return cachedTimeObject;
        }

        if (!timeString) {
          // when nothing is returned, the component shows the invalid state for the input
          return;
        }

        const amToken = searchAmOrPmToken(timeString, amString);
        const pmToken = searchAmOrPmToken(timeString, pmString);

        const numbersOnlyTimeString = timeString
          .replace(amToken || '', '')
          .replace(pmToken || '', '')
          .trim();

        // A regexp that allows to find the numbers with optional separator and continuing searching after it.
        const numbersRegExp = new RegExp('([\\d\\u0660-\\u0669]){1,2}(?:' + separator + ')?', 'g');

        let hours = numbersRegExp.exec(numbersOnlyTimeString);
        if (hours) {
          hours = parseDigitsIntoInteger(hours[0].replace(separator, ''));
          // handle 12 am -> 0
          // do not do anything if am & pm are not used or if those are the same,
          // as with locale bg-BG there is always ч. at the end of the time
          if (amToken !== pmToken) {
            if (hours === 12 && amToken) {
              hours = 0;
            }
            if (hours !== 12 && pmToken) {
              hours += 12;
            }
          }
          const minutes = numbersRegExp.exec(numbersOnlyTimeString);
          const seconds = minutes && numbersRegExp.exec(numbersOnlyTimeString);
          // detecting milliseconds from input, expects am/pm removed from end, eg. .0 or .00 or .000
          const millisecondRegExp = /[[\.][\d\u0660-\u0669]{1,3}$/;
          // reset to end or things can explode
          let milliseconds = seconds && includeMilliSeconds() && millisecondRegExp.exec(numbersOnlyTimeString);
          // handle case where last numbers are seconds and . is the separator (invalid regexp match)
          if (milliseconds && milliseconds['index'] <= seconds['index']) {
            milliseconds = undefined;
          }
          // hours is a number at this point, others are either arrays or null
          // the string in [0] from the arrays includes the separator too
          cachedTimeObject = hours !== undefined && {
            hours: hours,
            minutes: minutes ? parseDigitsIntoInteger(minutes[0].replace(separator, '')) : 0,
            seconds: seconds ? parseDigitsIntoInteger(seconds[0].replace(separator, '')) : 0,
            milliseconds:
              minutes && seconds && milliseconds ? parseMillisecondsIntoInteger(milliseconds[0].replace('.', '')) : 0
          };
          cachedTimeString = timeString;
          return cachedTimeObject;
        }
      }
    };

    if (previousValueObject) {
      when(
        () => timepicker.$,
        () => {
          const newValue = timepicker.i18n.formatTime(previousValueObject);
          // FIXME works but uses private API, needs fixes in web component
          if (timepicker.inputElement.value !== newValue) {
            timepicker.inputElement.value = newValue;
            timepicker.$.comboBox.value = newValue;
          }
        }
      );
    }
  };
};
