/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.
 */

#include "Timezone.hh"
#include "orc/OrcFile.hh"

#include <errno.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <atomic>
#include <map>
#include <sstream>

#ifndef _MSC_VER
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#endif

namespace orc {

  // default location of the timezone files
  static const char DEFAULT_TZDIR[] = "/usr/share/zoneinfo";

  // location of a symlink to the local timezone is /etc/localtime
  static const char LOCAL_TIMEZONE_DIR[] = "/etc";
  static const char LOCAL_TIMEZONE[] = "localtime";

  // US aliases from https://data.iana.org/time-zones/tzdb/backward
  static const std::map<const std::string, const std::string> TZ_ALIASES = {
      {"US/Alaska", "America/Anchorage"},
      {"US/Aleutian", "America/Adak"},
      {"US/Arizona", "America/Phoenix"},
      {"US/Central", "America/Chicago"},
      {"US/East-Indiana", "America/Indiana/Indianapolis"},
      {"US/Eastern", "America/New_York"},
      {"US/Hawaii", "Pacific/Honolulu"},
      {"US/Indiana-Starke", "America/Indiana/Knox"},
      {"US/Michigan", "America/Detroit"},
      {"US/Mountain", "America/Denver"},
      {"US/Pacific", "America/Los_Angeles"},
      {"US/Samoa", "Pacific/Pago_Pago"}};

  enum TransitionKind { TRANSITION_JULIAN, TRANSITION_DAY, TRANSITION_MONTH };

  static const int64_t MONTHS_PER_YEAR = 12;
  /**
   * The number of days in each month in non-leap and leap years.
   */
  static const int64_t DAYS_PER_MONTH[2][MONTHS_PER_YEAR] = {
      {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31},
      {31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}};
  static const int64_t DAYS_PER_WEEK = 7;

  // Leap years and day of the week repeat every 400 years, which makes it
  // a good cycle length.
  static const int64_t SECONDS_PER_400_YEARS =
      SECONDS_PER_DAY * (365 * (300 + 3) + 366 * (100 - 3));

  /**
   * Is the given year a leap year?
   */
  bool isLeap(int64_t year) {
    return (year % 4 == 0) && ((year % 100 != 0) || (year % 400 == 0));
  }

  /**
   * Find the position that is the closest and less than or equal to the
   * target.
   * @return -1 if the target < array[0] or array is empty or
   *          i if array[i] <= target and (i == n or array[i] < array[i+1])
   */
  int64_t binarySearch(const std::vector<int64_t>& array, int64_t target) {
    uint64_t size = array.size();
    if (size == 0) {
      return -1;
    }
    uint64_t min = 0;
    uint64_t max = size - 1;
    uint64_t mid = (min + max) / 2;
    while ((array[mid] != target) && (min < max)) {
      if (array[mid] < target) {
        min = mid + 1;
      } else if (mid == 0) {
        max = 0;
      } else {
        max = mid - 1;
      }
      mid = (min + max) / 2;
    }
    if (target < array[mid]) {
      return static_cast<int64_t>(mid) - 1;
    } else {
      return static_cast<int64_t>(mid);
    }
  }

  struct Transition {
    TransitionKind kind;
    int64_t day;
    int64_t week;
    int64_t month;
    int64_t time;

    std::string toString() const {
      std::stringstream buffer;
      switch (kind) {
        case TRANSITION_JULIAN:
          buffer << "julian " << day;
          break;
        case TRANSITION_DAY:
          buffer << "day " << day;
          break;
        case TRANSITION_MONTH:
          buffer << "month " << month << " week " << week << " day " << day;
          break;
      }
      buffer << " at " << (time / (60 * 60)) << ":" << ((time / 60) % 60) << ":" << (time % 60);
      return buffer.str();
    }

