// Copyright 2016 Google Inc. All Rights Reserved.
//
// 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.

// A command-line tool for exercising the CCTZ library.

#include <algorithm>
#include <cctype>
#include <chrono>
#include <cstring>
#include <ctime>
#include <iomanip>
#include <iostream>
#include <limits>
#include <sstream>
#include <string>

#include "cctz/civil_time.h"
#include "cctz/time_zone.h"
#include "time_zone_impl.h"

// Pulls in the aliases from cctz for brevity.
template <typename D>
using time_point = cctz::time_point<D>;
using sys_seconds = cctz::sys_seconds;

// parse() specifiers for command-line time arguments.
const char* const kFormats[] = {
  "%Y   %m   %d   %H   %M   %E*S",
  "%Y - %m - %d T %H : %M : %E*S",
  "%Y - %m - %d %H : %M : %E*S",
  "%Y - %m - %d T %H : %M",
  "%Y - %m - %d %H : %M",
  "%Y - %m - %d",
  "%a %b %d %H : %M : %E*S %Z %Y",
  "%a %e %b %Y %H : %M : %E*S",
  "%a %b %e %Y %H : %M : %E*S",
  "%e %b %Y %H : %M : %E*S",
  "%b %e %Y %H : %M : %E*S",
  "%a %e %b %Y %H : %M",
  "%a %b %e %Y %H : %M",
  "%e %b %Y %H : %M",
  "%b %e %Y %H : %M",
  "%a %e %b %Y",
  "%a %b %e %Y",
  "%e %b %Y",
  "%b %e %Y",
  nullptr
};

bool ParseTimeSpec(const std::string& args, time_point<sys_seconds>* when) {
  const cctz::time_zone ignored{};
  for (const char* const* fmt = kFormats; *fmt != NULL; ++fmt) {
    const std::string format = std::string(*fmt) + " %Ez";
    time_point<sys_seconds> tp;
    if (cctz::parse(format, args, ignored, &tp)) {
      *when = tp;
      return true;
    }
  }
  return false;
}

bool ParseBreakdownSpec(const std::string& args, cctz::time_zone zone,
                        cctz::civil_second* when) {
  for (const char* const* fmt = kFormats; *fmt != NULL; ++fmt) {
    time_point<sys_seconds> tp;
    if (cctz::parse(*fmt, args, zone, &tp)) {
      *when = cctz::convert(tp, zone);
      return true;
    }
  }
  return false;
}

// The FormatTime() specifier for output.
const char* const kFormat = "%Y-%m-%d %H:%M:%S %Ez (%Z)";

const char* WeekDayName(cctz::weekday wd) {
  switch (wd) {
    case cctz::weekday::monday: return "Mon";
    case cctz::weekday::tuesday: return "Tue";
    case cctz::weekday::wednesday: return "Wed";
    case cctz::weekday::thursday: return "Thu";
    case cctz::weekday::friday: return "Fri";
    case cctz::weekday::saturday: return "Sat";
    case cctz::weekday::sunday: return "Sun";
  }
  return "XXX";
}

std::string FormatTimeInZone(time_point<sys_seconds> when,
                             cctz::time_zone zone) {
  std::ostringstream oss;
  oss << std::setw(33) << std::left << cctz::format(kFormat, when, zone);
  cctz::time_zone::absolute_lookup al = zone.lookup(when);
  cctz::civil_day cd(al.cs);
  oss << " [wd=" << WeekDayName(cctz::get_weekday(cd))
      << " yd=" << std::setw(3) << std::setfill('0')
      << std::right << cctz::get_yearday(cd)
      << " dst=" << (al.is_dst ? 'T' : 'F')
      << " off=" << std::showpos << al.offset << std::noshowpos << "]";
  return oss.str();
}

