/-
Copyright (c) 2024 Lean FRO, LLC. All rights reserved.
Released under Apache 2.0 license as described in the file LICENSE.
Authors: Sofia Rodrigues
-/
prelude
import Std.Internal.Parsec
import Std.Time.Date
import Std.Time.Time
import Std.Time.Zoned
import Std.Time.DateTime

/-!
This module defines the `Formatter` types. It is based on the Java's `DateTimeFormatter` format.
-/

namespace Std
namespace Time
open Internal
open Std.Internal.Parsec.String
open Std.Internal.Parsec Lean PlainTime PlainDate TimeZone DateTime

set_option linter.all true


/--
`Text` represents different text formatting styles.
-/
inductive Text
  /-- Short form (e.g., "Tue") -/
  | short
  /-- Full form (e.g., "Tuesday") -/
  | full
  /-- Narrow form (e.g., "T") -/
  | narrow
  deriving Repr, Inhabited

namespace Text

/--
`classify` classifies the number of pattern letters into a `Text` type.
-/
def classify (num : Nat) : Option Text :=
  if num < 4 then
    some (.short)
  else if num = 4 then
    some (.full)
  else if num = 5 then
    some (.narrow)
  else
    none

end Text

/--
`Number` represents different number formatting styles.
-/
structure Number where
  /--
  The number of digits to pad, based on the count of pattern letters.
  -/
  padding : Nat
  deriving Repr, Inhabited

/--
`classifyNumberText` classifies the number of pattern letters into either a `Number` or `Text`.
-/
def classifyNumberText : Nat → Option (Number ⊕ Text)
  | n => if n < 3 then some (.inl ⟨n⟩) else .inr <$> (Text.classify n)

/--
`Fraction` represents the fraction of a second, which can either be full nanoseconds
or a truncated form with fewer digits.
-/
inductive Fraction
  /-- Nanosecond precision (up to 9 digits) -/
  | nano
  /-- Fewer digits (truncated precision) -/
  | truncated (digits : Nat)
  deriving Repr, Inhabited

namespace Fraction

/--
`classify` classifies the number of pattern letters into either a `Fraction`. It's used for `nano`.
-/
def classify (nat : Nat) : Option Fraction :=
  if nat < 9 then
    some (.truncated nat)
  else if nat = 9 then
    some (.nano)
  else
    none

end Fraction

/--
`Year` represents different year formatting styles based on the number of pattern letters.
-/
inductive Year
  /-- Any size (e.g., "19000000000000") -/
  | any
  /-- Two-digit year format (e.g., "23" for 2023) -/
  | twoDigit
  /-- Four-digit year format (e.g., "2023") -/
  | fourDigit
  /-- Extended year format for more than 4 digits (e.g., "002023") -/
  | extended (num : Nat)
  deriving Repr, Inhabited

namespace Year

/--
`classify` classifies the number of pattern letters into a `Year` format.
-/
def classify (num : Nat) : Option Year :=
  if num = 1 then
    some .any
  else if num = 2 then
    some .twoDigit
  else if num = 4 then
    some .fourDigit
  else if num > 4 ∨ num = 3 then
    some (.extended num)
  else
    none

end Year

/--
`ZoneId` represents different time zone ID formats based on the number of pattern letters.
-/
inductive ZoneId
  /-- Short form of time zone (e.g., "PST") -/
  | short
  /-- Full form of time zone (e.g., "Pacific Standard Time") -/
  | full
  deriving Repr, Inhabited

namespace ZoneId

/--
`classify` classifies the number of pattern letters into a `ZoneId` format.
- If 2 letters, it returns the short form.
- If 4 letters, it returns the full form.
- Otherwise, it returns none.
-/
def classify (num : Nat) : Option ZoneId :=
  if num = 2 then
    some (.short)
  else if num = 4 then
    some (.full)
  else
    none

end ZoneId

/--
`ZoneName` represents different zone name formats based on the number of pattern letters and
whether daylight saving time is considered.
-/
inductive ZoneName
  /-- Short form of zone name (e.g., "PST") -/
  | short
  /-- Full form of zone name (e.g., "Pacific Standard Time") -/
  | full
  deriving Repr, Inhabited

namespace ZoneName

/--
`classify` classifies the number of pattern letters and the letter type ('z' or 'v')
into a `ZoneName` format.
- For 'z', if less than 4 letters, it returns the short form; if 4 letters, it returns the full form.
- For 'v', if 1 letter, it returns the short form; if 4 letters, it returns the full form.
- Otherwise, it returns none.
-/
def classify (letter : Char) (num : Nat) : Option ZoneName :=
  if letter = 'z' then
    if num < 4 then
      some (.short)
    else if num = 4 then
      some (.full)
    else
      none
  else if letter = 'v' then
    if num = 1 then
      some (.short)
    else if num = 4 then
      some (.full)
    else
      none
  else
    none

end ZoneName

/--
`OffsetX` represents different offset formats based on the number of pattern letters.
The output will vary between the number of pattern letters, whether it's the hour, minute, second,
and whether colons are used.
-/
inductive OffsetX
  /-- Only the hour is output (e.g., "+01") -/
  | hour
  /-- Hour and minute without colon (e.g., "+0130") -/
  | hourMinute
  /-- Hour and minute with colon (e.g., "+01:30") -/
  | hourMinuteColon
  /-- Hour, minute, and second without colon (e.g., "+013015") -/
  | hourMinuteSecond
  /-- Hour, minute, and second with colon (e.g., "+01:30:15") -/
  | hourMinuteSecondColon
  deriving Repr, Inhabited

namespace OffsetX

/--
`classify` classifies the number of pattern letters into an `OffsetX` format.
-/
def classify (num : Nat) : Option OffsetX :=
  if num = 1 then
    some (.hour)
  else if num = 2 then
    some (.hourMinute)
  else if num = 3 then
    some (.hourMinuteColon)
  else if num = 4 then
    some (.hourMinuteSecond)
  else if num = 5 then
    some (.hourMinuteSecondColon)
  else
    none

end OffsetX

/--
`OffsetO` represents localized offset text formats based on the number of pattern letters.
-/
inductive OffsetO
  /-- Short form of the localized offset (e.g., "GMT+8") -/
  | short
  /-- Full form of the localized offset (e.g., "GMT+08:00") -/
  | full
  deriving Repr, Inhabited

namespace OffsetO

/--
`classify` classifies the number of pattern letters into an `OffsetO` format.
-/
def classify (num : Nat) : Option OffsetO :=
  match num with
  | 1 => some (.short)
  | 4 => some (.full)
  | _ => none

end OffsetO

/--
`OffsetZ` represents different offset formats based on the number of pattern letters (capital 'Z').
-/
inductive OffsetZ
  /-- Hour and minute without colon (e.g., "+0130") -/
  | hourMinute
  /-- Localized offset text in full form (e.g., "GMT+08:00") -/
  | full
  /-- Hour, minute, and second with colon (e.g., "+01:30:15") -/
  | hourMinuteSecondColon
  deriving Repr, Inhabited

