/*
 *  Copyright (C) 2010-2018 Team Kodi
 *  This file is part of Kodi - https://kodi.tv
 *
 *  SPDX-License-Identifier: GPL-2.0-or-later
 *  See LICENSES/README.md for more information.
 */

#include "FTPParse.h"

#include <cmath>

#include <pcrecpp.h>

CFTPParse::CFTPParse()
{
  m_flagtrycwd = 0;
  m_flagtryretr = 0;
  m_size = 0;
}

std::string CFTPParse::getName()
{
  return m_name;
}

int CFTPParse::getFlagtrycwd()
{
  return m_flagtrycwd;
}

int CFTPParse::getFlagtryretr()
{
  return m_flagtryretr;
}

uint64_t CFTPParse::getSize()
{
  return m_size;
}

time_t CFTPParse::getTime()
{
  return m_time;
}

void CFTPParse::setTime(const std::string& str)
{
  /* Variables used to capture patterns via the regexes */
  std::string month;
  std::string day;
  std::string year;
  std::string hour;
  std::string minute;
  std::string second;
  std::string am_or_pm;

  /* time struct used to set the time_t variable */
  struct tm time_struct = {};

  /* Regex to read Unix, NetWare and NetPresenz time format */
  pcrecpp::RE unix_re("^([A-Za-z]{3})" // month
    "\\s+(\\d{1,2})" // day of month
    "\\s+([:\\d]{4,5})$" // time of day or year
  );

  /* Regex to read MultiNet time format */
  pcrecpp::RE multinet_re("^(\\d{1,2})" // day of month
    "-([A-Za-z]{3})" // month
    "-(\\d{4})" // year
    "\\s+(\\d{2})" // hour
    ":(\\d{2})" // minute
    "(:(\\d{2}))?$" // second
  );

  /* Regex to read MSDOS time format */
  pcrecpp::RE msdos_re("^(\\d{2})" // month
    "-(\\d{2})" // day of month
    "-(\\d{2})" // year
    "\\s+(\\d{2})" // hour
    ":(\\d{2})" // minute
    "([AP]M)$" // AM or PM
  );

  if (unix_re.FullMatch(str, &month, &day, &year))
  {
    /* set the month */
    if (pcrecpp::RE("jan",
        pcrecpp::RE_Options().set_caseless(true)).FullMatch(month))
      time_struct.tm_mon = 0;
    else if (pcrecpp::RE("feb",
        pcrecpp::RE_Options().set_caseless(true)).FullMatch(month))
      time_struct.tm_mon = 1;
    else if (pcrecpp::RE("mar",
        pcrecpp::RE_Options().set_caseless(true)).FullMatch(month))
      time_struct.tm_mon = 2;
    else if (pcrecpp::RE("apr",
        pcrecpp::RE_Options().set_caseless(true)).FullMatch(month))
      time_struct.tm_mon = 3;
    else if (pcrecpp::RE("may",
        pcrecpp::RE_Options().set_caseless(true)).FullMatch(month))
      time_struct.tm_mon = 4;
    else if (pcrecpp::RE("jun",
        pcrecpp::RE_Options().set_caseless(true)).FullMatch(month))
      time_struct.tm_mon = 5;
    else if (pcrecpp::RE("jul",
        pcrecpp::RE_Options().set_caseless(true)).FullMatch(month))
      time_struct.tm_mon = 6;
    else if (pcrecpp::RE("aug",
        pcrecpp::RE_Options().set_caseless(true)).FullMatch(month))
      time_struct.tm_mon = 7;
    else if (pcrecpp::RE("sep",
        pcrecpp::RE_Options().set_caseless(true)).FullMatch(month))
      time_struct.tm_mon = 8;
    else if (pcrecpp::RE("oct",
        pcrecpp::RE_Options().set_caseless(true)).FullMatch(month))
      time_struct.tm_mon = 9;
    else if (pcrecpp::RE("nov",
        pcrecpp::RE_Options().set_caseless(true)).FullMatch(month))
      time_struct.tm_mon = 10;
    else if (pcrecpp::RE("dec",
        pcrecpp::RE_Options().set_caseless(true)).FullMatch(month))
      time_struct.tm_mon = 11;

    /* set the day of the month */
    time_struct.tm_mday = atoi(day.c_str());

    time_t t = time(NULL);
    struct tm *current_time;
#ifdef LOCALTIME_R
    struct tm result = {};
    current_time = localtime_r(&t, &result);
#else
    current_time = localtime(&t);
#endif
    if (pcrecpp::RE("(\\d{2}):(\\d{2})").FullMatch(year, &hour, &minute))
    {
      /* set the hour and minute */
      time_struct.tm_hour = atoi(hour.c_str());
      time_struct.tm_min = atoi(minute.c_str());

      /* set the year */
      if ((current_time->tm_mon - time_struct.tm_mon < 0) ||
         ((current_time->tm_mon - time_struct.tm_mon == 0) &&
          (current_time->tm_mday - time_struct.tm_mday < 0)))
        time_struct.tm_year = current_time->tm_year - 1;
      else
        time_struct.tm_year = current_time->tm_year;
    }
    else
    {
      /* set the year */
      time_struct.tm_year = atoi(year.c_str()) - 1900;
    }

    /* set the day of the week */
    time_struct.tm_wday = getDayOfWeek(time_struct.tm_mon + 1,
                                   time_struct.tm_mday,
                                   time_struct.tm_year + 1900);
  }
  else if (multinet_re.FullMatch(str, &day, &month, &year,
                            &hour, &minute, (void*)NULL, &second))
  {
    /* set the month */
    if (pcrecpp::RE("jan",
        pcrecpp::RE_Options().set_caseless(true)).FullMatch(month))
      time_struct.tm_mon = 0;
    else if (pcrecpp::RE("feb",
        pcrecpp::RE_Options().set_caseless(true)).FullMatch(month))
      time_struct.tm_mon = 1;
    else if (pcrecpp::RE("mar",
        pcrecpp::RE_Options().set_caseless(true)).FullMatch(month))
      time_struct.tm_mon = 2;
    else if (pcrecpp::RE("apr",
        pcrecpp::RE_Options().set_caseless(true)).FullMatch(month))
      time_struct.tm_mon = 3;
    else if (pcrecpp::RE("may",
        pcrecpp::RE_Options().set_caseless(true)).FullMatch(month))
      time_struct.tm_mon = 4;
    else if (pcrecpp::RE("jun",
        pcrecpp::RE_Options().set_caseless(true)).FullMatch(month))
      time_struct.tm_mon = 5;
    else if (pcrecpp::RE("jul",
        pcrecpp::RE_Options().set_caseless(true)).FullMatch(month))
      time_struct.tm_mon = 6;
    else if (pcrecpp::RE("aug",
        pcrecpp::RE_Options().set_caseless(true)).FullMatch(month))
      time_struct.tm_mon = 7;
    else if (pcrecpp::RE("sep",
        pcrecpp::RE_Options().set_caseless(true)).FullMatch(month))
      time_struct.tm_mon = 8;
    else if (pcrecpp::RE("oct",
        pcrecpp::RE_Options().set_caseless(true)).FullMatch(month))
      time_struct.tm_mon = 9;
    else if (pcrecpp::RE("nov",
        pcrecpp::RE_Options().set_caseless(true)).FullMatch(month))
      time_struct.tm_mon = 10;
    else if (pcrecpp::RE("dec",
        pcrecpp::RE_Options().set_caseless(true)).FullMatch(month))
      time_struct.tm_mon = 11;

    /* set the day of the month and year */
    time_struct.tm_mday = atoi(day.c_str());
    time_struct.tm_year = atoi(year.c_str()) - 1900;

    /* set the hour and minute */
    time_struct.tm_hour = atoi(hour.c_str());
    time_struct.tm_min = atoi(minute.c_str());

    /* set the second if given*/
    if (second.length() > 0)
      time_struct.tm_sec = atoi(second.c_str());

    /* set the day of the week */
    time_struct.tm_wday = getDayOfWeek(time_struct.tm_mon + 1,
                                   time_struct.tm_mday,
                                   time_struct.tm_year + 1900);
  }
  else if (msdos_re.FullMatch(str, &month, &day,
                              &year, &hour, &minute, &am_or_pm))
  {
    /* set the month and the day of the month */
    time_struct.tm_mon = atoi(month.c_str()) - 1;
    time_struct.tm_mday = atoi(day.c_str());

    /* set the year */
    time_struct.tm_year = atoi(year.c_str());
    if (time_struct.tm_year < 70)
      time_struct.tm_year += 100;

    /* set the hour */
    time_struct.tm_hour = atoi(hour.c_str());
    if (time_struct.tm_hour == 12)
      time_struct.tm_hour -= 12;
    if (pcrecpp::RE("PM").FullMatch(am_or_pm))
      time_struct.tm_hour += 12;

    /* set the minute */
    time_struct.tm_min = atoi(minute.c_str());

    /* set the day of the week */
    time_struct.tm_wday = getDayOfWeek(time_struct.tm_mon + 1,
                                   time_struct.tm_mday,
                                   time_struct.tm_year + 1900);
  }

  /* now set m_time */
  m_time = mktime(&time_struct);
}

