Periodic: Run after 6 hours, outside of init. Fix #42

defmodule Krihelinator.Periodic do
  use GenServer
  require Logger
  import Ecto.Query, only: [from: 2]
  alias Krihelinator.{Periodic, Pipeline, Repo, GithubRepo}

  @moduledoc """
  Every `:periodic_schedule` do the following:

  - Scrape the github trending page for interesting, active, projects.
  - Run the DB cleaner.
  - Pass all of the repos from the DB through the parser again, to update stats.
  """

  def start_link do
    GenServer.start_link(__MODULE__, [])
  end

  def init([]) do
    reschedule_work()
    {:ok, :nil}
  end

  def handle_info(:run, state) do
    Logger.info "Periodic process kicked in!"
    Logger.info "Running DB cleaner..."
    clean_db()
    Logger.info "Scraping repos (trending + existing)..."
    Stream.concat(scrape_trending(), existing_repos_to_scrape())
    |> Stream.uniq(fn repo -> repo.name end)
    |> Stream.map(&Pipeline.StatsScraper.scrape/1)
    |> Enum.each(&handle_scraped/1)
    Logger.info "Periodic process finished successfully!"
    reschedule_work()
    {:noreply, state}
  end

  @doc """
  Schedule the next run in `:periodic_schedule` milliseconds.
  """
  def reschedule_work do
    Process.send_after(self(), :run, Application.fetch_env!(:krihelinator, :periodic_schedule))
  end

  @doc """
  Scrape the github trending page and return stream of repos to scrape.
  """
  def scrape_trending do
    Repo.update_all(GithubRepo, set: [trending: false])

    %{body: body, status_code: 200} = HTTPoison.get!("https://github.com/trending")
    body
    |> Periodic.TrendingParser.parse
    |> Stream.map(fn repo -> Map.put(repo, :trending, true) end)
  end

  @doc """
  Keep only the top :max_repos_to_keep in the DB. Delete the rest.
  """
  def clean_db do
    non_user_requested = from(r in GithubRepo, where: not r.user_requested)
    count = Repo.one(from r in non_user_requested, select: count(r.id))
    keep = Application.fetch_env!(:krihelinator, :max_repos_to_keep)
    if count > keep do
      threshold = Repo.one(from r in non_user_requested,
                           order_by: [desc: r.krihelimeter],
                           offset: ^keep,
                           limit: 1,
                           select: r.krihelimeter)
      ["There are #{count} repos (excluding user requested) in the DB",
       "keeping only #{keep}",
       "new minimum krihelimeter is #{threshold}"]
      |> Enum.join(", ")
      |> Logger.info
      from(r in non_user_requested, where: r.krihelimeter < ^threshold)
      |> Repo.delete_all
    end
  end

  def existing_repos_to_scrape do
    GithubRepo
    |> Repo.all
    |> Stream.map(&Map.from_struct/1)
  end

  @doc """
  Decide what to do with the scraped data. Specific errors might trigger save,
  other deletes, or ignores.
  """
  def handle_scraped(%{error: :nil}=repo) do
    Pipeline.DataHandler.save_to_db(repo)
  end

  def handle_scraped(%{error: :dmca_takedown}=repo) do
    (from r in GithubRepo, where: r.name == ^repo.name)
    |> Repo.delete_all
  end

  def handle_scraped(%{error: error}=repo) do
    Logger.info "Failed to scrape #{repo.name} due to #{error}. No stats updated"
  end
end

defmodule Krihelinator.Periodic do
  use GenServer
  require Logger
  import Ecto.Query, only: [from: 2]
  alias Krihelinator.{Periodic, Pipeline, Repo, GithubRepo}

  @moduledoc """
  Every `:periodic_schedule` do the following:

  - Scrape the github trending page for interesting, active, projects.
  - Run the DB cleaner.
  - Pass all of the repos from the DB through the parser again, to update stats.
  """

  def start_link do
    GenServer.start_link(__MODULE__, [])
  end

  def init([]) do
    handle_info(:run, :nil)
    {:ok, :nil}
  end

  def handle_info(:run, state) do
    Logger.info "Periodic process kicked in!"
    Logger.info "Running DB cleaner..."
    clean_db()
    Logger.info "Scraping repos (trending + existing)..."
    Stream.concat(scrape_trending(), existing_repos_to_scrape())
    |> Stream.uniq(fn repo -> repo.name end)
    |> Stream.map(&Pipeline.StatsScraper.scrape/1)
    |> Enum.each(&handle_scraped/1)
    Logger.info "Periodic process finished successfully!"
    Process.send_after(self(), :run, Application.fetch_env!(:krihelinator, :periodic_schedule))
    {:noreply, state}
  end

  @doc """
  Scrape the github trending page and return stream of repos to scrape.
  """
  def scrape_trending do
    Repo.update_all(GithubRepo, set: [trending: false])

    %{body: body, status_code: 200} = HTTPoison.get!("https://github.com/trending")
    body
    |> Periodic.TrendingParser.parse
    |> Stream.map(fn repo -> Map.put(repo, :trending, true) end)
  end

  @doc """
  Keep only the top :max_repos_to_keep in the DB. Delete the rest.
  """
  def clean_db do
    non_user_requested = from(r in GithubRepo, where: not r.user_requested)
    count = Repo.one(from r in non_user_requested, select: count(r.id))
    keep = Application.fetch_env!(:krihelinator, :max_repos_to_keep)
    if count > keep do
      threshold = Repo.one(from r in non_user_requested,
                           order_by: [desc: r.krihelimeter],
                           offset: ^keep,
                           limit: 1,
                           select: r.krihelimeter)
      ["There are #{count} repos (excluding user requested) in the DB",
       "keeping only #{keep}",
       "new minimum krihelimeter is #{threshold}"]
      |> Enum.join(", ")
      |> Logger.info
      from(r in non_user_requested, where: r.krihelimeter < ^threshold)
      |> Repo.delete_all
    end
  end

  def existing_repos_to_scrape do
    GithubRepo
    |> Repo.all
    |> Stream.map(&Map.from_struct/1)
  end

  @doc """
  Decide what to do with the scraped data. Specific errors might trigger save,
  other deletes, or ignores.
  """
  def handle_scraped(%{error: :nil}=repo) do
    Pipeline.DataHandler.save_to_db(repo)
  end

  def handle_scraped(%{error: :dmca_takedown}=repo) do
    (from r in GithubRepo, where: r.name == ^repo.name)
    |> Repo.delete_all
  end

  def handle_scraped(%{error: error}=repo) do
    Logger.info "Failed to scrape #{repo.name} due to #{error}. No stats updated"
  end
end


Fix for cast example in Phoenix.HTML.Form docs

In the section of "With model data" for the "Phoenix.HTML.Form" docs, in the definition of the "User.changeset/2" function, the example calls a "cast/2" function but I think that the intention here is to call the "Ecto.Changeset.cast/4" function. Is that is the case this change could help.

