////////////////////////////////////////////////////////////////////////////////
//
// Copyright 2013 - 2015, Paul Beckingham, Federico Hernandez.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
// http://www.opensource.org/licenses/mit-license.php
//
////////////////////////////////////////////////////////////////////////////////

#include <cmake.h>
#include <stdlib.h>
#include <time.h>
#include <text.h>
#include <Dates.h>
#include <Date.h>
#include <Lexer.h>
#include <i18n.h>

////////////////////////////////////////////////////////////////////////////////
static bool isMonth (const std::string& name, int& i)
{
  i = Date::monthOfYear (name) - 1;
  return i != -2 ? true : false;
}

////////////////////////////////////////////////////////////////////////////////
static bool isDay (const std::string& name, int& i)
{
  i = Date::dayOfWeek (name);
  return i != -1 ? true : false;
}

////////////////////////////////////////////////////////////////////////////////
static void easter (struct tm* t)
{
  int Y = t->tm_year + 1900;
  int a = Y % 19;
  int b = Y / 100;
  int c = Y % 100;
  int d = b / 4;
  int e = b % 4;
  int f = (b + 8) / 25;
  int g = (b - f + 1) / 3;
  int h = (19 * a + b - d - g + 15) % 30;
  int i = c / 4;
  int k = c % 4;
  int L = (32 + 2 * e + 2 * i - h - k) % 7;
  int m = (a + 11 * h + 22 * L) / 451;
  int month = (h + L - 7 * m + 114) / 31;
  int day = ((h + L - 7 * m + 114) % 31) + 1;

  t->tm_isdst = -1;   // Requests that mktime determine summer time effect.
  t->tm_mday  = day;
  t->tm_mon   = month - 1;
  t->tm_year  = Y - 1900;
  t->tm_isdst = -1;
  t->tm_hour = t->tm_min = t->tm_sec = 0;
}

////////////////////////////////////////////////////////////////////////////////
static void midsommar (struct tm* t)
{
  t->tm_mon = 5;                          // June.
  t->tm_mday = 20;                        // Saturday after 20th.
  t->tm_hour = t->tm_min = t->tm_sec = 0; // Midnight.
  t->tm_isdst = -1;                       // Probably DST, but check.

  time_t then = mktime (t);               // Obtain the weekday of June 20th.
  struct tm* mid = localtime (&then);
  t->tm_mday += 6 - mid->tm_wday;         // How many days after 20th.
}

////////////////////////////////////////////////////////////////////////////////
static void midsommarafton (struct tm* t)
{
  t->tm_mon = 5;                          // June.
  t->tm_mday = 19;                        // Saturday after 20th.
  t->tm_hour = t->tm_min = t->tm_sec = 0; // Midnight.
  t->tm_isdst = -1;                       // Probably DST, but check.

  time_t then = mktime (t);               // Obtain the weekday of June 19th.
  struct tm* mid = localtime (&then);
  t->tm_mday += 5 - mid->tm_wday;         // How many days after 19th.
}

