/*
** Copyright 2011-2013 Centreon
**
** Licensed under the Apache License, Version 2.0 (the "License");
** you may not use this file except in compliance with the License.
** You may obtain a copy of the License at
**
**     http://www.apache.org/licenses/LICENSE-2.0
**
** Unless required by applicable law or agreed to in writing, software
** distributed under the License is distributed on an "AS IS" BASIS,
** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
** See the License for the specific language governing permissions and
** limitations under the License.
**
** For more information : contact@centreon.com
*/

#include <cstdio>
#include <cstring>
#include "com/centreon/broker/time/daterange.hh"

using namespace com::centreon::broker::time;

/**
 *  Constructor.
 *
 *  @param[in] type The date range type.
 */
daterange::daterange(type_range type)
 :_month_end(0),
  _month_start(0),
  _month_day_end(0),
  _month_day_start(0),
  _skip_interval(0),
  _type(type),
  _week_day_end(0),
  _week_day_start(0),
  _week_day_end_offset(0),
  _week_day_start_offset(0),
  _year_end(0),
  _year_start(0) {

}

/**
 *  Copy constructor.
 *
 *  @param[in] right The object to copy.
 */
daterange::daterange(daterange const& right) {
  operator=(right);
}

/**
 *  Destructor.
 */
daterange::~daterange() {

}

/**
 *  Copy operator.
 *
 *  @param[in] right The object to copy.
 *
 *  @return This object.
 */
daterange& daterange::operator=(daterange const& right) {
  if (this != &right) {
    _month_end = right._month_end;
    _month_start = right._month_start;
    _month_day_end = right._month_day_end;
    _month_day_start = right._month_day_start;
    _skip_interval = right._skip_interval;
    _timeranges = right._timeranges;
    _type = right._type;
    _week_day_end = right._week_day_end;
    _week_day_start = right._week_day_start;
    _week_day_end_offset = right._week_day_end_offset;
    _week_day_start_offset = right._week_day_start_offset;
    _year_end = right._year_end;
    _year_start = right._year_start;
  }
  return (*this);
}

/**
 *  Equal operator.
 *
 *  @param[in] right The object to compare.
 *
 *  @return True if object is the same, otherwise false.
 */
bool daterange::operator==(daterange const& right) const throw () {
  return (_month_end == right._month_end
          && _month_start == right._month_start
          && _month_day_end == right._month_day_end
          && _month_day_start == right._month_day_start
          && _skip_interval == right._skip_interval
          && _timeranges == right._timeranges
          && _type == right._type
          && _week_day_end == right._week_day_end
          && _week_day_start == right._week_day_start
          && _week_day_end_offset == right._week_day_end_offset
          && _week_day_start_offset == right._week_day_start_offset
          && _year_end == right._year_end
          && _year_start == right._year_start);
}

/**
 *  Not equal operator.
 *
 *  @param[in] right The object to compare.
 *
 *  @return True if object is not the same, otherwise false.
 */
bool daterange::operator!=(daterange const& right) const throw () {
  return (!operator==(right));
}

/**
 *  Less-than operator.
 *
 *  @param[in] right Object to compare to.
 *
 *  @return True if this object is less than right.
 */
bool daterange::operator<(daterange const& right) const throw () {
  if (_month_end != right._month_end)
    return (_month_end < right._month_end);
  else if (_month_start != right._month_start)
    return (_month_start < right._month_start);
  else if (_month_day_end != right._month_day_end)
    return (_month_day_end < right._month_day_end);
  else if (_month_day_start != right._month_day_start)
    return (_month_day_start < right._month_day_start);
  else if (_skip_interval != right._skip_interval)
    return (_skip_interval < right._skip_interval);
  else if (_type != right._type)
    return (_type < right._type);
  else if (_week_day_end != right._week_day_end)
    return (_week_day_end < right._week_day_end);
  else if (_week_day_start != right._week_day_start)
    return (_week_day_start < right._week_day_start);
  else if (_week_day_end_offset != right._week_day_end_offset)
    return (_week_day_end_offset < right._week_day_end_offset);
  else if (_week_day_start_offset != right._week_day_start_offset)
    return (_week_day_start_offset < right._week_day_start_offset);
  else if (_year_end != right._year_end)
    return (_year_end < right._year_end);
  else if (_year_start != right._year_start)
    return (_year_start < right._year_start);
  return (_timeranges < right._timeranges);
}

/**
 *  Set month_end value.
 *
 *  @param[in] value The new month_end value.
 */
void daterange::month_end(unsigned int value) {
  _month_end = value;
}