defmodule Phoenix.HTML.Form do
  @moduledoc ~S"""
  Helpers related to producing HTML forms.

  The functions in this module can be used in three
  distinct scenarios:

    * with model data - when information to populate
      the form comes from a model

    * with connection data - when a form is created based
      on the information in the connection (aka `Plug.Conn`)

    * without form data - when the functions are used directly,
      outside of a form

  We will explore all three scenarios below.

  ## With model data

  The entry point for defining forms in Phoenix is with
  the `form_for/4` function. For this example, we will
  use `Ecto.Changeset`, which integrate nicely with Phoenix
  forms via the `phoenix_ecto` package.

  Imagine you have the following action in your controller:

      def new(conn, _params) do
        changeset = User.changeset(%User{})
        render conn, "new.html", changeset: changeset
      end

  where `User.changeset/2` is defined as follows:

      def changeset(user, params \\ :empty) do
        cast(user, params, ~w(name age), ~w())
      end

  Now a `@changeset` assign is available in views which we
  can pass to the form:

      <%= form_for @changeset, user_path(@conn, :create), fn f -> %>
        <label>
          Name: <%= text_input f, :name %>
        </label>

        <label>
          Age: <%= select f, :age, 18..100 %>
        </label>

        <%= submit "Submit" %>
      <% end %>

  `form_for/4` receives the `Ecto.Changeset` and converts it
  to a form, which is passed to the function as the argument
  `f`. All the remaining functions in this module receive
  the form and automatically generate the input fields, often
  by extracting information from the given changeset. For example,
  if the user had a default value for age set, it will
  automatically show up as selected in the form.

  ## With connection data

  `form_for/4` expects as first argument any data structure that
  implements the `Phoenix.HTML.FormData` protocol. By default,
  Phoenix implements this protocol for `Plug.Conn`, allowing us
  to create forms based only on connection information.

  This is useful when you are creating forms that are not backed
  by any kind of model data, like a search form.

      <%= form_for @conn, search_path(@conn, :new), [name: :search], fn f -> %>
        <%= text_input f, :for %>
        <%= submit "Search" %>
      <% end %>

  `form_for/4` uses the `Plug.Conn` to set input values from the
  request parameters. In this case, the input's value would be set
  to `@conn.params["search"]["for"]`.

  ## Without form data

  Sometimes we may want to generate a `text_input/3` or any other
  tag outside of a form. The functions in this module also support
  such usage by simply passing an atom as first argument instead
  of the form.

      <%= text_input :user, :name, value: "This is a prepopulated value" %>

  ## Nested inputs

  If your model layer supports embedding or nested associations,
  you can use `inputs_for` to attach nested data to the form.

  Imagine the models:

      defmodule User do
        use Ecto.Model

        schema "users" do
          field :name
          embeds_one :permalink, Permalink
        end
      end

      defmodule Permalink do
        use Ecto.Model

        embedded_schema do
          field :url
        end
      end

  In the form, you now can:

      <%= form_for @changeset, user_path(@conn, :create), fn f -> %>
        <%= text_input f, :name %>

        <%= inputs_for f, :permalink, fn fp -> %>
          <%= text_input fp, :url %>
        <% end %>
      <% end %>

  The default option can be given to populate the fields if none
  is given:

      <%= inputs_for f, :permalink, [default: %Permalink{title: "default"}], fn fp -> %>
        <%= text_input fp, :url %>
      <% end %>

  `inputs_for/4` can be used to work with single entities or
  collections. When working with collections, `:prepend` and
  `:append` can be used to add entries to the collection
  stored in the changeset.
  """

  alias Phoenix.HTML.Form
  import Phoenix.HTML
  import Phoenix.HTML.Tag

  @doc """
  Defines the Phoenix.HTML.Form struct.

  Its fields are:

    * `:source` - the data structure given to `form_for/4` that
      implements the form data protocol

    * `:impl` - the module with the form data protocol implementation.
      This is used to avoid multiple protocol dispatches.

    * `:id` - the id to be used when generating input fields

    * `:name` - the name to be used when generating input fields

    * `:model` - the model used to lookup field data

    * `:params` - the parameters associated to this form in case
      they were sent as part of a previous request

    * `:hidden` - a keyword list of fields that are required for
      submitting the form behind the scenes as hidden inputs

    * `:options` - a copy of the options given when creating the
      form via `form_for/4` without any form data specific key

    * `:errors` - a keyword list of errors that associated with
      the form
  """
  defstruct source: nil, impl: nil, id: nil, name: nil, model: %{},
            hidden: [], params: %{}, errors: [], options: [], index: nil

  @type t :: %Form{source: Phoenix.HTML.FormData.t, name: String.t, model: %{atom => term},
                   params: %{binary => term}, hidden: Keyword.t, options: Keyword.t,
                   errors: Keyword.t, impl: module, id: String.t, index: nil | non_neg_integer}

  @doc """
  Converts an attribute/form field into its humanize version.

      iex> humanize(:username)
      "Username"
      iex> humanize(:created_at)
      "Created at"
      iex> humanize("user_id")
      "User"

  """
  def humanize(atom) when is_atom(atom),
    do: humanize(Atom.to_string(atom))
  def humanize(bin) when is_binary(bin) do
    bin =
      if String.ends_with?(bin, "_id") do
        binary_part(bin, 0, byte_size(bin) - 3)
      else
        bin
      end

    bin |> String.replace("_", " ") |> String.capitalize
  end

  @doc """
  Generates a form tag with a form builder.

  See the module documentation for examples of using this function.

  ## Options

    * `:as` - the name to be used in the form. May be inflected
      if a model is available

    * `:method` - the HTTP method. If the method is not "get" nor "post",
      an input tag with name `_method` is generated along-side the form tag.
      Defaults to "post".

    * `:multipart` - when true, sets enctype to "multipart/form-data".
      Required when uploading files

    * `:csrf_token` - for "post" requests, the form tag will automatically
      include an input tag with name `_csrf_token`. When set to false, this
      is disabled

    * `:enforce_utf8` - the form will have an `accept-charset` attribute set
      to "UTF-8" and a hidden input named `_utf8` containing a unicode
      character to force the browser to use UTF-8 as the charset. When set to
      false, this is disabled.

  See `Phoenix.HTML.Tag.form_tag/2` for more information on the
  options above.
  """
  @spec form_for(Phoenix.HTML.FormData.t, String.t,
                 Keyword.t, (t -> Phoenix.HTML.unsafe)) :: Phoenix.HTML.safe
  def form_for(form_data, action, options \\ [], fun) when is_function(fun, 1) do
    form = Phoenix.HTML.FormData.to_form(form_data, options)
    html_escape [form_tag(action, form.options), fun.(form), raw("</form>")]
  end

  @doc """
  Generate a new form builder for the given parameter in form.

  See the module documentation for examples of using this function.

  ## Options

    * `:id` - the id to be used in the form, defaults to the
      concatenation of the given `field` to the parent form id

    * `:as` - the name to be used in the form, defaults to the
      concatenation of the given `field` to the parent form name

    * `:default` - the value to use if none is available

    * `:prepend` - the values to prepend when rendering. This only
      applies if the field value is a list and no parameters were
      sent through the form.

    * `:append` - the values to append when rendering. This only
      applies if the field value is a list and no parameters were
      sent through the form.

    * `:skip_deleted` - do not show inputs for changesets that
      have been deleted (supported only when working with changesets)

  """
  @spec inputs_for(t, atom, Keyword.t, (t -> Phoenix.HTML.unsafe)) :: Phoenix.HTML.safe
  def inputs_for(form, field, options \\ [], fun) do
    forms = form.impl.to_form(form.source, form, field, options)
    html_escape Enum.map(forms, fn form ->
      hidden = Enum.map(form.hidden, fn {k, v} -> hidden_input(form, k, value: v) end)
      [hidden, fun.(form)]
    end)
  end

  @doc """
  Returns the HTML5 validations that would apply to
  the given field.
  """
  @spec input_validations(t, atom) :: boolean
  def input_validations(form, field) do
    form.impl.input_validations(form.source, field)
  end

  @mapping %{
    "url"      => :url_input,
    "email"    => :email_input,
    "search"   => :search_input,
    "password" => :password_input
  }

  @doc """
  Gets the input type for a given field.

  If the underlying input type is a `:text_field`,
  a mapping could be given to further inflect
  the input type based solely on the field name.
  The default mapping is:

      %{"url"      => :url_input,
        "email"    => :email_input,
        "search"   => :search_input,
        "password" => :password_input}

  """
  @spec input_type(t, atom) :: atom
  def input_type(form, field, mapping \\ @mapping) do
    type = form.impl.input_type(form.source, field)

    if type == :text_input do
      field = Atom.to_string(field)
      Enum.find_value(mapping, type, fn {k, v} ->
        String.contains?(field, k) && v
      end)
    else
      type
    end
  end

  ## Form helpers

  @doc """
  Generates a text input.

  The form should either be a `Phoenix.HTML.Form` emitted
  by `form_for` or an atom.

  All given options are forwarded to the underlying input,
  default values are provided for id, name and value if
  possible.

  ## Examples

      # Assuming form contains a User model
      text_input(form, :name)
      #=> <input id="user_name" name="user[name]" type="text" value="">

      text_input(:user, :name)
      #=> <input id="user_name" name="user[name]" type="text" value="">

  """
  def text_input(form, field, opts \\ []) do
    generic_input(:text, form, field, opts)
  end

  @doc """
  Generates a hidden input.

  See `text_input/3` for example and docs.
  """
  def hidden_input(form, field, opts \\ []) do
    generic_input(:hidden, form, field, opts)
  end

  @doc """
  Generates an email input.

  See `text_input/3` for example and docs.
  """
  def email_input(form, field, opts \\ []) do
    generic_input(:email, form, field, opts)
  end

  @doc """
  Generates a number input.

  See `text_input/3` for example and docs.
  """
  def number_input(form, field, opts \\ []) do
    generic_input(:number, form, field, opts)
  end

  @doc """
  Generates a password input.

  For security reasons, the model and parameter values
  are never re-used in `password_input/3`. Pass the value
  explicitly if you would like to set one.

  See `text_input/3` for example and docs.
  """
  def password_input(form, field, opts \\ []) do
    opts =
      opts
      |> Keyword.put_new(:type, "password")
      |> Keyword.put_new(:id, id_from(form, field))
      |> Keyword.put_new(:name, name_from(form, field))
    tag(:input, opts)
  end

  @doc """
  Generates an url input.

  See `text_input/3` for example and docs.
  """
  def url_input(form, field, opts \\ []) do
    generic_input(:url, form, field, opts)
  end

  @doc """
  Generates a search input.

  See `text_input/3` for example and docs.
  """
  def search_input(form, field, opts \\ []) do
    generic_input(:search, form, field, opts)
  end

  @doc """
  Generates a telephone input.

  See `text_input/3` for example and docs.
  """
  def telephone_input(form, field, opts \\ []) do
    generic_input(:tel, form, field, opts)
  end

  @doc """
  Generates a color input.

  Warning: this feature isn't available in all browsers.
  Check `http://caniuse.com/#feat=input-color` for further informations.

  See `text_input/3` for example and docs.
  """
  def color_input(form, field, opts \\ []) do
    generic_input(:color, form, field, opts)
  end

  @doc """
  Generates a range input.

  See `text_input/3` for example and docs.
  """
  def range_input(form, field, opts \\ []) do
    generic_input(:range, form, field, opts)
  end

  defp generic_input(type, form, field, opts) when is_atom(field) and is_list(opts) do
    opts =
      opts
      |> Keyword.put_new(:type, type)
      |> Keyword.put_new(:id, id_from(form, field))
      |> Keyword.put_new(:name, name_from(form, field))
      |> Keyword.put_new(:value, value_from(form, field))
    tag(:input, opts)
  end

  @doc """
  Generates a textarea input.

  All given options are forwarded to the underlying input,
  default values are provided for id, name and textarea
  content if possible.

  ## Examples

      # Assuming form contains a User model
      textarea(form, :description)
      #=> <textarea id="user_description" name="user[description]"></textarea>

  ## New lines

  Notice the generated textarea includes a new line after
  the opening tag. This is because the HTML spec says new
  lines after tags must be ignored and all major browser
  implementations do that.

  So in order to avoid new lines provided by the user
  from being ignored when the form is resubmitted, we
  automatically add a new line before the text area
  value.
  """
  def textarea(form, field, opts \\ []) do
    opts =
      opts
      |> Keyword.put_new(:id, id_from(form, field))
      |> Keyword.put_new(:name, name_from(form, field))

    {value, opts} = Keyword.pop(opts, :value, value_from(form, field) || "")
    content_tag(:textarea, html_escape(["\n", value]), opts)
  end

  @doc """
  Generates a file input.

  It requires the given form to be configured with `multipart: true`
  when invoking `form_for/4`, otherwise it fails with `ArgumentError`.

  See `text_input/3` for example and docs.
  """
  def file_input(form, field, opts \\ []) do
    if match?(%Form{}, form) and !form.options[:multipart] do
      raise ArgumentError, "file_input/3 requires the enclosing form_for/4 " <>
                           "to be configured with multipart: true"
    end

    opts =
      opts
      |> Keyword.put_new(:type, :file)
      |> Keyword.put_new(:id, id_from(form, field))
      |> Keyword.put_new(:name, name_from(form, field))

    tag(:input, opts)
  end

  @doc """
  Generates a submit input to send the form.

  All options are forwarded to the underlying input tag.

  ## Examples

      submit "Submit"
      #=> <input type="submit" value="Submit">

  """
  def submit(value, opts \\ []) do
    opts =
      opts
      |> Keyword.put_new(:type, "submit")
      |> Keyword.put_new(:value, value)
    tag(:input, opts)
  end

  @doc """
  Generates a reset input to reset all the form fields to
  their original state.

  All options are forwarded to the underlying input tag.

  ## Examples

      reset "Reset"
      #=> <input type="reset" value="Reset">

      reset "Reset", class: "btn"
      #=> <input type="reset" value="Reset" class="btn">

  """
  def reset(value, opts \\ []) do
    opts =
      opts
      |> Keyword.put_new(:type, "reset")
      |> Keyword.put_new(:value, value)
    tag(:input, opts)
  end

  @doc """
  Generates a radio button.

  Invoke this function for each possible value you to be
  sent to the server.

  ## Examples

      # Assuming form contains a User model
      radio_button(form, :role, "admin")
      #=> <input id="user_role_admin" name="user[role]" type="radio" value="admin">

  ## Options

  All options are simply forwarded to the underlying HTML tag.
  """
  def radio_button(form, field, value, opts \\ []) do
    value = html_escape(value)

    opts =
      opts
      |> Keyword.put_new(:type, "radio")
      |> Keyword.put_new(:id, id_from(form, field) <> "_" <> elem(value, 1))
      |> Keyword.put_new(:name, name_from(form, field))

    if value == html_escape(value_from(form, field)) do
      opts = Keyword.put_new(opts, :checked, true)
    end

    tag(:input, [value: value] ++ opts)
  end

  @doc """
  Generates a checkbox.

  This function is useful for sending boolean values to the server.

  ## Examples

      # Assuming form contains a User model
      checkbox(form, :famous)
      #=> <input name="user[famous]" type="hidden" value="false">
          <input checked="checked" id="user_famous" name="user[famous]" type="checkbox" value="true">

  ## Options

    * `:checked_value` - the value to be sent when the checkbox is checked.
      Defaults to "true"

    * `:unchecked_value` - the value to be sent then the checkbox is unchecked,
      Defaults to "false"

    * `:value` - the value used to check if a checkbox is checked or unchecked.
      The default value is extracted from the model if a model is available

  All other options are forwarded to the underlying HTML tag.

  ## Hidden fields

  Because an unchecked checkbox is not sent to the server, Phoenix
  automatically generates a hidden field with the unchecked_value
  *before* the checkbox field to ensure the `unchecked_value` is sent
  when the checkbox is not marked.
  """
  def checkbox(form, field, opts \\ []) do
    opts =
      opts
      |> Keyword.put_new(:type, "checkbox")
      |> Keyword.put_new(:id, id_from(form, field))
      |> Keyword.put_new(:name, name_from(form, field))

    {value, opts}           = Keyword.pop(opts, :value, value_from(form, field))
    {checked_value, opts}   = Keyword.pop(opts, :checked_value, true)
    {unchecked_value, opts} = Keyword.pop(opts, :unchecked_value, false)

    # We html escape all values to be sure we are comparing
    # apples to apples. After all we may have true in the model
    # but "true" in the params and both need to match.
    value           = html_escape(value)
    checked_value   = html_escape(checked_value)
    unchecked_value = html_escape(unchecked_value)

    if value == checked_value do
      opts = Keyword.put_new(opts, :checked, true)
    end

    html_escape [tag(:input, name: Keyword.get(opts, :name), type: "hidden", value: unchecked_value),
                 tag(:input, [value: checked_value] ++ opts)]
  end

  @doc """
  Generates a select tag with the given `values`.

  Values are expected to be an Enumerable containing two-item tuples
  (like a regular list or a map) or any Enumerable where the element
  will be used both as key and value for the generated select.

  ## Examples

      # Assuming form contains a User model
      select(form, :age, 0..120)
      #=> <select id="user_age" name="user[age]">
          <option value="0">0</option>
          ...
          <option value="120">120</option>
          </select>

      select(form, :role, ["Admin": "admin", "User": "user"])
      #=> <select id="user_role" name="user[role]">
          <option value="admin">Admin</option>
          <option value="user">User</option>
          </select>

      select(form, :role, ["Admin": "admin", "User": "user"], prompt: "Choose your role")
      #=> <select id="user_role" name="user[role]">
          <option value="">Choose your role</option>
          <option value="admin">Admin</option>
          <option value="user">User</option>
          </select>

  ## Options

    * `:prompt` - an option to include at the top of the options with
      the given prompt text

    * `:value` - the value used to select a given option.
      The default value is extracted from the model if a model is available

    * `:default` - the default value to use when none was given in
      `:value` and none was available in the model

  All other options are forwarded to the underlying HTML tag.
  """
  def select(form, field, values, opts \\ []) do
    {default, opts} = Keyword.pop(opts, :default)
    {value, opts}   = Keyword.pop(opts, :value, value_from(form, field) || default)

    {options, opts} = case Keyword.pop(opts, :prompt) do
      {nil, opts}    -> {raw(""), opts}
      {prompt, opts} -> {content_tag(:option, prompt, value: ""), opts}
    end

    opts =
      opts
      |> Keyword.put_new(:id, id_from(form, field))
      |> Keyword.put_new(:name, name_from(form, field))

    options = options_for_select(values, options, html_escape(value))
    content_tag(:select, options, opts)
  end

  defp options_for_select(values, options, value) do
    Enum.reduce values, options, fn
      {option_key, option_value}, acc ->
        option_key   = html_escape(option_key)
        option_value = html_escape(option_value)
        option(option_key, option_value, value, acc)
      option, acc ->
        option = html_escape(option)
        option(option, option, value, acc)
    end
  end

  defp option(option_key, option_value, values, acc) when is_list(values) do
    opts = [value: option_value, selected: option_value in values]
    html_escape [acc|content_tag(:option, option_key, opts)]
  end

  defp option(option_key, option_value, value, acc) do
    opts = [value: option_value, selected: value == option_value]
    html_escape [acc|content_tag(:option, option_key, opts)]
  end

  @doc """
  Generates a select tag with the given `values`.

  Values are expected to be an Enumerable containing two-item tuples
  (like maps and keyword lists) or any Enumerable where the element
  will be used both as key and value for the generated select.

  ## Examples

      # Assuming form contains a User model
      multiple_select(form, :roles, ["Admin": 1, "Power User": 2])
      #=> <select id="user_roles" name="user[roles][]">
          <option value="1">Admin</option>
          <option value="2">Power User</option>
          </select>

      multiple_select(form, :roles, ["Admin": 1, "Power User": 2], value: [1])
      #=> <select id="user_roles" name="user[roles]">
          <option value="1" selected="selected" >Admin</option>
          <option value="2">Power User</option>
          </select>

  ## Options

    * `:value` - an Enum of values used to select given options.

    * `:default` - the default value to use when none was given in
      `:values` and none was available in the model

  All other options are forwarded to the underlying HTML tag.
  """
  def multiple_select(form, field, values, opts \\ []) do
    {default, opts}  = Keyword.pop(opts, :default, [])
    {multiple, opts} = Keyword.pop(opts, :value, value_from(form, field) || default)

    opts =
      opts
      |> Keyword.put_new(:id, id_from(form, field))
      |> Keyword.put_new(:name, name_from(form, field) <> "[]")
      |> Keyword.put_new(:multiple, "")

    options = options_for_select(values, "", Enum.map(multiple, &html_escape/1))
    content_tag(:select, options, opts)
  end

  ## Datetime

  @doc ~S"""
  Generates select tags for datetime.

  ## Examples

      # Assuming form contains a User model
      datetime_select form, :born_at
      #=> <select id="user_born_at_year" name="user[born_at][year]">...</select> /
          <select id="user_born_at_month" name="user[born_at][month]">...</select> /
          <select id="user_born_at_day" name="user[born_at][day]">...</select> —
          <select id="user_born_at_hour" name="user[born_at][hour]">...</select> :
          <select id="user_born_at_min" name="user[born_at][min]">...</select>

  If you want to include the seconds field (hidden by default), pass `sec: []`:

      # Assuming form contains a User model
      datetime_select form, :born_at, sec: []

  If you want to configure the years range:

      # Assuming form contains a User model
      datetime_select form, :born_at, year: [options: 1900..2100]

  You are also able to configure `:month`, `:day`, `:hour`, `:min` and
  `:sec`. All options given to those keys will be forwarded to the
  underlying select. See `select/4` for more information.

  ## Options

    * `:value` - the value used to select a given option.
      The default value is extracted from the model if a model is available

    * `:default` - the default value to use when none was given in
      `:value` and none was available in the model

    * `:year`, `:month`, `:day`, `:hour`, `:min`, `:sec` - options passed
      to the underlying select. See `select/4` for more information.
      The available values can be given in `:options`.

    * `:builder` - specify how the select can be build. It must be a function
      that receives a builder that should be invoked with the select name
      and a set of options. See builder below for more information.

  ## Builder

  The generated datetime_select can be customized at will by providing a
  builder option. Here is an example from EEx:

      <%= datetime_select form, :born_at, builder: fn b -> %>
        Date: <%= b.(:day, []) %> / <%= b.(:month, []) %> / <%= b.(:hour, []) %>
        Time: <%= b.(:hour, []) %> : <%= b.(:min, []) %>
      <% end %>

  Although we have passed empty lists as options (they are required), you
  could pass any option there and it would be given to the underlying select
  input.

  In practice, we recommend you to create your own helper with your default
  builder:

      def my_datetime_select(form, field, opts \\ []) do
        builder = fn b ->
          ~e"\""
          Date: <%= b.(:day, []) %> / <%= b.(:month, []) %> / <%= b.(:hour, []) %>
          Time: <%= b.(:hour, []) %> : <%= b.(:min, []) %>
          "\""
        end

        datetime_select(form, field, [builder: builder] ++ opts)
      end

  Then you are able to use your own datetime_select throughout your whole
  application.

  ## Supported date values

  The following values are supported as date:

    * a map containing the `year`, `month` and `day` keys (either as strings or atoms)
    * a tuple with three elements: `{year, month, day}`
    * `nil`

  ## Supported time values

  The following values are supported as time:

    * a map containing the `hour` and `min` keys and an optional `sec` key (either as strings or atoms)
    * a tuple with three elements: `{hour, min, sec}`
    * a tuple with four elements: `{hour, min, sec, usec}`
    * `nil`

  """
  def datetime_select(form, field, opts \\ []) do
    value = Keyword.get(opts, :value, value_from(form, field) || Keyword.get(opts, :default))

    builder =
      Keyword.get(opts, :builder) || fn b ->
        date = date_builder(b, opts)
        time = time_builder(b, opts)
        html_escape [date, raw(" — "), time]
      end

    builder.(datetime_builder(form, field, date_value(value), time_value(value), opts))
  end

  @doc """
  Generates select tags for date.

  Check `datetime_select/3` for more information on options and supported values.
  """
  def date_select(form, field, opts \\ []) do
    value   = Keyword.get(opts, :value, value_from(form, field) || Keyword.get(opts, :default))
    builder = Keyword.get(opts, :builder) || &date_builder(&1, opts)
    builder.(datetime_builder(form, field, date_value(value), nil, opts))
  end

  defp date_builder(b, _opts) do
    html_escape [b.(:year, []), raw(" / "), b.(:month, []), raw(" / "), b.(:day, [])]
  end

  defp date_value(%{"year" => year, "month" => month, "day" => day}),
    do: %{year: year, month: month, day: day}
  defp date_value(%{year: year, month: month, day: day}),
    do: %{year: year, month: month, day: day}

  defp date_value({{year, month, day}, _}),
    do: %{year: year, month: month, day: day}
  defp date_value({year, month, day}),
    do: %{year: year, month: month, day: day}

  defp date_value(nil),
    do: %{year: nil, month: nil, day: nil}
  defp date_value(other),
    do: raise(ArgumentError, "unrecognized date #{inspect other}")

  @doc """
  Generates select tags for time.

  Check `datetime_select/3` for more information on options and supported values.
  """
  def time_select(form, field, opts \\ []) do
    value   = Keyword.get(opts, :value, value_from(form, field) || Keyword.get(opts, :default))
    builder = Keyword.get(opts, :builder) || &time_builder(&1, opts)
    builder.(datetime_builder(form, field, nil, time_value(value), opts))
  end

  defp time_builder(b, opts) do
    time = html_escape [b.(:hour, []), raw(" : "), b.(:min, [])]

    if Keyword.get(opts, :sec) do
      html_escape [time, raw(" : "), b.(:sec, [])]
    else
      time
    end
  end

  defp time_value(%{"hour" => hour, "min" => min} = map),
    do: %{hour: hour, min: min, sec: Map.get(map, "sec", 0)}
  defp time_value(%{hour: hour, min: min} = map),
    do: %{hour: hour, min: min, sec: Map.get(map, :sec, 0)}

  defp time_value({_, {hour, min, sec, _msec}}),
    do: %{hour: hour, min: min, sec: sec}
  defp time_value({hour, min, sec, _mseg}),
    do: %{hour: hour, min: min, sec: sec}
  defp time_value({_, {hour, min, sec}}),
    do: %{hour: hour, min: min, sec: sec}
  defp time_value({hour, min, sec}),
    do: %{hour: hour, min: min, sec: sec}

  defp time_value(nil),
    do: %{hour: nil, min: nil, sec: nil}
  defp time_value(other),
    do: raise(ArgumentError, "unrecognized time #{inspect other}")

  @months [
    {"January", "1"},
    {"February", "2"},
    {"March", "3"},
    {"April", "4"},
    {"May", "5"},
    {"June", "6"},
    {"July", "7"},
    {"August", "8"},
    {"September", "9"},
    {"October", "10"},
    {"November", "11"},
    {"December", "12"},
  ]

  map = &Enum.map(&1, fn i ->
    i = Integer.to_string(i)
    {String.rjust(i, 2, ?0), i}
  end)

  @days   map.(1..31)
  @hours  map.(0..23)
  @minsec map.(0..59)

  defp datetime_builder(form, field, date, time, parent) do
    id   = Keyword.get(parent, :id, id_from(form, field))
    name = Keyword.get(parent, :name, name_from(form, field))

    fn
      :year, opts when date != nil ->
        {year, _, _}  = :erlang.date()
        {value, opts} = datetime_options(:year, year-5..year+5, id, name, parent, date, opts)
        select(:datetime, :year, value, opts)
      :month, opts when date != nil ->
        {value, opts} = datetime_options(:month, @months, id, name, parent, date, opts)
        select(:datetime, :month, value, opts)
      :day, opts when date != nil ->
        {value, opts} = datetime_options(:day, @days, id, name, parent, date, opts)
        select(:datetime, :day, value, opts)
      :hour, opts when time != nil ->
        {value, opts} = datetime_options(:hour, @hours, id, name, parent, time, opts)
        select(:datetime, :hour, value, opts)
      :min, opts when time != nil ->
        {value, opts} = datetime_options(:min, @minsec, id, name, parent, time, opts)
        select(:datetime, :min, value, opts)
      :sec, opts when time != nil ->
        {value, opts} = datetime_options(:sec, @minsec, id, name, parent, time, opts)
        select(:datetime, :sec, value, opts)
    end
  end

  defp datetime_options(type, values, id, name, parent, datetime, opts) do
    opts = Keyword.merge Keyword.get(parent, type, []), opts
    suff = Atom.to_string(type)

    {value, opts} = Keyword.pop(opts, :options, values)

    {value,
      opts
      |> Keyword.put_new(:id, id <> "_" <> suff)
      |> Keyword.put_new(:name, name <> "[" <> suff <> "]")
      |> Keyword.put_new(:value, Map.get(datetime, type))}
  end

  @doc """
  Generates a label tag.

  The form should either be a `Phoenix.HTML.Form` emitted
  by `form_for` or an atom.

  All given options are forwarded to the underlying tag.
  A default value is provided for `for` attribute but can
  be overriden if you pass a value to the `for` option.
  Text content would be inferred from `field` if not specified.

  ## Examples

      # Assuming form contains a User model
      label(form, :name, "Name")
      #=> <label for="user_name">Name</label>

      label(:user, :email, "Email")
      #=> <label for="user_email">Email</label>

      label(:user, :email)
      #=> <label for="user_email">Email</label>

      label(:user, :email, class: "control-label")
      #=> <label for="user_email" class="control-label">Email</label>

      label :user, :email do
        "E-mail Address"
      end
      #=> <label for="user_email">E-mail Address</label>

      label :user, :email, class: "control-label" do
        "E-mail Address"
      end
      #=> <label class="control-label" for="user_email">E-mail Address</label>
  """
  def label(form, field) do
    label(form, field, humanize(field), [])
  end

  @doc """
  See `label/2`.
  """
  def label(form, field, text) when is_binary(text) do
    label(form, field, text, [])
  end
  def label(form, field, [do: block]) do
    label(form, field, [], do: block)
  end
  def label(form, field, opts) when is_list(opts) do
    label(form, field, humanize(field), opts)
  end

  @doc """
  See `label/2`.
  """
  def label(form, field, text, opts) when is_binary(text) and is_list(opts) do
    opts = Keyword.put_new(opts, :for, id_from(form, field))
    content_tag(:label, text, opts)
  end
  def label(form, field, opts, [do: block]) do
    opts = Keyword.put_new(opts, :for, id_from(form, field))
    content_tag(:label, opts, do: block)
  end

  ## Helpers

  defp value_from(%{model: model, params: params}, field) do
    case Map.fetch(params, Atom.to_string(field)) do
      {:ok, value} -> value
      :error -> Map.get(model, field)
    end
  end

  defp value_from(name, _field) when is_atom(name),
    do: nil

  defp id_from(%{id: id}, field),
    do: "#{id}_#{field}"
  defp id_from(name, field) when is_atom(name),
    do: "#{name}_#{field}"

  defp name_from(%{name: name}, field),
    do: "#{name}[#{field}]"
  defp name_from(name, field) when is_atom(name),
    do: "#{name}[#{field}]"
