///|
priv enum WktDialect {
  Wkt1
  Wkt2
} derive(Eq)

///|
priv struct WktParser {
  tokens : Array[WktToken]
  mut index : Int32
  limits : GeoLimits
  dialect : WktDialect
  mut coordinate_count : Int32
  mut geometry_count : Int32
}

///|
fn WktParser::new(
  tokens : Array[WktToken],
  limits : GeoLimits,
  dialect : WktDialect,
  index : Int32,
) -> WktParser {
  { tokens, index, limits, dialect, coordinate_count: 0, geometry_count: 0, }
}

///|
fn WktParser::current(self : WktParser) -> WktToken {
  self.tokens[self.index]
}

///|
fn WktParser::advance(self : WktParser) -> Unit {
  self.index += 1
}

///|
fn word_matches(value : String, expected : String) -> Bool {
  value.equal_ignore_ascii_case(expected)
}

///|
fn WktParser::current_is_word(self : WktParser, expected : String) -> Bool {
  let token = self.current()
  token.kind == Word && word_matches(token.text, expected)
}

///|
fn WktParser::take_dimension_suffix(self : WktParser) -> CoordinateDimension? {
  let token = self.current()
  if token.kind == Word {
    if word_matches(token.text, "ZM") {
      self.advance()
      Some(Xyzm)
    } else if word_matches(token.text, "Z") {
      self.advance()
      Some(Xyz)
    } else if word_matches(token.text, "M") {
      self.advance()
      Some(Xym)
    } else {
      None
    }
  } else {
    None
  }
}

///|
fn WktParser::expect_kind(
  self : WktParser,
  kind : WktTokenKind,
  message : String,
) -> Result[Unit, GeoError] {
  let token = self.current()
  if token.kind != kind {
    Err(InvalidWkt(message, token.offset))
  } else {
    self.advance()
    Ok(())
  }
}

///|
fn WktParser::parse_number(
  self : WktParser,
  message : String,
) -> Result[Double, GeoError] {
  let token = self.current()
  if token.kind != Number {
    return Err(InvalidWkt(message, token.offset))
  }
  let value = @string.parse_double(token.text.view()) catch {
    _ => return Err(InvalidWkt("number out of range", token.offset))
  }
  self.advance()
  Ok(value)
}

///|
fn WktParser::parse_coordinate(
  self : WktParser,
  dimension : CoordinateDimension,
) -> Result[Coordinate, GeoError] {
  let x = match self.parse_number("expected x ordinate") {
    Ok(value) => value
    Err(err) => return Err(err)
  }
  let y = match self.parse_number("expected y ordinate") {
    Ok(value) => value
    Err(err) => return Err(err)
  }
  let z : Double? = match dimension {
    Xyz | Xyzm =>
      match self.parse_number("expected z ordinate") {
        Ok(value) => Some(value)
        Err(err) => return Err(err)
      }
    _ => None
  }
  let m : Double? = match dimension {
    Xym | Xyzm =>
      match self.parse_number("expected m ordinate") {
        Ok(value) => Some(value)
        Err(err) => return Err(err)
      }
    _ => None
  }
  if self.current().kind == Number {
    return Err(InvalidWkt("too many ordinates", self.current().offset))
  }
  self.coordinate_count += 1
  match self.limits.check_coordinates(self.coordinate_count) {
    Ok(_) => ()
    Err(err) => return Err(err)
  }
  Ok({ x, y, z, m, })
}

///|
fn WktParser::parse_coordinate_list(
  self : WktParser,
  dimension : CoordinateDimension,
) -> Result[Array[Coordinate], GeoError] {
  let coordinates : Array[Coordinate] = []
  while true {
    let coordinate = match self.parse_coordinate(dimension) {
      Ok(value) => value
      Err(err) => return Err(err)
    }
    coordinates.push(coordinate)
    if self.current().kind == Comma {
      self.advance()
    } else {
      break
    }
  }
  Ok(coordinates)
}