/**
 *  Get month_end value.
 *
 *  @return The month_end value.
 */
unsigned int daterange::month_end() const throw () {
  return (_month_end);
}

/**
 *  Set month_start value.
 *
 *  @param[in] value The new month_start value.
 */
void daterange::month_start(unsigned int value) {
  _month_start = value;
}

/**
 *  Get month_start value.
 *
 *  @return The month_start value.
 */
unsigned int daterange::month_start() const throw () {
  return (_month_start);
}

/**
 *  Set month_day_end value.
 *
 *  @param[in] value The new month_day_end value.
 */
void daterange::month_day_end(int value) {
  _month_day_end = value;
}

/**
 *  Get month_day_end value.
 *
 *  @return The month_day_end value.
 */
int daterange::month_day_end() const throw () {
  return (_month_day_end);
}

/**
 *  Set month_day_start value.
 *
 *  @param[in] value The new month_day_start value.
 */
void daterange::month_day_start(int value) {
  _month_day_start = value;
}

/**
 *  Get month_day_start value.
 *
 *  @return The month_day_start value.
 */
int daterange::month_day_start() const throw () {
  return (_month_day_start);
}

/**
 *  Set skip_interval value.
 *
 *  @param[in] value The new skip_interval value.
 */
void daterange::skip_interval(unsigned int value) {
  _skip_interval = value;
}

/**
 *  Get skip_interval value.
 *
 *  @return The skip_interval value.
 */
unsigned int daterange::skip_interval() const throw () {
  return (_skip_interval);
}

/**
 *  Get timeranges value.
 *
 *  @return The timeranges value.
 */
void daterange::timeranges(std::list<timerange> const& value) {
  _timeranges = value;
}

/**
 *  Set timeranges value.
 *
 *  @param[in] value The new timeranges value.
 */
std::list<timerange> const& daterange::timeranges() const throw () {
  return (_timeranges);
}

/**
 *  Set type value.
 *
 *  @param[in] value The new type value.
 */
void daterange::type(type_range value) {
  _type = value;
}

/**
 *  Get type value.
 *
 *  @return The type value.
 */
daterange::type_range daterange::type() const throw () {
  return (_type);
}

/**
 *  Set week_day_end value.
 *
 *  @param[in] value The new week_day_end value.
 */
void daterange::week_day_end(unsigned int value) {
  _week_day_end = value;
}

/**
 *  Get week_day_end value.
 *
 *  @return The week_day_end value.
 */
unsigned int daterange::week_day_end() const throw () {
  return (_week_day_end);
}

/**
 *  Set week_day_start value.
 *
 *  @param[in] value The new week_day_start value.
 */
void daterange::week_day_start(unsigned int value) {
  _week_day_start = value;
}

/**
 *  Get week_day_start value.
 *
 *  @return The week_day_start value.
 */
unsigned int daterange::week_day_start() const throw () {
  return (_week_day_start);
}

/**
 *  Set week_day_end_offset value.
 *
 *  @param[in] value The new week_day_end_offset value.
 */
void daterange::week_day_end_offset(int value) {
  _week_day_end_offset = value;
}

/**
 *  Get week_day_end_offset value.
 *
 *  @return The week_day_end_offset value.
 */
int daterange::week_day_end_offset() const throw () {
  return (_week_day_end_offset);
}

/**
 *  Set week_day_start_offset value.
 *
 *  @param[in] value The new week_day_start_offset value.
 */
void daterange::week_day_start_offset(int value) {
  _week_day_start_offset = value;
}

/**
 *  Get week_day_start_offset value.
 *
 *  @return The week_day_start_offset value.
 */
int daterange::week_day_start_offset() const throw () {
  return (_week_day_start_offset);
}

/**
 *  Set year_end value.
 *
 *  @param[in] value The new year_end value.
 */
void daterange::year_end(unsigned int value) {
  _year_end = value;
}

/**
 *  Get year_end value.
 *
 *  @return The year_end value.
 */
unsigned int daterange::year_end() const throw () {
  return (_year_end);
}

/**
 *  Set year_start value.
 *
 *  @param[in] value The new year_start value.
 */
void daterange::year_start(unsigned int value) {
  _year_start = value;
}

/**
 *  Get year_start value.
 *
 *  @return The year_start value.
 */
unsigned int daterange::year_start() const throw () {
  return (_year_start);
}

// UTILITIES

/**
 *  Add a round number of days (expressed in seconds) to a date.
 *
 *  @param[in] middnight  Midnight of base day.
 *  @param[in] skip       Number of days to skip (in seconds).
 *
 *  @return Midnight of the day in skip seconds.
 */