    /**
     * Get the transition time for the given year.
     * @param year the year
     * @return the number of seconds past local Jan 1 00:00:00 that the
     *    transition happens.
     */
    int64_t getTime(int64_t year) const {
      int64_t result = time;
      switch (kind) {
        case TRANSITION_JULIAN:
          result += SECONDS_PER_DAY * day;
          if (day > 60 && isLeap(year)) {
            result += SECONDS_PER_DAY;
          }
          break;
        case TRANSITION_DAY:
          result += SECONDS_PER_DAY * day;
          break;
        case TRANSITION_MONTH: {
          bool inLeap = isLeap(year);
          int64_t adjustedMonth = (month + 9) % 12 + 1;
          int64_t adjustedYear = (month <= 2) ? (year - 1) : year;
          int64_t adjustedCentury = adjustedYear / 100;
          int64_t adjustedRemainder = adjustedYear % 100;

          // day of the week of the first day of month
          int64_t dayOfWeek = ((26 * adjustedMonth - 2) / 10 + 1 + adjustedRemainder +
                               adjustedRemainder / 4 + adjustedCentury / 4 - 2 * adjustedCentury) %
                              7;
          if (dayOfWeek < 0) {
            dayOfWeek += DAYS_PER_WEEK;
          }

          int64_t d = day - dayOfWeek;
          if (d < 0) {
            d += DAYS_PER_WEEK;
          }
          for (int w = 1; w < week; ++w) {
            if (d + DAYS_PER_WEEK >= DAYS_PER_MONTH[inLeap][month - 1]) {
              break;
            }
            d += DAYS_PER_WEEK;
          }
          result += d * SECONDS_PER_DAY;

          // Add in the time for the month
          for (int m = 0; m < month - 1; ++m) {
            result += DAYS_PER_MONTH[inLeap][m] * SECONDS_PER_DAY;
          }
          break;
        }
      }
      return result;
    }
  };

  /**
   * The current rule for finding timezone variants arbitrarily far in
   * the future.  They are based on a string representation that
   * specifies the standard name and offset. For timezones with
   * daylight savings, the string specifies the daylight variant name
   * and offset and the rules for switching between them.
   *
   * rule = <standard name><standard offset><daylight>?
   * name = string with no numbers or '+', '-', or ','
   * offset = [-+]?hh(:mm(:ss)?)?
   * daylight = <name><offset>,<start day>(/<offset>)?,<end day>(/<offset>)?
   * day = J<day without 2/29>|<day with 2/29>|M<month>.<week>.<day of week>
   */
  class FutureRuleImpl : public FutureRule {
    std::string ruleString_;
    TimezoneVariant standard_;
    bool hasDst_;
    TimezoneVariant dst_;
    Transition start_;
    Transition end_;

    // expanded time_t offsets of transitions
    std::vector<int64_t> offsets_;

    // Is the epoch (1 Jan 1970 00:00) in standard time?
    // This code assumes that the transition dates fall in the same order
    // each year. Hopefully no timezone regions decide to move across the
    // equator, which is about what it would take.
    bool startInStd_;

    void computeOffsets() {
      if (!hasDst_) {
        startInStd_ = true;
        offsets_.resize(1);
      } else {
        // Insert a transition for the epoch and two per a year for the next
        // 400 years. We assume that the all even positions are in standard
        // time if and only if startInStd and the odd ones are the reverse.
        offsets_.resize(400 * 2 + 1);
        startInStd_ = start_.getTime(1970) < end_.getTime(1970);
        int64_t base = 0;
        for (int64_t year = 1970; year < 1970 + 400; ++year) {
          if (startInStd_) {
            offsets_[static_cast<uint64_t>(year - 1970) * 2 + 1] =
                base + start_.getTime(year) - standard_.gmtOffset;
            offsets_[static_cast<uint64_t>(year - 1970) * 2 + 2] =
                base + end_.getTime(year) - dst_.gmtOffset;
          } else {
            offsets_[static_cast<uint64_t>(year - 1970) * 2 + 1] =
                base + end_.getTime(year) - dst_.gmtOffset;
            offsets_[static_cast<uint64_t>(year - 1970) * 2 + 2] =
                base + start_.getTime(year) - standard_.gmtOffset;
          }
          base += (isLeap(year) ? 366 : 365) * SECONDS_PER_DAY;
        }
      }
      offsets_[0] = 0;
    }