end

defmodule Phoenix.HTML.Form do
  @moduledoc ~S"""
  Helpers related to producing HTML forms.

  The functions in this module can be used in three
  distinct scenarios:

    * with model data - when information to populate
      the form comes from a model

    * with connection data - when a form is created based
      on the information in the connection (aka `Plug.Conn`)

    * without form data - when the functions are used directly,
      outside of a form

  We will explore all three scenarios below.

  ## With model data

  The entry point for defining forms in Phoenix is with
  the `form_for/4` function. For this example, we will
  use `Ecto.Changeset`, which integrate nicely with Phoenix
  forms via the `phoenix_ecto` package.

  Imagine you have the following action in your controller:

      def new(conn, _params) do
        changeset = User.changeset(%User{})
        render conn, "new.html", changeset: changeset
      end

  where `User.changeset/2` is defined as follows:

      def changeset(user, params \\ :empty) do
        cast(user, params)
      end

  Now a `@changeset` assign is available in views which we
  can pass to the form:

      <%= form_for @changeset, user_path(@conn, :create), fn f -> %>
        <label>
          Name: <%= text_input f, :name %>
        </label>

        <label>
          Age: <%= select f, :age, 18..100 %>
        </label>

        <%= submit "Submit" %>
      <% end %>

  `form_for/4` receives the `Ecto.Changeset` and converts it
  to a form, which is passed to the function as the argument
  `f`. All the remaining functions in this module receive
  the form and automatically generate the input fields, often
  by extracting information from the given changeset. For example,
  if the user had a default value for age set, it will
  automatically show up as selected in the form.

  ## With connection data

  `form_for/4` expects as first argument any data structure that
  implements the `Phoenix.HTML.FormData` protocol. By default,
  Phoenix implements this protocol for `Plug.Conn`, allowing us
  to create forms based only on connection information.

  This is useful when you are creating forms that are not backed
  by any kind of model data, like a search form.

      <%= form_for @conn, search_path(@conn, :new), [name: :search], fn f -> %>
        <%= text_input f, :for %>
        <%= submit "Search" %>
      <% end %>

  `form_for/4` uses the `Plug.Conn` to set input values from the
  request parameters. In this case, the input's value would be set
  to `@conn.params["search"]["for"]`.

  ## Without form data

  Sometimes we may want to generate a `text_input/3` or any other
  tag outside of a form. The functions in this module also support
  such usage by simply passing an atom as first argument instead
  of the form.

      <%= text_input :user, :name, value: "This is a prepopulated value" %>

  ## Nested inputs

  If your model layer supports embedding or nested associations,
  you can use `inputs_for` to attach nested data to the form.

  Imagine the models:

      defmodule User do
        use Ecto.Model

        schema "users" do
          field :name
          embeds_one :permalink, Permalink
        end
      end

      defmodule Permalink do
        use Ecto.Model

        embedded_schema do
          field :url
        end
      end

  In the form, you now can:

      <%= form_for @changeset, user_path(@conn, :create), fn f -> %>
        <%= text_input f, :name %>

        <%= inputs_for f, :permalink, fn fp -> %>
          <%= text_input fp, :url %>
        <% end %>
      <% end %>

  The default option can be given to populate the fields if none
  is given:

      <%= inputs_for f, :permalink, [default: %Permalink{title: "default"}], fn fp -> %>
        <%= text_input fp, :url %>
      <% end %>

  `inputs_for/4` can be used to work with single entities or
  collections. When working with collections, `:prepend` and
  `:append` can be used to add entries to the collection
  stored in the changeset.
  """

  alias Phoenix.HTML.Form
  import Phoenix.HTML
  import Phoenix.HTML.Tag

  @doc """
  Defines the Phoenix.HTML.Form struct.

  Its fields are:

    * `:source` - the data structure given to `form_for/4` that
      implements the form data protocol

    * `:impl` - the module with the form data protocol implementation.
      This is used to avoid multiple protocol dispatches.

    * `:id` - the id to be used when generating input fields

    * `:name` - the name to be used when generating input fields

    * `:model` - the model used to lookup field data

    * `:params` - the parameters associated to this form in case
      they were sent as part of a previous request

    * `:hidden` - a keyword list of fields that are required for
      submitting the form behind the scenes as hidden inputs

    * `:options` - a copy of the options given when creating the
      form via `form_for/4` without any form data specific key

    * `:errors` - a keyword list of errors that associated with
      the form
  """
  defstruct source: nil, impl: nil, id: nil, name: nil, model: %{},
            hidden: [], params: %{}, errors: [], options: [], index: nil

  @type t :: %Form{source: Phoenix.HTML.FormData.t, name: String.t, model: %{atom => term},
                   params: %{binary => term}, hidden: Keyword.t, options: Keyword.t,
                   errors: Keyword.t, impl: module, id: String.t, index: nil | non_neg_integer}

  @doc """
  Converts an attribute/form field into its humanize version.

      iex> humanize(:username)
      "Username"
      iex> humanize(:created_at)
      "Created at"
      iex> humanize("user_id")
      "User"

  """
  def humanize(atom) when is_atom(atom),
    do: humanize(Atom.to_string(atom))
  def humanize(bin) when is_binary(bin) do
    bin =
      if String.ends_with?(bin, "_id") do
        binary_part(bin, 0, byte_size(bin) - 3)
      else
        bin
      end

    bin |> String.replace("_", " ") |> String.capitalize
  end

  @doc """
  Generates a form tag with a form builder.

  See the module documentation for examples of using this function.

  ## Options

    * `:as` - the name to be used in the form. May be inflected
      if a model is available

    * `:method` - the HTTP method. If the method is not "get" nor "post",
      an input tag with name `_method` is generated along-side the form tag.
      Defaults to "post".

    * `:multipart` - when true, sets enctype to "multipart/form-data".
      Required when uploading files

    * `:csrf_token` - for "post" requests, the form tag will automatically
      include an input tag with name `_csrf_token`. When set to false, this
      is disabled

    * `:enforce_utf8` - the form will have an `accept-charset` attribute set
      to "UTF-8" and a hidden input named `_utf8` containing a unicode
      character to force the browser to use UTF-8 as the charset. When set to
      false, this is disabled.

  See `Phoenix.HTML.Tag.form_tag/2` for more information on the
  options above.
  """
  @spec form_for(Phoenix.HTML.FormData.t, String.t,
                 Keyword.t, (t -> Phoenix.HTML.unsafe)) :: Phoenix.HTML.safe
  def form_for(form_data, action, options \\ [], fun) when is_function(fun, 1) do
    form = Phoenix.HTML.FormData.to_form(form_data, options)
    html_escape [form_tag(action, form.options), fun.(form), raw("</form>")]
  end

  @doc """
  Generate a new form builder for the given parameter in form.

  See the module documentation for examples of using this function.

  ## Options

    * `:id` - the id to be used in the form, defaults to the
      concatenation of the given `field` to the parent form id

    * `:as` - the name to be used in the form, defaults to the
      concatenation of the given `field` to the parent form name

    * `:default` - the value to use if none is available

    * `:prepend` - the values to prepend when rendering. This only
      applies if the field value is a list and no parameters were
      sent through the form.

    * `:append` - the values to append when rendering. This only
      applies if the field value is a list and no parameters were
      sent through the form.

    * `:skip_deleted` - do not show inputs for changesets that
      have been deleted (supported only when working with changesets)

  """
  @spec inputs_for(t, atom, Keyword.t, (t -> Phoenix.HTML.unsafe)) :: Phoenix.HTML.safe
  def inputs_for(form, field, options \\ [], fun) do
    forms = form.impl.to_form(form.source, form, field, options)
    html_escape Enum.map(forms, fn form ->
      hidden = Enum.map(form.hidden, fn {k, v} -> hidden_input(form, k, value: v) end)
      [hidden, fun.(form)]
    end)
  end

  @doc """
  Returns the HTML5 validations that would apply to
  the given field.
  """
  @spec input_validations(t, atom) :: boolean
  def input_validations(form, field) do
    form.impl.input_validations(form.source, field)
  end

  @mapping %{
    "url"      => :url_input,
    "email"    => :email_input,
    "search"   => :search_input,
    "password" => :password_input
  }

  @doc """
  Gets the input type for a given field.

  If the underlying input type is a `:text_field`,
  a mapping could be given to further inflect
  the input type based solely on the field name.
  The default mapping is:

      %{"url"      => :url_input,
        "email"    => :email_input,
        "search"   => :search_input,
        "password" => :password_input}

  """
  @spec input_type(t, atom) :: atom
  def input_type(form, field, mapping \\ @mapping) do
    type = form.impl.input_type(form.source, field)

    if type == :text_input do
      field = Atom.to_string(field)
      Enum.find_value(mapping, type, fn {k, v} ->
        String.contains?(field, k) && v
      end)
    else
      type
    end
  end

  ## Form helpers

  @doc """
  Generates a text input.

  The form should either be a `Phoenix.HTML.Form` emitted
  by `form_for` or an atom.

  All given options are forwarded to the underlying input,
  default values are provided for id, name and value if
  possible.

  ## Examples

      # Assuming form contains a User model
      text_input(form, :name)
      #=> <input id="user_name" name="user[name]" type="text" value="">

      text_input(:user, :name)
      #=> <input id="user_name" name="user[name]" type="text" value="">

  """
  def text_input(form, field, opts \\ []) do
    generic_input(:text, form, field, opts)
  end

  @doc """
  Generates a hidden input.

  See `text_input/3` for example and docs.
  """
  def hidden_input(form, field, opts \\ []) do
    generic_input(:hidden, form, field, opts)
  end

  @doc """
  Generates an email input.

  See `text_input/3` for example and docs.
  """
  def email_input(form, field, opts \\ []) do
    generic_input(:email, form, field, opts)
  end

  @doc """
  Generates a number input.

  See `text_input/3` for example and docs.
  """
  def number_input(form, field, opts \\ []) do
    generic_input(:number, form, field, opts)
  end

  @doc """
  Generates a password input.

  For security reasons, the model and parameter values
  are never re-used in `password_input/3`. Pass the value
  explicitly if you would like to set one.

  See `text_input/3` for example and docs.
  """
  def password_input(form, field, opts \\ []) do
    opts =
      opts
      |> Keyword.put_new(:type, "password")
      |> Keyword.put_new(:id, id_from(form, field))
      |> Keyword.put_new(:name, name_from(form, field))
    tag(:input, opts)
  end

  @doc """
  Generates an url input.

  See `text_input/3` for example and docs.
  """
  def url_input(form, field, opts \\ []) do
    generic_input(:url, form, field, opts)
  end

  @doc """
  Generates a search input.

  See `text_input/3` for example and docs.
  """
  def search_input(form, field, opts \\ []) do
    generic_input(:search, form, field, opts)
  end

  @doc """
  Generates a telephone input.

  See `text_input/3` for example and docs.
  """
  def telephone_input(form, field, opts \\ []) do
    generic_input(:tel, form, field, opts)
  end

  @doc """
  Generates a color input.

  Warning: this feature isn't available in all browsers.
  Check `http://caniuse.com/#feat=input-color` for further informations.

  See `text_input/3` for example and docs.
  """
  def color_input(form, field, opts \\ []) do
    generic_input(:color, form, field, opts)
  end

  @doc """
  Generates a range input.

  See `text_input/3` for example and docs.
  """
  def range_input(form, field, opts \\ []) do
    generic_input(:range, form, field, opts)
  end

  defp generic_input(type, form, field, opts) when is_atom(field) and is_list(opts) do
    opts =
      opts
      |> Keyword.put_new(:type, type)
      |> Keyword.put_new(:id, id_from(form, field))
      |> Keyword.put_new(:name, name_from(form, field))
      |> Keyword.put_new(:value, value_from(form, field))
    tag(:input, opts)
  end

  @doc """
  Generates a textarea input.

  All given options are forwarded to the underlying input,
  default values are provided for id, name and textarea
  content if possible.

  ## Examples

      # Assuming form contains a User model
      textarea(form, :description)
      #=> <textarea id="user_description" name="user[description]"></textarea>

  ## New lines

  Notice the generated textarea includes a new line after
  the opening tag. This is because the HTML spec says new
  lines after tags must be ignored and all major browser
  implementations do that.

  So in order to avoid new lines provided by the user
  from being ignored when the form is resubmitted, we
  automatically add a new line before the text area
  value.
  """
  def textarea(form, field, opts \\ []) do
    opts =
      opts
      |> Keyword.put_new(:id, id_from(form, field))
      |> Keyword.put_new(:name, name_from(form, field))

    {value, opts} = Keyword.pop(opts, :value, value_from(form, field) || "")
    content_tag(:textarea, html_escape(["\n", value]), opts)
  end

  @doc """
  Generates a file input.

  It requires the given form to be configured with `multipart: true`
  when invoking `form_for/4`, otherwise it fails with `ArgumentError`.

  See `text_input/3` for example and docs.
  """
  def file_input(form, field, opts \\ []) do
    if match?(%Form{}, form) and !form.options[:multipart] do
      raise ArgumentError, "file_input/3 requires the enclosing form_for/4 " <>
                           "to be configured with multipart: true"
    end

    opts =
      opts
      |> Keyword.put_new(:type, :file)
      |> Keyword.put_new(:id, id_from(form, field))
      |> Keyword.put_new(:name, name_from(form, field))

    tag(:input, opts)
  end

  @doc """
  Generates a submit input to send the form.

  All options are forwarded to the underlying input tag.

  ## Examples

      submit "Submit"
      #=> <input type="submit" value="Submit">

  """
  def submit(value, opts \\ []) do
    opts =
      opts
      |> Keyword.put_new(:type, "submit")
      |> Keyword.put_new(:value, value)
    tag(:input, opts)
  end

  @doc """
  Generates a reset input to reset all the form fields to
  their original state.

  All options are forwarded to the underlying input tag.

  ## Examples

      reset "Reset"
      #=> <input type="reset" value="Reset">

      reset "Reset", class: "btn"
      #=> <input type="reset" value="Reset" class="btn">

  """
  def reset(value, opts \\ []) do
    opts =
      opts
      |> Keyword.put_new(:type, "reset")
      |> Keyword.put_new(:value, value)
    tag(:input, opts)
  end

  @doc """
  Generates a radio button.

  Invoke this function for each possible value you to be
  sent to the server.

  ## Examples

      # Assuming form contains a User model
      radio_button(form, :role, "admin")
      #=> <input id="user_role_admin" name="user[role]" type="radio" value="admin">

  ## Options

  All options are simply forwarded to the underlying HTML tag.
  """
  def radio_button(form, field, value, opts \\ []) do
    value = html_escape(value)

    opts =
      opts
      |> Keyword.put_new(:type, "radio")
      |> Keyword.put_new(:id, id_from(form, field) <> "_" <> elem(value, 1))
      |> Keyword.put_new(:name, name_from(form, field))

    if value == html_escape(value_from(form, field)) do
      opts = Keyword.put_new(opts, :checked, true)
    end

    tag(:input, [value: value] ++ opts)
  end

  @doc """
  Generates a checkbox.

  This function is useful for sending boolean values to the server.

  ## Examples

      # Assuming form contains a User model
      checkbox(form, :famous)
      #=> <input name="user[famous]" type="hidden" value="false">
          <input checked="checked" id="user_famous" name="user[famous]" type="checkbox" value="true">

  ## Options

    * `:checked_value` - the value to be sent when the checkbox is checked.
      Defaults to "true"

    * `:unchecked_value` - the value to be sent then the checkbox is unchecked,
      Defaults to "false"

    * `:value` - the value used to check if a checkbox is checked or unchecked.
      The default value is extracted from the model if a model is available

  All other options are forwarded to the underlying HTML tag.

  ## Hidden fields

  Because an unchecked checkbox is not sent to the server, Phoenix
  automatically generates a hidden field with the unchecked_value
  *before* the checkbox field to ensure the `unchecked_value` is sent
  when the checkbox is not marked.
  """
  def checkbox(form, field, opts \\ []) do
    opts =
      opts
      |> Keyword.put_new(:type, "checkbox")
      |> Keyword.put_new(:id, id_from(form, field))
      |> Keyword.put_new(:name, name_from(form, field))

    {value, opts}           = Keyword.pop(opts, :value, value_from(form, field))
    {checked_value, opts}   = Keyword.pop(opts, :checked_value, true)
    {unchecked_value, opts} = Keyword.pop(opts, :unchecked_value, false)

    # We html escape all values to be sure we are comparing
    # apples to apples. After all we may have true in the model
    # but "true" in the params and both need to match.
    value           = html_escape(value)
    checked_value   = html_escape(checked_value)
    unchecked_value = html_escape(unchecked_value)

    if value == checked_value do
      opts = Keyword.put_new(opts, :checked, true)
    end

    html_escape [tag(:input, name: Keyword.get(opts, :name), type: "hidden", value: unchecked_value),
                 tag(:input, [value: checked_value] ++ opts)]
  end

  @doc """
  Generates a select tag with the given `values`.

  Values are expected to be an Enumerable containing two-item tuples
  (like a regular list or a map) or any Enumerable where the element
  will be used both as key and value for the generated select.

  ## Examples

      # Assuming form contains a User model
      select(form, :age, 0..120)
      #=> <select id="user_age" name="user[age]">
          <option value="0">0</option>
          ...
          <option value="120">120</option>
          </select>

      select(form, :role, ["Admin": "admin", "User": "user"])
      #=> <select id="user_role" name="user[role]">
          <option value="admin">Admin</option>
          <option value="user">User</option>
          </select>

      select(form, :role, ["Admin": "admin", "User": "user"], prompt: "Choose your role")
      #=> <select id="user_role" name="user[role]">
          <option value="">Choose your role</option>
          <option value="admin">Admin</option>
          <option value="user">User</option>
          </select>

  ## Options

    * `:prompt` - an option to include at the top of the options with
      the given prompt text

    * `:value` - the value used to select a given option.
      The default value is extracted from the model if a model is available

    * `:default` - the default value to use when none was given in
      `:value` and none was available in the model

  All other options are forwarded to the underlying HTML tag.
  """
  def select(form, field, values, opts \\ []) do
    {default, opts} = Keyword.pop(opts, :default)
    {value, opts}   = Keyword.pop(opts, :value, value_from(form, field) || default)

    {options, opts} = case Keyword.pop(opts, :prompt) do
      {nil, opts}    -> {raw(""), opts}
      {prompt, opts} -> {content_tag(:option, prompt, value: ""), opts}
    end

    opts =
      opts
      |> Keyword.put_new(:id, id_from(form, field))
      |> Keyword.put_new(:name, name_from(form, field))

    options = options_for_select(values, options, html_escape(value))
    content_tag(:select, options, opts)
  end

  defp options_for_select(values, options, value) do
    Enum.reduce values, options, fn
      {option_key, option_value}, acc ->
        option_key   = html_escape(option_key)
        option_value = html_escape(option_value)
        option(option_key, option_value, value, acc)
      option, acc ->
        option = html_escape(option)
        option(option, option, value, acc)
    end
  end

  defp option(option_key, option_value, values, acc) when is_list(values) do
    opts = [value: option_value, selected: option_value in values]
    html_escape [acc|content_tag(:option, option_key, opts)]
  end

  defp option(option_key, option_value, value, acc) do
    opts = [value: option_value, selected: value == option_value]
    html_escape [acc|content_tag(:option, option_key, opts)]
  end

  @doc """
  Generates a select tag with the given `values`.

  Values are expected to be an Enumerable containing two-item tuples
  (like maps and keyword lists) or any Enumerable where the element
  will be used both as key and value for the generated select.

  ## Examples

      # Assuming form contains a User model
      multiple_select(form, :roles, ["Admin": 1, "Power User": 2])
      #=> <select id="user_roles" name="user[roles][]">
          <option value="1">Admin</option>
          <option value="2">Power User</option>
          </select>

      multiple_select(form, :roles, ["Admin": 1, "Power User": 2], value: [1])
      #=> <select id="user_roles" name="user[roles]">
          <option value="1" selected="selected" >Admin</option>
          <option value="2">Power User</option>
          </select>

  ## Options

    * `:value` - an Enum of values used to select given options.

    * `:default` - the default value to use when none was given in
      `:values` and none was available in the model

  All other options are forwarded to the underlying HTML tag.
  """
  def multiple_select(form, field, values, opts \\ []) do
    {default, opts}  = Keyword.pop(opts, :default, [])
    {multiple, opts} = Keyword.pop(opts, :value, value_from(form, field) || default)

    opts =
      opts
      |> Keyword.put_new(:id, id_from(form, field))
      |> Keyword.put_new(:name, name_from(form, field) <> "[]")
      |> Keyword.put_new(:multiple, "")

    options = options_for_select(values, "", Enum.map(multiple, &html_escape/1))
    content_tag(:select, options, opts)
  end

  ## Datetime

  @doc ~S"""
  Generates select tags for datetime.

  ## Examples

      # Assuming form contains a User model
      datetime_select form, :born_at
      #=> <select id="user_born_at_year" name="user[born_at][year]">...</select> /
          <select id="user_born_at_month" name="user[born_at][month]">...</select> /
          <select id="user_born_at_day" name="user[born_at][day]">...</select> —
          <select id="user_born_at_hour" name="user[born_at][hour]">...</select> :
          <select id="user_born_at_min" name="user[born_at][min]">...</select>

  If you want to include the seconds field (hidden by default), pass `sec: []`:

      # Assuming form contains a User model
      datetime_select form, :born_at, sec: []

  If you want to configure the years range:

      # Assuming form contains a User model
      datetime_select form, :born_at, year: [options: 1900..2100]

  You are also able to configure `:month`, `:day`, `:hour`, `:min` and
  `:sec`. All options given to those keys will be forwarded to the
  underlying select. See `select/4` for more information.

  ## Options

    * `:value` - the value used to select a given option.
      The default value is extracted from the model if a model is available

    * `:default` - the default value to use when none was given in
      `:value` and none was available in the model

    * `:year`, `:month`, `:day`, `:hour`, `:min`, `:sec` - options passed
      to the underlying select. See `select/4` for more information.
      The available values can be given in `:options`.

    * `:builder` - specify how the select can be build. It must be a function
      that receives a builder that should be invoked with the select name
      and a set of options. See builder below for more information.

  ## Builder

  The generated datetime_select can be customized at will by providing a
  builder option. Here is an example from EEx:

      <%= datetime_select form, :born_at, builder: fn b -> %>
        Date: <%= b.(:day, []) %> / <%= b.(:month, []) %> / <%= b.(:hour, []) %>
        Time: <%= b.(:hour, []) %> : <%= b.(:min, []) %>
      <% end %>

  Although we have passed empty lists as options (they are required), you
  could pass any option there and it would be given to the underlying select
  input.

  In practice, we recommend you to create your own helper with your default
  builder:

      def my_datetime_select(form, field, opts \\ []) do
        builder = fn b ->
          ~e"\""
          Date: <%= b.(:day, []) %> / <%= b.(:month, []) %> / <%= b.(:hour, []) %>
          Time: <%= b.(:hour, []) %> : <%= b.(:min, []) %>
          "\""
        end

        datetime_select(form, field, [builder: builder] ++ opts)
      end

  Then you are able to use your own datetime_select throughout your whole
  application.

  ## Supported date values

  The following values are supported as date:

    * a map containing the `year`, `month` and `day` keys (either as strings or atoms)
    * a tuple with three elements: `{year, month, day}`
    * `nil`

  ## Supported time values

  The following values are supported as time:

    * a map containing the `hour` and `min` keys and an optional `sec` key (either as strings or atoms)
    * a tuple with three elements: `{hour, min, sec}`
    * a tuple with four elements: `{hour, min, sec, usec}`
    * `nil`

  """
  def datetime_select(form, field, opts \\ []) do
    value = Keyword.get(opts, :value, value_from(form, field) || Keyword.get(opts, :default))

    builder =
      Keyword.get(opts, :builder) || fn b ->
        date = date_builder(b, opts)
        time = time_builder(b, opts)
        html_escape [date, raw(" — "), time]
      end

    builder.(datetime_builder(form, field, date_value(value), time_value(value), opts))
  end

  @doc """
  Generates select tags for date.

  Check `datetime_select/3` for more information on options and supported values.
  """
  def date_select(form, field, opts \\ []) do
    value   = Keyword.get(opts, :value, value_from(form, field) || Keyword.get(opts, :default))
    builder = Keyword.get(opts, :builder) || &date_builder(&1, opts)
    builder.(datetime_builder(form, field, date_value(value), nil, opts))
  end

  defp date_builder(b, _opts) do
    html_escape [b.(:year, []), raw(" / "), b.(:month, []), raw(" / "), b.(:day, [])]
  end

  defp date_value(%{"year" => year, "month" => month, "day" => day}),
    do: %{year: year, month: month, day: day}
  defp date_value(%{year: year, month: month, day: day}),
    do: %{year: year, month: month, day: day}

  defp date_value({{year, month, day}, _}),
    do: %{year: year, month: month, day: day}
  defp date_value({year, month, day}),
    do: %{year: year, month: month, day: day}

  defp date_value(nil),
    do: %{year: nil, month: nil, day: nil}
  defp date_value(other),
    do: raise(ArgumentError, "unrecognized date #{inspect other}")

  @doc """
  Generates select tags for time.

  Check `datetime_select/3` for more information on options and supported values.
  """
  def time_select(form, field, opts \\ []) do
    value   = Keyword.get(opts, :value, value_from(form, field) || Keyword.get(opts, :default))
    builder = Keyword.get(opts, :builder) || &time_builder(&1, opts)
    builder.(datetime_builder(form, field, nil, time_value(value), opts))
  end

  defp time_builder(b, opts) do
    time = html_escape [b.(:hour, []), raw(" : "), b.(:min, [])]

    if Keyword.get(opts, :sec) do
      html_escape [time, raw(" : "), b.(:sec, [])]
    else
      time
    end
  end

  defp time_value(%{"hour" => hour, "min" => min} = map),
    do: %{hour: hour, min: min, sec: Map.get(map, "sec", 0)}
  defp time_value(%{hour: hour, min: min} = map),
    do: %{hour: hour, min: min, sec: Map.get(map, :sec, 0)}

  defp time_value({_, {hour, min, sec, _msec}}),
    do: %{hour: hour, min: min, sec: sec}
  defp time_value({hour, min, sec, _mseg}),
    do: %{hour: hour, min: min, sec: sec}
  defp time_value({_, {hour, min, sec}}),
    do: %{hour: hour, min: min, sec: sec}
  defp time_value({hour, min, sec}),
    do: %{hour: hour, min: min, sec: sec}

  defp time_value(nil),
    do: %{hour: nil, min: nil, sec: nil}
  defp time_value(other),
    do: raise(ArgumentError, "unrecognized time #{inspect other}")

  @months [
    {"January", "1"},
    {"February", "2"},
    {"March", "3"},
    {"April", "4"},
    {"May", "5"},
    {"June", "6"},
    {"July", "7"},
    {"August", "8"},
    {"September", "9"},
    {"October", "10"},
    {"November", "11"},
    {"December", "12"},
  ]

  map = &Enum.map(&1, fn i ->
    i = Integer.to_string(i)
    {String.rjust(i, 2, ?0), i}
  end)

  @days   map.(1..31)
  @hours  map.(0..23)
  @minsec map.(0..59)

  defp datetime_builder(form, field, date, time, parent) do
    id   = Keyword.get(parent, :id, id_from(form, field))
    name = Keyword.get(parent, :name, name_from(form, field))

    fn
      :year, opts when date != nil ->
        {year, _, _}  = :erlang.date()
        {value, opts} = datetime_options(:year, year-5..year+5, id, name, parent, date, opts)
        select(:datetime, :year, value, opts)
      :month, opts when date != nil ->
        {value, opts} = datetime_options(:month, @months, id, name, parent, date, opts)
        select(:datetime, :month, value, opts)
      :day, opts when date != nil ->
        {value, opts} = datetime_options(:day, @days, id, name, parent, date, opts)
        select(:datetime, :day, value, opts)
      :hour, opts when time != nil ->
        {value, opts} = datetime_options(:hour, @hours, id, name, parent, time, opts)
        select(:datetime, :hour, value, opts)
      :min, opts when time != nil ->
        {value, opts} = datetime_options(:min, @minsec, id, name, parent, time, opts)
        select(:datetime, :min, value, opts)
      :sec, opts when time != nil ->
        {value, opts} = datetime_options(:sec, @minsec, id, name, parent, time, opts)
        select(:datetime, :sec, value, opts)
    end
  end

  defp datetime_options(type, values, id, name, parent, datetime, opts) do
    opts = Keyword.merge Keyword.get(parent, type, []), opts
    suff = Atom.to_string(type)

    {value, opts} = Keyword.pop(opts, :options, values)

    {value,
      opts
      |> Keyword.put_new(:id, id <> "_" <> suff)
      |> Keyword.put_new(:name, name <> "[" <> suff <> "]")
      |> Keyword.put_new(:value, Map.get(datetime, type))}
  end

  @doc """
  Generates a label tag.

  The form should either be a `Phoenix.HTML.Form` emitted
  by `form_for` or an atom.

  All given options are forwarded to the underlying tag.
  A default value is provided for `for` attribute but can
  be overriden if you pass a value to the `for` option.
  Text content would be inferred from `field` if not specified.

  ## Examples

      # Assuming form contains a User model
      label(form, :name, "Name")
      #=> <label for="user_name">Name</label>

      label(:user, :email, "Email")
      #=> <label for="user_email">Email</label>

      label(:user, :email)
      #=> <label for="user_email">Email</label>

      label(:user, :email, class: "control-label")
      #=> <label for="user_email" class="control-label">Email</label>

      label :user, :email do
        "E-mail Address"
      end
      #=> <label for="user_email">E-mail Address</label>

      label :user, :email, class: "control-label" do
        "E-mail Address"
      end
      #=> <label class="control-label" for="user_email">E-mail Address</label>
  """
  def label(form, field) do
    label(form, field, humanize(field), [])
  end

  @doc """
  See `label/2`.
  """
  def label(form, field, text) when is_binary(text) do
    label(form, field, text, [])
  end
  def label(form, field, [do: block]) do
    label(form, field, [], do: block)
  end
  def label(form, field, opts) when is_list(opts) do
    label(form, field, humanize(field), opts)
  end

  @doc """
  See `label/2`.
  """
  def label(form, field, text, opts) when is_binary(text) and is_list(opts) do
    opts = Keyword.put_new(opts, :for, id_from(form, field))
    content_tag(:label, text, opts)
  end
  def label(form, field, opts, [do: block]) do
    opts = Keyword.put_new(opts, :for, id_from(form, field))
    content_tag(:label, opts, do: block)
  end

  ## Helpers

  defp value_from(%{model: model, params: params}, field) do
    case Map.fetch(params, Atom.to_string(field)) do
      {:ok, value} -> value
      :error -> Map.get(model, field)
    end
  end

  defp value_from(name, _field) when is_atom(name),
    do: nil

  defp id_from(%{id: id}, field),
    do: "#{id}_#{field}"
  defp id_from(name, field) when is_atom(name),
    do: "#{name}_#{field}"

  defp name_from(%{name: name}, field),
    do: "#{name}[#{field}]"
  defp name_from(name, field) when is_atom(name),
    do: "#{name}[#{field}]"