namespace OffsetZ

/--
`classify` classifies the number of pattern letters into an `OffsetZ` format.
-/
def classify (num : Nat) : Option OffsetZ :=
  match num with
  | 1 | 2 | 3 => some (.hourMinute)
  | 4 => some (.full)
  | 5 => some (.hourMinuteSecondColon)
  | _ => none

end OffsetZ

/--
The `Modifier` inductive type represents various formatting options for date and time components,
matching the format symbols used in date and time strings.
These modifiers can be applied in formatting functions to generate custom date and time outputs.
-/
inductive Modifier
  /--
  `G`: Era (e.g., AD, Anno Domini, A).
  -/
  | G (presentation : Text)

  /--
  `y`: Year of era (e.g., 2004, 04, 0002, 2).
  -/
  | y (presentation : Year)

  /--
  `u`: Year (e.g., 2004, 04, -0001, -1).
  -/
  | u (presentation : Year)

  /--
  `D`: Day of year (e.g., 189).
  -/
  | D (presentation : Number)

  /--
  `M`: Month of year as number or text (e.g., 7, 07, Jul, July, J).
  -/
  | MorL (presentation : Number ⊕ Text)

  /--
  `d`: Day of month (e.g., 10).
  -/
  | d (presentation : Number)

  /--
  `Q`: Quarter of year as number or text (e.g., 3, 03, Q3, 3rd quarter).
  -/
  | Qorq (presentation : Number ⊕ Text)

  /--
  `w`: Week of week-based year (e.g., 27).
  -/
  | w (presentation : Number)

  /--
  `W`: Week of month (e.g., 4).
  -/
  | W (presentation : Number)

  /--
  `E`: Day of week as text (e.g., Tue, Tuesday, T).
  -/
  | E (presentation : Text)

  /--
  `e`: Localized day of week as number or text (e.g., 2, 02, Tue, Tuesday, T).
  -/
  | eorc (presentation : Number ⊕ Text)

  /--
  `F`: Aligned week of month (e.g., 3).
  -/
  | F (presentation : Number)

  /--
  `a`: AM/PM of day (e.g., PM).
  -/
  | a (presentation : Text)

  /--
  `h`: Clock hour of AM/PM (1-12) (e.g., 12).
  -/
  | h (presentation : Number)

  /--
  `K`: Hour of AM/PM (0-11) (e.g., 0).
  -/
  | K (presentation : Number)

  /--
  `k`: Clock hour of day (1-24) (e.g., 24).
  -/
  | k (presentation : Number)

  /--
  `H`: Hour of day (0-23) (e.g., 0).
  -/
  | H (presentation : Number)

  /--
  `m`: Minute of hour (e.g., 30).
  -/
  | m (presentation : Number)

  /--
  `s`: Second of minute (e.g., 55).
  -/
  | s (presentation : Number)

  /--
  `S`: Fraction of second (e.g., 978).
  -/
  | S (presentation : Fraction)

  /--
  `A`: Millisecond of day (e.g., 1234).
  -/
  | A (presentation : Number)

  /--
  `n`: Nanosecond of second (e.g., 987654321).
  -/
  | n (presentation : Number)

  /--
  `N`: Nanosecond of day (e.g., 1234000000).
  -/
  | N (presentation : Number)

  /--
  `V`: Time zone ID (e.g., America/Los_Angeles, Z, -08:30).
  -/
  | V

  /--
  `z`: Time zone name (e.g., Pacific Standard Time, PST).
  -/
  | z (presentation : ZoneName)

  /--
  `O`: Localized zone offset (e.g., GMT+8, GMT+08:00, UTC-08:00).
  -/
  | O (presentation : OffsetO)

  /--
  `X`: Zone offset with 'Z' for zero (e.g., Z, -08, -0830, -08:30).
  -/
  | X (presentation : OffsetX)

  /--
  `x`: Zone offset without 'Z' (e.g., +0000, -08, -0830, -08:30).
  -/
  | x (presentation : OffsetX)

  /--
  `Z`: Zone offset with 'Z' for UTC (e.g., +0000, -0800, -08:00).
  -/
  | Z (presentation : OffsetZ)
  deriving Repr, Inhabited

/--
`abstractParse` abstracts the parsing logic for any type that has a classify function.
It takes a constructor function to build the `Modifier` and a classify function that maps the pattern length to a specific type.
-/
private def parseMod (constructor : α → Modifier) (classify : Nat → Option α) (p : String) : Parser Modifier :=
  let len := p.length
  match classify len with
  | some res => pure (constructor res)
  | none => fail s!"invalid quantity of characters for '{p.get 0}'"

private def parseText (constructor : Text → Modifier) (p : String) : Parser Modifier :=
  parseMod constructor Text.classify p

private def parseFraction (constructor : Fraction → Modifier) (p : String) : Parser Modifier :=
  parseMod constructor Fraction.classify p

private def parseNumber (constructor : Number → Modifier) (p : String) : Parser Modifier :=
  pure (constructor ⟨p.length⟩)

private def parseYear (constructor : Year → Modifier) (p : String) : Parser Modifier :=
  parseMod constructor Year.classify p

private def parseOffsetX (constructor : OffsetX → Modifier) (p : String) : Parser Modifier :=
  parseMod constructor OffsetX.classify p

private def parseOffsetZ (constructor : OffsetZ → Modifier) (p : String) : Parser Modifier :=
  parseMod constructor OffsetZ.classify p

private def parseOffsetO (constructor : OffsetO → Modifier) (p : String) : Parser Modifier :=
  parseMod constructor OffsetO.classify p

private def parseZoneId (p : String) : Parser Modifier :=
  if p.length = 2 then pure .V else fail s!"invalid quantity of characters for '{p.get 0}'"

private def parseNumberText (constructor : (Number ⊕ Text) → Modifier) (p : String) : Parser Modifier :=
  parseMod constructor classifyNumberText p

private def parseZoneName (constructor : ZoneName → Modifier) (p : String) : Parser Modifier :=
  let len := p.length
  match ZoneName.classify (p.get 0) len with
  | some res => pure (constructor res)
  | none => fail s!"invalid quantity of characters for '{p.get 0}'"