static time_t _add_round_days_to_midnight(time_t midnight, time_t skip) {
  // Compute expected time with no DST.
  time_t next_day_time(midnight + skip);
  struct tm next_day;
  localtime_r(&next_day_time, &next_day);

  // There was a DST shift in between.
  if (next_day.tm_hour || next_day.tm_min || next_day.tm_sec) {
    /*
    ** The trick here is to move from midnight to noon, add the skip
    ** seconds and break time down in a tm structure. We're now sure to
    ** be in the proper day (DST shift is +-1h) we only have to reset
    ** time to midnight, convert back and we're done.
    */
    next_day_time += 12 * 60 * 60 + skip;
    localtime_r(&next_day_time, &next_day);
    next_day.tm_hour = 0;
    next_day.tm_min = 0;
    next_day.tm_sec = 0;
    next_day_time = mktime(&next_day);
  }

  return (next_day_time);
}

/**
 *  Returns a time (midnight) of particular (3rd, last) day in a given
 *  month.
 *
 *  @param[in] year      Year.
 *  @param[in] month     Month.
 *  @param[in] monthday  Day in month.
 *
 *  @return Requested timestamp, (time_t)-1 if conversion failed.
 */
static time_t calculate_time_from_day_of_month(
                int year,
                int month,
                int monthday) {
  time_t midnight;
  tm t;

  // Positive day (3rd day).
  if (monthday > 0) {
    t.tm_sec = 0;
    t.tm_min = 0;
    t.tm_hour = 0;
    t.tm_year = year;
    t.tm_mon = month;
    t.tm_mday = monthday;
    t.tm_isdst = -1;
    midnight = mktime(&t);

    // If we rolled over to the next month, time is invalid, assume the
    // user's intention is to keep it in the current month.
    if (t.tm_mon != month)
      midnight = (time_t)-1;
  }
  // Negative offset (last day, 3rd to last day).
  else {
    // Find last day in the month.
    int day(32);
    do {
      // Back up a day.
      --day;

      // Make the new time.
      t.tm_mon = month;
      t.tm_year = year;
      t.tm_mday = day;
      t.tm_isdst = -1;
      midnight = mktime(&t);
    } while ((midnight == (time_t)-1)
             || (t.tm_mon != month));

    // Now that we know the last day, back up more.
    t.tm_mon = month;
    t.tm_year = year;
    // Beware to roll over the whole month.
    if (-monthday >= t.tm_mday)
      t.tm_mday = 1;
    // -1 means last day of month, so add one to make this correct.
    else
      t.tm_mday += monthday + 1;
    t.tm_isdst = -1;
    midnight = mktime(&t);
  }

  return (midnight);
}

/**
 *  Returns a time (midnight) of particular (3rd, last) weekday in a
 *  given month.
 *
 *  @param[in] year            Year.
 *  @param[in] month           Month.
 *  @param[in] weekday         Target weekday.
 *  @param[in] weekday_offset  Weekday offset (1 is first, 2 is second,
 *                             -1 is last).
 *
 *  @return Requested timestamp, (time_t)-1 if conversion failed.
 */
static time_t calculate_time_from_weekday_of_month(
                int year,
                int month,
                int weekday,
                int weekday_offset) {
  // Compute first day of month (to get weekday).
  tm t;
  t.tm_sec = 0;
  t.tm_min = 0;
  t.tm_hour = 0;
  t.tm_year = year;
  t.tm_mon = month;
  t.tm_mday = 1;
  t.tm_isdst = -1;
  time_t midnight(mktime(&t));

  // How many days must we advance to reach the first instance of the
  // weekday this month ?
  int days(weekday - (t.tm_wday));
  if (days < 0)
    days += 7;

  // Positive offset (3rd thursday).
  if (weekday_offset > 0) {
    // How many weeks must we advance (no more than 5 possible).
    int weeks((weekday_offset > 5) ? 5 : weekday_offset);
    days += ((weeks - 1) * 7);

    // Make the new time.
    t.tm_mon = month;
    t.tm_year = year;
    t.tm_mday = days + 1;
    t.tm_isdst = -1;
    midnight = mktime(&t);

    // If we rolled over to the next month, time is invalid, assume the
    // user's intention is to keep it in the current month.
    if (t.tm_mon != month)
      midnight = (time_t)-1;
  }
  // Negative offset (last thursday, 3rd to last tuesday).
  else {
    // Find last instance of weekday in the month.
    days += (5 * 7);
    do {
      // Back up a week.
      days -= 7;

      // Make the new time.
      t.tm_mon = month;
      t.tm_year = year;
      t.tm_mday = days + 1;
      t.tm_isdst = -1;
      midnight = mktime(&t);
    } while ((midnight == (time_t)-1)
             || (t.tm_mon != month));

    // Now that we know the last instance of the weekday, back up more.
    days = ((weekday_offset + 1) * 7);
    t.tm_mon = month;
    t.tm_year = year;
    // Beware to roll over the whole month.
    if (-days >= t.tm_mday)
      t.tm_mday = t.tm_mday % 7;
    else
      t.tm_mday += days;
    t.tm_isdst = -1;
    midnight = mktime(&t);
  }

  return (midnight);
}