////////////////////////////////////////////////////////////////////////////////
// <day>
// <month>
// - Nth
// - socy, eocy
// - socq, eocq
// socm, eocm
// som, eom
// soq, eoq
// soy, eoy
// socw, eocw
// sow, eow
// soww, eoww
// sod, eod
// yesterday
// today
// now
// tomorrow
// later          = midnight, Jan 18th, 2038.
// someday        = midnight, Jan 18th, 2038.
// - easter
// - eastermonday
// - ascension
// - pentecost
// - goodfriday
// - midsommar      = midnight, 1st Saturday after 20th June
// - midsommarafton = midnight, 1st Friday after 19th June
//
bool namedDates (const std::string& name, Variant& value)
{
  time_t now = time (NULL);
  struct tm* t = localtime (&now);
  int i;

  // Dynamics.
  if (name == "now")
  {
    value = Variant (now, Variant::type_date);
  }

  else if (name == "today")
  {
    t->tm_hour = t->tm_min = t->tm_sec = 0;
    t->tm_isdst = -1;
    value = Variant (mktime (t), Variant::type_date);
  }

  else if (name == "sod")
  {
    t->tm_mday++;
    t->tm_hour = t->tm_min = t->tm_sec = 0;
    t->tm_isdst = -1;
    value = Variant (mktime (t), Variant::type_date);
  }

  else if (name == "eod")
  {
    t->tm_mday++;
    t->tm_hour = t->tm_min = 0;
    t->tm_sec = -1;
    t->tm_isdst = -1;
    value = Variant (mktime (t), Variant::type_date);
  }

  else if (name == "tomorrow")
  {
    t->tm_mday++;
    t->tm_hour = t->tm_min = t->tm_sec = 0;
    t->tm_isdst = -1;
    value = Variant (mktime (t), Variant::type_date);
  }

  else if (name == "yesterday")
  {
    t->tm_hour = t->tm_min = t->tm_sec = 0;
    t->tm_isdst = -1;
    value = Variant (mktime (t) - 86400, Variant::type_date);
  }

  else if (isDay (name, i))
  {
    if (t->tm_wday >= i)
      t->tm_mday += i - t->tm_wday + 7;
    else
      t->tm_mday += i - t->tm_wday;

    t->tm_hour = t->tm_min = t->tm_sec = 0;
    t->tm_isdst = -1;
    value = Variant (mktime (t), Variant::type_date);
  }

  else if (isMonth (name, i))
  {
    if (t->tm_mon >= i)
      t->tm_year++;

    t->tm_mon = i;
    t->tm_mday = 1;
    t->tm_hour = t->tm_min = t->tm_sec = 0;
    t->tm_isdst = -1;
    value = Variant (mktime (t), Variant::type_date);
  }

  else if (name == "later" || name == "someday")
  {
    t->tm_hour = t->tm_min = t->tm_sec = 0;
    t->tm_year = 138;
    t->tm_mon = 0;
    t->tm_mday = 18;
    t->tm_isdst = -1;
    value = Variant (mktime (t), Variant::type_date);
  }

  else if (name == "eoy")
  {
    t->tm_hour = t->tm_min = 0;
    t->tm_sec = -1;
    t->tm_mon = 0;
    t->tm_mday = 1;
    t->tm_year++;
    t->tm_isdst = -1;
    value = Variant (mktime (t), Variant::type_date);
  }

  else if (name == "soy")
  {
    t->tm_hour = t->tm_min = t->tm_sec = 0;
    t->tm_mon = 0;
    t->tm_mday = 1;
    t->tm_year++;
    t->tm_isdst = -1;
    value = Variant (mktime (t), Variant::type_date);
  }

  else if (name == "eoq")
  {
    t->tm_hour = t->tm_min = 0;
    t->tm_sec = -1;
    t->tm_mon += 3 - (t->tm_mon % 3);
    if (t->tm_mon > 11)
    {
      t->tm_mon -= 12;
      ++t->tm_year;
    }

    t->tm_mday = 1;
    t->tm_isdst = -1;
    value = Variant (mktime (t), Variant::type_date);
  }

  else if (name == "soq")
  {
    t->tm_hour = t->tm_min = t->tm_sec = 0;
    t->tm_mon += 3 - (t->tm_mon % 3);
    if (t->tm_mon > 11)
    {
      t->tm_mon -= 12;
      ++t->tm_year;
    }

    t->tm_mday = 1;
    t->tm_isdst = -1;
    value = Variant (mktime (t), Variant::type_date);
  }

  else if (name == "socm")
  {
    t->tm_hour = t->tm_min = t->tm_sec = 0;
    t->tm_mday = 1;
    t->tm_isdst = -1;
    value = Variant (mktime (t), Variant::type_date);
  }

  else if (name == "som")
  {
    t->tm_hour = t->tm_min = t->tm_sec = 0;

    t->tm_mon++;
    if (t->tm_mon == 12)
    {
      t->tm_year++;
      t->tm_mon = 0;
    }

    t->tm_mday = 1;
    t->tm_isdst = -1;
    value = Variant (mktime (t), Variant::type_date);
  }

  else if (name == "eom" || name == "eocm")
  {
    t->tm_hour = 24;
    t->tm_min = 0;
    t->tm_sec = -1;
    t->tm_mday = Date::daysInMonth (t->tm_mon + 1, t->tm_year + 1900);
    t->tm_isdst = -1;
    value = Variant (mktime (t), Variant::type_date);
  }

  else if (name == "socw")
  {
    t->tm_hour = t->tm_min = t->tm_sec = 0;
    int extra = t->tm_wday * 86400;
    t->tm_isdst = -1;
    value = Variant (mktime (t) - extra, Variant::type_date);
  }

  else if (name == "eow" || name == "eocw")
  {
    t->tm_hour = t->tm_min = 0;
    t->tm_sec = -1;
    int extra = (7 - t->tm_wday) * 86400;
    t->tm_isdst = -1;
    value = Variant (mktime (t) + extra, Variant::type_date);
  }

  else if (name == "sow")
  {
    t->tm_hour = t->tm_min = t->tm_sec = 0;
    int extra = (7 - t->tm_wday) * 86400;
    t->tm_isdst = -1;
    value = Variant (mktime (t) + extra, Variant::type_date);
  }

  else if (name == "soww")
  {
    t->tm_hour = t->tm_min = t->tm_sec = 0;
    int extra = (t->tm_wday - 1) * 86400;
    if (extra > 0)
      extra += 7 * 86400;

    t->tm_isdst = -1;
    value = Variant (mktime (t) - extra, Variant::type_date);
  }

  else if (name == "eoww")
  {
    t->tm_hour = 24;
    t->tm_min = 0;
    t->tm_sec = -1;
    int extra = (5 - t->tm_wday) * 86400;
    if (extra < 0)
      extra += 7 * 86400;

    t->tm_isdst = -1;
    value = Variant (mktime (t) + extra, Variant::type_date);
  }

  // Support "21st" to indicate the next date that is the 21st day.
  // 1st
  // 2nd
  // 3rd
  // 4th
  else if ((
            name.length () == 3 &&
            Lexer::isDigit (name[0])   &&
            ((name[1] == 's' && name[2] == 't') ||
             (name[1] == 'n' && name[2] == 'd') ||
             (name[1] == 'r' && name[2] == 'd') ||
             (name[1] == 't' && name[2] == 'h'))
           )
           ||
           (
            name.length () == 4 &&
            Lexer::isDigit (name[0])   &&
            Lexer::isDigit (name[1])   &&
            ((name[2] == 's' && name[3] == 't') ||
             (name[2] == 'n' && name[3] == 'd') ||
             (name[2] == 'r' && name[3] == 'd') ||
             (name[2] == 't' && name[3] == 'h'))
           )
          )
  {
    int number;
    std::string ordinal;

    if (Lexer::isDigit (name[1]))
    {
      number = strtol (name.substr (0, 2).c_str (), NULL, 10);
      ordinal = lowerCase (name.substr (2));
    }
    else
    {
      number = strtol (name.substr (0, 1).c_str (), NULL, 10);
      ordinal = lowerCase (name.substr (1));
    }

    // Sanity check.
    if (number <= 31)
    {
      int remainder1 = number % 10;
      int remainder2 = number % 100;
      if ((remainder2 != 11 && remainder1 == 1 && ordinal == "st") ||
          (remainder2 != 12 && remainder1 == 2 && ordinal == "nd") ||
          (remainder2 != 13 && remainder1 == 3 && ordinal == "rd") ||
          ((remainder2 == 11 ||
            remainder2 == 12 ||
            remainder2 == 13 ||
            remainder1 == 0 ||
            remainder1 > 3) && ordinal == "th"))
      {
        int y = t->tm_year + 1900;
        int m = t->tm_mon + 1;
        int d = t->tm_mday;

        // If it is this month.
        if (d < number &&
            number <= Date::daysInMonth (m, y))
        {
          t->tm_hour = t->tm_min = t->tm_sec = 0;
          t->tm_mon  = m - 1;
          t->tm_mday = number;
          t->tm_year = y - 1900;
          t->tm_isdst = -1;
          value = Variant (mktime (t), Variant::type_date);
        }
        else
        {
          if (++m > 12)
          {
            m = 1;
            y++;
          }

          t->tm_hour = t->tm_min = t->tm_sec = 0;
          t->tm_mon  = m - 1;
          t->tm_mday = number;
          t->tm_year = y - 1900;
          t->tm_isdst = -1;
          value = Variant (mktime (t), Variant::type_date);
        }
      }
      else
        throw std::string (STRING_DATES_ORD_MISMATCH);
    }
    else
      throw std::string (STRING_DATES_MONTH_31);
  }

  else if (name == "easter"       ||
           name == "eastermonday" ||
           name == "ascension"    ||
           name == "pentecost"    ||
           name == "goodfriday")
  {
    Variant valueNow = Variant (mktime (t), Variant::type_date);
    easter (t);
    value = Variant (mktime (t), Variant::type_date);

    // If the result is earlier this year, then recalc for next year.
    if (value < valueNow)
    {
      t = localtime (&now);
      t->tm_year++;
      easter (t);
    }

         if (name == "goodfriday")   t->tm_mday -= 2;
    else if (name == "eastermonday") t->tm_mday += 1;
    else if (name == "ascension")    t->tm_mday += 39;
    else if (name == "pentecost")    t->tm_mday += 49;

    value = Variant (mktime (t), Variant::type_date);
  }

  else if (name == "midsommar")
  {
    Variant valueNow = Variant (mktime (t), Variant::type_date);
    midsommar (t);
    value = Variant (mktime (t), Variant::type_date);

    // If the result is earlier this year, then recalc for next year.
    if (value < valueNow)
    {
      t = localtime (&now);
      t->tm_year++;
      midsommar (t);
    }

    value = Variant (mktime (t), Variant::type_date);
  }

  else if (name == "midsommarafton")
  {
    Variant valueNow = Variant (mktime (t), Variant::type_date);
    midsommarafton (t);
    value = Variant (mktime (t), Variant::type_date);

    // If the result is earlier this year, then recalc for next year.
    if (value < valueNow)
    {
      t = localtime (&now);
      t->tm_year++;
      midsommarafton (t);
    }

    value = Variant (mktime (t), Variant::type_date);
  }

  else
    return false;

  value.source (name);
  return true;
}

////////////////////////////////////////////////////////////////////////////////