   public:
    virtual ~FutureRuleImpl() override;
    bool isDefined() const override;
    const TimezoneVariant& getVariant(int64_t clk) const override;
    void print(std::ostream& out) const override;

    friend class FutureRuleParser;
  };

  FutureRule::~FutureRule() {
    // PASS
  }

  FutureRuleImpl::~FutureRuleImpl() {
    // PASS
  }

  bool FutureRuleImpl::isDefined() const {
    return ruleString_.size() > 0;
  }

  const TimezoneVariant& FutureRuleImpl::getVariant(int64_t clk) const {
    if (!hasDst_) {
      return standard_;
    } else {
      int64_t adjusted = clk % SECONDS_PER_400_YEARS;
      if (adjusted < 0) {
        adjusted += SECONDS_PER_400_YEARS;
      }
      int64_t idx = binarySearch(offsets_, adjusted);
      if (startInStd_ == (idx % 2 == 0)) {
        return standard_;
      } else {
        return dst_;
      }
    }
  }

  void FutureRuleImpl::print(std::ostream& out) const {
    if (isDefined()) {
      out << "  Future rule: " << ruleString_ << "\n";
      out << "  standard " << standard_.toString() << "\n";
      if (hasDst_) {
        out << "  dst " << dst_.toString() << "\n";
        out << "  start " << start_.toString() << "\n";
        out << "  end " << end_.toString() << "\n";
      }
    }
  }

  /**
   * A parser for the future rule strings.
   */
  class FutureRuleParser {
   public:
    FutureRuleParser(const std::string& str, FutureRuleImpl* rule)
        : ruleString_(str), length_(str.size()), position_(0), output_(*rule) {
      output_.ruleString_ = str;
      if (position_ != length_) {
        parseName(output_.standard_.name);
        output_.standard_.gmtOffset = -parseOffset();
        output_.standard_.isDst = false;
        output_.hasDst_ = position_ < length_;
        if (output_.hasDst_) {
          parseName(output_.dst_.name);
          output_.dst_.isDst = true;
          if (ruleString_[position_] != ',') {
            output_.dst_.gmtOffset = -parseOffset();
          } else {
            output_.dst_.gmtOffset = output_.standard_.gmtOffset + 60 * 60;
          }
          parseTransition(output_.start_);
          parseTransition(output_.end_);
        }
        if (position_ != length_) {
          throwError("Extra text");
        }
        output_.computeOffsets();
      }
    }

   private:
    const std::string& ruleString_;
    size_t length_;
    size_t position_;
    FutureRuleImpl& output_;

    void throwError(const char* msg) {
      std::stringstream buffer;
      buffer << msg << " at " << position_ << " in '" << ruleString_ << "'";
      throw TimezoneError(buffer.str());
    }

    /**
     * Parse the names of the form:
     *    ([^-+0-9,]+|<[^>]+>)
     * and set the output string.
     */
    void parseName(std::string& result) {
      if (position_ == length_) {
        throwError("name required");
      }
      size_t start = position_;
      if (ruleString_[position_] == '<') {
        while (position_ < length_ && ruleString_[position_] != '>') {
          position_ += 1;
        }
        if (position_ == length_) {
          throwError("missing close '>'");
        }
        position_ += 1;
      } else {
        while (position_ < length_) {
          char ch = ruleString_[position_];
          if (isdigit(ch) || ch == '-' || ch == '+' || ch == ',') {
            break;
          }
          position_ += 1;
        }
      }
      if (position_ == start) {
        throwError("empty string not allowed");
      }
      result = ruleString_.substr(start, position_ - start);
    }