int CFTPParse::getDayOfWeek(int month, int date, int year)
{
  /* Here we use the Doomsday rule to calculate the day of the week */

  /* First determine the anchor day */
  int anchor;
  if (year >= 1900 && year < 2000)
    anchor = 3;
  else if (year >= 2000 && year < 2100)
    anchor = 2;
  else if (year >= 2100 && year < 2200)
    anchor = 0;
  else if (year >= 2200 && year < 2300)
    anchor = 5;
  else // must have been given an invalid year :-/
    return -1;

  /* Now determine the doomsday */
  int y = year % 100;
  int dday =
    ((y/12 + (y % 12) + ((y % 12)/4)) % 7) + anchor;

  /* Determine if the given year is a leap year */
  int leap_year = 0;
  if (((year % 4 == 0) && (year % 100 != 0)) || (year % 400 == 0))
    leap_year = 1;

  /* Now select a doomsday for the given month */
  int mdday = 1;
  if (month == 1)
  {
    if (leap_year)
      mdday = 4;
    else
      mdday = 3;
  }
  if (month == 2)
  {
    if (leap_year)
      mdday = 1;
    else
      mdday = 7;
  }
  if (month == 3)
    mdday = 7;
  if (month == 4)
    mdday = 4;
  if (month == 5)
    mdday = 9;
  if (month == 6)
    mdday = 6;
  if (month == 7)
    mdday = 11;
  if (month == 8)
    mdday = 8;
  if (month == 9)
    mdday = 5;
  if (month == 10)
    mdday = 10;
  if (month == 11)
    mdday = 9;
  if (month == 12)
    mdday = 12;

  /* Now calculate the day of the week
   * Sunday = 0, Monday = 1, Tuesday = 2, Wednesday = 3, Thursday = 4,
   * Friday = 5, Saturday = 6.
   */
  int day_of_week = ((date - 1) % 7) - ((mdday - 1) % 7) + dday;
  if (day_of_week >= 7)
    day_of_week -= 7;

  return day_of_week;
}

