defmodule Postgrex.Utils do
  @moduledoc false

  @extensions [
    Postgrex.Extensions.Array,
    Postgrex.Extensions.BitString,
    Postgrex.Extensions.Bool,
    Postgrex.Extensions.Box,
    Postgrex.Extensions.Circle,
    Postgrex.Extensions.Date,
    Postgrex.Extensions.Float4,
    Postgrex.Extensions.Float8,
    Postgrex.Extensions.HStore,
    Postgrex.Extensions.INET,
    Postgrex.Extensions.Int2,
    Postgrex.Extensions.Int4,
    Postgrex.Extensions.Int8,
    Postgrex.Extensions.Interval,
    Postgrex.Extensions.JSON,
    Postgrex.Extensions.JSONB,
    Postgrex.Extensions.Line,
    Postgrex.Extensions.LineSegment,
    Postgrex.Extensions.Lquery,
    Postgrex.Extensions.Ltree,
    Postgrex.Extensions.Ltxtquery,
    Postgrex.Extensions.MACADDR,
    Postgrex.Extensions.Multirange,
    Postgrex.Extensions.Name,
    Postgrex.Extensions.Numeric,
    Postgrex.Extensions.OID,
    Postgrex.Extensions.Path,
    Postgrex.Extensions.Point,
    Postgrex.Extensions.Polygon,
    Postgrex.Extensions.Range,
    Postgrex.Extensions.Raw,
    Postgrex.Extensions.Record,
    Postgrex.Extensions.TID,
    Postgrex.Extensions.Time,
    Postgrex.Extensions.Timestamp,
    Postgrex.Extensions.TimestampTZ,
    Postgrex.Extensions.TimeTZ,
    Postgrex.Extensions.TSVector,
    Postgrex.Extensions.UUID,
    Postgrex.Extensions.VoidBinary,
    Postgrex.Extensions.VoidText,
    Postgrex.Extensions.Xid8
  ]

  @doc """
  Checks if a given extension is a default extension.
  """
  for ext <- @extensions do
    def default_extension?(unquote(ext)), do: true
  end

  def default_extension?(_), do: false

  @doc """
  List all default extensions.
  """
  @spec default_extensions(Keyword.t()) :: [{module(), Keyword.t()}]
  def default_extensions(opts \\ []) do
    Enum.map(@extensions, &{&1, opts})
  end

  @doc """
  Converts pg major.minor.patch (http://www.postgresql.org/support/versioning) version to an integer
  """
  def parse_version(version) do
    segments =
      version
      |> String.split(" ", parts: 2)
      |> hd()
      |> String.split(".", parts: 4)
      |> Enum.map(&parse_version_bit/1)

    case segments do
      [major, minor, patch, _] -> {major, minor, patch}
      [major, minor, patch] -> {major, minor, patch}
      [major, minor] -> {major, minor, 0}
      [major] -> {major, 0, 0}
    end
  end

  @doc """
  Fills in the given `opts` with default options.
  Only adds keys extracted via PGHOST if no endpoint-related keys are explicitly provided.
  """
  @spec default_opts(Keyword.t()) :: Keyword.t()
  def default_opts(opts) do
    {field, value} = extract_host(System.get_env("PGHOST"))

    endpoint_keys = [:socket, :socket_dir, :hostname, :endpoints]
    has_endpoint? = Enum.any?(endpoint_keys, &Keyword.has_key?(opts, &1))

    opts
    |> Keyword.put_new(:username, System.get_env("PGUSER") || System.get_env("USER"))
    |> Keyword.put_new(:password, System.get_env("PGPASSWORD"))
    |> Keyword.put_new(:database, System.get_env("PGDATABASE"))
    |> then(fn opts ->
      if has_endpoint?, do: opts, else: Keyword.put(opts, field, value)
    end)
    |> Keyword.put_new(:port, System.get_env("PGPORT"))
    |> Keyword.update!(:port, &normalize_port/1)
    |> Keyword.put_new(:types, Postgrex.DefaultTypes)
    |> Enum.reject(fn {_k, v} -> is_nil(v) end)
  end

  defp extract_host("/" <> _ = dir), do: {:socket_dir, dir}
  defp extract_host(<<d, ?:>> <> _ = dir) when d in ?a..?z or d in ?A..?Z, do: {:socket_dir, dir}
  defp extract_host("@" <> abstract_socket), do: {:socket, <<0>> <> abstract_socket}
  defp extract_host(host), do: {:hostname, host || "localhost"}

  defp normalize_port(port) when is_binary(port), do: String.to_integer(port)
  defp normalize_port(port), do: port

  @doc """
  Return encode error message.
  """
  def encode_msg(%Postgrex.TypeInfo{type: type}, observed, expected) do
    "Postgrex expected #{to_desc(expected)} that can be encoded/cast to " <>
      "type #{inspect(type)}, got #{inspect(observed)}. Please make sure the " <>
      "value you are passing matches the definition in your table or in your " <>
      "query or convert the value accordingly."
  end

  @doc """
  Return encode error message.
  """
  def encode_msg(%Date{calendar: calendar} = observed, _expected) when calendar != Calendar.ISO do
    "Postgrex expected a %Date{} in the `Calendar.ISO` calendar, got #{inspect(observed)}. " <>
      "Postgrex (and PostgreSQL) support dates in the `Calendar.ISO` calendar only."
  end

  def encode_msg(%NaiveDateTime{calendar: calendar} = observed, _expected)
      when calendar != Calendar.ISO do
    "Postgrex expected a %NaiveDateTime{} in the `Calendar.ISO` calendar, got #{inspect(observed)}. " <>
      "Postgrex (and PostgreSQL) support naive datetimes in the `Calendar.ISO` calendar only."
  end

  def encode_msg(%DateTime{calendar: calendar} = observed, _expected)
      when calendar != Calendar.ISO do
    "Postgrex expected a %DateTime{} in the `Calendar.ISO` calendar, got #{inspect(observed)}. " <>
      "Postgrex (and PostgreSQL) support datetimes in the `Calendar.ISO` calendar only."
  end

  def encode_msg(%Time{calendar: calendar} = observed, _expected) when calendar != Calendar.ISO do
    "Postgrex expected a %Time{} in the `Calendar.ISO` calendar, got #{inspect(observed)}. " <>
      "Postgrex (and PostgreSQL) support times in the `Calendar.ISO` calendar only."
  end

  def encode_msg(observed, expected) do
    "Postgrex expected #{to_desc(expected)}, got #{inspect(observed)}. " <>
      "Please make sure the value you are passing matches the definition in " <>
      "your table or in your query or convert the value accordingly."
  end

  @doc """
  Return type error message.
  """
  def type_msg(%Postgrex.TypeInfo{type: json}, module)
      when json in ["json", "jsonb"] do
    "type `#{json}` can not be handled by the types module #{inspect(module)}, " <>
      "it must define a `:json` library in its options to support JSON types"
  end

  def type_msg(%Postgrex.TypeInfo{type: type}, module) do
    "type `#{type}` can not be handled by the types module #{inspect(module)}"
  end

  ## Helpers

  defp parse_version_bit(bit) do
    {int, _} = Integer.parse(bit)
    int
  end

  defp to_desc(struct) when is_atom(struct), do: "%#{inspect(struct)}{}"
  defp to_desc(%Range{} = range), do: "an integer in #{inspect(range)}"
  defp to_desc({a, b}), do: to_desc(a) <> " or " <> to_desc(b)
  defp to_desc(desc) when is_binary(desc), do: desc
end