    /**
     * Parse an integer of the form [0-9]+ and return it.
     */
    int64_t parseNumber() {
      if (position_ >= length_) {
        throwError("missing number");
      }
      int64_t result = 0;
      while (position_ < length_) {
        char ch = ruleString_[position_];
        if (isdigit(ch)) {
          result = result * 10 + (ch - '0');
          position_ += 1;
        } else {
          break;
        }
      }
      return result;
    }

    /**
     * Parse the offsets of the form:
     *    [-+]?[0-9]+(:[0-9]+(:[0-9]+)?)?
     * and convert it into a number of seconds.
     */
    int64_t parseOffset() {
      int64_t scale = 3600;
      bool isNegative = false;
      if (position_ < length_) {
        char ch = ruleString_[position_];
        isNegative = ch == '-';
        if (ch == '-' || ch == '+') {
          position_ += 1;
        }
      }
      int64_t result = parseNumber() * scale;
      while (position_ < length_ && scale > 1 && ruleString_[position_] == ':') {
        scale /= 60;
        position_ += 1;
        result += parseNumber() * scale;
      }
      if (isNegative) {
        result = -result;
      }
      return result;
    }

    /**
     * Parse a transition of the following form:
     *   ,(J<number>|<number>|M<number>.<number>.<number>)(/<offset>)?
     */
    void parseTransition(Transition& transition) {
      if (length_ - position_ < 2 || ruleString_[position_] != ',') {
        throwError("missing transition");
      }
      position_ += 1;
      char ch = ruleString_[position_];
      if (ch == 'J') {
        transition.kind = TRANSITION_JULIAN;
        position_ += 1;
        transition.day = parseNumber();
      } else if (ch == 'M') {
        transition.kind = TRANSITION_MONTH;
        position_ += 1;
        transition.month = parseNumber();
        if (position_ == length_ || ruleString_[position_] != '.') {
          throwError("missing first .");
        }
        position_ += 1;
        transition.week = parseNumber();
        if (position_ == length_ || ruleString_[position_] != '.') {
          throwError("missing second .");
        }
        position_ += 1;
        transition.day = parseNumber();
      } else {
        transition.kind = TRANSITION_DAY;
        transition.day = parseNumber();
      }
      if (position_ < length_ && ruleString_[position_] == '/') {
        position_ += 1;
        transition.time = parseOffset();
      } else {
        transition.time = 2 * 60 * 60;
      }
    }
  };

  /**
   * Parse the POSIX TZ string.
   */
  std::shared_ptr<FutureRule> parseFutureRule(const std::string& ruleString) {
    auto result = std::make_shared<FutureRuleImpl>();
    FutureRuleParser parser(ruleString, dynamic_cast<FutureRuleImpl*>(result.get()));
    return result;
  }

  std::string TimezoneVariant::toString() const {
    std::stringstream buffer;
    buffer << name << " " << gmtOffset;
    if (isDst) {
      buffer << " (dst)";
    }
    return buffer.str();
  }

  /**
   * An abstraction of the differences between versions.
   */
  class VersionParser {
   public:
    virtual ~VersionParser();

    /**
     * Get the version number.
     */
    virtual uint64_t getVersion() const = 0;

    /**
     * Get the number of bytes
     */
    virtual uint64_t getTimeSize() const = 0;

    /**
     * Parse the time at the given location.
     */
    virtual int64_t parseTime(const unsigned char* ptr) const = 0;

    /**
     * Parse the future string
     */
    virtual std::string parseFutureString(const unsigned char* ptr, uint64_t offset,
                                          uint64_t length) const = 0;
  };

  VersionParser::~VersionParser() {
    // PASS
  }

  static uint32_t decode32(const unsigned char* ptr) {
    return static_cast<uint32_t>(ptr[0] << 24) | static_cast<uint32_t>(ptr[1] << 16) |
           static_cast<uint32_t>(ptr[2] << 8) | static_cast<uint32_t>(ptr[3]);
  }

  class Version1Parser : public VersionParser {
   public:
    virtual ~Version1Parser() override;

    virtual uint64_t getVersion() const override {
      return 1;
    }

