defmodule PhilomenaQuery.Parse.IpParser do
  @moduledoc false

  import NimbleParsec

  ipv4_octet =
    choice([
      ascii_char(~c"2") |> ascii_char(~c"5") |> ascii_char([?0..?5]),
      ascii_char(~c"2") |> ascii_char([?0..?4]) |> ascii_char([?0..?9]),
      ascii_char(~c"1") |> ascii_char([?0..?9]) |> ascii_char([?0..?9]),
      ascii_char([?1..?9]) |> ascii_char([?0..?9]),
      ascii_char([?0..?9])
    ])
    |> reduce({List, :to_string, []})

  ipv4_address =
    times(ipv4_octet |> string("."), 3)
    |> concat(ipv4_octet)

  ipv4_prefix =
    ascii_char(~c"/")
    |> choice([
      ascii_char(~c"3") |> ascii_char([?0..?2]),
      ascii_char([?1..?2]) |> ascii_char([?0..?9]),
      ascii_char([?0..?9])
    ])
    |> reduce({List, :to_string, []})

  ipv6_hexadectet = ascii_string(~c"0123456789abcdefABCDEF", min: 1, max: 4)

  ipv6_ls32 =
    choice([
      ipv6_hexadectet |> string(":") |> concat(ipv6_hexadectet),
      ipv4_address
    ])

  ipv6_fragment = ipv6_hexadectet |> string(":")

  ipv6_address =
    choice([
      times(ipv6_fragment, 6) |> concat(ipv6_ls32),
      string("::") |> times(ipv6_fragment, 5) |> concat(ipv6_ls32),
      ipv6_hexadectet |> string("::") |> times(ipv6_fragment, 4) |> concat(ipv6_ls32),
      string("::") |> times(ipv6_fragment, 4) |> concat(ipv6_ls32),
      times(ipv6_fragment, 1)
      |> concat(ipv6_hexadectet)
      |> string("::")
      |> times(ipv6_fragment, 3)
      |> concat(ipv6_ls32),
      ipv6_hexadectet |> string("::") |> times(ipv6_fragment, 3) |> concat(ipv6_ls32),
      string("::") |> times(ipv6_fragment, 3) |> concat(ipv6_ls32),
      times(ipv6_fragment, 2)
      |> concat(ipv6_hexadectet)
      |> string("::")
      |> times(ipv6_fragment, 2)
      |> concat(ipv6_ls32),
      times(ipv6_fragment, 1)
      |> concat(ipv6_hexadectet)
      |> string("::")
      |> times(ipv6_fragment, 2)
      |> concat(ipv6_ls32),
      ipv6_hexadectet |> string("::") |> times(ipv6_fragment, 2) |> concat(ipv6_ls32),
      string("::") |> times(ipv6_fragment, 2) |> concat(ipv6_ls32),
      times(ipv6_fragment, 3)
      |> concat(ipv6_hexadectet)
      |> string("::")
      |> concat(ipv6_fragment)
      |> concat(ipv6_ls32),
      times(ipv6_fragment, 2)
      |> concat(ipv6_hexadectet)
      |> string("::")
      |> concat(ipv6_fragment)
      |> concat(ipv6_ls32),
      times(ipv6_fragment, 1)
      |> concat(ipv6_hexadectet)
      |> string("::")
      |> concat(ipv6_fragment)
      |> concat(ipv6_ls32),
      ipv6_hexadectet |> string("::") |> concat(ipv6_fragment) |> concat(ipv6_ls32),
      string("::") |> concat(ipv6_fragment) |> concat(ipv6_ls32),
      times(ipv6_fragment, 4) |> concat(ipv6_hexadectet) |> string("::") |> concat(ipv6_ls32),
      times(ipv6_fragment, 3) |> concat(ipv6_hexadectet) |> string("::") |> concat(ipv6_ls32),
      times(ipv6_fragment, 2) |> concat(ipv6_hexadectet) |> string("::") |> concat(ipv6_ls32),
      times(ipv6_fragment, 1) |> concat(ipv6_hexadectet) |> string("::") |> concat(ipv6_ls32),
      ipv6_hexadectet |> string("::") |> concat(ipv6_ls32),
      string("::") |> concat(ipv6_ls32),
      times(ipv6_fragment, 5)
      |> concat(ipv6_hexadectet)
      |> string("::")
      |> concat(ipv6_hexadectet),
      times(ipv6_fragment, 4)
      |> concat(ipv6_hexadectet)
      |> string("::")
      |> concat(ipv6_hexadectet),
      times(ipv6_fragment, 3)
      |> concat(ipv6_hexadectet)
      |> string("::")
      |> concat(ipv6_hexadectet),
      times(ipv6_fragment, 2)
      |> concat(ipv6_hexadectet)
      |> string("::")
      |> concat(ipv6_hexadectet),
      times(ipv6_fragment, 1)
      |> concat(ipv6_hexadectet)
      |> string("::")
      |> concat(ipv6_hexadectet),
      ipv6_hexadectet |> string("::") |> concat(ipv6_hexadectet),
      string("::") |> concat(ipv6_hexadectet),
      times(ipv6_fragment, 6) |> concat(ipv6_hexadectet) |> string("::"),
      times(ipv6_fragment, 5) |> concat(ipv6_hexadectet) |> string("::"),
      times(ipv6_fragment, 4) |> concat(ipv6_hexadectet) |> string("::"),
      times(ipv6_fragment, 3) |> concat(ipv6_hexadectet) |> string("::"),
      times(ipv6_fragment, 2) |> concat(ipv6_hexadectet) |> string("::"),
      times(ipv6_fragment, 1) |> concat(ipv6_hexadectet) |> string("::"),
      ipv6_hexadectet |> string("::"),
      string("::")
    ])

  ipv6_prefix =
    ascii_char(~c"/")
    |> choice([
      ascii_char(~c"1") |> ascii_char(~c"2") |> ascii_char([?0..?8]),
      ascii_char(~c"1") |> ascii_char([?0..?1]) |> ascii_char([?0..?9]),
      ascii_char([?1..?9]) |> ascii_char([?0..?9]),
      ascii_char([?0..?9])
    ])
    |> reduce({List, :to_string, []})

  ip =
    choice([
      ipv4_address |> optional(ipv4_prefix),
      ipv6_address |> optional(ipv6_prefix)
    ])
    |> reduce({Enum, :join, []})
    |> unwrap_and_tag(:ip)
    |> eos()
    |> label("a valid IPv4 or IPv6 address and optional CIDR prefix")

  defparsec(:parse, ip)
end