// TO TIME_T

/**
 *  Calculate start time and end time for date range calendar date.
 *
 *  @param[in]  ti     Time informations.
 *  @param[out] start  Variable to fill start time.
 *  @param[out] end    Variable to fill end time.
 *
 *  @return True on success, otherwise false.
 */
bool daterange::_calendar_date_to_time_t(time_t& start,
                                         time_t& end) const {
  tm t;
  t.tm_sec = 0;
  t.tm_min = 0;
  t.tm_hour = 0;
  t.tm_isdst = -1;

  t.tm_mday = _month_day_start;
  t.tm_mon = _month_start;
  t.tm_year =  _year_start - 1900;
  if ((start = mktime(&t)) == (time_t)-1)
    return (false);

  if (_year_end) {
    t.tm_mday = _month_day_end;
    t.tm_mon = _month_end;
    t.tm_year = _year_end - 1900;
    t.tm_hour = 23;
    t.tm_min = 59;
    t.tm_sec = 59;
    /*
    ** We don't have to care about DST even if it would occur at
    ** midnight because end time is used as the upper bound of the date
    ** range. This make it invalid for use as midnight but perfectly
    ** valid to check that we're less than or equal to 23:59:59, which
    ** value is provided by mktime().
    */
    if ((end = mktime(&t)) == (time_t)-1)
      return (false);
    ++end;
  }
  else
    end = (time_t)-1;

  return (true);
}

/**
 *  Calculate start time and end time for date range month date.
 *
 *  @param[in]  ti     Time informations.
 *  @param[out] start  Variable to fill start time.
 *  @param[out] end    Variable to fill end time.
 *
 *  @return True on success, otherwise false.
 */
bool daterange::_month_date_to_time_t(time_info const& ti,
                                      time_t& start,
                                      time_t& end) const {
  // what year should we use?
  int year = ti.preftime.tm_year;
  //int year(std::max(ti.preftime.tm_year, ti.curtime.tm_year));
  // advance an additional year if we already passed
  // the end month date
  /*if (_month_end < ti.curtime.tm_mon
      || (_month_end == ti.curtime.tm_mon
          && _month_day_end < ti.curtime.tm_mday))
    ++year;*/
  start = calculate_time_from_day_of_month(year, _month_start, _month_day_start);

  // start date was bad.
  if (!start)
    return (false);

  // use same year as was calculated for start time above
  end = calculate_time_from_day_of_month(year, _month_end, _month_day_end);
  // advance a year if necessary: august 5 - february 2
  if (end < start)
    end = calculate_time_from_day_of_month( ++year, _month_end, _month_day_end);

  // end date was bad - see if we can handle the error
  if (!end) {
    // end date can't be helped, so skip it
    if (_month_day_end < 0)
      return (false);
    // else end date slipped past end of month, so use last
    // day of month as end date
    end = calculate_time_from_day_of_month(year, _month_end, -1);
  }
  return (true);
}

/**
 *  Calculate start time and end time for date range month day.
 *
 *  @param[in]  ti     Time informations.
 *  @param[out] start  Variable to fill start time.
 *  @param[out] end    Variable to fill end time.
 *
 *  @return True on success, otherwise false.
 */
bool daterange::_month_day_to_time_t(time_info const& ti,
                                     time_t& start,
                                     time_t& end) const {
  // What year/month should we use ?
  int year;
  int month;
  year = ti.preftime.tm_year;
  month = ti.preftime.tm_mon;
  // Advance an additional month (and possibly the year) if
  // we already passed the end day of month.
  if (ti.preftime.tm_mday > _month_day_end) {
    if (month != 11)
      ++month;
    else {
      month = 0;
      ++year;
    }
  }

  // Compute start date.
  start = calculate_time_from_day_of_month(year, month, _month_day_start);
  if (start == (time_t)-1)
    return (false);

  // Use same year and month as was calculated for start time above.
  end = calculate_time_from_day_of_month(year, month, _month_day_end);
  if (end == (time_t)-1) {
    // End date can't be helped, so skip it.
    if (_month_day_end < 0)
      return (false);

    // Else end date slipped past end of month,
    // so use first day of next month.
    if (month != 11)
      ++month;
    else {
      month = 0;
      ++year;
    }
    end = calculate_time_from_day_of_month(year, month, 0);
  }
  else
    end = _add_round_days_to_midnight(end, 24 * 60 * 60);
  return (true);
}

