defmodule Vnu do
  @moduledoc "General purpose validation functions for HTML, CSS, and SVG documents."

  alias Vnu.{Error, Result, Validator}

  @doc ~S"""
  Validates the given HTML document.

  Returns `{:ok, %Vnu.Result{}}` if the validation process finished successfully, and `{:error, %Vnu.Error{}}` otherwise.
  Note that the `{:ok, %Vnu.Result{}}` return value does not mean necessarily that the document is valid.
  See `Vnu.valid?/1` and `Vnu.Message` for interpreting the result.

  ## Options
  - `:server_url` - The URL of [the Checker server](https://github.com/validator/validator).
  Defaults to `http://localhost:8888`.
  - `:filter` - A module implementing the `Vnu.MessageFilter` behavior that will be used to exclude messages matching the filter from the result.
  Defaults to `nil` (no excluded messages).
  - `:http_client` - A module implementing the `Vnu.HTTPClient` behaviour that will be used to make the HTTP request to the server.
  Defaults to `Vnu.HTTPClient.Hackney`.


  ## Examples

      iex> Vnu.validate_html(~S(
      ...><!DOCTYPE html>
      ...><html>
      ...><head>
      ...>  <meta charset="utf-8">
      ...></head>
      ...><body>
      ...></body>
      ...></html>
      ...>), server_url: System.get_env("VNU_SERVER_URL") || "http://localhost:8888")
      {:ok, %Vnu.Result{messages: [
        %Vnu.Message{
          type: :error,
          message: "Element “head” is missing a required instance of child element “title”.",
          extract: "=\"utf-8\">\n</head>\n<body",
          first_line: 6,
          last_line: 6,
          first_column: 1,
          last_column: 7,
          hilite_length: 7,
          hilite_start: 10
        },
        %Vnu.Message{
          type: :info,
          sub_type: :warning,
          message: "Consider adding a “lang” attribute to the “html” start tag to declare the language of this document.",
          extract: "TYPE html>\n<html>\n<head",
          first_line: 2,
          last_line: 3,
          first_column: 16,
          last_column: 6,
          hilite_length: 7,
          hilite_start: 10,
        }
      ]}}

      iex> Vnu.validate_html("", server_url: "http://wrong-domain")
      {:error, %Vnu.Error{
        reason: :unexpected_server_response,
        message: "Could not contact the server, got error: :nxdomain"
      }}
  """

  @spec validate_html(String.t(), Keyword.t()) :: {:ok, Result.t()} | {:error, Error.t()}
  def validate_html(html, opts \\ []) when is_bitstring(html) and is_list(opts) do
    Validator.validate(html, Keyword.merge(opts, format: :html))
  end

  @doc ~S"""
  Same as `validate_svg/2` but returns `%Vnu.Result{}` or raises `%Vnu.Error{}`.
  """

  @spec validate_html!(String.t(), Keyword.t()) :: Result.t() | no_return()
  def validate_html!(html, opts \\ []) when is_bitstring(html) and is_list(opts) do
    Validator.validate!(html, Keyword.merge(opts, format: :html))
  end

  @doc ~S"""
  Validates the given CSS document.

  See `validate_html/2` for the list of options and other details.

  ## Examples

      iex> Vnu.validate_css(".button { display: banana; }", server_url: System.get_env("VNU_SERVER_URL") || "http://localhost:8888")
      {:ok, %Vnu.Result{messages: [
        %Vnu.Message{
          type: :error,
          message: "“display”: “banana” is not a “display” value.",
          extract: ".button { display: banana; }\n",
          first_line: 1,
          last_line: 1,
          first_column: 20,
          last_column: 25,
          hilite_length: 6,
          hilite_start: 19,
        }
      ]}}

      iex> Vnu.validate_css("", server_url: "http://wrong-domain")
      {:error, %Vnu.Error{
        reason: :unexpected_server_response,
        message: "Could not contact the server, got error: :nxdomain"
      }}
  """

  @spec validate_css(String.t(), Keyword.t()) :: {:ok, Result.t()} | {:error, Error.t()}
  def validate_css(css, opts \\ []) when is_bitstring(css) and is_list(opts) do
    Validator.validate(css, Keyword.merge(opts, format: :css))
  end

  @doc ~S"""
  Same as `validate_css/2` but returns `%Vnu.Result{}` or raises `%Vnu.Error{}`.
  """

  @spec validate_css!(String.t(), Keyword.t()) :: Result.t() | no_return()
  def validate_css!(css, opts \\ []) when is_bitstring(css) and is_list(opts) do
    Validator.validate!(css, Keyword.merge(opts, format: :css))
  end

  @doc ~S"""
  Validates the given SVG document.

  See `validate_html/2` for the list of options and other details.

  ## Examples

      iex> Vnu.validate_svg(~S(
      ...><svg width="5cm" height="4cm" version="1.1" xmlns="http://www.w3.org/2000/svg">
      ...><desc>Rectangle</desc>
      ...><rect x="0.5cm" y="0.5cm" height="1cm"/>
      ...></svg>
      ...> ), server_url: System.get_env("VNU_SERVER_URL") || "http://localhost:8888")
      {:ok, %Vnu.Result{messages: [
        %Vnu.Message{
          type: :info,
          message: "Using the preset for SVG 1.1 + URL + HTML + MathML 3.0 based on the root namespace."
        },
        %Vnu.Message{
          type: :error,
          message: "SVG element “rect” is missing required attribute “width”.",
          extract: "le</desc>\n<rect x=\"0.5cm\" y=\"0.5cm\" height=\"1cm\"/>\n</svg",
          first_line: 4,
          last_line: 4,
          first_column: 1,
          last_column: 40,
          hilite_length: 40,
          hilite_start: 10,
        }
      ]}}

      iex> Vnu.validate_svg("", server_url: "http://wrong-domain")
      {:error, %Vnu.Error{
        reason: :unexpected_server_response,
        message: "Could not contact the server, got error: :nxdomain"
      }}
  """

  @spec validate_svg(String.t(), Keyword.t()) :: {:ok, Result.t()} | {:error, Error.t()}
  def validate_svg(svg, opts \\ []) when is_bitstring(svg) and is_list(opts) do
    Validator.validate(svg, Keyword.merge(opts, format: :svg))
  end

  @doc ~S"""
  Same as `validate_svg/2` but returns `%Vnu.Result{}` or raises `%Vnu.Error{}`.
  """

  @spec validate_svg!(String.t(), Keyword.t()) :: Result.t() | no_return()
  def validate_svg!(svg, opts \\ []) when is_bitstring(svg) and is_list(opts) do
    Validator.validate!(svg, Keyword.merge(opts, format: :svg))
  end

  @doc ~S"""
  Checks if the results of `Vnu.validate_html/2`, `Vnu.validate_css/2`, or `Vnu.validate_svg/2` determined the document to be valid.

  ## Options
  - `:server_url` - The URL of [the Checker server](https://github.com/validator/validator). Defaults to `http://localhost:8888`.
  - `:fail_on_warnings` - Messages of type `:info` and subtype `:warning` will be treated as if they were validation errors.
    Their presence will mean the document is invalid. Defaults to `false`.
  - `:http_client` - A module implementing the `Vnu.HTTPClient` behaviour that will be used to make the HTTP request to the server.
  Defaults to `Vnu.HTTPClient.Hackney`.

  ## Examples

      iex> {:ok, result} = Vnu.validate_html("", server_url: System.get_env("VNU_SERVER_URL") || "http://localhost:8888")
      iex> Vnu.valid?(result)
      false

      iex> {:ok, result} = Vnu.validate_html(~S(
      ...><!DOCTYPE html>
      ...><html>
      ...><head>
      ...>  <meta charset="utf-8">
      ...>  <title>Hello World</title>
      ...></head>
      ...><body>
      ...></body>
      ...></html>
      ...>), server_url: System.get_env("VNU_SERVER_URL") || "http://localhost:8888")
      iex> [message] = result.messages
      iex> message.message
      "Consider adding a “lang” attribute to the “html” start tag to declare the language of this document."
      iex> message.sub_type
      :warning
      iex> Vnu.valid?(result)
      true
      iex> Vnu.valid?(result, fail_on_warnings: true)
      false
  """

  @spec valid?(Result.t()) :: true | false
  def valid?(%Result{} = result, opts \\ []) do
    Validator.valid?(result, opts)
  end
end