///|
fn WktParser::parse_ring(
  self : WktParser,
  dimension : CoordinateDimension,
) -> Result[Array[Coordinate], GeoError] {
  match self.expect_kind(LParen, "expected '(' to start polygon ring") {
    Ok(_) => ()
    Err(err) => return Err(err)
  }
  let coordinates = match self.parse_coordinate_list(dimension) {
    Ok(value) => value
    Err(err) => return Err(err)
  }
  match self.expect_kind(RParen, "expected ')' after polygon ring") {
    Ok(_) => ()
    Err(err) => return Err(err)
  }
  Ok(coordinates)
}

///|
fn WktParser::parse_point_geometry(
  self : WktParser,
  dimension : CoordinateDimension,
) -> Result[Geometry, GeoError] {
  match self.expect_kind(LParen, "expected '(' after POINT") {
    Ok(_) => ()
    Err(err) => return Err(err)
  }
  let coordinate = match self.parse_coordinate(dimension) {
    Ok(value) => value
    Err(err) => return Err(err)
  }
  match self.expect_kind(RParen, "expected ')' after POINT coordinate") {
    Ok(_) => ()
    Err(err) => return Err(err)
  }
  Ok(Point(coordinate))
}

///|
fn WktParser::parse_line_string_geometry(
  self : WktParser,
  dimension : CoordinateDimension,
) -> Result[Geometry, GeoError] {
  match self.expect_kind(LParen, "expected '(' after LINESTRING") {
    Ok(_) => ()
    Err(err) => return Err(err)
  }
  let coordinates = match self.parse_coordinate_list(dimension) {
    Ok(value) => value
    Err(err) => return Err(err)
  }
  match self.expect_kind(RParen, "expected ')' after LINESTRING coordinates") {
    Ok(_) => ()
    Err(err) => return Err(err)
  }
  Ok(LineString(coordinates))
}

///|
fn WktParser::parse_polygon_geometry(
  self : WktParser,
  dimension : CoordinateDimension,
) -> Result[Geometry, GeoError] {
  match self.expect_kind(LParen, "expected '(' after POLYGON") {
    Ok(_) => ()
    Err(err) => return Err(err)
  }
  let rings : Array[Array[Coordinate]] = []
  while true {
    let ring = match self.parse_ring(dimension) {
      Ok(value) => value
      Err(err) => return Err(err)
    }
    rings.push(ring)
    if self.current().kind == Comma {
      self.advance()
    } else {
      break
    }
  }
  match self.expect_kind(RParen, "expected ')' after POLYGON rings") {
    Ok(_) => ()
    Err(err) => return Err(err)
  }
  Ok(Polygon(rings))
}

///|
fn WktParser::parse_multi_point_geometry(
  self : WktParser,
  dimension : CoordinateDimension,
) -> Result[Geometry, GeoError] {
  match self.expect_kind(LParen, "expected '(' after MULTIPOINT") {
    Ok(_) => ()
    Err(err) => return Err(err)
  }
  let points : Array[Coordinate] = []
  let wrapped = self.current().kind == LParen
  if self.dialect == Wkt2 && !wrapped {
    return Err(
      InvalidWkt(
        "WKT2 requires parenthesized MULTIPOINT coordinates",
        self.current().offset,
      ),
    )
  }
  while true {
    let coordinate = if wrapped {
      match
        self.expect_kind(LParen, "expected '(' before MULTIPOINT coordinate") {
        Ok(_) => ()
        Err(err) => return Err(err)
      }
      let coordinate = match self.parse_coordinate(dimension) {
        Ok(value) => value
        Err(err) => return Err(err)
      }
      match
        self.expect_kind(RParen, "expected ')' after MULTIPOINT coordinate") {
        Ok(_) => ()
        Err(err) => return Err(err)
      }
      coordinate
    } else {
      match self.parse_coordinate(dimension) {
        Ok(value) => value
        Err(err) => return Err(err)
      }
    }
    points.push(coordinate)
    if self.current().kind == Comma {
      self.advance()
      if wrapped && self.current().kind != LParen {
        return Err(
          InvalidWkt(
            "expected '(' before MULTIPOINT coordinate",
            self.current().offset,
          ),
        )
      }
      if !wrapped && self.current().kind == LParen {
        return Err(
          InvalidWkt("mixed MULTIPOINT coordinate forms", self.current().offset),
        )
      }
    } else {
      break
    }
  }
  match self.expect_kind(RParen, "expected ')' after MULTIPOINT coordinates") {
    Ok(_) => ()
    Err(err) => return Err(err)
  }
  Ok(MultiPoint(points))
}

