defmodule Inky.RpiHAL do
  @default_io_mod Inky.RpiIO

  @moduledoc """
  An `Inky.HAL` implementation responsible for sending commands to the Inky
  screen. It delegates to whatever IO module its user provides at init, but
  defaults to #{inspect(@default_io_mod)}
  """

  @behaviour Inky.HAL

  @color_map_black %{black: 0, miss: 1}
  @color_map_accent %{red: 1, yellow: 1, accent: 1, miss: 0}

  alias Inky.Display
  alias Inky.HAL
  alias Inky.PixelUtil

  defmodule State do
    @moduledoc false

    @state_fields [:display, :io_mod, :io_state]

    @enforce_keys @state_fields
    defstruct @state_fields
  end

  #
  # API
  #

  @impl HAL
  def init(args) do
    display = args[:display] || raise(ArgumentError, message: ":display missing in args")
    io_mod = args[:io_mod] || @default_io_mod

    io_args = args[:io_args] || []
    io_args = if :gpio_mod in io_args, do: io_args, else: [gpio_mod: Circuits.GPIO] ++ io_args
    io_args = if :spi_mod in io_args, do: io_args, else: [spi_mod: Circuits.SPI] ++ io_args

    %State{
      display: display,
      io_mod: io_mod,
      io_state: io_mod.init(io_args)
    }
  end

  @impl HAL
  def handle_update(pixels, border, push_policy, state = %State{}) do
    display = %Display{width: w, height: h, rotation: r} = state.display
    black_bits = PixelUtil.pixels_to_bits(pixels, w, h, r, @color_map_black)
    accent_bits = PixelUtil.pixels_to_bits(pixels, w, h, r, @color_map_accent)

    reset(state)
    soft_reset(state)

    case pre_update(state, push_policy) do
      :cont -> do_update(state, display, border, black_bits, accent_bits)
      :halt -> {:error, :device_busy}
    end
  end

  #
  # procedures
  #

  defp pre_update(state, :await) do
    await_device(state)
    :cont
  end

  defp pre_update(state, :once) do
    case read_busy(state) do
      0 -> :cont
      1 -> :halt
    end
  end

  defp do_update(state, display, border, buf_black, buf_accent) do
    d_pd = display.packed_dimensions

    state
    |> set_analog_block_control()
    |> set_digital_block_control()
    |> set_gate(d_pd.height)
    |> set_gate_driving_voltage()
    |> dummy_line_period()
    |> set_gate_line_width()
    |> set_data_entry_mode()
    |> power_on()
    |> vcom_register()
    |> set_border_color(border)
    |> configure_if_yellow(display.accent)
    |> configure_if_red_what(display.accent, display.type)
    |> set_luts(display.luts)
    |> set_dimensions(d_pd.width, d_pd.height)
    |> push_pixel_data_bw(buf_black)
    |> push_pixel_data_ry(buf_accent)
    |> display_update_sequence()
    |> trigger_display_update()
    |> sleep(50)
    |> await_device()
    |> deep_sleep()

    :ok
  end

  #
  # "routines" and serial commands
  #

  defp reset(state) do
    state
    |> set_reset(0)
    |> sleep(100)
    |> set_reset(1)
    |> sleep(100)
  end

  defp soft_reset(state), do: write_command(state, 0x12)
  defp set_analog_block_control(state), do: write_command(state, 0x74, 0x54)
  defp set_digital_block_control(state), do: write_command(state, 0x7E, 0x3B)
  defp set_gate(state, packed_height), do: write_command(state, 0x01, packed_height <> <<0x00>>)
  defp set_gate_driving_voltage(state), do: write_command(state, 0x03, [0b10000, 0b0001])
  defp dummy_line_period(state), do: write_command(state, 0x3A, 0x07)
  defp set_gate_line_width(state), do: write_command(state, 0x3B, 0x04)
  # Data entry mode setting 0x03 = X/Y increment
  defp set_data_entry_mode(state), do: write_command(state, 0x11, 0x03)
  defp power_on(state), do: write_command(state, 0x04)

  defp vcom_register(state) do
    # VCOM Register, 0x3c = -1.5v?
    write_command(state, 0x2C, 0x3C)
  end

  defp set_border_color(state, border) do
    accent = state.display.accent

    border_data =
      case border do
        # GS Transition Define A + VSS + LUT0
        :black ->
          0b00000000

        # Fix Level Define A + VSH2 + LUT3
        c when c in [:red, :accent] and accent == :red ->
          0b01110011

        # GS Transition Define A + VSH2 + LUT3
        c when c in [:yellow, :accent] and accent == :yellow ->
          0b00110011

        # GS Transition Define A + VSH2 + LUT1
        :white ->
          0b00110001

        _ ->
          raise ArgumentError,
            message: "Invalid border #{inspect(border)} provided. Accent was #{inspect(accent)}"
      end

    write_command(state, 0x3C, border_data)
  end

  # Set voltage of VSH and VSL on Yellow device
  defp configure_if_yellow(state, :yellow), do: write_command(state, 0x04, 0x07)

  defp configure_if_yellow(state, _), do: state

  # Set voltage of VSH and VSL on red device
  defp configure_if_red_what(state, :red, :what),
    do: write_command(state, 0x04, <<0x30, 0xAC, 0x22>>)

  defp configure_if_red_what(state, _, _), do: state

  defp set_luts(state, luts), do: write_command(state, 0x32, luts)

  defp set_dimensions(state, width_data, packed_height) do
    height_data = <<0, 0>> <> packed_height
    width_data = <<0>> <> width_data

    state
    # Set RAM X Start/End
    |> write_command(0x44, width_data)
    # Set RAM Y Start/End
    |> write_command(0x45, height_data)
  end

  # 0x24 == RAM B/W
  defp push_pixel_data_bw(state, buffer_black),
    do: do_push_pixel_data(state, 0x24, buffer_black)

  # 0x26 == RAM Red/Yellow/etc
  defp push_pixel_data_ry(state, buffer_accent),
    do: do_push_pixel_data(state, 0x26, buffer_accent)

  defp do_push_pixel_data(state, pixel_cmd, pixel_buffer) do
    # Set RAM X Pointer start
    write_command(state, 0x4E, 0x00)

    # Set RAM Y Pointer start
    write_command(state, 0x4F, <<0x00, 0x00>>)
    write_command(state, pixel_cmd, pixel_buffer)
  end

  defp display_update_sequence(state), do: write_command(state, 0x22, 0xC7)
  defp trigger_display_update(state), do: write_command(state, 0x20)
  defp deep_sleep(state), do: write_command(state, 0x10, 0x01)

  #
  # waiting
  #

  defp await_device(state) do
    case read_busy(state) do
      1 ->
        sleep(state, 10)
        await_device(state)

      0 ->
        state
    end
  end

  #
  # pipe-able wrappers
  #

  defp sleep(state, sleep_time) do
    io_call(state, :handle_sleep, [sleep_time])
    state
  end

  defp set_reset(state, value) do
    io_call(state, :handle_reset, [value])
    state
  end

  defp read_busy(state) do
    io_call(state, :handle_read_busy)
  end

  defp write_command(state, command) do
    io_call(state, :handle_command, [command])
    state
  end

  defp write_command(state, command, data) do
    io_call(state, :handle_command, [command, data])
    state
  end

  #
  # Behaviour dispatching
  #

  # Dispatch to the IO callback module that's held in state, using the previously obtained state
  defp io_call(state, op, args \\ []) do
    apply(state.io_mod, op, [state.io_state | args])
  end
end
