require "./rfc_2822"

struct Time::Format
  # Parse a time string using the formats specified by [RFC 2616](https://tools.ietf.org/html/rfc2616#section-3.3.1) and (non-RFC-compliant) [IIS date format](https://docs.microsoft.com/en-us/windows/desktop/wininet/http-cookies#set-cookie-header).
  #
  # Supported formats:
  # * [RFC 1123](https://tools.ietf.org/html/rfc1123#page-55)
  # * [RFC 850](https://tools.ietf.org/html/rfc850#section-2.1.4)
  # * [IIS date format](https://docs.microsoft.com/en-us/windows/desktop/wininet/http-cookies#set-cookie-header)
  # * [asctime](http://en.cppreference.com/w/c/chrono/asctime)
  #
  # ```
  # Time::Format::HTTP_DATE.parse("Sun, 14 Feb 2016 21:00:00 GMT")  # => 2016-02-14 21:00:00Z
  # Time::Format::HTTP_DATE.parse("Sunday, 14-Feb-16 21:00:00 GMT") # => 2016-02-14 21:00:00Z
  # Time::Format::HTTP_DATE.parse("Sun, 14-Feb-2016 21:00:00 GMT")  # => 2016-02-14 21:00:00Z
  # Time::Format::HTTP_DATE.parse("Sun Feb 14 21:00:00 2016")       # => 2016-02-14 21:00:00Z
  #
  # Time::Format::HTTP_DATE.format(Time.utc(2016, 2, 15)) # => "Mon, 15 Feb 2016 00:00:00 GMT"
  # ```
  module HTTP_DATE
    # Parses a string into a `Time`.
    def self.parse(string, location = Time::Location::UTC) : Time
      parser = Parser.new(string)
      parser.http_date
      parser.time(location)
    end

    # Formats a `Time` into the given *io*.
    #
    # *time* is always converted to UTC.
    def self.format(time : Time, io : IO)
      formatter = Formatter.new(time.to_utc, io)
      formatter.rfc_2822(time_zone_gmt: true, two_digit_day: true)
      io
    end

    # Formats a `Time` into a `String`.
    #
    # *time* is always converted to UTC.
    def self.format(time : Time) : String
      String.build do |io|
        format(time, io)
      end
    end
  end

  struct Parser
    def http_date : Time::Location
      ansi_c_format = http_date_short_day_name_with_comma?

      if ansi_c_format
        return http_date_ansi_c
      end

      day_of_month_zero_padded

      if current_char.ascii_whitespace?
        whitespace
        short_month_name
        whitespace
        year
      else
        char '-'
        short_month_name
        char '-'
        # Intentional departure from standard `year_modulo_100` because of IIS
        # non-RFC-compliant date format, see https://docs.microsoft.com/en-us/windows/desktop/wininet/http-cookies#set-cookie-header
        full_or_short_year
      end

      whitespace
      twenty_four_hour_time_with_seconds
      whitespace
      time_zone_gmt_or_rfc2822
    end

    def http_date_ansi_c : Time::Location
      short_month_name
      whitespace
      day_of_month_blank_padded

      whitespace

      twenty_four_hour_time_with_seconds

      whitespace

      year

      @location = Time::Location::UTC
    end

    def http_date_rfc1123?(ansi_c_format)
      !ansi_c_format && current_char.ascii_whitespace?
    end

    def http_date_short_day_name_with_comma? : Bool
      return false unless current_char.ascii_letter?

      short_day_name

      ansi_c_format = current_char != ','
      next_char unless ansi_c_format

      raise "Invalid date format" unless current_char.ascii_whitespace?
      whitespace

      ansi_c_format
    end
  end
end