/**
 *  Calculate start time and end time for date range month week day.
 *
 *  @param[in]  ti     Time informations.
 *  @param[out] start  Variable to fill start time.
 *  @param[out] end    Variable to fill end time.
 *
 *  @return True on success, otherwise false.
 */
bool daterange::_month_week_day_to_time_t(time_info const& ti,
                                         time_t& start,
                                         time_t& end) const {
  // What year should we use?
  int year  = ti.preftime.tm_year;

  while (true) {
    // Calculate time of specified weekday of specific month.
    start = calculate_time_from_weekday_of_month(
              year,
              _month_start,
              _week_day_start,
              _week_day_start_offset);
    if ((time_t)-1 == start)
      return (false);

    // Use same year as was calculated for start time above.
    end = calculate_time_from_weekday_of_month(
            year,
            _month_end,
            _week_day_end,
            _week_day_end_offset);

    // Advance a year if necessary :
    // thursday 2 august - monday 3 february
    if ((end != (time_t)-1) && (end < start))
      end = calculate_time_from_weekday_of_month(
              year + 1,
              _month_end,
              _week_day_end,
              _week_day_end_offset);

    if ((time_t)-1 == end) {
      // End date can't be helped, so skip it.
      if (_week_day_end_offset < 0)
        return (false);

      // Else end date slipped past end of month, so use last day
      // of month as end date.
      int end_month;
      int end_year;
      if (_month_end != 11) {
        end_month = _month_end + 1;
        end_year = year;
      }
      else {
        end_month = 0;
        end_year = year + 1;
      }
      end = calculate_time_from_day_of_month(
              end_year,
              end_month,
              0);
      if ((time_t)-1 == end)
        return (false);
    }
    else
      end = _add_round_days_to_midnight(end, 24 * 60 * 60);

    // We should have an interval that includes or is above
    // preferred time.
    if (ti.preferred_time < end)
      break ;
    // Advance to next year if we've passed this month
    // weekday already this year.
    else
      ++year;
  }

  return (true);
}

/**
 *  Calculate start time and end time for date range week day.
 *
 *  @param[in]  ti     Time informations.
 *  @param[out] start  Variable to fill start time.
 *  @param[out] end    Variable to fill end time.
 *
 *  @return True on success, otherwise false.
 */
bool daterange::_week_day_to_time_t(time_info const& ti,
                                    time_t& start,
                                    time_t& end) const {
  // What year/month should we use ?
  int year;
  int month;
  year = ti.preftime.tm_year;
  month = ti.preftime.tm_mon;

  while (true) {
    // Calculate time of specified weekday of month.
    start = calculate_time_from_weekday_of_month(
              year,
              month,
              _week_day_start,
              _week_day_start_offset);

    // Use same year and month as was calculated for start time above.
    end = calculate_time_from_weekday_of_month(
            year,
            month,
            _week_day_end,
            _week_day_end_offset);
    if (end == (time_t)-1) {
      // End date can't be helped, so skip it.
      if (_week_day_end_offset < 0)
        return (false);

      // Else end date slipped past end of month, so use last day
      // of month as end date.
      int end_month;
      int end_year;
      if (month != 11) {
        end_month = month + 1;
        end_year = year;
      }
      else {
        end_month = 0;
        end_year = year + 1;
      }
      end = calculate_time_from_day_of_month(end_year, end_month, 0);
    }
    else
      end = _add_round_days_to_midnight(end, 24 * 60 * 60);

    // Error checking.
    if (((time_t)-1 == start)
        || ((time_t)-1 == end)
        || (start > end))
      return (false);

    // We should have an interval that includes or is above
    // preferred time.
    if (ti.preferred_time < end)
      break ;
    // Advance to next month (or year) if we've passed this weekday of
    // this month already.
    else {
      month = ti.preftime.tm_mon;
      if (month != 11)
        ++month;
      else {
        month = 0;
        ++year;
      }
    }
  }

  return (true);
}

/**
 *  Calculate start time and end time for date range.
 *
 *  @param[in]  preferred_time  Preferred time.
 *  @param[out] start  Variable to fill start time.
 *  @param[out] end    Variable to fill end time.
 *
 *  @return True on success, otherwise false.
 */
