# This file is part of Libusb for Ruby.
#
# Libusb for Ruby is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Libusb for Ruby is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with Libusb for Ruby.  If not, see <http://www.gnu.org/licenses/>.
#
#   This file is for compatibility with ruby-usb and libusb-0.1.
#
#   Please visit the project website at http://github.com/larskanis/libusb
#   for support.

require 'libusb'
require 'forwardable'

# Compatibility layer for ruby-usb[http://www.a-k-r.org/ruby-usb/] (API based on libusb-0.1)
#
# This module provides some limited compatibility to ruby-usb.
#
# Usage example:
#   begin
#     require 'usb'
#   rescue LoadError
#     require 'libusb/compat'
#   end
#   p USB.devices => [#<USB::Device ...>]
#
# Known issues:
# * Exceptions are different between ruby-usb and libusb and they don't get converted
# * libusb-1.0 doesn't explicitly manage USB-buses, so only one Bus is used currently
module USB
  @default_context = nil
  def self.default_context
    @default_context ||= LIBUSB::Context.new
  end

  @default_bus = nil
  def self.default_bus
    @default_bus ||= Bus.new(default_context)
  end

  USB_CLASS_PER_INTERFACE = LIBUSB::CLASS_PER_INTERFACE
  USB_CLASS_AUDIO = LIBUSB::CLASS_AUDIO
  USB_CLASS_COMM = LIBUSB::CLASS_COMM
  USB_CLASS_HID = LIBUSB::CLASS_HID
  USB_CLASS_PRINTER = LIBUSB::CLASS_PRINTER
  USB_CLASS_PTP = LIBUSB::CLASS_PTP
  USB_CLASS_MASS_STORAGE = LIBUSB::CLASS_MASS_STORAGE
  USB_CLASS_HUB = LIBUSB::CLASS_HUB
  USB_CLASS_DATA = LIBUSB::CLASS_DATA
  USB_CLASS_VENDOR_SPEC = LIBUSB::CLASS_VENDOR_SPEC

  USB_DT_DEVICE = LIBUSB::DT_DEVICE
  USB_DT_CONFIG = LIBUSB::DT_CONFIG
  USB_DT_STRING = LIBUSB::DT_STRING
  USB_DT_INTERFACE = LIBUSB::DT_INTERFACE
  USB_DT_ENDPOINT = LIBUSB::DT_ENDPOINT
  USB_DT_HID = LIBUSB::DT_HID
  USB_DT_REPORT = LIBUSB::DT_REPORT
  USB_DT_PHYSICAL = LIBUSB::DT_PHYSICAL
  USB_DT_HUB = LIBUSB::DT_HUB
  USB_DT_DEVICE_SIZE = LIBUSB::DT_DEVICE_SIZE
  USB_DT_CONFIG_SIZE = LIBUSB::DT_CONFIG_SIZE
  USB_DT_INTERFACE_SIZE = LIBUSB::DT_INTERFACE_SIZE
  USB_DT_ENDPOINT_SIZE = LIBUSB::DT_ENDPOINT_SIZE
  USB_DT_ENDPOINT_AUDIO_SIZE = LIBUSB::DT_ENDPOINT_AUDIO_SIZE
  USB_DT_HUB_NONVAR_SIZE = LIBUSB::DT_HUB_NONVAR_SIZE

  USB_ENDPOINT_ADDRESS_MASK = LIBUSB::ENDPOINT_ADDRESS_MASK
  USB_ENDPOINT_DIR_MASK = LIBUSB::ENDPOINT_DIR_MASK
  USB_ENDPOINT_IN = LIBUSB::ENDPOINT_IN
  USB_ENDPOINT_OUT = LIBUSB::ENDPOINT_OUT

  USB_ENDPOINT_TYPE_MASK = LIBUSB::TRANSFER_TYPE_MASK
  USB_ENDPOINT_TYPE_CONTROL = LIBUSB::TRANSFER_TYPE_CONTROL
  USB_ENDPOINT_TYPE_ISOCHRONOUS = LIBUSB::TRANSFER_TYPE_ISOCHRONOUS
  USB_ENDPOINT_TYPE_BULK = LIBUSB::TRANSFER_TYPE_BULK
  USB_ENDPOINT_TYPE_INTERRUPT = LIBUSB::TRANSFER_TYPE_INTERRUPT

  USB_REQ_GET_STATUS = LIBUSB::REQUEST_GET_STATUS
  USB_REQ_CLEAR_FEATURE = LIBUSB::REQUEST_CLEAR_FEATURE
  USB_REQ_SET_FEATURE = LIBUSB::REQUEST_SET_FEATURE
  USB_REQ_SET_ADDRESS = LIBUSB::REQUEST_SET_ADDRESS
  USB_REQ_GET_DESCRIPTOR = LIBUSB::REQUEST_GET_DESCRIPTOR
  USB_REQ_SET_DESCRIPTOR = LIBUSB::REQUEST_SET_DESCRIPTOR
  USB_REQ_GET_CONFIGURATION = LIBUSB::REQUEST_GET_CONFIGURATION
  USB_REQ_SET_CONFIGURATION = LIBUSB::REQUEST_SET_CONFIGURATION
  USB_REQ_GET_INTERFACE = LIBUSB::REQUEST_GET_INTERFACE
  USB_REQ_SET_INTERFACE = LIBUSB::REQUEST_SET_INTERFACE
  USB_REQ_SYNCH_FRAME = LIBUSB::REQUEST_SYNCH_FRAME
  USB_TYPE_STANDARD = LIBUSB::REQUEST_TYPE_STANDARD
  USB_TYPE_CLASS = LIBUSB::REQUEST_TYPE_CLASS
  USB_TYPE_VENDOR = LIBUSB::REQUEST_TYPE_VENDOR
  USB_TYPE_RESERVED = LIBUSB::REQUEST_TYPE_RESERVED
  USB_RECIP_DEVICE = LIBUSB::RECIPIENT_DEVICE
  USB_RECIP_INTERFACE = LIBUSB::RECIPIENT_INTERFACE
  USB_RECIP_ENDPOINT = LIBUSB::RECIPIENT_ENDPOINT
  USB_RECIP_OTHER = LIBUSB::RECIPIENT_OTHER

  HAS_GET_DRIVER_NP = !FFI::Platform.windows?
  HAS_DETACH_KERNEL_DRIVER_NP = !FFI::Platform.windows?

  # not defined by libusb-1.0, but typical values are:
  USB_MAXENDPOINTS = 32
  USB_MAXINTERFACES = 32
  USB_MAXALTSETTING = 128
  USB_MAXCONFIG = 8


  def USB.busses
    [default_bus]
  end

  def USB.devices; default_context.devices.map{|c| Device.new(c) }; end
  def USB.configurations() USB.devices.map {|d| d.configurations }.flatten end
  def USB.interfaces() USB.configurations.map {|d| d.interfaces }.flatten end
  def USB.settings() USB.interfaces.map {|d| d.settings }.flatten end
  def USB.endpoints() USB.settings.map {|d| d.endpoints }.flatten end

  def USB.find_bus(n)
    default_bus
  end

  def USB.each_device_by_class(devclass, subclass=nil, protocol=nil)
    devs = default_context.devices bClass: devclass, bSubClass: subclass, bProtocol: protocol
    devs.each do |dev|
      yield Device.new(dev)
    end
    nil
  end

  class Bus
    def initialize(context)
      @ct = context
    end
    def devices
      @ct.devices.map{|d| Device.new(d) }
    end

    def configurations() self.devices.map{|d| d.configurations }.flatten end
    def interfaces() self.configurations.map {|d| d.interfaces }.flatten end
    def settings() self.interfaces.map {|d| d.settings }.flatten end
    def endpoints() self.settings.map {|d| d.endpoints }.flatten end

    def find_device(n)
      raise NotImplementedError
    end
  end

  def USB.dev_string(base_class, sub_class, protocol)
    LIBUSB.dev_string(base_class, sub_class, protocol)
  end

  class Device
    extend Forwardable
    include Comparable

    def initialize(dev)
      @dev = dev
    end

    def_delegators :@dev, :bLength, :bDescriptorType, :bcdUSB, :bDeviceClass,
        :bDeviceSubClass, :bDeviceProtocol, :bMaxPacketSize0, :idVendor, :idProduct,
        :bcdDevice, :iManufacturer, :iProduct, :iSerialNumber, :bNumConfigurations,
        :manufacturer, :product, :serial_number,
        :inspect

    def <=>(o)
      @dev<=>o.instance_variable_get(:@dev)
    end

    def open
      h = DevHandle.new(@dev.open)
      if block_given?
        begin
          yield h
        ensure
          h.usb_close
        end
      else
        h
      end
    end

    def bus; default_bus; end
    def configurations; @dev.configurations.map{|c| Configuration.new(c) }; end
    def interfaces; @dev.interfaces.map{|c| Interface.new(c) }; end
    def settings; @dev.settings.map{|c| Setting.new(c) }; end
    def endpoints; @dev.endpoints.map{|c| Endpoint.new(c) }; end
  end

  class Configuration
    extend Forwardable
    include Comparable

    def initialize(cd)
      @cd = cd
    end

    def_delegators :@cd, :bLength, :bDescriptorType, :wTotalLength, :bNumInterfaces,
        :bConfigurationValue, :iConfiguration, :bmAttributes, :bMaxPower,
        :inspect

    def <=>(o)
      @cd<=>o.instance_variable_get(:@cd)
    end

    def bus; default_bus; end
    def device() Device.new(@cd.device) end
    def interfaces; @cd.interfaces.map{|c| Interface.new(c) }; end
    def settings() self.interfaces.map {|d| d.settings }.flatten end
    def endpoints() self.settings.map {|d| d.endpoints }.flatten end
  end

  class Interface
    extend Forwardable
    include Comparable

    def initialize(i)
      @i = i
    end

    def_delegators :@i, :inspect

    def <=>(o)
      @i<=>o.instance_variable_get(:@i)
    end

    def bus() self.configuration.device.bus end
    def device() self.configuration.device end
    def configuration; Configuration.new(@i.configuration); end
    def settings; @i.alt_settings.map{|c| Setting.new(c) }; end
    def endpoints() self.settings.map {|d| d.endpoints }.flatten end
  end

  class Setting
    extend Forwardable
    include Comparable

    def initialize(id)
      @id = id
    end

    def_delegators :@id, :bLength, :bDescriptorType, :bInterfaceNumber, :bAlternateSetting,
        :bNumEndpoints, :bInterfaceClass, :bInterfaceSubClass, :bInterfaceProtocol,
        :iInterface, :inspect

    def <=>(o)
      @id<=>o.instance_variable_get(:@id)
    end

    def bus() self.interface.configuration.device.bus end
    def device() self.interface.configuration.device end
    def configuration() self.interface.configuration end
    def interface; Interface.new(@id.interface); end
    def endpoints() @id.endpoints.map {|d| Endpoint.new(d) }.flatten end
  end

  class Endpoint
    extend Forwardable
    include Comparable

    def initialize(ep)
      @ep = ep
    end

    def_delegators :@ep, :bLength, :bDescriptorType, :bEndpointAddress, :bmAttributes,
        :wMaxPacketSize, :bInterval, :bRefresh, :bSynchAddress,
        :inspect

    def <=>(o)
      @ep<=>o.instance_variable_get(:@ep)
    end

    def bus() self.setting.interface.configuration.device.bus end
    def device() self.setting.interface.configuration.device end
    def configuration() self.setting.interface.configuration end
    def interface() self.setting.interface end
    def setting; Setting.new(@ep.setting); end
  end

  class DevHandle
    def initialize(dev)
      @dev = dev
    end

    def usb_close; @dev.close; end
    def usb_set_configuration(c); @dev.configuration=c; end
    def usb_set_altinterface(c); @dev.set_interface_alt_setting=c; end
    def usb_clear_halt(c); @dev.clear_halt(c); end
    def usb_reset; @dev.reset_device; end
    def usb_claim_interface(c); @dev.claim_interface(c); end
    def usb_release_interface(c); @dev.release_interface(c); end
    def usb_get_string(index, langid, buffer)
      t = @dev.string_descriptor(index, langid)
      buffer[0, t.length] = t
      t.length
    end
    def usb_get_string_simple(index, buffer)
      t = @dev.string_descriptor_ascii(index)
      buffer[0, t.length] = t
      t.length
    end

    def usb_control_msg(requesttype, request, value, index, bytes, timeout)
      if requesttype&LIBUSB::ENDPOINT_IN != 0
        # transfer direction in
        res = @dev.control_transfer(bmRequestType: requesttype, bRequest: request,
            wValue: value, wIndex: index, dataIn: bytes.bytesize, timeout: timeout)
        bytes[0, res.bytesize] = res
        res.bytesize
      else
        # transfer direction out
        @dev.control_transfer(bmRequestType: requesttype, bRequest: request, wValue: value,
            wIndex: index, dataOut: bytes, timeout: timeout)
      end
    end

    def usb_bulk_write(endpoint, bytes, timeout)
      @dev.bulk_transfer(endpoint: endpoint, dataOut: bytes, timeout: timeout)
    end
    def usb_bulk_read(endpoint, bytes, timeout)
      res = @dev.bulk_transfer(endpoint: endpoint, dataIn: bytes.bytesize, timeout: timeout)
      bytes[0, res.bytesize] = res
      res.bytesize
    end

    def usb_interrupt_write(endpoint, bytes, timeout)
      @dev.interrupt_transfer(endpoint: endpoint, dataOut: bytes, timeout: timeout)
    end
    def usb_interrupt_read(endpoint, bytes, timeout)
      res = @dev.interrupt_transfer(endpoint: endpoint, dataIn: bytes.bytesize, timeout: timeout)
      bytes[0, res.bytesize] = res
      res.bytesize
    end

#   rb_define_method(rb_cUSB_DevHandle, "usb_get_descriptor", rusb_get_descriptor, 3);
#   rb_define_method(rb_cUSB_DevHandle, "usb_get_descriptor_by_endpoint", rusb_get_descriptor_by_endpoint, 4);

    if HAS_DETACH_KERNEL_DRIVER_NP
      def usb_detach_kernel_driver_np(interface, dummy=nil)
        @dev.detach_kernel_driver(interface)
      end
    end

    if HAS_GET_DRIVER_NP
      def usb_get_driver_np(interface, buffer)
        if @dev.kernel_driver_active?(interface)
          t = "unknown driver"
          buffer[0, t.length] = t
        else
          raise Errno::ENODATA, "No data available"
        end
        nil
      end
    end

    alias set_configuration usb_set_configuration
    alias set_altinterface usb_set_altinterface
    alias clear_halt usb_clear_halt
    alias claim_interface usb_claim_interface
    alias release_interface usb_release_interface

    def get_string_simple(index)
      @dev.string_descriptor_ascii(index)
    end
  end
end