    /**
     * Get the number of bytes
     */
    virtual uint64_t getTimeSize() const override {
      return 4;
    }

    /**
     * Parse the time at the given location.
     */
    virtual int64_t parseTime(const unsigned char* ptr) const override {
      // sign extend from 32 bits
      return static_cast<int32_t>(decode32(ptr));
    }

    virtual std::string parseFutureString(const unsigned char*, uint64_t, uint64_t) const override {
      return "";
    }
  };

  Version1Parser::~Version1Parser() {
    // PASS
  }

  class Version2Parser : public VersionParser {
   public:
    virtual ~Version2Parser() override;

    virtual uint64_t getVersion() const override {
      return 2;
    }

    /**
     * Get the number of bytes
     */
    virtual uint64_t getTimeSize() const override {
      return 8;
    }

    /**
     * Parse the time at the given location.
     */
    virtual int64_t parseTime(const unsigned char* ptr) const override {
      return static_cast<int64_t>(decode32(ptr)) << 32 | decode32(ptr + 4);
    }

    virtual std::string parseFutureString(const unsigned char* ptr, uint64_t offset,
                                          uint64_t length) const override {
      return std::string(reinterpret_cast<const char*>(ptr) + offset + 1, length - 2);
    }
  };

  Version2Parser::~Version2Parser() {
    // PASS
  }

  class TimezoneImpl : public Timezone {
   public:
    TimezoneImpl(const std::string& filename, const std::vector<unsigned char>& buffer);
    virtual ~TimezoneImpl() override;

    /**
     * Get the variant for the given time (time_t).
     */
    const TimezoneVariant& getVariant(int64_t clk) const override;

    void print(std::ostream&) const override;

    uint64_t getVersion() const override {
      return version_;
    }

    int64_t getEpoch() const override {
      return epoch_;
    }

    int64_t convertToUTC(int64_t clk) const override {
      return clk + getVariant(clk).gmtOffset;
    }

    int64_t convertFromUTC(int64_t clk) const override {
      int64_t adjustedTime = clk - getVariant(clk).gmtOffset;
      const auto& adjustedReader = getVariant(adjustedTime);
      return clk - adjustedReader.gmtOffset;
    }

   private:
    void parseTimeVariants(const unsigned char* ptr, uint64_t variantOffset, uint64_t variantCount,
                           uint64_t nameOffset, uint64_t nameCount);
    void parseZoneFile(const unsigned char* ptr, uint64_t sectionOffset, uint64_t fileLength,
                       const VersionParser& version);
    // filename
    std::string filename_;

    // the version of the file
    uint64_t version_;

    // the list of variants for this timezone
    std::vector<TimezoneVariant> variants_;

    // the list of the times where the local rules change
    std::vector<int64_t> transitions_;

    // the variant that starts at this transition.
    std::vector<uint64_t> currentVariant_;

    // the variant before the first transition
    uint64_t ancientVariant_;

    // the rule for future times
    std::shared_ptr<FutureRule> futureRule_;

    // the last explicit transition after which we use the future rule
    int64_t lastTransition_;

    // The ORC epoch time in this timezone.
    int64_t epoch_;
  };

  DIAGNOSTIC_PUSH
#ifdef __clang__
  DIAGNOSTIC_IGNORE("-Wglobal-constructors")
  DIAGNOSTIC_IGNORE("-Wexit-time-destructors")
#endif
  static std::mutex timezone_mutex;
  static std::map<std::string, std::shared_ptr<Timezone> > timezoneCache;
  DIAGNOSTIC_POP

  Timezone::~Timezone() {
    // PASS
  }

  TimezoneImpl::TimezoneImpl(const std::string& filename, const std::vector<unsigned char>& buffer)
      : filename_(filename) {
    parseZoneFile(&buffer[0], 0, buffer.size(), Version1Parser());
    // Build the literal for the ORC epoch
    // 2015 Jan 1 00:00:00
    tm epochStruct;
    epochStruct.tm_sec = 0;
    epochStruct.tm_min = 0;
    epochStruct.tm_hour = 0;
    epochStruct.tm_mday = 1;
    epochStruct.tm_mon = 0;
    epochStruct.tm_year = 2015 - 1900;
    epochStruct.tm_isdst = 0;
    time_t utcEpoch = timegm(&epochStruct);
    epoch_ = utcEpoch - getVariant(utcEpoch).gmtOffset;
  }

