struct Time::Format
  # Even though the standard library has Time parsers given a *fixed* format,
  # the format in YAML, http://yaml.org/type/timestamp.html,
  # can consist of just the date part, and following it any number of spaces,
  # or 't', or 'T' can follow, with many optional components. So, we implement
  # this in a more efficient way to avoid parsing the same string with many
  # possible formats (there's also no way to specify an "or" like in a Regex).
  #
  # As an additional note, Ruby's Psych YAML parser also implements a
  # custom time parser, probably for this same reason.
  module YAML_DATE
    # Parses a string into a `Time`.
    def self.parse?(string) : Time?
      parser = Parser.new(string)
      if parser.yaml_date_time?
        parser.time(Time::Location::UTC) rescue nil
      else
        nil
      end
    end

    # Formats a `Time` into the given *io*.
    def self.format(time : Time, io : IO)
      formatter = Formatter.new(time, io)

      if time.hour == 0 && time.minute == 0 && time.second == 0 && time.nanosecond == 0
        formatter.year_month_day_iso_8601
      else
        formatter.yaml_date_time
      end
    end

    # Formats a `Time` into a `String`.
    def self.format(time : Time) : String
      String.build do |io|
        format(time, io)
      end
    end
  end

  struct Parser
    def yaml_date_time?
      if (year = consume_number?(4)) && char?('-')
        @year = year
      else
        return false
      end

      if (month = consume_number?(2)) && char?('-')
        @month = month
      else
        return false
      end

      if day = consume_number?(2)
        @day = day
      else
        return false
      end

      case current_char
      when 'T', 't'
        next_char
        return yaml_time?
      when ' ', '\t'
        skip_whitespaces_and_tabs

        if @reader.has_next?
          return yaml_time?
        end
      else
        if @reader.has_next?
          return false
        end
      end

      true
    end

    def yaml_time?
      if (hour = consume_number?(2)) && char?(':')
        @hour = hour
      else
        return false
      end

      if (minute = consume_number?(2)) && char?(':')
        @minute = minute
      else
        return false
      end

      if second = consume_number?(2)
        @second = second
      else
        return false
      end

      second_fraction?

      skip_whitespaces_and_tabs

      if @reader.has_next?
        begin
          time_zone_z_or_offset(force_zero_padding: false, force_minutes: false)
        rescue Time::Format::Error
          return false
        end

        return false if @reader.has_next?
      end

      true
    end

    private def skip_whitespaces_and_tabs
      while current_char.in?(' ', '\t')
        next_char
      end
    end
  end

  struct Formatter
    def yaml_date_time
      year_month_day
      char ' '
      twenty_four_hour_time_with_seconds
      second_fraction?

      unless time.utc?
        time_zone_z_or_offset(force_colon: true)
      end
    end
  end
end