void InstantInfo(const std::string& label, time_point<sys_seconds> when,
                 cctz::time_zone zone) {
  const cctz::time_zone utc = cctz::utc_time_zone();  // might == zone
  const std::string time_label = "time_t";
  const std::string utc_label = "UTC";
  const std::string zone_label = "in-tz";  // perhaps zone.name()?
  int width = static_cast<int>(
      2 + std::max(std::max(time_label.size(), utc_label.size()),
                   zone_label.size()));
  std::cout << label << " {\n";
  std::cout << std::setw(width) << std::right << time_label << ": ";
  std::cout << std::setw(10) << cctz::format("%s", when, utc);
  std::cout << "\n";
  std::cout << std::setw(width) << std::right << utc_label << ": ";
  std::cout << FormatTimeInZone(when, utc) << "\n";
  std::cout << std::setw(width) << std::right << zone_label << ": ";
  std::cout << FormatTimeInZone(when, zone) << "\n";
  std::cout << "}\n";
}

// Report everything we know about a cctz::civil_second (YMDHMS).
int BreakdownInfo(const cctz::civil_second& cs, cctz::time_zone zone) {
  const cctz::time_zone::Impl& impl = cctz::time_zone::Impl::get(zone);
  std::cout << "tz: " << zone.name() << " [" << impl.Description() << "]\n";
  cctz::time_zone::civil_lookup cl = zone.lookup(cs);
  switch (cl.kind) {
    case cctz::time_zone::civil_lookup::UNIQUE: {
      std::cout << "kind: UNIQUE\n";
      InstantInfo("when", cl.pre, zone);
      break;
    }
    case cctz::time_zone::civil_lookup::SKIPPED: {
      std::cout << "kind: SKIPPED\n";
      InstantInfo("post", cl.post, zone);  // might == trans-1
      InstantInfo("trans-1", cl.trans - std::chrono::seconds(1), zone);
      InstantInfo("trans", cl.trans, zone);
      InstantInfo("pre", cl.pre, zone);  // might == trans
      break;
    }
    case cctz::time_zone::civil_lookup::REPEATED: {
      std::cout << "kind: REPEATED\n";
      InstantInfo("pre", cl.pre, zone);  // might == trans-1
      InstantInfo("trans-1", cl.trans - std::chrono::seconds(1), zone);
      InstantInfo("trans", cl.trans, zone);
      InstantInfo("post", cl.post, zone);  // might == trans
      break;
    }
  }
  return 0;
}

// Report everything we know about a time_point<sys_seconds>.
int TimeInfo(time_point<sys_seconds> when, cctz::time_zone zone) {
  const cctz::time_zone::Impl& impl = cctz::time_zone::Impl::get(zone);
  std::cout << "tz: " << zone.name() << " [" << impl.Description() << "]\n";
  std::cout << "kind: UNIQUE\n";
  InstantInfo("when", when, zone);
  return 0;
}

// Report everything we know about a time_zone.
int ZoneDump(bool zdump, cctz::time_zone zone,
             cctz::year_t lo_year, cctz::year_t hi_year) {
  const cctz::time_zone utc = cctz::utc_time_zone();
  const cctz::time_zone::Impl& impl = cctz::time_zone::Impl::get(zone);
  if (zdump) {
    std::cout << zone.name() << "  "
              << std::numeric_limits<cctz::sys_seconds::rep>::min()
              << " = NULL\n";
    std::cout << zone.name() << "  "
              << std::numeric_limits<cctz::sys_seconds::rep>::min() + 86400
              << " = NULL\n";
  } else {
    std::cout << zone.name() << " [" << impl.Description() << "]\n";
  }

  auto trans = convert(cctz::civil_second(lo_year, 1, 1, 0, 0, -1), zone);
  while (impl.NextTransition(&trans)) {
    if (convert(trans, zone).year() >= hi_year) break;
    if (!zdump) std::cout << "\n";
    for (int count_down = 1; count_down >= 0; --count_down) {
      auto tp = trans - std::chrono::seconds(count_down);
      if (zdump) {
        std::cout << zone.name() << "  " << cctz::format("%c UT", tp, utc)
                  << " = " << cctz::format("%c %Z", tp, zone);
      } else {
        std::cout << std::setw(10) << cctz::ToUnixSeconds(tp);
        std::cout << " = " << cctz::format(kFormat, tp, utc);
        std::cout << " = " << cctz::format(kFormat, tp, zone);
      }
      auto al = zone.lookup(tp);
      if (zdump) {
        std::cout << " isdst=" << (al.is_dst ? '1' : '0')
                  << " gmtoff=" << al.offset << "\n";
      } else {
        const char* wd = WeekDayName(get_weekday(cctz::civil_day(al.cs)));
        std::cout << " [wd=" << wd << " dst=" << (al.is_dst ? 'T' : 'F')
                  << " off=" << al.offset << "]\n";
      }
    }
  }

  if (zdump) {
    std::cout << zone.name() << "  "
              << std::numeric_limits<cctz::sys_seconds::rep>::max() - 86400
              << " = NULL\n";
    std::cout << zone.name() << "  "
              << std::numeric_limits<cctz::sys_seconds::rep>::max()
              << " = NULL\n";
  }
  return 0;
}