  std::string getTimezoneDirectory() {
    const char* dir = getenv("TZDIR");
    if (!dir) {
      // this is present if we're in an activated conda environment
      const char* condaPrefix = getenv("CONDA_PREFIX");
      if (condaPrefix) {
        std::string condaDir(condaPrefix);
        condaDir += "/share/zoneinfo";
        return condaDir;
      } else {
        dir = DEFAULT_TZDIR;
      }
    }
    return dir;
  }

  static std::vector<unsigned char> loadTZDB(const std::string& filename) {
    std::vector<unsigned char> buffer;
    if (!fileExists(filename.c_str())) {
      std::stringstream ss;
      ss << "Time zone file " << filename << " does not exist."
         << " Please install IANA time zone database and set TZDIR env.";
      throw TimezoneError(ss.str());
    }
    try {
      std::unique_ptr<InputStream> file = readFile(filename);
      size_t size = static_cast<size_t>(file->getLength());
      buffer.resize(size);
      file->read(&buffer[0], size, 0);
    } catch (ParseError& err) {
      throw TimezoneError(err.what());
    }
    return buffer;
  }

  class LazyTimezone : public Timezone {
   private:
    std::string filename_;
    mutable std::unique_ptr<TimezoneImpl> impl_;
    mutable std::once_flag initialized_;

    TimezoneImpl* getImpl() const {
      std::call_once(initialized_, [&]() {
        auto buffer = loadTZDB(filename_);
        impl_ = std::make_unique<TimezoneImpl>(filename_, std::move(buffer));
      });
      return impl_.get();
    }

   public:
    LazyTimezone(const std::string& filename) : filename_(filename) {}

    const TimezoneVariant& getVariant(int64_t clk) const override {
      return getImpl()->getVariant(clk);
    }
    int64_t getEpoch() const override {
      return getImpl()->getEpoch();
    }
    void print(std::ostream& os) const override {
      return getImpl()->print(os);
    }
    uint64_t getVersion() const override {
      return getImpl()->getVersion();
    }

    int64_t convertToUTC(int64_t clk) const override {
      return getImpl()->convertToUTC(clk);
    }

    int64_t convertFromUTC(int64_t clk) const override {
      return getImpl()->convertFromUTC(clk);
    }
  };

  /**
   * Get a timezone by absolute filename.
   * Results are cached.
   */
  const Timezone& getTimezoneByFilename(const std::string& dir, const std::string& zone) {
    std::string filename(dir);
    filename += "/";
    filename += zone;
    // ORC-110
    std::lock_guard<std::mutex> timezone_lock(timezone_mutex);
    std::map<std::string, std::shared_ptr<Timezone> >::iterator itr = timezoneCache.find(filename);
    if (itr != timezoneCache.end()) {
      return *(itr->second).get();
    }
    auto it = TZ_ALIASES.find(zone);
    if (it == TZ_ALIASES.end()) {
      timezoneCache[filename] = std::make_shared<LazyTimezone>(filename);
    } else {
      std::string newfilename(dir);
      newfilename += "/";
      newfilename += it->second;
      timezoneCache[newfilename] = std::make_shared<LazyTimezone>(newfilename);
      timezoneCache[filename] = timezoneCache[newfilename];
    }
    return *timezoneCache[filename].get();
  }