bool daterange::to_time_t(time_t const preferred_time,
                          time_t& start,
                          time_t& end) const {
  bool ret = false;

  // Compute time information.
  time_info ti;
  ti.preferred_time = preferred_time;
  localtime_r(&preferred_time, &ti.preftime);
  ti.preftime.tm_sec = 0;
  ti.preftime.tm_min = 0;
  ti.preftime.tm_hour = 0;
  ti.midnight = mktime(&ti.preftime);

  switch (_type) {
  case calendar_date:
    ret = _calendar_date_to_time_t(start, end);
    break;
  case month_date:
    ret = _month_date_to_time_t(ti, start, end);
    break;
  case month_day:
    ret = _month_day_to_time_t(ti, start, end);
    break;
  case month_week_day:
    ret = _month_week_day_to_time_t(ti, start, end);
    break;
  case week_day:
    ret = _week_day_to_time_t(ti, start, end);
    break;
  default:
    break;
  }
  if (!ret)
    return (false);

  // If skipping days...
  if (_skip_interval > 1) {
    // Advance to the next possible skip date
    if (start < ti.preferred_time) {
      // How many days have passed between skip start date
      // and preferred time ?
      unsigned long days((ti.midnight - (unsigned long)start)
                         / (3600 * 24));

      // Advance start date to next skip day
      if (!(days % _skip_interval))
        start = _add_round_days_to_midnight(start, days * 24 * 60 * 60);
      else
        start = _add_round_days_to_midnight(
                  start,
                  ((days - (days % _skip_interval) + _skip_interval)
                   * 24 * 60 * 60));
    }
  }

  return (true);
}

/**
 *  Add a week day.
 *
 *  @param[in] key   The week day.
 *  @param[in] value The range.
 *
 *  @return True on success, otherwise false.
 */
/*bool _add_week_day(
       std::string const& key,
       std::string const& value) {
  unsigned int day_id;
  if (!_get_day_id(key, day_id))
    return (false);

  if (!_build_timeranges(value, _timeranges[day_id]))
    return (false);

  return (true);
}*/

/**
 *  Get the month id.
 *
 *  @param[in]  name The month name.
 *  @param[out] id   The id to fill.
 *
 *  @return True on success, otherwise false.
 */
static bool _get_month_id(
          std::string const& name,
          unsigned int& id) {
  static std::string const months[] = {
    "january",
    "february",
    "march",
    "april",
    "may",
    "june",
    "july",
    "august",
    "september",
    "october",
    "november",
    "december"
  };
  for (id = 0; id < sizeof(months) / sizeof(months[0]); ++id)
    if (name == months[id])
      return (true);
  return (false);
}

/**
 *  Get the week day id.
 *
 *  @param[in]  name The week day name.
 *  @param[out] id   The id to fill.
 *
 *  @return True on success, otherwise false.
 */
static bool _get_day_id(
          std::string const& name,
          unsigned int& id) {
  static std::string const days[] = {
    "sunday",
    "monday",
    "tuesday",
    "wednesday",
    "thursday",
    "friday",
    "saturday"
  };
  for (id = 0; id < sizeof(days) / sizeof(days[0]); ++id)
    if (name == days[id])
      return (true);
  return (false);
}

/**
 *  Build a calendar date.
 *
 *  @param[in] line   The string to parse.
 *  @param[out] list  A vector of dateranges to be filled.
 *
 *  @return           True if the line was correctly parsed.
 */