private def parseModifier : Parser Modifier
  := (parseText Modifier.G =<< many1Chars (pchar 'G'))
  <|> parseYear Modifier.y =<< many1Chars (pchar 'y')
  <|> parseYear Modifier.u =<< many1Chars (pchar 'u')
  <|> parseNumber Modifier.D =<< many1Chars (pchar 'D')
  <|> parseNumberText Modifier.MorL =<< many1Chars (pchar 'M')
  <|> parseNumberText Modifier.MorL =<< many1Chars (pchar 'L')
  <|> parseNumber Modifier.d =<< many1Chars (pchar 'd')
  <|> parseNumberText Modifier.Qorq =<< many1Chars (pchar 'Q')
  <|> parseNumberText Modifier.Qorq =<< many1Chars (pchar 'q')
  <|> parseNumber Modifier.w =<< many1Chars (pchar 'w')
  <|> parseNumber Modifier.W =<< many1Chars (pchar 'W')
  <|> parseText Modifier.E =<< many1Chars (pchar 'E')
  <|> parseNumberText Modifier.eorc =<< many1Chars (pchar 'e')
  <|> parseNumberText Modifier.eorc =<< many1Chars (pchar 'c')
  <|> parseNumber Modifier.F =<< many1Chars (pchar 'F')
  <|> parseText Modifier.a =<< many1Chars (pchar 'a')
  <|> parseNumber Modifier.h =<< many1Chars (pchar 'h')
  <|> parseNumber Modifier.K =<< many1Chars (pchar 'K')
  <|> parseNumber Modifier.k =<< many1Chars (pchar 'k')
  <|> parseNumber Modifier.H =<< many1Chars (pchar 'H')
  <|> parseNumber Modifier.m =<< many1Chars (pchar 'm')
  <|> parseNumber Modifier.s =<< many1Chars (pchar 's')
  <|> parseFraction Modifier.S =<< many1Chars (pchar 'S')
  <|> parseNumber Modifier.A =<< many1Chars (pchar 'A')
  <|> parseNumber Modifier.n =<< many1Chars (pchar 'n')
  <|> parseNumber Modifier.N =<< many1Chars (pchar 'N')
  <|> parseZoneId =<< many1Chars (pchar 'V')
  <|> parseZoneName Modifier.z =<< many1Chars (pchar 'z')
  <|> parseOffsetO Modifier.O =<< many1Chars (pchar 'O')
  <|> parseOffsetX Modifier.X =<< many1Chars (pchar 'X')
  <|> parseOffsetX Modifier.x =<< many1Chars (pchar 'x')
  <|> parseOffsetZ Modifier.Z =<< many1Chars (pchar 'Z')

/--
The part of a formatting string. A string is just a text and a modifier is in the format described in
the `Modifier` type.
-/
inductive FormatPart
  /--
  A string literal.
  -/
  | string (val : String)

  /--
  A modifier that renders some data into text.
  -/
  | modifier (modifier : Modifier)
  deriving Repr

instance : Coe String FormatPart where
  coe := .string

instance : Coe Modifier FormatPart where
  coe := .modifier

/--
The format of date and time string.
-/
abbrev FormatString := List FormatPart

/--
If the format is aware of some timezone data it parses or if it parses any timezone.
-/
inductive Awareness
  /-- The format only parses a single timezone. -/
  | only : TimeZone → Awareness
  /-- The format parses any timezone. -/
  | any

namespace Awareness

instance : Coe TimeZone Awareness where
  coe := .only

@[simp]
private def type (x : Awareness) : Type :=
  match x with
  | .any => ZonedDateTime
  | .only tz => DateTime tz

instance : Inhabited (type aw) where
  default := by
    simp [type]
    split <;> exact Inhabited.default

private def getD (x : Awareness) (default : TimeZone) : TimeZone :=
  match x with
  | .any => default
  | .only tz => tz

end Awareness

/--
Configuration options for formatting and parsing date/time strings.
-/
structure FormatConfig where
  /--
  Whether to allow leap seconds, such as `2016-12-31T23:59:60Z`.
  Default is `false`.
  -/
  allowLeapSeconds : Bool := false

deriving Inhabited, Repr

/--
A specification on how to format a data or parse some string.
-/
structure GenericFormat (awareness : Awareness) where
  /--
  Configuration options for formatting behavior.
  -/
  config : FormatConfig

  /--
  The format string used for parsing and formatting.
  -/
  string : FormatString
  deriving Inhabited, Repr

private def parseFormatPart : Parser FormatPart
  := (.modifier <$> parseModifier)
  <|> (pchar '\\') *> any <&> (.string ∘ toString)
  <|> (pchar '\"' *>  many1Chars (satisfy (· ≠ '\"')) <* pchar '\"') <&> .string
  <|> (pchar '\'' *>  many1Chars (satisfy (· ≠ '\'')) <* pchar '\'') <&> .string
  <|> many1Chars (satisfy (fun x => ¬Char.isAlpha x ∧ x ≠ '\'' ∧ x ≠ '\"')) <&> .string

private def specParser : Parser FormatString :=
  (Array.toList <$> many parseFormatPart) <* eof

private def specParse (s : String) : Except String FormatString :=
  specParser.run s

-- Pretty printer

private def leftPad (n : Nat) (a : Char) (s : String) : String :=
  "".pushn a (n -  s.length) ++ s

private def rightPad (n : Nat) (a : Char) (s : String) : String :=
  s ++ "".pushn a (n - s.length)

private def pad (size : Nat)  (n : Int) (cut : Bool := false) : String :=
  let (sign, n) := if n < 0 then ("-", -n) else ("", n)

  let numStr := toString n
  if numStr.length > size then
    sign ++ if cut then numStr.drop (numStr.length - size) else numStr
  else
    sign ++ leftPad size '0' numStr

private def rightTruncate (size : Nat)  (n : Int) (cut : Bool := false) : String :=
  let (sign, n) := if n < 0 then ("-", -n) else ("", n)

  let numStr := toString n
  if numStr.length > size then
    sign ++ if cut then numStr.take size else numStr
  else
    sign ++ rightPad size '0' numStr

private def formatMonthLong : Month.Ordinal → String
  | ⟨1, _⟩ => "January"
  | ⟨2, _⟩ => "February"
  | ⟨3, _⟩ => "March"
  | ⟨4, _⟩ => "April"
  | ⟨5, _⟩ => "May"
  | ⟨6, _⟩ => "June"
  | ⟨7, _⟩ => "July"
  | ⟨8, _⟩ => "August"
  | ⟨9, _⟩ => "September"
  | ⟨10, _⟩ => "October"
  | ⟨11, _⟩ => "November"
  | ⟨12, _⟩ => "December"

private def formatMonthShort : Month.Ordinal → String
  | ⟨1, _⟩ => "Jan"
  | ⟨2, _⟩ => "Feb"
  | ⟨3, _⟩ => "Mar"
  | ⟨4, _⟩ => "Apr"
  | ⟨5, _⟩ => "May"
  | ⟨6, _⟩ => "Jun"
  | ⟨7, _⟩ => "Jul"
  | ⟨8, _⟩ => "Aug"
  | ⟨9, _⟩ => "Sep"
  | ⟨10, _⟩ => "Oct"
  | ⟨11, _⟩ => "Nov"
  | ⟨12, _⟩ => "Dec"