  /**
   * Get the local timezone.
   */
  const Timezone& getLocalTimezone() {
#ifdef _MSC_VER
    return getTimezoneByName("UTC");
#else
    // The absence of LOCAL_TIMEZONE means UTC conventionally
    {
      std::string filename(LOCAL_TIMEZONE_DIR);
      filename += "/";
      filename += LOCAL_TIMEZONE;
      struct stat _ignored;
      if (stat(filename.c_str(), &_ignored) == -1) {
        return getTimezoneByName("UTC");
      }
    }

    return getTimezoneByFilename(LOCAL_TIMEZONE_DIR, LOCAL_TIMEZONE);
#endif
  }

  /**
   * Get a timezone by name (eg. America/Los_Angeles).
   * Results are cached.
   */
  const Timezone& getTimezoneByName(const std::string& zone) {
    return getTimezoneByFilename(getTimezoneDirectory(), zone);
  }

  /**
   * Parse a set of bytes as a timezone file as if they came from filename.
   */
  std::unique_ptr<Timezone> getTimezone(const std::string& filename,
                                        const std::vector<unsigned char>& b) {
    return std::make_unique<TimezoneImpl>(filename, b);
  }

  TimezoneImpl::~TimezoneImpl() {
    // PASS
  }

  void TimezoneImpl::parseTimeVariants(const unsigned char* ptr, uint64_t variantOffset,
                                       uint64_t variantCount, uint64_t nameOffset,
                                       uint64_t nameCount) {
    for (uint64_t variant = 0; variant < variantCount; ++variant) {
      variants_[variant].gmtOffset =
          static_cast<int32_t>(decode32(ptr + variantOffset + 6 * variant));
      variants_[variant].isDst = ptr[variantOffset + 6 * variant + 4] != 0;
      uint64_t nameStart = ptr[variantOffset + 6 * variant + 5];
      if (nameStart >= nameCount) {
        std::stringstream buffer;
        buffer << "name out of range in variant " << variant << " - " << nameStart
               << " >= " << nameCount;
        throw TimezoneError(buffer.str());
      }
      variants_[variant].name =
          std::string(reinterpret_cast<const char*>(ptr) + nameOffset + nameStart);
    }
  }

