lib/digital.ex

#
#  Created by Boyd Multerer on August 8, 2018.
#  Copyright © 2018 Kry10 Industries. All rights reserved.
#

defmodule Scenic.Clock.Digital do
  @moduledoc """
  A component that runs an digital clock.

  See the [Components](Scenic.Clock.Components.html#digital_clock/2) module for usage


  """
  use Scenic.Component, has_children: false

  alias Scenic.Graph
  import Scenic.Primitives, only: [{:text, 2}, {:text, 3}]

  # formats setup
  @default_format :hours_12

  # --------------------------------------------------------
  @doc false
  @impl Scenic.Component
  def validate(nil), do: {:ok, nil}

  def validate(data) do
    {
      :error,
      """
      #{IO.ANSI.red()}Invalid Scenic.Clock.Digital
      Received: #{inspect(data)}
      #{IO.ANSI.yellow()}
      """
    }
  end

  # --------------------------------------------------------
  @doc false
  @impl Scenic.Scene
  def init(scene, _param, opts) do
    styles = Keyword.get(opts, :styles, [])

    format =
      case opts[:format] do
        :hours_12 -> :hours_12
        :hours_24 -> :hours_24
        _ -> @default_format
      end

    # set up the requested graph
    graph =
      Graph.build(styles)
      |> text("", id: :time)

    scene =
      scene
      |> assign(
        graph: graph,
        format: format,
        seconds: !!opts[:seconds]
      )
      |> assign_new(timer: nil, last: nil)
      |> update_time()

    # send a message to self to start the clock a fraction of a second
    # into the future to hopefully line it up closer to when the seconds
    # actually are. Note that I want it to arrive just slightly after
    # the one second mark, which is way better than just slighty before.
    # avoid trunc errors and such that way even if it means the second
    # timer is one millisecond behind the actual time.
    {microseconds, _} = Time.utc_now().microsecond
    Process.send_after(self(), :start_clock, 1001 - trunc(microseconds / 1000))

    {:ok, scene}
  end

  # --------------------------------------------------------
  @doc false
  # should be shortly after the actual one-second mark
  @impl GenServer
  def handle_info(:start_clock, scene) do
    # start the timer on a one-second interval
    {:ok, timer} = :timer.send_interval(1000, :tick_tock)

    scene =
      scene
      |> assign(:timer, timer)
      |> update_time()

    # update the clock
    {:noreply, scene}
  end

  # --------------------------------------------------------
  def handle_info(:tick_tock, scene) do
    {:noreply, update_time(scene)}
  end

  # --------------------------------------------------------
  defp update_time(
         %{
           assigns: %{
             format: format,
             seconds: seconds,
             graph: graph,
             last: last
           }
         } = scene
       ) do
    time = :calendar.local_time()
    base_time = base_time(time, seconds)

    case base_time != last do
      true ->
        graph = Graph.modify(graph, :time, &text(&1, format_time(time, format, seconds)))

        scene
        |> assign(last: base_time, graph: graph)
        |> push_graph(graph)

      _ ->
        scene
    end
  end

  # --------------------------------------------------------
  defp format_time({_, {h, m, s}}, :hours_12, seconds) do
    {h, am_pm} =
      cond do
        h > 12 -> {h - 12, "PM"}
        true -> {h, "AM"}
      end

    case seconds do
      true -> "#{h}:#{format_ms(m)}:#{format_ms(s)} #{am_pm}"
      false -> "#{h}:#{format_ms(m)} #{am_pm}"
    end
  end

  defp format_time({_, {h, m, s}}, :hours_24, seconds) do
    case seconds do
      true -> "#{h}:#{format_ms(m)}:#{format_ms(s)}"
      false -> "#{h}:#{format_ms(m)}"
    end
  end

  defp format_ms(m) when m >= 0 and m < 10, do: "0#{m}"
  defp format_ms(m), do: to_string(m)

  # --------------------------------------------------------
  defp base_time(time, true), do: time
  defp base_time({d, {h, m, _}}, false), do: {d, {h, m}}
end