private def formatMonthNarrow : Month.Ordinal → String
  | ⟨1, _⟩  => "J"
  | ⟨2, _⟩  => "F"
  | ⟨3, _⟩  => "M"
  | ⟨4, _⟩  => "A"
  | ⟨5, _⟩  => "M"
  | ⟨6, _⟩  => "J"
  | ⟨7, _⟩  => "J"
  | ⟨8, _⟩  => "A"
  | ⟨9, _⟩  => "S"
  | ⟨10, _⟩ => "O"
  | ⟨11, _⟩ => "N"
  | ⟨12, _⟩ => "D"

private def formatWeekdayLong : Weekday → String
  | .sunday => "Sunday"
  | .monday => "Monday"
  | .tuesday => "Tuesday"
  | .wednesday => "Wednesday"
  | .thursday => "Thursday"
  | .friday => "Friday"
  | .saturday => "Saturday"

private def formatWeekdayShort : Weekday → String
  | .sunday => "Sun"
  | .monday => "Mon"
  | .tuesday => "Tue"
  | .wednesday => "Wed"
  | .thursday => "Thu"
  | .friday => "Fri"
  | .saturday => "Sat"

private def formatWeekdayNarrow : Weekday → String
  | .sunday => "S"
  | .monday => "M"
  | .tuesday => "T"
  | .wednesday => "W"
  | .thursday => "T"
  | .friday => "F"
  | .saturday => "S"

private def formatEraShort : Year.Era → String
  | .bce => "BCE"
  | .ce  => "CE"

private def formatEraLong : Year.Era → String
  | .bce => "Before Common Era"
  | .ce  => "Common Era"

private def formatEraNarrow : Year.Era → String
  | .bce => "B"
  | .ce  => "C"

private def formatQuarterNumber : Month.Quarter → String
  |⟨1, _⟩ => "1"
  |⟨2, _⟩ => "2"
  |⟨3, _⟩ => "3"
  |⟨4, _⟩ => "4"

private def formatQuarterShort : Month.Quarter → String
  | ⟨1, _⟩ => "Q1"
  | ⟨2, _⟩ => "Q2"
  | ⟨3, _⟩ => "Q3"
  | ⟨4, _⟩ => "Q4"

private def formatQuarterLong : Month.Quarter → String
  | ⟨1, _⟩ => "1st quarter"
  | ⟨2, _⟩ => "2nd quarter"
  | ⟨3, _⟩ => "3rd quarter"
  | ⟨4, _⟩ => "4th quarter"

private def formatMarkerShort (marker : HourMarker) : String :=
  match marker with
  | .am => "AM"
  | .pm => "PM"

private def formatMarkerLong (marker : HourMarker) : String :=
  match marker with
  | .am => "Ante Meridiem"
  | .pm => "Post Meridiem"

private def formatMarkerNarrow (marker : HourMarker) : String :=
  match marker with
  | .am => "A"
  | .pm => "P"

private def toSigned (data : Int) : String :=
  if data < 0 then toString data else "+" ++ toString data

private def toIsoString (offset : Offset) (withMinutes : Bool) (withSeconds : Bool) (colon : Bool) : String :=
  let (sign, time) := if offset.second.val ≥ 0 then ("+", offset.second) else ("-", -offset.second)
  let time := PlainTime.ofSeconds time
  let pad := leftPad 2 '0' ∘ toString

  let data := s!"{sign}{pad time.hour.val}"
  let data := if withMinutes then s!"{data}{if colon then ":" else ""}{pad time.minute.val}" else data
  let data := if withSeconds ∧ time.second.val ≠ 0 then s!"{data}{if colon then ":" else ""}{pad time.second.val}" else data

  data

private def TypeFormat : Modifier → Type
  | .G _ => Year.Era
  | .y _ => Year.Offset
  | .u _ => Year.Offset
  | .D _ => Sigma Day.Ordinal.OfYear
  | .MorL _ => Month.Ordinal
  | .d _ => Day.Ordinal
  | .Qorq _ => Month.Quarter
  | .w _ => Week.Ordinal
  | .W _ => Week.Ordinal.OfMonth
  | .E _ => Weekday
  | .eorc _ => Weekday
  | .F _ => Bounded.LE 1 5
  | .a _ => HourMarker
  | .h _ => Bounded.LE 1 12
  | .K _ => Bounded.LE 0 11
  | .k _ => Bounded.LE 1 24
  | .H _ => Hour.Ordinal
  | .m _ => Minute.Ordinal
  | .s _ => Second.Ordinal true
  | .S _ => Nanosecond.Ordinal
  | .A _ => Millisecond.Offset
  | .n _ => Nanosecond.Ordinal
  | .N _ => Nanosecond.Offset
  | .V => String
  | .z _ => String
  | .O _ => Offset
  | .X _ => Offset
  | .x _ => Offset
  | .Z _ => Offset

private def formatWith (modifier : Modifier) (data: TypeFormat modifier) : String :=
  match modifier with
  | .G format =>
    match format with
    | .short => formatEraShort data
    | .full => formatEraLong data
    | .narrow => formatEraNarrow data
  | .y format =>
    let info := data.toInt
    let info := if info ≤ 0 then -info + 1 else info
    match format with
    | .any => pad 0 (data.toInt)
    | .twoDigit => pad 2 (info % 100)
    | .fourDigit => pad 4 info
    | .extended n => pad n info
  | .u format =>
    match format with
    | .any => pad 0 (data.toInt)
    | .twoDigit => pad 2 (data.toInt % 100)
    | .fourDigit => pad 4 data.toInt
    | .extended n => pad n data.toInt
  | .D format =>
    pad format.padding data.snd.val
  | .MorL format =>
    match format with
    | .inl format => pad format.padding data.val
    | .inr .short => formatMonthShort data
    | .inr .full => formatMonthLong data
    | .inr .narrow => formatMonthNarrow data
  | .d format =>
    pad format.padding data.val
  | .Qorq format =>
    match format with
    | .inl format => pad format.padding data.val
    | .inr .short => formatQuarterShort data
    | .inr .full => formatQuarterLong data
    | .inr .narrow => formatQuarterNumber data
  | .w format =>
    pad format.padding data.val
  | .W format =>
    pad format.padding data.val
  | .E format =>
    match format with
    | .short => formatWeekdayShort data
    | .full => formatWeekdayLong data
    | .narrow => formatWeekdayNarrow data
  | .eorc format =>
    match format with
    | .inl format => pad format.padding data.toOrdinal.val
    | .inr .short => formatWeekdayShort data
    | .inr .full => formatWeekdayLong data
    | .inr .narrow => formatWeekdayNarrow data
  | .F format =>
    pad format.padding data.val
  | .a format =>
    match format with
    | .short => formatMarkerShort data
    | .full => formatMarkerLong data
    | .narrow => formatMarkerNarrow data
  | .h format => pad format.padding (data.val % 12)
  | .K format => pad format.padding (data.val % 12)
  | .k format => pad format.padding data.val
  | .H format => pad format.padding data.val
  | .m format => pad format.padding data.val
  | .s format => pad format.padding data.val
  | .S format =>
    match format with
    | .nano => pad 9 data.val
    | .truncated n => rightTruncate n data.val (cut := true)
  | .A format =>
    pad format.padding data.val
  | .n format =>
    pad format.padding data.val
  | .N format =>
    pad format.padding data.val
  | .V => data
  | .z format =>
    match format with
    | .short => data
    | .full => data
  | .O format =>
    match format with
    | .short => s!"GMT{toSigned data.second.toHours.toInt}"
    | .full => s!"GMT{toIsoString data true false true}"
  | .X format =>
    if data.second == 0 then
      "Z"
    else
      match format with
        | .hour => toIsoString data false false false
        | .hourMinute => toIsoString data true false false
        | .hourMinuteColon => toIsoString data true false true
        | .hourMinuteSecond => toIsoString data true true false
        | .hourMinuteSecondColon => toIsoString data true true true
  | .x format =>
    match format with
    | .hour =>
      toIsoString data (data.second.toMinutes.val % 60 ≠ 0) false false
    | .hourMinute =>
      toIsoString data true false false
    | .hourMinuteColon =>
      toIsoString data true (data.second.val % 60 ≠ 0) true
    | .hourMinuteSecond =>
      toIsoString data true (data.second.val % 60 ≠ 0) false
    | .hourMinuteSecondColon =>
      toIsoString data true true true
  | .Z format =>
    match format with
    | .hourMinute =>
      toIsoString data true false false
    | .full =>
      if data.second.val = 0
        then "GMT"
        else s!"GMT{toIsoString data true false true}"
    | .hourMinuteSecondColon =>
      if data.second == 0
        then "Z"
        else  toIsoString data true (data.second.val % 60 ≠ 0) true