  /**
   * Parse the zone file to get the bits we need.
   * There are two versions of the timezone file:
   *
   * Version 1(version = 0x00):
   *   Magic(version)
   *   Header
   *   TransitionTimes(4 byte)
   *   TransitionRules
   *   Rules
   *   LeapSeconds(4 byte)
   *   IsStd
   *   IsGmt
   *
   * Version2:
   *   Version1(0x32) = a version 1 copy of the data for old clients
   *   Magic(0x32)
   *   Header
   *   TransitionTimes(8 byte)
   *   TransitionRules
   *   Rules
   *   LeapSeconds(8 byte)
   *   IsStd
   *   IsGmt
   *   FutureString
   */
  void TimezoneImpl::parseZoneFile(const unsigned char* ptr, uint64_t sectionOffset,
                                   uint64_t fileLength, const VersionParser& versionParser) {
    const uint64_t magicOffset = sectionOffset + 0;
    const uint64_t headerOffset = magicOffset + 20;

    // check for validity before we start parsing
    if (fileLength < headerOffset + 6 * 4 ||
        strncmp(reinterpret_cast<const char*>(ptr) + magicOffset, "TZif", 4) != 0) {
      std::stringstream buffer;
      buffer << "non-tzfile " << filename_;
      throw TimezoneError(buffer.str());
    }

    const uint64_t isGmtCount = decode32(ptr + headerOffset + 0);
    const uint64_t isStdCount = decode32(ptr + headerOffset + 4);
    const uint64_t leapCount = decode32(ptr + headerOffset + 8);
    const uint64_t timeCount = decode32(ptr + headerOffset + 12);
    const uint64_t variantCount = decode32(ptr + headerOffset + 16);
    const uint64_t nameCount = decode32(ptr + headerOffset + 20);

    const uint64_t timeOffset = headerOffset + 24;
    const uint64_t timeVariantOffset = timeOffset + versionParser.getTimeSize() * timeCount;
    const uint64_t variantOffset = timeVariantOffset + timeCount;
    const uint64_t nameOffset = variantOffset + variantCount * 6;
    const uint64_t sectionLength = nameOffset + nameCount +
                                   (versionParser.getTimeSize() + 4) * leapCount + isGmtCount +
                                   isStdCount;

    if (sectionLength > fileLength) {
      std::stringstream buffer;
      buffer << "tzfile too short " << filename_ << " needs " << sectionLength << " and has "
             << fileLength;
      throw TimezoneError(buffer.str());
    }

    // if it is version 2, skip over the old layout and read the new one.
    if (sectionOffset == 0 && ptr[magicOffset + 4] != 0) {
      parseZoneFile(ptr, sectionLength, fileLength, Version2Parser());
      return;
    }
    version_ = versionParser.getVersion();
    variants_.resize(variantCount);
    transitions_.resize(timeCount);
    currentVariant_.resize(timeCount);
    parseTimeVariants(ptr, variantOffset, variantCount, nameOffset, nameCount);
    bool foundAncient = false;
    for (uint64_t t = 0; t < timeCount; ++t) {
      transitions_[t] = versionParser.parseTime(ptr + timeOffset + t * versionParser.getTimeSize());
      currentVariant_[t] = ptr[timeVariantOffset + t];
      if (currentVariant_[t] >= variantCount) {
        std::stringstream buffer;
        buffer << "tzfile rule out of range " << filename_ << " references rule "
               << currentVariant_[t] << " of " << variantCount;
        throw TimezoneError(buffer.str());
      }
      // find the oldest standard time and use that as the ancient value
      if (!foundAncient && !variants_[currentVariant_[t]].isDst) {
        foundAncient = true;
        ancientVariant_ = currentVariant_[t];
      }
    }
    if (!foundAncient) {
      ancientVariant_ = 0;
    }
    futureRule_ = parseFutureRule(
        versionParser.parseFutureString(ptr, sectionLength, fileLength - sectionLength));

    // find the lower bound for applying the future rule
    if (futureRule_->isDefined()) {
      if (timeCount > 0) {
        lastTransition_ = transitions_[timeCount - 1];
      } else {
        lastTransition_ = INT64_MIN;
      }
    } else {
      lastTransition_ = INT64_MAX;
    }
  }

  const TimezoneVariant& TimezoneImpl::getVariant(int64_t clk) const {
    // if it is after the last explicit entry in the table,
    // use the future rule to get an answer
    if (clk > lastTransition_) {
      return futureRule_->getVariant(clk);
    } else {
      int64_t transition = binarySearch(transitions_, clk);
      uint64_t idx;
      if (transition < 0) {
        idx = ancientVariant_;
      } else {
        idx = currentVariant_[static_cast<size_t>(transition)];
      }
      return variants_[idx];
    }
  }

  void TimezoneImpl::print(std::ostream& out) const {
    out << "Timezone file: " << filename_ << "\n";
    out << "  Version: " << version_ << "\n";
    futureRule_->print(out);
    for (uint64_t r = 0; r < variants_.size(); ++r) {
      out << "  Variant " << r << ": " << variants_[r].toString() << "\n";
    }
    for (uint64_t t = 0; t < transitions_.size(); ++t) {
      tm timeStruct;
      tm* result = nullptr;
      char buffer[25];
      if (sizeof(time_t) >= 8) {
        time_t val = transitions_[t];
        result = gmtime_r(&val, &timeStruct);
        if (result) {
          strftime(buffer, sizeof(buffer), "%F %H:%M:%S", &timeStruct);
        }
      }
      out << "  Transition: " << (result == nullptr ? "null" : buffer) << " (" << transitions_[t]
          << ") -> " << variants_[currentVariant_[t]].name << "\n";
    }
  }

  TimezoneError::TimezoneError(const std::string& what) : std::runtime_error(what) {
    // PASS
  }

  TimezoneError::TimezoneError(const TimezoneError& other) : std::runtime_error(other) {
    // PASS
  }

  TimezoneError::~TimezoneError() noexcept {
    // PASS
  }

}  // namespace orc