///|
fn WktParser::parse_multi_line_string_geometry(
  self : WktParser,
  dimension : CoordinateDimension,
) -> Result[Geometry, GeoError] {
  match self.expect_kind(LParen, "expected '(' after MULTILINESTRING") {
    Ok(_) => ()
    Err(err) => return Err(err)
  }
  let lines : Array[Array[Coordinate]] = []
  while true {
    match
      self.expect_kind(LParen, "expected '(' before MULTILINESTRING component") {
      Ok(_) => ()
      Err(err) => return Err(err)
    }
    let coordinates = match self.parse_coordinate_list(dimension) {
      Ok(value) => value
      Err(err) => return Err(err)
    }
    match
      self.expect_kind(RParen, "expected ')' after MULTILINESTRING component") {
      Ok(_) => ()
      Err(err) => return Err(err)
    }
    lines.push(coordinates)
    if self.current().kind == Comma {
      self.advance()
    } else {
      break
    }
  }
  match
    self.expect_kind(RParen, "expected ')' after MULTILINESTRING coordinates") {
    Ok(_) => ()
    Err(err) => return Err(err)
  }
  Ok(MultiLineString(lines))
}

///|
fn WktParser::parse_multi_polygon_geometry(
  self : WktParser,
  dimension : CoordinateDimension,
) -> Result[Geometry, GeoError] {
  match self.expect_kind(LParen, "expected '(' after MULTIPOLYGON") {
    Ok(_) => ()
    Err(err) => return Err(err)
  }
  let polygons : Array[Array[Array[Coordinate]]] = []
  while true {
    match self.expect_kind(LParen, "expected '(' before MULTIPOLYGON polygon") {
      Ok(_) => ()
      Err(err) => return Err(err)
    }
    let rings : Array[Array[Coordinate]] = []
    while true {
      let ring = match self.parse_ring(dimension) {
        Ok(value) => value
        Err(err) => return Err(err)
      }
      rings.push(ring)
      if self.current().kind == Comma {
        self.advance()
      } else {
        break
      }
    }
    match self.expect_kind(RParen, "expected ')' after MULTIPOLYGON polygon") {
      Ok(_) => ()
      Err(err) => return Err(err)
    }
    polygons.push(rings)
    if self.current().kind == Comma {
      self.advance()
    } else {
      break
    }
  }
  match self.expect_kind(RParen, "expected ')' after MULTIPOLYGON polygons") {
    Ok(_) => ()
    Err(err) => return Err(err)
  }
  Ok(MultiPolygon(polygons))
}

///|
fn WktParser::parse_geometry_collection_geometry(
  self : WktParser,
  dimension : CoordinateDimension?,
  depth : Int32,
) -> Result[Geometry, GeoError] {
  match self.expect_kind(LParen, "expected '(' after GEOMETRYCOLLECTION") {
    Ok(_) => ()
    Err(err) => return Err(err)
  }
  let geometries : Array[Geometry] = []
  while true {
    let geometry = match self.parse_geometry(dimension, depth) {
      Ok(value) => value
      Err(err) => return Err(err)
    }
    geometries.push(geometry)
    if self.current().kind == Comma {
      self.advance()
    } else {
      break
    }
  }
  match
    self.expect_kind(RParen, "expected ')' after GEOMETRYCOLLECTION members") {
    Ok(_) => ()
    Err(err) => return Err(err)
  }
  Ok(GeometryCollection(geometries))
}