private def dateFromModifier (date : DateTime tz) : TypeFormat modifier :=
  match modifier with
  | .G _ => date.era
  | .y _ => date.year
  | .u _ => date.year
  | .D _ => Sigma.mk _ date.dayOfYear
  | .MorL _ => date.month
  | .d _ => date.day
  | .Qorq _ => date.quarter
  | .w _ => date.weekOfYear
  | .W _ => date.alignedWeekOfMonth
  | .E _ =>  date.weekday
  | .eorc _ => date.weekday
  | .F _ => date.weekOfMonth
  | .a _ => HourMarker.ofOrdinal date.hour
  | .h _ => HourMarker.toRelative date.hour |>.fst
  | .K _ => date.hour.emod 12 (by decide)
  | .k _ => date.hour.shiftTo1BasedHour
  | .H _ => date.hour
  | .m _ => date.minute
  | .s _ => date.date.get.time.second
  | .S _ => date.nanosecond
  | .A _ => date.date.get.time.toMilliseconds
  | .n _ => date.nanosecond
  | .N _ => date.date.get.time.toNanoseconds
  | .V => tz.name
  | .z .short => tz.abbreviation
  | .z .full => tz.name
  | .O _ => tz.offset
  | .X _ => tz.offset
  | .x _ => tz.offset
  | .Z _ => tz.offset

private def parseMonthLong : Parser Month.Ordinal
   := pstring "January" *> pure ⟨1, by decide⟩
  <|> pstring "February" *> pure ⟨2, by decide⟩
  <|> pstring "March" *> pure ⟨3, by decide⟩
  <|> pstring "April" *> pure ⟨4, by decide⟩
  <|> pstring "May" *> pure ⟨5, by decide⟩
  <|> pstring "June" *> pure ⟨6, by decide⟩
  <|> pstring "July" *> pure ⟨7, by decide⟩
  <|> pstring "August" *> pure ⟨8, by decide⟩
  <|> pstring "September" *> pure ⟨9, by decide⟩
  <|> pstring "October" *> pure ⟨10, by decide⟩
  <|> pstring "November" *> pure ⟨11, by decide⟩
  <|> pstring "December" *> pure ⟨12, by decide⟩

/--
Parses a short value of a `Month.Ordinal`
-/
def parseMonthShort : Parser Month.Ordinal
   := pstring "Jan" *> pure ⟨1, by decide⟩
  <|> pstring "Feb" *> pure ⟨2, by decide⟩
  <|> pstring "Mar" *> pure ⟨3, by decide⟩
  <|> pstring "Apr" *> pure ⟨4, by decide⟩
  <|> pstring "May" *> pure ⟨5, by decide⟩
  <|> pstring "Jun" *> pure ⟨6, by decide⟩
  <|> pstring "Jul" *> pure ⟨7, by decide⟩
  <|> pstring "Aug" *> pure ⟨8, by decide⟩
  <|> pstring "Sep" *> pure ⟨9, by decide⟩
  <|> pstring "Oct" *> pure ⟨10, by decide⟩
  <|> pstring "Nov" *> pure ⟨11, by decide⟩
  <|> pstring "Dec" *> pure ⟨12, by decide⟩

private def parseMonthNarrow : Parser Month.Ordinal
   := pstring "J" *> pure ⟨1, by decide⟩
  <|> pstring "F" *> pure ⟨2, by decide⟩
  <|> pstring "M" *> pure ⟨3, by decide⟩
  <|> pstring "A" *> pure ⟨4, by decide⟩
  <|> pstring "M" *> pure ⟨5, by decide⟩
  <|> pstring "J" *> pure ⟨6, by decide⟩
  <|> pstring "J" *> pure ⟨7, by decide⟩
  <|> pstring "A" *> pure ⟨8, by decide⟩
  <|> pstring "S" *> pure ⟨9, by decide⟩
  <|> pstring "O" *> pure ⟨10, by decide⟩
  <|> pstring "N" *> pure ⟨11, by decide⟩
  <|> pstring "D" *> pure ⟨12, by decide⟩

private def parseWeekdayLong : Parser Weekday
   := pstring "Sunday" *> pure Weekday.sunday
  <|> pstring "Monday" *> pure Weekday.monday
  <|> pstring "Tuesday" *> pure Weekday.tuesday
  <|> pstring "Wednesday" *> pure Weekday.wednesday
  <|> pstring "Thursday" *> pure Weekday.thursday
  <|> pstring "Friday" *> pure Weekday.friday
  <|> pstring "Saturday" *> pure Weekday.saturday

private def parseWeekdayShort : Parser Weekday
   := pstring "Sun" *> pure Weekday.sunday
  <|> pstring "Mon" *> pure Weekday.monday
  <|> pstring "Tue" *> pure Weekday.tuesday
  <|> pstring "Wed" *> pure Weekday.wednesday
  <|> pstring "Thu" *> pure Weekday.thursday
  <|> pstring "Fri" *> pure Weekday.friday
  <|> pstring "Sat" *> pure Weekday.saturday