end


Use atom instead of string for link method (#274)

defmodule Phoenix.HTML.Link do
  @moduledoc """
  Conveniences for working with links and URLs in HTML.
  """

  import Phoenix.HTML.Tag

  @doc """
  Generates a link to the given URL.

  ## Examples

      link("hello", to: "/world")
      #=> hello

      link("<hello>", to: "/world")
      #=> <hello>

      link("<hello>", to: "/world", class: "btn")
      #=> <hello>

      link("delete", to: "/the_world", data: [confirm: "Really?"])
      #=> delete

      # If you supply a method other than `:get`:
      link("delete", to: "/everything", method: :delete)
      #=> delete

      # You can use a `do ... end` block too:
      link to: "/hello" do
        "world"
      end

  ## Options

    * `:to` - the page to link to. This option is required

    * `:method` - the method to use with the link. In case the
      method is not `:get`, the link is generated inside the form
      which sets the proper information. In order to submit the
      form, JavaScript must be enabled

    * `:csrf_token` - a custom token to use for links with a method
      other than `:get`.

  All other options are forwarded to the underlying `<a>` tag.

  ## JavaScript dependency

  In order to support links where `:method` is not `:get` or use the above
  data attributes, `Phoenix.HTML` relies on JavaScript. You can load
  `priv/static/phoenix_html.js` into your build tool.

  ### Data attributes

  Data attributes are added as a keyword list passed to the `data` key.
  The following data attributes are supported:

    * `data-confirm` - shows a confirmation prompt before
      generating and submitting the form when `:method`
      is not `:get`.

  ### Overriding the default confirm behaviour

  `phoenix_html.js` does trigger a custom event `phoenix.link.click` on the
  clicked DOM element when a click happened. This allows you to intercept the
  event on it's way bubbling up to `window` and do your own custom logic to
  enhance or replace how the `data-confirm` attribute is handled.

  You could for example replace the browsers `confirm()` behavior with a
  custom javascript implementation:

  ```javascript
  // listen on document.body, so it's executed before the default of
  // phoenix_html, which is listening on the window object
  document.body.addEventListener('phoenix.link.click', function (e) {
    // Prevent default implementation
    e.stopPropagation();

    // Introduce alternative implementation
    var message = e.target.getAttribute("data-confirm");
    if(!message){ return true; }
    vex.dialog.confirm({
      message: message,
      callback: function (value) {
        if (value == false) { e.preventDefault(); }
      }
    })
  }, false);
  ```

  Or you could attach your own custom behavior.

  ```javascript
  window.addEventListener('phoenix.link.click', function (e) {
    // Introduce custom behaviour
    var message = e.target.getAttribute("data-prompt");
    var answer = e.target.getAttribute("data-prompt-answer");
    if(message && answer && (answer != window.prompt(message))) {
      e.preventDefault();
    }
  }, false);
  ```

  The latter could also be bound to any `click` event, but this way you can be
  sure your custom code is only executed when the code of `phoenix_html.js` is run.

  ## CSRF Protection

  By default, CSRF tokens are generated through `Plug.CSRFProtection`.
  """
  @valid_uri_schemes [
    "http:",
    "https:",
    "ftp:",
    "ftps:",
    "mailto:",
    "news:",
    "irc:",
    "gopher:",
    "nntp:",
    "feed:",
    "telnet:",
    "mms:",
    "rtsp:",
    "svn:",
    "tel:",
    "fax:",
    "xmpp:"
  ]

  def link(text, opts)

  def link(opts, do: contents) when is_list(opts) do
    link(contents, opts)
  end

  def link(_text, opts) when not is_list(opts) do
    raise ArgumentError, "link/2 requires a keyword list as second argument"
  end

  def link(text, opts) do
    {to, opts} = pop_required_option!(opts, :to, "expected non-nil value for :to in link/2")
    to = valid_destination!(to, "link/2")
    {method, opts} = Keyword.pop(opts, :method, :get)

    if method == :get do
      opts = skip_csrf(opts)
      content_tag(:a, text, [href: to] ++ opts)
    else
      {csrf_data, opts} = csrf_data(to, opts)
      opts = Keyword.put_new(opts, :rel, "nofollow")
      content_tag(:a, text, [href: to, data: [method: method, to: to] ++ csrf_data] ++ opts)
    end
  end

  @doc false
  # No docs since this function is only called when a `do` block is passed as
  # `do:` instead of `do...end` (and that case is documented in `link/2`).
  def link(opts) when is_list(opts) do
    error = "link/2 requires a text as first argument or contents in the :do block"
    {contents, opts} = pop_required_option!(opts, :do, error)
    link(contents, opts)
  end

  @doc """
  Generates a button that uses a regular HTML form to submit to the given URL.

  Useful to ensure that links that change data are not triggered by
  search engines and other spidering software.

  ## Examples

      button("hello", to: "/world")
      #=> <button class="button" data-csrf="csrf_token" data-method="post" data-to="/world">hello</button>

      button("hello", to: "/world", method: :get, class: "btn")
      #=> <button class="btn" data-method="get" data-to="/world">hello</button>

  ## Options

    * `:to` - the page to link to. This option is required

    * `:method` - the method to use with the button. Defaults to :post.

  All other options are forwarded to the underlying button input.

  ## Data attributes

  Data attributes are added as a keyword list passed to the
  `data` key. The following data attributes are supported:

    * `data-confirm` - shows a confirmation prompt before generating and
      submitting the form.
  """
  def button(opts, do: contents) do
    button(contents, opts)
  end

  def button(text, opts) do
    {to, opts} = pop_required_option!(opts, :to, "option :to is required in button/2")
    {method, opts} = Keyword.pop(opts, :method, :post)

    to = valid_destination!(to, "button/2")

    if method == :get do
      opts = skip_csrf(opts)
      content_tag(:button, text, [data: [method: method, to: to]] ++ opts)
    else
      {csrf_data, opts} = csrf_data(to, opts)
      content_tag(:button, text, [data: [method: method, to: to] ++ csrf_data] ++ opts)
    end
  end

  defp skip_csrf(opts) do
    Keyword.delete(opts, :csrf_token)
  end

  defp csrf_data(to, opts) do
    case Keyword.pop(opts, :csrf_token, true) do
      {csrf, opts} when is_binary(csrf) ->
        {[csrf: csrf], opts}

      {true, opts} ->
        {[csrf: Plug.CSRFProtection.get_csrf_token_for(to)], opts}

      {false, opts} ->
        {[], opts}
    end
  end

  defp pop_required_option!(opts, key, error_message) do
    {value, opts} = Keyword.pop(opts, key)

    unless value do
      raise ArgumentError, error_message
    end

    {value, opts}
  end

  defp valid_destination!({:safe, to}, context) do
    {:safe, valid_string_destination!(IO.iodata_to_binary(to), context)}
  end

  defp valid_destination!({other, to}, _context) when is_atom(other) do
    [Atom.to_string(other), ?:, to]
  end

  defp valid_destination!(to, context) do
    valid_string_destination!(IO.iodata_to_binary(to), context)
  end

  for scheme <- @valid_uri_schemes do
    defp valid_string_destination!(unquote(scheme) <> _ = string, _context), do: string
  end

  defp valid_string_destination!(to, context) do
    if not match?("/" <> _, to) and String.contains?(to, ":") do
      raise ArgumentError, """
      unsupported scheme given to #{context}. In case you want to link to an
      unknown or unsafe scheme, such as javascript, use a tuple: {:javascript, rest}
      """
    else
      to
    end
  end
end

defmodule Phoenix.HTML.Link do
  @moduledoc """
  Conveniences for working with links and URLs in HTML.
  """

  import Phoenix.HTML.Tag

  @doc """
  Generates a link to the given URL.

  ## Examples

      link("hello", to: "/world")
      #=> hello

      link("<hello>", to: "/world")
      #=> <hello>

      link("<hello>", to: "/world", class: "btn")
      #=> <hello>

      link("delete", to: "/the_world", data: [confirm: "Really?"])
      #=> delete

      # If you supply a method other than `:get`:
      link("delete", to: "/everything", method: :delete)
      #=> delete

      # You can use a `do ... end` block too:
      link to: "/hello" do
        "world"
      end

  ## Options

    * `:to` - the page to link to. This option is required

    * `:method` - the method to use with the link. In case the
      method is not `:get`, the link is generated inside the form
      which sets the proper information. In order to submit the
      form, JavaScript must be enabled

    * `:csrf_token` - a custom token to use for links with a method
      other than `:get`.

  All other options are forwarded to the underlying `<a>` tag.

  ## JavaScript dependency

  In order to support links where `:method` is not `:get` or use the above
  data attributes, `Phoenix.HTML` relies on JavaScript. You can load
  `priv/static/phoenix_html.js` into your build tool.

  ### Data attributes

  Data attributes are added as a keyword list passed to the `data` key.
  The following data attributes are supported:

    * `data-confirm` - shows a confirmation prompt before
      generating and submitting the form when `:method`
      is not `:get`.

  ### Overriding the default confirm behaviour

  `phoenix_html.js` does trigger a custom event `phoenix.link.click` on the
  clicked DOM element when a click happened. This allows you to intercept the
  event on it's way bubbling up to `window` and do your own custom logic to
  enhance or replace how the `data-confirm` attribute is handled.

  You could for example replace the browsers `confirm()` behavior with a
  custom javascript implementation:

  ```javascript
  // listen on document.body, so it's executed before the default of
  // phoenix_html, which is listening on the window object
  document.body.addEventListener('phoenix.link.click', function (e) {
    // Prevent default implementation
    e.stopPropagation();

    // Introduce alternative implementation
    var message = e.target.getAttribute("data-confirm");
    if(!message){ return true; }
    vex.dialog.confirm({
      message: message,
      callback: function (value) {
        if (value == false) { e.preventDefault(); }
      }
    })
  }, false);
  ```

  Or you could attach your own custom behavior.

  ```javascript
  window.addEventListener('phoenix.link.click', function (e) {
    // Introduce custom behaviour
    var message = e.target.getAttribute("data-prompt");
    var answer = e.target.getAttribute("data-prompt-answer");
    if(message && answer && (answer != window.prompt(message))) {
      e.preventDefault();
    }
  }, false);
  ```

  The latter could also be bound to any `click` event, but this way you can be
  sure your custom code is only executed when the code of `phoenix_html.js` is run.

  ## CSRF Protection

  By default, CSRF tokens are generated through `Plug.CSRFProtection`.
  """
  @valid_uri_schemes [
    "http:",
    "https:",
    "ftp:",
    "ftps:",
    "mailto:",
    "news:",
    "irc:",
    "gopher:",
    "nntp:",
    "feed:",
    "telnet:",
    "mms:",
    "rtsp:",
    "svn:",
    "tel:",
    "fax:",
    "xmpp:"
  ]

  def link(text, opts)

  def link(opts, do: contents) when is_list(opts) do
    link(contents, opts)
  end

  def link(_text, opts) when not is_list(opts) do
    raise ArgumentError, "link/2 requires a keyword list as second argument"
  end

  def link(text, opts) do
    {to, opts} = pop_required_option!(opts, :to, "expected non-nil value for :to in link/2")
    to = valid_destination!(to, "link/2")
    {method, opts} = Keyword.pop(opts, :method, :get)

    if method == :get do
      opts = skip_csrf(opts)
      content_tag(:a, text, [href: to] ++ opts)
    else
      {csrf_data, opts} = csrf_data(to, opts)
      opts = Keyword.put_new(opts, :rel, "nofollow")
      content_tag(:a, text, [href: to, data: [method: method, to: to] ++ csrf_data] ++ opts)
    end
  end

  @doc false
  # No docs since this function is only called when a `do` block is passed as
  # `do:` instead of `do...end` (and that case is documented in `link/2`).
  def link(opts) when is_list(opts) do
    error = "link/2 requires a text as first argument or contents in the :do block"
    {contents, opts} = pop_required_option!(opts, :do, error)
    link(contents, opts)
  end

  @doc """
  Generates a button that uses a regular HTML form to submit to the given URL.

  Useful to ensure that links that change data are not triggered by
  search engines and other spidering software.

  ## Examples

      button("hello", to: "/world")
      #=> <button class="button" data-csrf="csrf_token" data-method="post" data-to="/world">hello</button>

      button("hello", to: "/world", method: "get", class: "btn")
      #=> <button class="btn" data-method="get" data-to="/world">hello</button>

  ## Options

    * `:to` - the page to link to. This option is required

    * `:method` - the method to use with the button. Defaults to :post.

  All other options are forwarded to the underlying button input.

  ## Data attributes

  Data attributes are added as a keyword list passed to the
  `data` key. The following data attributes are supported:

    * `data-confirm` - shows a confirmation prompt before generating and
      submitting the form.
  """
  def button(opts, do: contents) do
    button(contents, opts)
  end

  def button(text, opts) do
    {to, opts} = pop_required_option!(opts, :to, "option :to is required in button/2")
    {method, opts} = Keyword.pop(opts, :method, :post)

    to = valid_destination!(to, "button/2")

    if method == :get do
      opts = skip_csrf(opts)
      content_tag(:button, text, [data: [method: method, to: to]] ++ opts)
    else
      {csrf_data, opts} = csrf_data(to, opts)
      content_tag(:button, text, [data: [method: method, to: to] ++ csrf_data] ++ opts)
    end
  end

  defp skip_csrf(opts) do
    Keyword.delete(opts, :csrf_token)
  end

  defp csrf_data(to, opts) do
    case Keyword.pop(opts, :csrf_token, true) do
      {csrf, opts} when is_binary(csrf) ->
        {[csrf: csrf], opts}

      {true, opts} ->
        {[csrf: Plug.CSRFProtection.get_csrf_token_for(to)], opts}

      {false, opts} ->
        {[], opts}
    end
  end

  defp pop_required_option!(opts, key, error_message) do
    {value, opts} = Keyword.pop(opts, key)

    unless value do
      raise ArgumentError, error_message
    end

    {value, opts}
  end

  defp valid_destination!({:safe, to}, context) do
    {:safe, valid_string_destination!(IO.iodata_to_binary(to), context)}
  end

  defp valid_destination!({other, to}, _context) when is_atom(other) do
    [Atom.to_string(other), ?:, to]
  end

  defp valid_destination!(to, context) do
    valid_string_destination!(IO.iodata_to_binary(to), context)
  end

  for scheme <- @valid_uri_schemes do
    defp valid_string_destination!(unquote(scheme) <> _ = string, _context), do: string
  end

  defp valid_string_destination!(to, context) do
    if not match?("/" <> _, to) and String.contains?(to, ":") do
      raise ArgumentError, """
      unsupported scheme given to #{context}. In case you want to link to an
      unknown or unsafe scheme, such as javascript, use a tuple: {:javascript, rest}
      """
    else
      to
    end
  end
end