bool daterange::build_calendar_date(std::string const& line,
                                    std::vector<std::list<daterange> >& list) {
  int ret(0);
  int pos(0);
  unsigned int month_start(0);
  unsigned int month_end(0);
  unsigned int month_day_start(0);
  unsigned int month_day_end(0);
  unsigned int year_start(0);
  unsigned int year_end(0);
  unsigned int skip_interval(0);

  if ((ret = sscanf(
               line.c_str(),
               "%4u-%2u-%2u - %4u-%2u-%2u / %u %n",
               &year_start,
               &month_start,
               &month_day_start,
               &year_end,
               &month_end,
               &month_day_end,
               &skip_interval,
               &pos)) == 7)
    ;
  else if ((ret = sscanf(
                    line.c_str(),
                    "%4u-%2u-%2u - %4u-%2u-%2u %n",
                    &year_start,
                    &month_start,
                    &month_day_start,
                    &year_end,
                    &month_end,
                    &month_day_end,
                    &pos)) == 6)
    ;
  else if ((ret = sscanf(
                    line.c_str(),
                    "%4u-%2u-%2u / %u %n",
                    &year_start,
                    &month_start,
                    &month_day_start,
                    &skip_interval,
                    &pos)) == 4) {
    year_end = 0;
    month_end = 0;
    month_day_end = 0;
  }
  else if ((ret = sscanf(
                    line.c_str(),
                    "%4u-%2u-%2u %n",
                    &year_start,
                    &month_start,
                    &month_day_start,
                    &pos)) == 3) {
    year_end = year_start;
    month_end = month_start;
    month_day_end = month_day_start;
  }

  if (ret) {
    std::list<timerange> timeranges;
    if (!timerange::build_timeranges_from_string(line.substr(pos),
                                                 timeranges))
      return (false);

    daterange range(daterange::calendar_date);
    range.year_start(year_start);
    range.month_start(month_start - 1);
    range.month_day_start(month_day_start);
    range.year_end(year_end);
    range.month_end(month_end - 1);
    range.month_day_end(month_day_end);
    range.skip_interval(skip_interval);
    range.timeranges(timeranges);

    list[daterange::calendar_date].push_front(range);
    return (true);
  }
  return (false);
}

/**
 *  Build a non-calendar date.
 *
 *  @param[in] line   The string to parse.
 *  @param[out] list  A vector of dateranges to be filled.
 *
 *  @return           True if the line was correctly parsed.
 */