private def parseWeekdayNarrow : Parser Weekday
   := pstring "S" *> pure Weekday.sunday
  <|> pstring "M" *> pure Weekday.monday
  <|> pstring "T" *> pure Weekday.tuesday
  <|> pstring "W" *> pure Weekday.wednesday
  <|> pstring "T" *> pure Weekday.thursday
  <|> pstring "F" *> pure Weekday.friday
  <|> pstring "S" *> pure Weekday.saturday

private def parseEraShort : Parser Year.Era
   := pstring "BCE" *> pure Year.Era.bce
  <|> pstring "CE" *> pure Year.Era.ce

private def parseEraLong : Parser Year.Era
   := pstring "Before Common Era" *> pure Year.Era.bce
  <|> pstring "Common Era" *> pure Year.Era.ce

private def parseEraNarrow : Parser Year.Era
   := pstring "B" *> pure Year.Era.bce
  <|> pstring "C" *> pure Year.Era.ce

private def parseQuarterNumber : Parser Month.Quarter
   := pstring "1" *> pure ⟨1, by decide⟩
  <|> pstring "2" *> pure ⟨2, by decide⟩
  <|> pstring "3" *> pure ⟨3, by decide⟩
  <|> pstring "4" *> pure ⟨4, by decide⟩

private def parseQuarterLong : Parser Month.Quarter
   := pstring "1st quarter" *> pure ⟨1, by decide⟩
  <|> pstring "2nd quarter" *> pure ⟨2, by decide⟩
  <|> pstring "3rd quarter" *> pure ⟨3, by decide⟩
  <|> pstring "4th quarter" *> pure ⟨4, by decide⟩

private def parseQuarterShort : Parser Month.Quarter
   := pstring "Q1" *> pure ⟨1, by decide⟩
  <|> pstring "Q2" *> pure ⟨2, by decide⟩
  <|> pstring "Q3" *> pure ⟨3, by decide⟩
  <|> pstring "Q4" *> pure ⟨4, by decide⟩

private def parseMarkerShort : Parser HourMarker
   := pstring "AM" *> pure HourMarker.am
  <|> pstring "PM" *> pure HourMarker.pm

private def parseMarkerLong : Parser HourMarker
   := pstring "Ante Meridiem" *> pure HourMarker.am
  <|> pstring "Post Meridiem" *> pure HourMarker.pm

private def parseMarkerNarrow : Parser HourMarker
   := pstring "A" *> pure HourMarker.am
  <|> pstring "P" *> pure HourMarker.pm

private def exactly (parse : Parser α) (size : Nat) : Parser (Array α) :=
  let rec go (acc : Array α) (count : Nat) : Parser (Array α) :=
    if count ≥ size then
      pure acc
    else do
      let res ← parse
      go (acc.push res) count.succ
  termination_by size - count

  go #[] 12

private def exactlyChars (parse : Parser Char) (size : Nat) : Parser String :=
  let rec go (acc : String) (count : Nat) : Parser String :=
    if count ≥ size then
      pure acc
    else do
      let res ← parse
      go (acc.push res) count.succ
  termination_by size - count

  go "" 0

private def parseSigned (parser : Parser Nat) : Parser Int := do
  let signed ← optional (pstring "-")
  let res ← parser
  return if signed.isSome then -res else res

private def parseNum (size : Nat) : Parser Nat :=
  String.toNat! <$> exactlyChars (satisfy Char.isDigit) size

private def parseAtLeastNum (size : Nat) : Parser Nat :=
  String.toNat! <$> do
    let start ← exactlyChars (satisfy Char.isDigit) size
    let end_ ← manyChars (satisfy Char.isDigit)
    pure (start ++ end_)

private def parseFlexibleNum (size : Nat) : Parser Nat :=
  if size = 1 then parseAtLeastNum 1 else parseNum size

private def parseFractionNum (size : Nat) (pad : Nat) : Parser Nat :=
  String.toNat! <$> rightPad pad '0' <$> exactlyChars (satisfy Char.isDigit) size

private def parseIdentifier : Parser String :=
  many1Chars (satisfy (fun x => x.isAlpha ∨ x.isDigit ∨ x = '_' ∨ x = '-' ∨ x = '/'))

private def parseNatToBounded { n m : Nat } (parser : Parser Nat) : Parser (Bounded.LE n m) := do
  let res ← parser
  if h : n ≤ res ∧ res ≤ m then
    return Bounded.LE.ofNat' res h
  else
    fail s!"need a natural number in the interval of {n} to {m}"

private inductive Reason
  | yes
  | no
  | optional

private def parseOffset (withMinutes : Reason) (withSeconds : Reason) (withColon : Bool) : Parser Offset := do
  let sign ← (pchar '+' *> pure 1) <|> (pchar '-' *> pure (-1))
  let hours : Hour.Offset ← UnitVal.ofInt <$> parseNum 2

  if hours.val < 0 ∨ hours.val > 23 then
    fail s!"invalid hour offset: {hours.val}. Must be between 0 and 23."

  let colon := if withColon then pchar ':' else pure ':'

  let parseUnit {n} (reason : Reason) : Parser (Option (UnitVal n)) :=
    match reason with
    | .yes => some <$> (colon *> UnitVal.ofInt <$> parseNum 2)
    | .no => pure none
    | .optional => optional (colon *> UnitVal.ofInt <$> parseNum 2)

  let minutes : Option Minute.Offset ← parseUnit withMinutes

  if let some m := minutes then
    if m.val > 59 then
      fail s!"invalid minute offset: {m.val}. Must be between 0 and 59."

  let seconds : Option Second.Offset ← parseUnit withSeconds

  if let some s := seconds then
    if s.val > 59 then
      fail s!"invalid second offset: {s.val}. Must be between 0 and 59."

  let hours := hours.toSeconds + (minutes.getD 0).toSeconds + (seconds.getD 0)

  return Offset.ofSeconds ⟨hours.val * sign⟩

