#!/usr/bin/ruby

require 'terrapin'
require 'yaml'
# require 'pry' # just for debugging
require 'logger'

# error from device specifid not being found:
# Detected 0 device(s), submitted device 0 is out of range
#
# list of devices:
# Dev=0, CorsairLink Device Found: H150i Pro!

class OpenCorsairLink
  def initialize
    @dryrun        = false

    @binary_path   = '/usr/sbin/OpenCorsairLink'
    @config_path   = '/etc/ocl.yml'
    @logger        = logger

    @devices       = find_devices
    @loaded_config = load_config

    terrapin_config = { logger: @logger }
    @cmdlines = {
      'fan'  => Terrapin::CommandLine.new(@binary_path, '--device :device_id --fan  channel=:channel,mode=:mode', terrapin_config),
      'pump' => Terrapin::CommandLine.new(@binary_path, '--device :device_id --pump mode=:mode',                  terrapin_config),
      'led'  => Terrapin::CommandLine.new(@binary_path, '--device :device_id --led  channel=:channel,mode=:mode', terrapin_config),
    }
  end

  def run
    @loaded_config['devices'].each do |device_name, device_config|

      device_id = @devices[device_name]
      if not(device_id)
        bail("Can not find device ID for #{device_name}")
      end

      handle_device(device_id, device_name, device_config)
    end
  end

  private
  def handle_device(device_id, device_name, device_config)
    base_subconfig = {device_id: device_id}

    device_config.each do |section, subconfig|

      section_cmdline = @cmdlines[section]
      if not(section_cmdline)
        bail("Unknown section '#{section}' found. Subconfig: #{subconfig.inspect}")
      end

      if subconfig.is_a? Hash
        subconfig.merge!(base_subconfig)
        cmdline(section_cmdline, subconfig)
      elsif subconfig.is_a? Array
        subconfig.each do |config|
          config.merge!(base_subconfig)
          cmdline(section_cmdline, config)
        end
      else
        bail("Do not know how to handle #{subconfig.class}")
      end

    end
  end

  def cmdline(section_cmdline, config)
    expanded_cmdline = section_cmdline.command(config)

    # TODO:
    # so terrapin doesnt seems to have a way to handle optional arguments in the string.
    # so we might have to manually construct the argument string for Terrapin::Commandline.new
    # based on the parameters in the hash, but then we could maybe use the ruby shellquote function
    # directly and get rid of Terrapin all together
    # https://github.com/thoughtbot/terrapin/blob/master/lib/terrapin/command_line.rb#L179

    if @dryrun
      @logger.info("DryRun: #{expanded_cmdline}")
    else
      # TODO: So it seems this thing never errors out and we need to parse the output. ugh.
      begin
        output = section_cmdline.run(config)
        @logger.debug(output)
      rescue Terrapin::ExitStatusError => e
        @logger.error(e.message)
      end
    end
  end

  def load_config
    # TODO: error handling
    # TODO: validate config:
    # LED:
    # --led channel=N,mode=N,colors=HHHHHH:HHHHHH:HHHHHH,temp=TEMP:TEMP:TEMP
    #   Channel: <led number> :Selects a led channel to setup. Accepted values are 1 or 2.
    #   Mode:
    #      0 - Static
    #      1 - Blink (Only Commander Pro and Asetek Pro)
    #      2 - Color Pulse (Only Commander Pro and Asetek Pro)
    #      3 - Color Shift (Only Commander Pro and Asetek Pro)
    #      4 - Rainbow (Only Commander Pro and Asetek Pro)
    #      5 - Temperature (Only Commander Pro, Asetek, and Asetek Pro)
    #   Colors: <HTML Color Code>     :Define Color for LED.
    #   Warn: <HTML Color Code>   :Define Color for Warning Temp.
    #   Temp: <Temperature in Celsius>  :Define Warning Temperature.
    # Fan:
    # --fan channel=N,mode=N,pwm=PWM,rpm=RPM,temps=TEMP:TEMP:TEMP,speeds=SPEED:SPEED:SPEED
    #   Channel: <fan number> :Selects a fan to setup. Accepted values are 1, 2, 3 or 4.
    #   Modes:
    #      0 - Fixed PWM (requires to specify the PWM)
    #      1 - Fixed RPM (requires to specify the RPM)
    #      2 - Default
    #      3 - Quiet
    #      4 - Balanced
    #      5 - Performance
    #      6 - Custom Curve
    #   PWM <PWM Percent>   :The desired PWM for the selected fan. NOTE: it only works when fan mode is set to Fixed PWM
    #   RPM <fan RPM>   :The desired RPM for the selected fan. NOTE: it works only when fan mode is set to Fixed RPM
    #   For Custom Curves:
    #     Temps <C> :Define Celsius Temperatures for Fan.
    #     Speeds <Percentage> :Define Values of RPM Percentage for Fan.
    # Pump:
    # --pump mode=<mode>
    #   Modes:
    #      3 - Quiet
    #      5 - Performance
    # Without options, OpenCorsairLink will show the status of any detected Corsair Link device.

    YAML.safe_load(File.read(@config_path))
  end

  def find_devices
    devices = {}
    device_regexp = /Dev=(?<device_id>\d+), CorsairLink Device Found: (?<device_name>.*)!/

    IO.popen (@binary_path) do |process|
      process.each_line do |l|
        if mo=device_regexp.match(l)
          devices[mo[:device_name]] = mo[:device_id]
        end
      end
    end

    if devices.empty?
      msg = "Did not find any devices."
      if Process.euid != 0
        msg += " You have to run this as root."
      end
      bail(msg)
    end

    devices
  end

  def logger
    new_logger = Logger.new(STDERR)
    new_logger.level = Logger::INFO
    new_logger
  end

  def bail(errorcode=1, msg)
    @logger.error(msg)
    @logger.error('Exiting ...')
    exit(errorcode)
  end
end

ocl = OpenCorsairLink.new
ocl.run