///|
fn geometry_kind_from_name(name : String) -> GeometryKind? {
  if word_matches(name, "POINT") {
    Some(Point)
  } else if word_matches(name, "LINESTRING") {
    Some(LineString)
  } else if word_matches(name, "POLYGON") {
    Some(Polygon)
  } else if word_matches(name, "MULTIPOINT") {
    Some(MultiPoint)
  } else if word_matches(name, "MULTILINESTRING") {
    Some(MultiLineString)
  } else if word_matches(name, "MULTIPOLYGON") {
    Some(MultiPolygon)
  } else if word_matches(name, "GEOMETRYCOLLECTION") {
    Some(GeometryCollection)
  } else {
    None
  }
}

///|
fn is_unsupported_wkt2_geometry(name : String) -> Bool {
  word_matches(name, "CIRCULARSTRING") ||
  word_matches(name, "COMPOUNDCURVE") ||
  word_matches(name, "CURVEPOLYGON") ||
  word_matches(name, "MULTICURVE") ||
  word_matches(name, "MULTISURFACE") ||
  word_matches(name, "CURVE") ||
  word_matches(name, "SURFACE") ||
  word_matches(name, "POLYHEDRALSURFACE") ||
  word_matches(name, "TIN") ||
  word_matches(name, "TRIANGLE")
}

///|
fn WktParser::finish(
  self : WktParser,
  geometry : Geometry,
) -> Result[Geometry, GeoError] {
  let token = self.current()
  if token.kind != End {
    Err(InvalidWkt("unexpected token after geometry", token.offset))
  } else {
    Ok(geometry)
  }
}

///|
fn WktParser::parse_geometry(
  self : WktParser,
  inherited_dimension : CoordinateDimension?,
  depth : Int32,
) -> Result[Geometry, GeoError] {
  match self.limits.check_depth(depth) {
    Ok(_) => ()
    Err(err) => return Err(err)
  }
  self.geometry_count += 1
  match self.limits.check_geometries(self.geometry_count) {
    Ok(_) => ()
    Err(err) => return Err(err)
  }
  let header = self.current()
  if header.kind != Word {
    return Err(InvalidWkt("expected geometry type", header.offset))
  }
  self.advance()
  let name = header.text
  let suffix_token = self.current()
  let suffix = self.take_dimension_suffix()
  let declared_dimension : CoordinateDimension? = match
    (inherited_dimension, suffix) {
    (Some(parent), Some(child)) =>
      if parent == child {
        Some(parent)
      } else {
        return Err(
          InvalidWkt(
            "dimension suffix conflicts with enclosing geometry",
            suffix_token.offset,
          ),
        )
      }
    (Some(parent), None) => Some(parent)
    (None, Some(child)) => Some(child)
    (None, None) => None
  }
  let dimension = match declared_dimension {
    Some(value) => value
    None => Xy
  }
  let kind = match geometry_kind_from_name(name) {
    Some(value) => value
    None => {
      if self.dialect == Wkt2 && is_unsupported_wkt2_geometry(name) {
        return Err(UnsupportedGeometry(name))
      }
      return Err(InvalidWkt("unknown geometry type", header.offset))
    }
  }
  if self.current_is_word("EMPTY") {
    self.advance()
    let geometry = match kind {
      Point => EmptyPoint
      LineString => LineString([])
      Polygon => Polygon([])
      MultiPoint => MultiPoint([])
      MultiLineString => MultiLineString([])
      MultiPolygon => MultiPolygon([])
      GeometryCollection => GeometryCollection([])
    }
    return Ok(geometry)
  }
  match kind {
    Point =>
      match self.parse_point_geometry(dimension) {
        Ok(value) => Ok(value)
        Err(err) => Err(err)
      }
    LineString =>
      match self.parse_line_string_geometry(dimension) {
        Ok(value) => Ok(value)
        Err(err) => Err(err)
      }
    Polygon =>
      match self.parse_polygon_geometry(dimension) {
        Ok(value) => Ok(value)
        Err(err) => Err(err)
      }
    MultiPoint =>
      match self.parse_multi_point_geometry(dimension) {
        Ok(value) => Ok(value)
        Err(err) => Err(err)
      }
    MultiLineString =>
      match self.parse_multi_line_string_geometry(dimension) {
        Ok(value) => Ok(value)
        Err(err) => Err(err)
      }
    MultiPolygon =>
      match self.parse_multi_polygon_geometry(dimension) {
        Ok(value) => Ok(value)
        Err(err) => Err(err)
      }
    GeometryCollection =>
      match
        self.parse_geometry_collection_geometry(declared_dimension, depth + 1) {
        Ok(value) => Ok(value)
        Err(err) => Err(err)
      }
  }
}

