#frozen_string_literal: true

module FFI::WiringPi::GPIO
  extend FFI::Library

  INPUT = 0
  OUTPUT = 1
  PWM_OUTPUT = 2
  GPIO_CLOCK = 3
  SOFT_PWM_OUTPUT = 4
  SOFT_TONE_OUTPUT = 5
  PWM_TONE_OUTPUT = 6

  LOW = 0
  HIGH = 1

  PUD_OFF = 0
  PUD_DOWN = 1
  PUD_UP = 2

  PWM_MODE_MS = 0
  PWM_MODE_BAL = 1

  ffi_lib_flags :now, :global
  ffi_lib 'wiringPi'

  attach_function :setup, :wiringPiSetup, [], :int
  attach_function :setup_as_gpio, :wiringPiSetupGpio, [], :int
  attach_function :setup_as_physical, :wiringPiSetupPhys, [], :int

  # This initialises wiringPi but uses the /sys/class/gpio interface rather
  # than accessing the hardware directly. This can be called as a non-root user
  # provided the GPIO pins have been exported before-hand using the gpio program.
  # Pin numbering in this mode is the native Broadcom GPIO numbers – the same as
  # wiringPiSetupGpio() above, so be aware of the differences between Rev 1 and Rev 2 boards.
  # Note: In this mode you can only use the pins which have been exported via the /sys/class/gpio
  #   interface before you run your program. You can do this in a separate shell-script, or by
  #   using the system() function from inside your program to call the gpio program.
  # Also note that some functions have no effect when using this mode as they’re
  # not currently possible to action unless called with root privileges.
  # (although you can use system() to call gpio to set/change modes if needed)
  #
  # @see http://wiringpi.com/reference/setup/
  attach_function :setup_system_mode, :wiringPiSetupSys, [], :int

  #
  # @param pin [Integer] pin position (depends on setup mode)
  # @param mode [Integer] `FFI::WiringPi::GPIO::INPUT`, `FFI::WiringPi::GPIO::OUTPUT`,
  #   `FFI::WiringPi::GPIO::PWM_OUTPUT` or `FFI::WiringPi::GPIO::GPIO_CLOCK`
  attach_function :set_pin_mode, :pinMode, [:int, :int], :void

  def self.get(pin, mode = FFI::WiringPi::GPIO::OUTPUT)
    set_pin_mode(pin, mode)
    Pin.new(pin, mode)
  end

  # @param pin [Integer] pin position (depends on setup mode)
  # @param mode [Integer] `FFI::WiringPi::GPIO::LOW` or `FFI::WiringPi::GPIO::HIGH`
  attach_function :digital_write, :digitalWrite, [:int, :int], :void

  # Write pin state (aka digital_write)
  #
  # @param pin [Integer] pin position (depends on setup mode)
  # @param state [Boolean] `true` to set to HIGH, `false` to set to LOW
  #
  def self.write(pin, state)
    digital_write(pin, state ? HIGH : LOW)
  end

  # This returns the value read on the supplied analog input pin.
  # You will need to register additional analog modules to enable this function
  # for devices such as the Gertboard, quick2Wire analog board, etc.
  attach_function :analog_read, :analogRead, [:int], :int

  # This writes the given value to the supplied analog pin.
  # You will need to register additional analog modules to enable this function
  # for devices such as the Gertboard.
  attach_function :analog_write, :analogWrite, [:int, :int], :void

  # Sets pin to HIGH state
  #
  # @param pin [Integer] pin position (depends on setup mode)
  #
  def self.up(pin)
    write(pin, true)
  end

  # Sets pin to LOW state
  #
  # @param pin [Integer] pin position (depends on setup mode)
  #
  def self.down(pin)
    write(pin, false)
  end

  # @param pin [Integer] pin position (depends on setup mode)
  #
  # @returns [Integer] `FFI::WiringPi::GPIO::LOW` or `FFI::WiringPi::GPIO::HIGH`
  attach_function :digital_read, :digitalRead, [:int], :int

  # Read pin state (aka digital_read)
  #
  # @param pin [Integer] pin position (depends on setup mode)
  # @returns [Boolean] `true` if pin is in high state, `false` if in low
  #
  def self.read(pin)
    result = digital_read(pin)
    case result
    when LOW
      false
    when HIGH
      true
    else
      raise "Unknown result: #{result.inspect}"
    end
  end

  # @param pin [Integer] pin position (depends on setup mode)
  # @param value [Integer] 0-1023
  attach_function :pwm_write, :pwmWrite, [:int, :int], :void

  # This writes the 8-bit byte supplied to the first 8 GPIO pins.
  # It’s the fastest way to set all 8 bits at once to a particular value,
  # although it still takes two write operations to the Pi’s GPIO hardware.
  # @param pin [Integer] pin position (depends on setup mode)
  # @param value [Integer] 0-1023
  attach_function :digital_write_byte, :digitalWriteByte, [:int], :void

  def self.batch_write(boolean_array)
    digital_write_byte boolean_array.each_with_index.sum { |bit, i| (bit ? 1 : 0 ) << i }
  end

  # The PWM generator can run in 2 modes – :balanced and :mark_space
  # The :mark_space mode is traditional, however the default mode in the Pi is :balanced.
  # @param value [Integer] PWM_MODE_BAL or PWM_MODE_MS
  attach_function :pwm_set_mode, :pwmSetMode, [:int], :void

  def self.pwm_mode(mode = :balanced)
    raise ArgumentError("mode is invalid: #{mode.inspect}") unless mode.in?([:balanced, :mark_space])
    pwm_set_mode mode == :balanced ? PWM_MODE_BAL : PWM_MODE_MS
  end

  # This sets the range register in the PWM generator. The default is 1024.
  # @param value [Integer]
  attach_function :pwm_set_range, :pwmSetRange, [:uint], :void

  def self.pwm_range(range = 1024)
    raise ArgumentError("range is invalid: #{range.inspect}") unless range.is_a?(Integer) && range >= 0
    pwm_set_range range
  end

  # This sets the divisor for the PWM clock.
  # @param value [Integer]
  attach_function :pwm_set_clock, :pwmSetClock, [:int], :void

  class Pin
    def initialize(position, mode)
      @position = position
      @mode = mode
    end

    def up!
      raise ArgumentError('Can only set in OUTPUT mode') && return unless @mode == FFI::WiringPi::GPIO::OUTPUT
      FFI::WiringPi::GPIO.up(@position)
    end

    def down!
      raise ArgumentError('Can only set in OUTPUT mode') && return unless @mode == FFI::WiringPi::GPIO::OUTPUT
      FFI::WiringPi::GPIO.down(@position)
    end

    def value
      raise ArgumentError('Can only set in INPUT mode') && return unless @mode == FFI::WiringPi::GPIO::INPUT
      FFI::WiringPi::GPIO.read(@position)
    end
  end
end