private def parseWith (config : FormatConfig) : (mod : Modifier) → Parser (TypeFormat mod)
  | .G format =>
    match format with
    | .short => parseEraShort
    | .full => parseEraLong
    | .narrow => parseEraNarrow
  | .y format =>
    match format with
    | .any => Int.ofNat <$> parseAtLeastNum 1
    | .twoDigit => (2000 + ·) <$> Int.ofNat <$> parseNum 2
    | .fourDigit => Int.ofNat <$> parseNum 4
    | .extended n => Int.ofNat <$> parseNum n
  | .u format =>
    match format with
    | .any => parseSigned <| parseAtLeastNum 1
    | .twoDigit => (2000 + ·) <$> Int.ofNat <$> parseNum 2
    | .fourDigit => parseSigned <| parseNum 4
    | .extended n => parseSigned <| parseNum n
  | .D format => Sigma.mk true <$> parseNatToBounded (parseFlexibleNum format.padding)
  | .MorL format =>
    match format with
    | .inl format => parseNatToBounded (parseFlexibleNum format.padding)
    | .inr .short => parseMonthShort
    | .inr .full => parseMonthLong
    | .inr .narrow => parseMonthNarrow
  | .d format => parseNatToBounded (parseFlexibleNum format.padding)
  | .Qorq format =>
    match format with
    | .inl format => parseNatToBounded (parseFlexibleNum format.padding)
    | .inr .short => parseQuarterShort
    | .inr .full => parseQuarterLong
    | .inr .narrow => parseQuarterNumber
  | .w format => parseNatToBounded (parseFlexibleNum format.padding)
  | .W format => parseNatToBounded (parseFlexibleNum format.padding)
  | .E format =>
    match format with
    | .short => parseWeekdayShort
    | .full => parseWeekdayLong
    | .narrow => parseWeekdayNarrow
  | .eorc format =>
    match format with
    | .inl format => Weekday.ofOrdinal <$> parseNatToBounded (parseFlexibleNum format.padding)
    | .inr .short => parseWeekdayShort
    | .inr .full => parseWeekdayLong
    | .inr .narrow => parseWeekdayNarrow
  | .F format => parseNatToBounded (parseFlexibleNum format.padding)
  | .a format =>
    match format with
    | .short => parseMarkerShort
    | .full => parseMarkerLong
    | .narrow => parseMarkerNarrow
  | .h format => parseNatToBounded (parseFlexibleNum format.padding)
  | .K format => parseNatToBounded (parseFlexibleNum format.padding)
  | .k format => parseNatToBounded (parseFlexibleNum format.padding)
  | .H format => parseNatToBounded (parseFlexibleNum format.padding)
  | .m format => parseNatToBounded (parseFlexibleNum format.padding)
  | .s format =>
    if config.allowLeapSeconds then
      parseNatToBounded (parseFlexibleNum format.padding)
    else do
      let res : Bounded.LE 0 59 ← parseNatToBounded (parseFlexibleNum format.padding)
      return res.expandTop (by decide)
  | .S format =>
    match format with
    | .nano => parseNatToBounded (parseFlexibleNum 9)
    | .truncated n => parseNatToBounded (parseFractionNum n 9)
  | .A format => Millisecond.Offset.ofNat <$> (parseFlexibleNum format.padding)
  | .n format => parseNatToBounded (parseFlexibleNum format.padding)
  | .N format => Nanosecond.Offset.ofNat <$> (parseFlexibleNum format.padding)
  | .V => parseIdentifier
  | .z format =>
    match format with
    | .short => parseIdentifier
    | .full => parseIdentifier
  | .O format =>
    match format with
    | .short => pstring "GMT" *> parseOffset .no .no false
    | .full => pstring "GMT" *> parseOffset .yes .optional false
  | .X format =>
    let p : Parser Offset :=
      match format with
        | .hour => parseOffset .no .no false
        | .hourMinute => parseOffset .yes .no false
        | .hourMinuteColon => parseOffset .yes .no true
        | .hourMinuteSecond => parseOffset .yes .yes false
        | .hourMinuteSecondColon => parseOffset .yes .yes true
    p <|> (pstring "Z" *> pure (Offset.ofSeconds 0))
  | .x format =>
    match format with
    | .hour =>
      parseOffset .optional .no false
    | .hourMinute =>
      parseOffset .yes .no false
    | .hourMinuteColon =>
      parseOffset .yes .optional true
    | .hourMinuteSecond =>
      parseOffset .yes .optional false
    | .hourMinuteSecondColon =>
      parseOffset .yes .yes true
  | .Z format =>
    match format with
    | .hourMinute =>
      parseOffset .yes .no false
    | .full => do
      skipString "GMT"
      let res ← optional (parseOffset .yes .no true)
      return res.getD Offset.zero
    | .hourMinuteSecondColon =>
      (skipString "Z" *> pure Offset.zero)
      <|> (parseOffset .yes .optional true)

private def formatPartWithDate (date : DateTime tz) (part : FormatPart) : String :=
  match part with
  | .modifier mod => formatWith mod (dateFromModifier date)
  | .string s => s

@[simp]
private def FormatType (result : Type) : FormatString → Type
  | .modifier entry :: xs => (TypeFormat entry) → (FormatType result xs)
  | .string _ :: xs => (FormatType result xs)
  | [] => result

namespace GenericFormat

private structure DateBuilder where
  G : Option Year.Era := none
  y : Option Year.Offset := none
  u : Option Year.Offset := none
  D : Option (Sigma Day.Ordinal.OfYear) := none
  MorL : Option Month.Ordinal := none
  d : Option Day.Ordinal := none
  Qorq : Option Month.Quarter := none
  w : Option Week.Ordinal := none
  W : Option Week.Ordinal.OfMonth := none
  E : Option Weekday := none
  eorc : Option Weekday := none
  F : Option (Bounded.LE 1 5) := none
  a : Option HourMarker := none
  h : Option (Bounded.LE 1 12) := none
  K : Option (Bounded.LE 0 11) := none
  k : Option (Bounded.LE 1 24) := none
  H : Option Hour.Ordinal := none
  m : Option Minute.Ordinal := none
  s : Option (Second.Ordinal true) := none
  S : Option Nanosecond.Ordinal := none
  A : Option Millisecond.Offset := none
  n : Option Nanosecond.Ordinal := none
  N : Option Nanosecond.Offset := none
  V : Option String := none
  z : Option String := none
  zabbrev : Option String := none
  O : Option Offset := none
  X : Option Offset := none
  x : Option Offset := none
  Z : Option Offset := none

namespace DateBuilder

private def insert (date : DateBuilder) (modifier : Modifier) (data : TypeFormat modifier) : DateBuilder :=
  match modifier with
  | .G _ => { date with G := some data }
  | .y _ => { date with y := some data }
  | .u _ => { date with u := some data }
  | .D _ => { date with D := some data }
  | .MorL _ => { date with MorL := some data }
  | .d _ => { date with d := some data }
  | .Qorq _ => { date with Qorq := some data }
  | .w _ => { date with w := some data }
  | .W _ => { date with W := some data }
  | .E _ => { date with E := some data }
  | .eorc _ => { date with eorc := some data }
  | .F _ => { date with F := some data }
  | .a _ => { date with a := some data }
  | .h _ => { date with h := some data }
  | .K _ => { date with K := some data }
  | .k _ => { date with k := some data }
  | .H _ => { date with H := some data }
  | .m _ => { date with m := some data }
  | .s _ => { date with s := some data }
  | .S _ => { date with S := some data }
  | .A _ => { date with A := some data }
  | .n _ => { date with n := some data }
  | .N _ => { date with N := some data }
  | .V => { date with V := some data }
  | .z .full => { date with z := some data }
  | .z .short => { date with zabbrev := some data }
  | .O _ => { date with O := some data }
  | .X _ => { date with X := some data }
  | .x _ => { date with x := some data }
  | .Z _ => { date with Z := some data }