///|
fn parse_wkt_dialect(
  source : StringView,
  limits : GeoLimits,
  dialect : WktDialect,
) -> Result[Geometry, GeoError] {
  let tokens = match tokenize_wkt(source, limits) {
    Ok(value) => value
    Err(err) => return Err(err)
  }
  let parser = WktParser::new(tokens, limits, dialect, 0)
  let geometry = match parser.parse_geometry(None, 1) {
    Ok(value) => value
    Err(err) => return Err(err)
  }
  parser.finish(geometry)
}

///|
pub fn parse_wkt(
  source : StringView,
  limits : GeoLimits,
) -> Result[Geometry, GeoError] {
  parse_wkt_dialect(source, limits, Wkt1)
}

///|
pub fn parse_wkt2(
  source : StringView,
  limits : GeoLimits,
) -> Result[Geometry, GeoError] {
  parse_wkt_dialect(source, limits, Wkt2)
}

///|
fn parse_srid_number(token : WktToken) -> Result[Int32, GeoError] {
  let value = @string.parse_int(token.text.view()) catch {
    _ => return Err(InvalidWkt("invalid SRID", token.offset))
  }
  if value < 0 {
    Err(InvalidSrid(value))
  } else {
    Ok(value)
  }
}

///|
pub fn parse_ewkt(
  source : StringView,
  limits : GeoLimits,
) -> Result[SpatialGeometry, GeoError] {
  let tokens = match tokenize_wkt(source, limits) {
    Ok(value) => value
    Err(err) => return Err(err)
  }
  let mut index = 0
  let mut srid : Int32? = None
  let first = tokens[0]
  if first.kind == Word && word_matches(first.text, "SRID") {
    index = 1
    let equals = tokens[index]
    if equals.kind != Equals {
      return Err(InvalidWkt("expected '=' after SRID", equals.offset))
    }
    index += 1
    let number = tokens[index]
    if number.kind != Number {
      return Err(InvalidWkt("expected SRID number", number.offset))
    }
    let value = match parse_srid_number(number) {
      Ok(value) => value
      Err(err) => return Err(err)
    }
    srid = Some(value)
    index += 1
    let semicolon = tokens[index]
    if semicolon.kind != Semicolon {
      return Err(InvalidWkt("expected ';' after SRID", semicolon.offset))
    }
    index += 1
  }
  let parser = WktParser::new(tokens, limits, Wkt1, index)
  let geometry = match parser.parse_geometry(None, 1) {
    Ok(value) => value
    Err(err) => return Err(err)
  }
  match parser.finish(geometry) {
    Ok(value) => Ok({ srid, geometry: value, })
    Err(err) => Err(err)
  }
}