const char* Basename(const char* p) {
  if (const char* b = strrchr(p, '/')) return ++b;
  return p;
}

// std::regex doesn't work before gcc 4.9.
bool LooksLikeNegOffset(const char* s) {
  if (s[0] == '-' && std::isdigit(s[1]) && std::isdigit(s[2])) {
    int i = (s[3] == ':') ? 4 : 3;
    if (std::isdigit(s[i]) && std::isdigit(s[i + 1])) {
      return s[i + 2] == '\0';
    }
  }
  return false;
}

// Parses [<lo-year>,]<hi-year>.
bool ParseYearRange(bool zdump, const std::string& args,
                    cctz::year_t* lo_year, cctz::year_t* hi_year) {
  std::size_t pos = 0;
  std::size_t digit_pos = pos + (args[pos] == '-' ? 1 : 0);
  if (digit_pos >= args.size() || !std::isdigit(args[digit_pos])) {
    return false;
  }
  const cctz::year_t first = std::stoll(args, &pos);
  if (pos == args.size()) {
    *lo_year = (zdump ? -292277022656 : first);
    *hi_year = (zdump ? first : first + 1);
    return true;
  }
  if (args[pos] != ' ' || ++pos == args.size()) {
    // Any comma was already converted to a space.
    return false;
  }
  digit_pos = pos + (args[pos] == '-' ? 1 : 0);
  if (digit_pos >= args.size() || !std::isdigit(args[digit_pos])) {
    return false;
  }
  const std::string rem = args.substr(pos);
  const cctz::year_t second = std::stoll(rem, &pos);
  if (pos == rem.size()) {
    *lo_year = first;
    *hi_year = (zdump ? second : second + 1);
    return true;
  }
  return false;
}