int CFTPParse::FTPParse(const std::string& str)
{
  /* Various variable to capture patterns via the regexes */
  std::string permissions;
  std::string link_count;
  std::string owner;
  std::string group;
  std::string size;
  std::string date;
  std::string name;
  std::string type;
  std::string stuff;
  std::string facts;
  std::string version;
  std::string file_id;

  /* Regex for standard Unix listing formats */
  pcrecpp::RE unix_re("^([-bcdlps])" // type
    "([-rwxXsStT]{9})" // permissions
    "\\s+(\\d+)" // hard link count
    "\\s+(\\w+)" // owner
    "\\s+(\\w+)" // group
    "\\s+(\\d+)" // size
    "\\s+([A-Za-z]{3}\\s+\\d{1,2}\\s+[:\\d]{4,5})" // modification date
    "\\s+(.+)$" // name
  );

  /* Regex for NetWare listing formats */
  /* See http://www.novell.com/documentation/oes/ftp_enu/data/a3ep22p.html#fbhbaijf */
  pcrecpp::RE netware_re("^([-d])" // type
    "\\s+(\\[[-SRWCIEMFA]{8}\\])" // rights
    "\\s+(\\w+)" // owner
    "\\s+(\\d+)" // size
    "\\s+([A-Za-z]{3}\\s+\\d{1,2}\\s+[:\\d]{4,5})" // time
    "\\s+(.+)$" // name
  );

  /* Regex for NetPresenz */
  /* See http://files.stairways.com/other/ftp-list-specs-info.txt */
  /* Here we will capture permissions and size if given */
  pcrecpp::RE netpresenz_re("^([-dl])" // type
    "([-rwx]{9}|)" // permissions
    "\\s+(.*)" // stuff
    "\\s+(\\d+|)" // size
    "\\s+([A-Za-z]{3}\\s+\\d{1,2}\\s+[:\\d]{4,5})" // modification date
    "\\s+(.+)$" // name
  );

  /* Regex for EPLF */
  /* See http://cr.yp.to/ftp/list/eplf.html */
  /* SAVE: "(/,|r,|s\\d+,|m\\d+,|i[\\d!#@$%^&*()]+(\\.[\\d!#@$%^&*()]+|),)+" */
  pcrecpp::RE eplf_re("^\\+" // initial "plus" sign
    "([^\\s]+)" // facts
    "\\s(.+)$" // name
  );

  /* Regex for MultiNet */
  /* Best documentation found was
   * http://www-sld.slac.stanford.edu/SLDWWW/workbook/vms_files.html */
  pcrecpp::RE multinet_re("^([^;]+)" // name
    ";(\\d+)" // version
    "\\s+([\\d/]+)" // file id
    "\\s+(\\d{1,2}-[A-Za-z]{3}-\\d{4}\\s+\\d{2}:\\d{2}(:\\d{2})?)" // date
    "\\s+\\[([^\\]]+)\\]" // owner,group
    "\\s+\\(([^\\)]+)\\)$" // permissions
  );

  /* Regex for MSDOS */
  pcrecpp::RE msdos_re("^(\\d{2}-\\d{2}-\\d{2}\\s+\\d{2}:\\d{2}[AP]M)" // date
    "\\s+(<DIR>|[\\d]+)" // dir or size
    "\\s+(.+)$" // name
  );

  if (unix_re.FullMatch(str, &type, &permissions, &link_count, &owner, &group, &size, &date, &name))
  {
    m_name = name;
    m_size = (uint64_t)strtod(size.c_str(), NULL);
    if (pcrecpp::RE("d").FullMatch(type))
      m_flagtrycwd = 1;
    if (pcrecpp::RE("-").FullMatch(type))
      m_flagtryretr = 1;
    if (pcrecpp::RE("l").FullMatch(type))
    {
      m_flagtrycwd = m_flagtryretr = 1;
      // handle symlink
      size_t found = m_name.find(" -> ");
      if (found != std::string::npos)
        m_name = m_name.substr(0, found);
    }
    setTime(date);

    return 1;
  }
  if (netware_re.FullMatch(str, &type, &permissions, &owner, &size, &date, &name))
  {
    m_name = name;
    m_size = (uint64_t)strtod(size.c_str(), NULL);
    if (pcrecpp::RE("d").FullMatch(type))
      m_flagtrycwd = 1;
    if (pcrecpp::RE("-").FullMatch(type))
      m_flagtryretr = 1;
    setTime(date);

    return 1;
  }
  if (netpresenz_re.FullMatch(str, &type, &permissions, &stuff, &size, &date, &name))
  {
    m_name = name;
    m_size = (uint64_t)strtod(size.c_str(), NULL);
    if (pcrecpp::RE("d").FullMatch(type))
      m_flagtrycwd = 1;
    if (pcrecpp::RE("-").FullMatch(type))
      m_flagtryretr = 1;
    if (pcrecpp::RE("l").FullMatch(type))
    {
      m_flagtrycwd = m_flagtryretr = 1;
      // handle symlink
      size_t found = m_name.find(" -> ");
      if (found != std::string::npos)
        m_name = m_name.substr(0, found);
    }
    setTime(date);

    return 1;
  }
  if (eplf_re.FullMatch(str, &facts, &name))
  {
    /* Get the type, size, and date from the facts */
    pcrecpp::RE("(\\+|,)(r|/),").PartialMatch(facts, (void*)NULL, &type);
    pcrecpp::RE("(\\+|,)s(\\d+),").PartialMatch(facts, (void*)NULL, &size);
    pcrecpp::RE("(\\+|,)m(\\d+),").PartialMatch(facts, (void*)NULL, &date);

    m_name = name;
    m_size = (uint64_t)strtod(size.c_str(), NULL);
    if (pcrecpp::RE("/").FullMatch(type))
      m_flagtrycwd = 1;
    if (pcrecpp::RE("r").FullMatch(type))
      m_flagtryretr = 1;
    /* eplf stores the date in time_t format already */
    m_time = atoi(date.c_str());

    return 1;
  }
  if (multinet_re.FullMatch(str, &name, &version, &file_id, &date, (void*)NULL, &owner, &permissions))
  {
    if (pcrecpp::RE("\\.DIR$").PartialMatch(name))
    {
      name.resize(name.size() - 4);
      m_flagtrycwd = 1;
    }
    else
      m_flagtryretr = 1;
    m_name = name;
    setTime(date);
    /* Multinet doesn't provide a size */
    m_size = 0;

    return 1;
  }
  if (msdos_re.FullMatch(str, &date, &size, &name))
  {
    m_name = name;
    if (pcrecpp::RE("<DIR>").FullMatch(size))
    {
      m_flagtrycwd = 1;
      m_size = 0;
    }
    else
    {
      m_flagtryretr = 1;
      m_size = (uint64_t)strtod(size.c_str(), NULL);
    }
    setTime(date);

    return 1;
  }

  return 0;
}