private def convertYearAndEra (year : Year.Offset) : Year.Era → Year.Offset
  | .ce => year
  | .bce => -(year + 1)

private def build (builder : DateBuilder) (aw : Awareness) : Option aw.type :=
  let offset := builder.O <|> builder.X <|> builder.x <|> builder.Z |>.getD Offset.zero

  let tz : TimeZone := {
    offset,
    name := builder.V <|> builder.z |>.getD (offset.toIsoString true),
    abbreviation := builder.zabbrev |>.getD (offset.toIsoString true),
    isDST := false,
  }

  let month := builder.MorL |>.getD 0
  let day := builder.d |>.getD 0
  let era := (builder.G.getD .ce)

  let year
    := builder.u
    <|> ((convertYearAndEra · era) <$> builder.y)
    |>.getD 0

  let hour : Option (Bounded.LE 0 23) :=
    if let some marker := builder.a then
      marker.toAbsolute <$> builder.h
      <|> marker.toAbsolute <$> ((Bounded.LE.add · 1) <$> builder.K)
    else
      none

  let hour :=
    hour <|> (
      let one : Option (Bounded.LE 0 23) := builder.H
      let other : Option (Bounded.LE 0 23) := (Bounded.LE.sub · 1) <$> builder.k
      (one <|> other))
      |>.getD ⟨0, by decide⟩

  let minute := builder.m |>.getD 0
  let second := builder.s |>.getD 0
  let nano := (builder.n <|> builder.S) |>.getD 0

  let time
    :=  PlainTime.ofNanoseconds <$> builder.N
    <|> PlainTime.ofMilliseconds <$> builder.A
    |>.getD (PlainTime.mk hour minute second nano)

  let datetime : Option PlainDateTime :=
    if valid : year.Valid month day then
      let date : PlainDate := { year, month, day, valid }
      some { date, time }
    else
      none

  match aw with
    | .only newTz => (ofPlainDateTime · newTz) <$> datetime
    | .any => (ZonedDateTime.ofPlainDateTime · (ZoneRules.ofTimeZone tz)) <$> datetime

end DateBuilder

private def parseWithDate (date : DateBuilder) (config : FormatConfig) (mod : FormatPart) : Parser DateBuilder := do
  match mod with
  | .modifier s => do
    let res ← parseWith config s
    return date.insert s res
  | .string s => pstring s *> pure date

/--
Constructs a new `GenericFormat` specification for a date-time string. Modifiers can be combined to create
custom formats, such as "YYYY, MMMM, D".
-/
def spec (input : String) (config : FormatConfig := {}) : Except String (GenericFormat tz) := do
  let string ← specParser.run input
  return ⟨config, string⟩

/--
Builds a `GenericFormat` from the input string. If parsing fails, it will panic
-/
def spec! (input : String) (config : FormatConfig := {}) : GenericFormat tz :=
  match specParser.run input with
  | .ok res => ⟨config, res⟩
  | .error res => panic! res

/--
Formats a `DateTime` value into a string using the given `GenericFormat`.
-/
def format (format : GenericFormat aw) (date : DateTime tz) : String :=
  let mapper (part : FormatPart) :=
    match aw with
    | .any => formatPartWithDate date part
    | .only tz => formatPartWithDate (date.convertTimeZone tz) part

  format.string.map mapper
  |> String.join

private def parser (format : FormatString) (config : FormatConfig) (aw : Awareness) : Parser (aw.type) :=
  let rec go (builder : DateBuilder) (x : FormatString) : Parser aw.type :=
    match x with
    | x :: xs => parseWithDate builder config x >>= (go · xs)
    | [] =>
      match builder.build aw with
      | some res => pure res
      | none => fail "could not parse the date"
  go {} format

/--
Parser for a format with a builder.
-/
def builderParser (format: FormatString) (config : FormatConfig) (func: FormatType (Option α) format) : Parser α :=
  let rec go (format : FormatString) (func: FormatType (Option α) format) : Parser α :=
    match format with
    | .modifier x :: xs => do
      let res ← parseWith config x
      go xs (func res)
    | .string s :: xs => skipString s *> (go xs func)
    | [] =>
        match func with
        | some res => eof *> pure res
        | none => fail "invalid date."
  go format func

/--
Parses the input string into a `ZoneDateTime`.
-/
def parse (format : GenericFormat aw) (input : String) : Except String aw.type :=
  (parser format.string format.config aw <* eof).run input

/--
Parses the input string into a `ZoneDateTime` and panics if its wrong.
-/
def parse! (format : GenericFormat aw) (input : String) : aw.type :=
  match parse format input with
  | .ok res => res
  | .error err => panic! err

/--
Parses an input string using a builder function to produce a value.
-/
def parseBuilder (format : GenericFormat aw)  (builder : FormatType (Option α) format.string) (input : String) : Except String α :=
  (builderParser format.string format.config builder).run input

/--
Parses an input string using a builder function, panicking on errors.
-/
def parseBuilder! [Inhabited α] (format : GenericFormat aw)  (builder : FormatType (Option α) format.string) (input : String) : α :=
  match parseBuilder format builder input with
  | .ok res => res
  | .error err => panic! err

/--
Formats the date using the format into a String, using a `getInfo` function to get the information needed to build the `String`.
-/
def formatGeneric (format : GenericFormat aw) (getInfo : (typ : Modifier) → Option (TypeFormat typ)) : Option String :=
  let rec go (data : String) : (format : FormatString) → Option String
    | .modifier x :: xs => do go (data ++ formatWith x (← getInfo x)) xs
    | .string x :: xs => go (data ++ x) xs
    | [] => some data
  go "" format.string

/--
Constructs a `FormatType` function to format a date into a string using a `GenericFormat`.
-/
def formatBuilder (format : GenericFormat aw) : FormatType String format.string :=
  let rec go (data : String) : (format : FormatString) → FormatType String format
    | .modifier x :: xs => fun res => go (data ++ formatWith x res) xs
    | .string x :: xs => go (data ++ x) xs
    | [] => data
  go "" format.string

end GenericFormat

/--
Typeclass for formatting and parsing values with the given format type.
-/
class Format (f : Type) (typ : Type → f → Type) where
  /--
  Converts a format `f` into a string.
  -/
  format : (fmt : f) → typ String fmt

  /--
  Parses a string into a format using the provided format type `f`.
  -/
  parse : (fmt : f) → typ (Option α) fmt → String → Except String α

instance : Format (GenericFormat aw) (FormatType · ·.string) where
  format := GenericFormat.formatBuilder
  parse := GenericFormat.parseBuilder

end Time
