defmodule Rapport do
  @moduledoc """
  Rapport aims to provide a robust set of modules to generate
  HTML reports that both looks good in the browser and when being printed.
  """

  alias Rapport.Report
  alias Rapport.Page
  alias Rapport.PageNumbering

  @normalize_css File.read!(Path.join(__DIR__, "base_template/normalize.css"))
  @paper_css File.read!(Path.join(__DIR__, "base_template/paper.css"))
  @base_template File.read!(Path.join(__DIR__, "base_template/base_template.html.eex"))

  @spec add_page(Report.t(), String.t(), map) :: Report.t()
  defdelegate add_page(report, page_template, fields), to: Page
  @spec add_page(Report.t(), Page.t()) :: Report.t()
  defdelegate add_page(report, page), to: Page
  @spec add_pages(Report.t(), list(Page.t())) :: Report.t()
  defdelegate add_pages(report, pages), to: Page

  @spec generate_pages([Page.t()], Report.padding()) :: String.t()
  @spec generate_pages([Page.t()], Report.padding(), Rapport.PageNumbering.t()) :: String.t()
  defdelegate generate_pages(pages, padding), to: Page
  defdelegate generate_pages(pages, padding, page_number_opts), to: Page

  @spec add_page_numbers(Report.t()) :: Report.t()
  @spec add_page_numbers(Report.t(), atom, any) :: Report.t()
  defdelegate add_page_numbers(report, page_number_position, formatter), to: PageNumbering
  @spec add_page_numbers(Report.t(), atom) :: Report.t()
  defdelegate add_page_numbers(report, page_number_position), to: PageNumbering
  defdelegate add_page_numbers(report), to: PageNumbering

  @spec new(String.t(), map()) :: Report.t()
  @doc """
  Creates a new report.

  An optional EEx template can be passed to the `new` function. This template
  is meant to hold global things like styles, fonts etc that can be used on all
  pages thats added to the report.

  The `new` function sets the default paper size to `:A4`, the rotation
  to `:portrait`, the page padding to 10mm and the report title to "Report".
  Those defaults can easily be overridden by using `set_paper_size/2`,
  `set_rotation/2`, `set_padding/2` and `set_title/2`.

  Returns a `Rapport.Report` struct.

  ## Options

    * `template` - An optional EEx template for the report.
    * `fields` - A map with fields to assign to the EEx report template
  """

  def new(template \\ "", fields \\ %{}) do
    %Report{
      title: "Report",
      paper_size: :A4,
      rotation: :portrait,
      pages: [],
      template: template,
      padding: 10,
      fields: fields,
      page_number_opts: %PageNumbering{
        add_page_numbers: false,
        page_number_position: :bottom_right,
        page_number_formatter: fn cnt_page, _tot_pages -> "#{cnt_page}" end
      }
    }
  end

  @spec set_title(Report.t(), String.t()) :: Report.t()
  @doc """
  Sets the title for a report. This is the title of the generated html report.

  ## Options

    * `report` - The `Rapport.Report` you want to set the title for.
    * `title` - The new title
  """

  def set_title(%Report{} = report, title) when is_binary(title) do
    Map.put(report, :title, title)
  end

  @spec set_paper_size(
          Report.t(),
          Report.paper_size()
        ) :: Report.t()
  @doc """
  Sets the paper size for the report.

  It expects the paper size to be an atom and must be
  `:A4`, `:A3`, `:A5`, `:half_letter`, `:letter`, `:legal`, `:junior_legal`
   or `:ledger`, otherwise `ArgumentError` will be raised.

  ## Options

    * `report` - The `Rapport.Report` that you want set the paper size for
    * `paper_size` - The paper size.
  """

  def set_paper_size(%Report{} = report, paper_size) do
    validate_list(
      paper_size,
      [:A4, :A3, :A5, :half_letter, :letter, :legal, :junior_legal, :ledger],
      "Invalid paper size"
    )

    Map.put(report, :paper_size, paper_size)
  end

  @spec set_rotation(Report.t(), Report.rotation()) :: Report.t()
  @doc """
  Sets the rotation for the report.

  It expects the rotation to an atom and must be `:portrait` or `:landscape`,
  otherwise `ArgumentError` will be raised.

  ## Options

    *  `report` - The `Rapport.Report` that you want set the rotation for
    *  `rotation` - The rotation.
  """

  def set_rotation(%Report{} = report, rotation) do
    validate_list(rotation, [:portrait, :landscape], "Invalid rotation")
    Map.put(report, :rotation, rotation)
  end

  @spec set_padding(Report.t(), Report.padding()) :: Report.t()
  @doc """
  Sets the padding (in millimeters) for the report.

  It expects the padding to be an integer and must be `10`, `15`, `20` or `25` mm,
  otherwise `ArgumentError` will be raised.

  ## Options

    * `report` - The `Rapport.Report` that you want set the padding for
    * `rotation` - The padding.
  """

  def set_padding(%Report{} = report, padding) when is_integer(padding) do
    validate_list(padding, [10, 15, 20, 25], "Invalid padding")
    Map.put(report, :padding, padding)
  end

  @spec generate_html(Report.t()) :: String.t()
  @doc """
  Generates HTML for the report.

  ## Options

    * `report` - The `Rapport.Report` that you want to generate to HTML.
  """
  def generate_html(%Report{} = report) do
    paper_settings = paper_settings_css(report)
    add_page_numbers? = report.page_number_opts.add_page_numbers

    pages =
      case add_page_numbers? do
        true -> generate_pages(report.pages, report.padding, report.page_number_opts)
        false -> generate_pages(report.pages, report.padding)
      end

    report_template = EEx.eval_string(report.template, assigns: report.fields)

    assigns = [
      title: report.title,
      paper_settings: paper_settings,
      normalize_css: @normalize_css,
      paper_css: @paper_css,
      pages: pages,
      report_template: report_template
    ]

    EEx.eval_string(@base_template, assigns: assigns)
  end

  @spec save_to_file(Report.t(), binary) :: :ok
  @doc """
  Convenient function to save a report to file.

  ## Options
    * `report` - The `Rapport.Report` that you want to save to a HTML file
    * `file_path` - The path to the HTML file you want to save.
  """
  def save_to_file(%Report{} = report, file_path) when is_binary(file_path) do
    html_report = generate_html(report)
    File.write!(file_path, html_report)
  end

  defp paper_settings_css(%Report{} = report) do
    paper_size = Atom.to_string(report.paper_size)
    rotation = Atom.to_string(report.rotation)
    if rotation == "portrait", do: paper_size, else: "#{paper_size} #{rotation}"
  end

  @spec validate_list(any, list(), String.t()) :: nil
  @doc false
  def validate_list(what, list, msg) do
    if what not in list, do: raise(ArgumentError, message: msg)
  end
end
