# 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/>.

require 'libusb/call'

module LIBUSB
  # A structure representing the Binary Device Object Store (BOS) descriptor.
  # This descriptor is documented in section 9.6.2 of the USB 3.0 specification.
  # All multiple-byte fields are represented in host-endian format.
  class Bos < FFI::Struct

    module GenericMethods
      # @return [Integer]  Size of this descriptor (in bytes)
      def bLength
        self[:bLength]
      end

      # @return [Integer]  Descriptor type. Will have value LIBUSB::DT_DEVICE_CAPABILITY
      # in this context.
      def bDescriptorType
        self[:bDescriptorType]
      end

      # @return [Integer] Device Capability type
      def bDevCapabilityType
        self[:bDevCapabilityType]
      end

      def inspect
        "\#<#{self.class} cap: #{bDevCapabilityType} data: #{dev_capability_data.unpack1("H*")}>"
      end

      # @return [String]  Device Capability data (bLength - 3 bytes)
      def dev_capability_data
        pointer.read_bytes(bLength - 3)
      end
    end

    # A generic representation of a BOS Device Capability descriptor.
    class DeviceCapability < FFI::Struct
      include GenericMethods

      layout :bLength, :uint8,
          :bDescriptorType, :uint8,
          :bDevCapabilityType, :uint8

      def initialize( bos, *args)
        # Avoid that the bos struct is GC'ed before this instance
        @bos = bos
        super(*args)
      end
    end

    # A structure representing the USB 2.0 Extension descriptor
    # This descriptor is documented in section 9.6.2.1 of the USB 3.0 specification.
    # All multiple-byte fields are represented in host-endian format.
    class Usb20Extension < FFI::Struct
      include GenericMethods
      include ContextReference

      layout :bLength, :uint8,
          :bDescriptorType, :uint8,
          :bDevCapabilityType, :uint8,
          :bmAttributes, :uint32

      def initialize(ctx, *args)
        super(*args)

        register_context(ctx, :libusb_free_usb_2_0_extension_descriptor)
      end

      # Bitmap encoding of supported device level features.
      # A value of one in a bit location indicates a feature is
      # supported; a value of zero indicates it is not supported.
      # @see Call::Usb20ExtensionAttributes
      def bmAttributes
        self[:bmAttributes]
      end

      # @return [Boolean] Supports Link Power Management (LPM)
      def bm_lpm_support?
        (bmAttributes & BM_LPM_SUPPORT) != 0
      end

      def inspect
        attrs = Call::Usb20ExtensionAttributes.to_h.map do |k, v|
          (bmAttributes & v) ? k.to_s : nil
        end
        "\#<#{self.class} #{attrs.compact.join(",")}>"
      end
    end

    # A structure representing the SuperSpeed USB Device Capability descriptor
    # This descriptor is documented in section 9.6.2.2 of the USB 3.0 specification.
    # All multiple-byte fields are represented in host-endian format.
    class SsUsbDeviceCapability < FFI::Struct
      include GenericMethods
      include ContextReference

      layout :bLength, :uint8,
          :bDescriptorType, :uint8,
          :bDevCapabilityType, :uint8,
          :bmAttributes, :uint8,
          :wSpeedSupported, :uint16,
          :bFunctionalitySupport, :uint8,
          :bU1DevExitLat, :uint8,
          :bU2DevExitLat, :uint16

      def initialize(ctx, *args)
        super(*args)

        register_context(ctx, :libusb_free_ss_usb_device_capability_descriptor)
      end

      # Bitmap encoding of supported device level features.
      # A value of one in a bit location indicates a feature is
      # supported; a value of zero indicates it is not supported.
      #
      # @return [Integer]
      # @see Call::SsUsbDeviceCapabilityAttributes
      def bmAttributes
        self[:bmAttributes]
      end

      # @return [Boolean] Supports Latency Tolerance Messages (LTM)
      def bm_ltm_support?
        (bmAttributes & BM_LTM_SUPPORT) != 0
      end

      def inspect
        attrs = Call::SsUsbDeviceCapabilityAttributes.to_h.map do |k,v|
          (bmAttributes & v) != 0 ? k.to_s : nil
        end
        "\#<#{self.class} #{attrs.compact.join(",")} #{supported_speeds.join(",")}>"
      end

      # Bitmap encoding of the speed supported by this device when
      # operating in SuperSpeed mode.
      #
      # @return [Integer]
      # @see Call::SupportedSpeeds
      def wSpeedSupported
        self[:wSpeedSupported]
      end

      # @return [Array<Symbol>]  speeds supported by this device when
      #     operating in SuperSpeed mode {Call::SupportedSpeeds}
      def supported_speeds
        speeds = Call::SupportedSpeeds.to_h.map do |k,v|
          (wSpeedSupported & v) != 0 ? k : nil
        end
        speeds.compact
      end

      # The lowest speed at which all the functionality supported
      # by the device is available to the user. For example if the
      # device supports all its functionality when connected at
      # full speed and above then it sets this value to 1.
      #
      # 0 - low speed
      # 1 - full speed
      # 2 - high speed
      # 3 - super speed
      # @return [Integer]
      def bFunctionalitySupport
        self[:bFunctionalitySupport]
      end

      # @return [Integer]  U1 Device Exit Latency.
      def bU1DevExitLat
        self[:bU1DevExitLat]
      end

      # @return [Integer]  U2 Device Exit Latency.
      def bU2DevExitLat
        self[:bU2DevExitLat]
      end
    end

    # A structure representing the Container ID descriptor.
    # This descriptor is documented in section 9.6.2.3 of the USB 3.0 specification.
    # All multiple-byte fields, except UUIDs, are represented in host-endian format.
    class ContainerId < FFI::Struct
      include GenericMethods
      include ContextReference

      layout :bLength, :uint8,
          :bDescriptorType, :uint8,
          :bDevCapabilityType, :uint8,
          :bReserved, :uint8,
          :ContainerID, [:uint8, 16]

      def initialize(ctx, *args)
        super(*args)

        register_context(ctx, :libusb_free_container_id_descriptor)
      end

      # Reserved field
      def bReserved
        self[:bReserved]
      end

      # @return [String] 128 bit UUID
      def container_id
        self[:ContainerID].to_ptr.read_bytes(16)
      end

      def inspect
        "\#<#{self.class} #{container_id.unpack1("H*")}>"
      end
    end


    # A structure representing a Platform descriptor.
    # This descriptor is documented in section 9.6.2.4 of the USB 3.2 specification.
    class PlatformDescriptor < FFI::Struct
      include GenericMethods
      include ContextReference

      layout :bLength, :uint8,
          :bDescriptorType, :uint8,
          # Capability type. Will have value
          # libusb_capability_type::LIBUSB_BT_PLATFORM_DESCRIPTOR
          # LIBUSB_BT_CONTAINER_ID in this context.
          :bDevCapabilityType, :uint8,
          # Reserved field
          :bReserved, :uint8,
          # 128 bit UUID
          :PlatformCapabilityUUID, [:uint8, 16],
          # Capability data (bLength - 20)
          :CapabilityData, [:uint8, 0]

      def initialize(ctx, *args)
        super(*args)

        register_context(ctx, :libusb_free_platform_descriptor)
      end

      # Reserved field
      def bReserved
        self[:bReserved]
      end

      # @return [String] 128 bit UUID
      def platformCapabilityUUID
        self[:PlatformCapabilityUUID].to_ptr.read_bytes(16)
      end

      # This is a variable-length field containing data associated with the platform specific capability.
      # This field may be zero bytes in length.
      # @return [String]
      def capabilityData
        self[:CapabilityData].to_ptr.read_bytes(bLength - 20)
      end

      def inspect
        "\#<#{self.class} #{platformCapabilityUUID.unpack1("H*")} (#{capabilityData.unpack1("H*")})>"
      end
    end

    include ContextReference

    def initialize(ctx, *args)
      @ctx = ctx
      super(*args)

      register_context(ctx, :libusb_free_bos_descriptor)
    end

    layout :bLength, :uint8,
        :bDescriptorType, :uint8,
        :wTotalLength, :uint16,
        :bNumDeviceCaps, :uint8,
        :dev_capability, [:pointer, 0]

    # @return [Integer]  Size of this descriptor (in bytes)
    def bLength
      self[:bLength]
    end

    # @return [Integer]  Descriptor type. Will have value LIBUSB::DT_BOS LIBUSB_DT_BOS
    # in this context.
    def bDescriptorType
      self[:bDescriptorType]
    end

    # @return [Integer]  Length of this descriptor and all of its sub descriptors
    def wTotalLength
      self[:wTotalLength]
    end

    # @return [Integer]  The number of separate device capability descriptors in
    # the BOS
    def bNumDeviceCaps
      self[:bNumDeviceCaps]
    end

    # bNumDeviceCap Device Capability Descriptors
    #
    # @return [Array<Bos::DeviceCapability, Bos::Usb20Extension, Bos::SsUsbDeviceCapability, Bos::ContainerId>]
    def device_capabilities
      pp_ext = FFI::MemoryPointer.new :pointer
      caps = []
      # Capabilities are appended to the bos header
      ptr = pointer + offset_of(:dev_capability)
      bNumDeviceCaps.times do
        cap = DeviceCapability.new self, ptr.read_pointer
        case cap.bDevCapabilityType
          when LIBUSB::BT_WIRELESS_USB_DEVICE_CAPABILITY
            # no struct defined in libusb -> use generic DeviceCapability
          when LIBUSB::BT_USB_2_0_EXTENSION
            res = Call.libusb_get_usb_2_0_extension_descriptor(@ctx, cap.pointer, pp_ext)
            cap = Usb20Extension.new(@ctx, pp_ext.read_pointer) if res==0
          when LIBUSB::BT_SS_USB_DEVICE_CAPABILITY
            res = Call.libusb_get_ss_usb_device_capability_descriptor(@ctx, cap.pointer, pp_ext)
            cap = SsUsbDeviceCapability.new(@ctx, pp_ext.read_pointer) if res==0
          when LIBUSB::BT_CONTAINER_ID
            res = Call.libusb_get_container_id_descriptor(@ctx, cap.pointer, pp_ext)
            cap = ContainerId.new(@ctx, pp_ext.read_pointer) if res==0
          when LIBUSB::BT_PLATFORM_DESCRIPTOR
            res = Call.libusb_get_platform_descriptor(@ctx, cap.pointer, pp_ext)
            cap = PlatformDescriptor.new(@ctx, pp_ext.read_pointer) if res==0
          else
            # unknown capability -> use generic DeviceCapability
        end
        ptr += FFI.type_size(:pointer)
        caps << cap
      end
      caps
    end

    # @return [Array<Symbol>]  Types of Capabilities
    #
    # @see Call::BosTypes
    def device_capability_types
      # Capabilities are appended to the bos header
      ptr = pointer + offset_of(:dev_capability)
      bNumDeviceCaps.times.map do
        cap = DeviceCapability.new self, ptr.read_pointer
        ptr += FFI.type_size(:pointer)
        Call::BosTypes.find cap.bDevCapabilityType
      end
    end

    def inspect
      "\#<#{self.class} #{device_capability_types.join(", ")}>"
    end
  end
end