bool daterange::build_other_date(std::string const& line,
                                 std::vector<std::list<daterange> >& list) {
  int pos(0);
  daterange::type_range type(daterange::none);
  unsigned int month_start(0);
  unsigned int month_end(0);
  int month_day_start(0);
  int month_day_end(0);
  unsigned int skip_interval(0);
  unsigned int week_day_start(0);
  unsigned int week_day_end(0);
  int week_day_start_offset(0);
  int week_day_end_offset(0);
  char buffer[4][4096];

  if (line.size() > 1024)
    return (false);

  if (sscanf(
        line.c_str(),
        "%[a-z] %d %[a-z] - %[a-z] %d %[a-z] / %u %n",
        buffer[0],
        &week_day_start_offset,
        buffer[1],
        buffer[2],
        &week_day_end_offset,
        buffer[3],
        &skip_interval,
        &pos) == 7) {
    // wednesday 1 january - thursday 2 july / 3
    if (_get_day_id(buffer[0], week_day_start)
        && _get_month_id(buffer[1], month_start)
        && _get_day_id(buffer[2], week_day_end)
        && _get_month_id(buffer[3], month_end))
      type = daterange::month_week_day;
  }
  else if (sscanf(
             line.c_str(),
             "%[a-z] %d - %[a-z] %d / %u %n",
             buffer[0],
             &month_day_start,
             buffer[1],
             &month_day_end,
             &skip_interval,
             &pos) == 5) {
    // monday 2 - thursday 3 / 2
    if (_get_day_id(buffer[0], week_day_start)
        && _get_day_id(buffer[1], week_day_end)) {
      week_day_start_offset = month_day_start;
      week_day_end_offset = month_day_end;
      type = daterange::week_day;
    }
    // february 1 - march 15 / 3
    else if (_get_month_id(buffer[0], month_start)
       && _get_month_id(buffer[1], month_end))
      type = daterange::month_date;
    // day 4 - 6 / 2
    else if (!strcmp(buffer[0], "day")
             && !strcmp(buffer[1], "day"))
      type = daterange::month_day;
  }
  else if (sscanf(
             line.c_str(),
             "%[a-z] %d - %d / %u %n",
             buffer[0],
             &month_day_start,
             &month_day_end,
             &skip_interval,
             &pos) == 4) {
    // thursday 2 - 4
    if (_get_day_id(buffer[0], week_day_start)) {
      week_day_start_offset = month_day_start;
      week_day_end = week_day_start;
      week_day_end_offset = month_day_end;
      type = daterange::week_day;
    }
    // february 3 - 5
    else if (_get_month_id(buffer[0], month_start)) {
      month_end = month_start;
      type = daterange::month_date;
    }
    // day 1 - 4
    else if (!strcmp(buffer[0], "day"))
      type = daterange::month_day;
  }
  else if (sscanf(
             line.c_str(),
             "%[a-z] %d %[a-z] - %[a-z] %d %[a-z] %n",
             buffer[0],
             &week_day_start_offset,
             buffer[1],
             buffer[2],
             &week_day_end_offset,
             buffer[3],
             &pos) == 6) {
    // wednesday 1 january - thursday 2 july
    if (_get_day_id(buffer[0], week_day_start)
        && _get_month_id(buffer[1], month_start)
        && _get_day_id(buffer[2], week_day_end)
        && _get_month_id(buffer[3], month_end))
      type = daterange::month_week_day;
  }
  else if (sscanf(
             line.c_str(),
             "%[a-z] %d - %d %n",
             buffer[0],
             &month_day_start,
             &month_day_end,
             &pos) == 3) {
    // thursday 2 - 4
    if (_get_day_id(buffer[0], week_day_start)) {
      week_day_start_offset = month_day_start;
      week_day_end = week_day_start;
      week_day_end_offset = month_day_end;
      type = daterange::week_day;
    }
    // february 3 - 5
    else if (_get_month_id(buffer[0], month_start)) {
      month_end = month_start;
      type = daterange::month_date;
    }
    // day 1 - 4
    else if (!strcmp(buffer[0], "day"))
      type = daterange::month_day;
  }
  else if (sscanf(
             line.c_str(),
             "%[a-z] %d - %[a-z] %d %n",
             buffer[0],
             &month_day_start,
             buffer[1],
             &month_day_end,
             &pos) == 4) {
    // monday 2 - thursday 3
    if (_get_day_id(buffer[0], week_day_start)
        && _get_day_id(buffer[1], week_day_end)) {
      week_day_start_offset = month_day_start;
      week_day_end_offset = month_day_end;
      type = daterange::week_day;
    }
    // february 1 - march 15
    else if (_get_month_id(buffer[0], month_start)
       && _get_month_id(buffer[1], month_end))
      type = daterange::month_date;
    // day 1 - day 5
    else if (!strcmp(buffer[0], "day")
             && !strcmp(buffer[1], "day"))
      type = daterange::month_day;
  }
  else if (sscanf(
             line.c_str(),
             "%[a-z] %d %[a-z] %n",
             buffer[0],
             &week_day_start_offset,
             buffer[1],
             &pos) == 3) {
    // thursday 3 february
    if (_get_day_id(buffer[0], week_day_start)
        && _get_month_id(buffer[1], month_start)) {
      month_end = month_start;
      week_day_end = week_day_start;
      week_day_end_offset = week_day_start_offset;
      type = daterange::month_week_day;
    }
  }
  else if (sscanf(
             line.c_str(),
             "%[a-z] %d %n",
             buffer[0],
             &month_day_start,
             &pos) == 2) {
    // thursday 2
    if (_get_day_id(buffer[0], week_day_start)) {
      week_day_start_offset = month_day_start;
      week_day_end = week_day_start;
      week_day_end_offset = week_day_start_offset;
      type = daterange::week_day;
    }
    // february 3
    else if (_get_month_id(buffer[0], month_start)) {
      month_end = month_start;
      month_day_end = month_day_start;
      type = daterange::month_date;
    }
    // day 1
    else if (!strcmp(buffer[0], "day")) {
      month_day_end = month_day_start;
      type = daterange::month_day;
    }
  }

  if (type != daterange::none) {
    daterange range(type);
    if (type == daterange::month_day) {
      range.month_day_start(month_day_start);
      range.month_day_end(month_day_end);
    }
    else if (type == daterange::month_week_day) {
      range.month_start(month_start);
      range.week_day_start(week_day_start);
      range.week_day_start_offset(week_day_start_offset);
      range.month_end(month_end);
      range.week_day_end(week_day_end);
      range.week_day_end_offset(week_day_end_offset);
    }
    else if (type == daterange::week_day) {
      range.week_day_start(week_day_start);
      range.week_day_start_offset(week_day_start_offset);
      range.week_day_end(week_day_end);
      range.week_day_end_offset(week_day_end_offset);
    }
    else if (type == daterange::month_date) {
      range.month_start(month_start);
      range.month_day_start(month_day_start);
      range.month_end(month_end);
      range.month_day_end(month_day_end);
    }
    range.skip_interval(skip_interval);

    std::list<timerange> timeranges;
    if (!timerange::build_timeranges_from_string(line.substr(pos),
                                                 timeranges))
      return (false);

    range.timeranges(timeranges);
    list[type].push_front(range);
    return (true);
  }

  return (false);
}

/**
 *  Build a daterange from a string.
 *
 *  @param[in] value  A string containing a daterange.
 *
 *  @param[out] list  A vector of dateranges to be filled.
 *
 *  @return           True if the string was correctly parsed.
 */
bool daterange::build_dateranges_from_string(std::string const& value,
                             std::vector<std::list<daterange> >& list) {
  return (build_calendar_date(value, list) || build_other_date(value, list));
}