int main(int argc, char** argv) {
  const char* argv0 = (argc > 0) ? (argc--, *argv++) : (argc = 0, "time_tool");
  const std::string prog = Basename(argv0);

  // Escape arguments that look like negative offsets so that they
  // don't look like flags.
  for (int i = 0; i < argc; ++i) {
    if (strcmp(argv[i], "--") == 0) break;
    if (LooksLikeNegOffset(argv[i])) {
      char* buf = new char[strlen(argv[i] + 2)];
      buf[0] = ' ';  // will later be ignorned
      strcpy(buf + 1, argv[i]);
      argv[i] = buf;
    }
  }

  // Determine the time zone and other options.
  cctz::time_zone zone = cctz::local_time_zone();
  bool zone_dump = (prog == "zone_dump");
  bool zdump = false;  // Use zdump(8) format.
  int optind = 0;
  int opterr = 0;
  for (; optind < argc && opterr == 0; ++optind) {
    const char* opt = argv[optind];
    if (*opt++ != '-') break;
    if (*opt != '-') {  // short options
      while (char c = *opt++) {
        if (c == 'z') {
          if (*opt != '\0') {
            if (!cctz::load_time_zone(opt, &zone)) {
              std::cerr << opt << ": Unrecognized time zone\n";
              return 1;
            }
            break;
          }
          if (optind + 1 == argc) {
            std::cerr << argv0 << ": option requires an argument -- 'z'\n";
            ++opterr;
            break;
          }
          if (!cctz::load_time_zone(argv[++optind], &zone)) {
            std::cerr << argv[optind] << ": Unrecognized time zone\n";
            return 1;
          }
        } else if (c == 'D') {
          zdump = true;
        } else if (c == 'd') {
          zone_dump = true;
        } else {
          std::cerr << argv0 << ": invalid option -- '" << c << "'\n";
          ++opterr;
          break;
        }
      }
    } else {  // long options
      if (*++opt == '\0') {  // "--"
        ++optind;
        break;
      }
      if (strcmp(opt, "tz") == 0) {
        if (optind + 1 == argc) {
          std::cerr << argv0 << ": option '--tz' requires an argument\n";
          ++opterr;
        } else {
          if (!cctz::load_time_zone(argv[++optind], &zone)) {
            std::cerr << argv[optind] << ": Unrecognized time zone\n";
            return 1;
          }
        }
      } else if (strncmp(opt, "tz=", 3) == 0) {
        if (!cctz::load_time_zone(opt + 3, &zone)) {
          std::cerr << (opt + 3) << ": Unrecognized time zone\n";
          return 1;
        }
      } else if (strcmp(opt, "zdump") == 0) {
        zdump = true;
      } else if (strcmp(opt, "zone_dump") == 0) {
        zone_dump = true;
      } else {
        std::cerr << argv0 << ": unrecognized option '--" << opt << "'\n";
        ++opterr;
      }
    }
  }
  if (opterr != 0) {
    std::cerr << "Usage: " << prog << " [--tz=<zone>]";
    if (prog == "zone_dump") {
      std::cerr << " [[<lo-year>,]<hi-year>|<time-spec>]\n";
      std::cerr << "  Default years are last year and next year,"
                << " respectively.\n";
    } else {
      std::cerr << " [<time-spec>]\n";
    }
    std::cerr << "  Default <time-spec> is 'now'.\n";
    return 1;
  }

  // Determine the time point.
  time_point<sys_seconds> tp = std::chrono::time_point_cast<sys_seconds>(
      std::chrono::system_clock::now());
  std::string args;
  for (int i = optind; i < argc; ++i) {
    if (i != optind) args += " ";
    args += argv[i];
  }
  std::replace(args.begin(), args.end(), ',', ' ');
  std::replace(args.begin(), args.end(), '/', '-');
  bool have_time = ParseTimeSpec(args, &tp);
  if (!have_time && !args.empty()) {
    std::string spec = args.substr((args[0] == '@') ? 1 : 0);
    if ((spec.size() > 0 && std::isdigit(spec[0])) ||
        (spec.size() > 1 && spec[0] == '-' && std::isdigit(spec[1]))) {
      std::size_t end;
      const time_t t = std::stoll(spec, &end);
      if (end == spec.size()) {
        tp = cctz::FromUnixSeconds(t);
        have_time = true;
      }
    }
  }
  cctz::civil_second when = cctz::convert(tp, zone);
  bool have_break_down = !have_time && ParseBreakdownSpec(args, zone, &when);

  if (zone_dump || zdump) {
    cctz::year_t lo_year = (zdump ? -292277026596 : when.year());
    cctz::year_t hi_year = (zdump ? 292277026596 : when.year() + 1);
    if (!args.empty() && !ParseYearRange(zdump, args, &lo_year, &hi_year)) {
      if (!have_time && !have_break_down) {
        std::cerr << args << ": Malformed year range\n";
        return 1;
      }
    }
    return ZoneDump(zdump, zone, lo_year, hi_year);
  }

  if (have_break_down) return BreakdownInfo(when, zone);
  if (have_time || args.empty()) return TimeInfo(tp, zone);

  std::cerr << args << ": Malformed time spec\n";
  return 1;
}
