defmodule Contex.Gallery.Sample do
  alias Contex.Plot

  @moduledoc """
  Renders plots to be used in ExDocs and tests.

  The idea is that each graph is displayed along its
  source code, so it's very easy to spot what is what.
  To shortern the source code displayed, though, aliases
  are imported but not shown.
  """

  @doc """
  Adds a graph to a documentation block.

  Usage:

      import Contex.Gallery.Sample, only: [graph: 1]

      graph(title: "A stacked sample",
            file: "bar_charts_log_stacked.sample",
            info: "Some Markdown description")

  If there are rendering errors, a message is printed
  on stdout and the full text of the error is shown
  on the docs. We don't want this to break, as docs
  are generated at compile time, so if they break, your
  code does not compile!

  """

  def graph(options \\ []) do
    path = Keyword.get(options, :path, "lib/chart/gallery")
    file = Keyword.get(options, :file, nil)
    aliases = Keyword.get(options, :aliases, "00_aliases.sample")
    title = Keyword.get(options, :title, "")
    bgcolor = Keyword.get(options, :bgcolor, "#fff")
    extra_info_text = Keyword.get(options, :info, "")

    case safely_evaluate_svg(["#{path}/#{aliases}", "#{path}/#{file}"]) do
      {:ok, source_code, svg, time} ->
        """
        # #{title}

        #{extra_info_text}

        #{encode_svg(svg, bgcolor)}
        __Rendering took #{time} ms - Size: #{String.length(svg) / 1000} Kb__


        ```
        #{source_code}
        ```


        """

      {:error, error_text, filename, code_run, time} ->
        with IO.puts("Error processing #{filename} - see generated docs") do
          """
          # Error: #{title}

          ```
          #{code_run}
          ```

          Raised error:

          ```
          #{error_text}
          ```


          __Rendering took #{time} ms__
          """
        end
    end
  end

  defp uid(), do: make_ref() |> inspect() |> String.slice(11, 99) |> String.replace(">", "")

  @doc """
  Will try and evaluate a set of files, by sticking them  in order
  one after the other.

  If all goes well, it will return the SVG generated and
  how long execution took.

  If there are any errors, it will return the error, the complete
  source code that was evaluated (with includes) and how long
  execution took in ms.
  """
  def safely_evaluate_svg(files) do
    code_to_evaluate =
      files
      |> Enum.map(fn f ->
        {:ok, code} = File.read(f)

        """
        ## Source: #{f}

        #{code}

        """
      end)
      |> Enum.join()

    filename =
      files
      |> List.last()

    {:ok, source} =
      filename
      |> File.read()

    timer = mkTimer()

    try do
      {plot, _} = Code.eval_string(code_to_evaluate)
      {:safe, svg_list} = Plot.to_svg(plot)
      {:ok, source, List.to_string(svg_list), timer.()}
    rescue
      e ->
        {:error, Exception.format(:error, e, __STACKTRACE__), filename, code_to_evaluate,
         timer.()}
    end
  end

  @doc """
  Encodes a div for our SVG.

  Unfortunately, we need to split it into multiple
  lines, as ExDoc is veeeeery slow with long
  text lines.

  It also complains of SVG code being improperly formatted,
  and breaks the page.

  So we encode the SVG as one long JS line, and then
  stick it into the container DIV we just created.

  """
  def encode_svg(svg, bgcolor) do
    encoded_svg = URI.encode(svg)

    chunked_svg =
      encoded_svg
      |> String.codepoints()
      |> Enum.chunk_every(50)
      |> Enum.map(&Enum.join/1)
      |> Enum.map(fn c -> "\"#{c}\"" end)
      |> Enum.join(" + \n")

    block_id = uid()

    """

    <div id="#{block_id}" style="background: #{bgcolor}">
    </div>


    <script>
    document.getElementById('#{block_id}').innerHTML =  decodeURI( #{chunked_svg} );

    </script>
    """
  end

  @doc """
  Returns a timer function.

  By calling it, we get the number of elapsed milliseconds
  since the function was created.

  """

  def mkTimer() do
    t0 = :erlang.monotonic_time(:millisecond)
    fn -> :erlang.monotonic_time(:millisecond) - t0 end
  end
end
