require_relative 'slp/functions'
require_relative 'slp/constants'
require_relative 'slp/structs'
require_relative 'slp/helper'

module OpenSLP
  class SLP
    include OpenSLP::Functions
    include OpenSLP::Constants
    include OpenSLP::Structs
    extend OpenSLP::Functions

    # The version of the rslp library.
    VERSION = '0.0.1'.freeze

    # The language tag for requests.
    attr_reader :lang

    # Returns whether or not the SLP instance is asynchronous.
    attr_reader :async

    # Creates and returns an OpenSLP::SLP object. This establishes a handle
    # that is used for the life of the object.
    #
    # The +lang+ argument may be an RFC 1766 language tag, e.g. 'en-us', that
    # sets the natural language local for requests. By default it uses your
    # machine's locale.
    #
    # The +async+ argument may be set to true or false and establishes whether
    # the underlying handle is set to handl asynchronous operations or not. By
    # default this value is false.
    #
    # If a block is given, then the object itself is yielded to the block, and
    # it is automatically closed at the end of the block.
    #
    # Examples:
    #
    #   OpenSLP::SLP.new('en-us') do |slp|
    #     # ... do stuff
    #   end
    #
    #   slp = OpenSLP::SLP.new('en-us')
    #   # Do stuff
    #   slp.close
    #
    def initialize(lang = '', async = false)
      @lang = lang
      @async = async

      ptr = FFI::MemoryPointer.new(:ulong)

      if SLPOpen(lang, async, ptr) != SLP_OK
        raise SystemCallError.new('SLPOpen', FFI.errno)
      end

      @handle = ptr.read_ulong

      if block_given?
        begin
          yield self
        ensure
          close
        end
      end
    end

    # Close the OpenSLP::SLP object. The block form of the constructor will
    # automatically call this for you.
    #
    def close
      SLPClose(@handle)
    end

    # Register a :url for the given :lifetime. You may also specify a hash
    # of :attributes, specify whether or not this is a :fresh service.
    #
    # The lifetime defaults to SLP_LIFETIME_DEFAULT.
    #
    # The attributes defaults to an empty string. Note that the has you provide
    # automatically converts the attributes into the appropriate format.
    #
    # By default it will be considered a fresh service.
    #
    # Returns the url if successful.
    #
    def register(options = {})
      options[:lifetime] ||= SLP_LIFETIME_DEFAULT
      options[:attributes] ||= ""
      options[:fresh] ||= true

      options[:callback] ||= Proc.new{ |hslp, err, cookie| }

      if options[:attributes] && options[:attributes] != ""
        attributes = options[:attributes].map{ |k,v| "(#{k}=#{v})" }.join(',')
      else
        attributes = ""
      end

      cookie = FFI::MemoryPointer.new(:void)

      result = SLPReg(
        @handle,
        options[:url],
        options[:lifetime],
        nil,
        attributes,
        options[:fresh],
        options[:callback],
        cookie
      )

      raise SystemCallError.new('SLPReg', result) if result != SLP_OK

      options[:url]
    end

    # Deregisters the advertisement for +url+ in all scopes where the service
    # is registered and all language locales.
    #
    def deregister(url)
      callback = Proc.new{ |hslp, err, cookie| }
      cookie = FFI::MemoryPointer.new(:void)

      result = SLPDereg(@handle, url, callback, cookie)
      raise SystemCallError.new('SLPDereg', result) if result != SLP_OK

      true
    end

    # Deletes specified attributes from a registered service. The attributes
    # argument should be a comma separated list (in string form) of attribute
    # ids.
    #
    # Returns the list of deleted attributes if successful.
    #
    def delete_service_attributes(url, attributes)
      callback = Proc.new{ |hslp, err, cookie| }
      cookie = FFI::MemoryPointer.new(:void)

      result = SLPDelAttrs(@handle, url, attributes, callback, cookie)
      raise SystemCallError.new('SLPDelAttrs', result) if result != SLP_OK

      attributes
    end

    # Returns a list of scopes. The most desirable values are always first in
    # the list. There is always at least one value ("DEFAULT") in the list.
    #
    def find_scopes
      begin
        pptr = FFI::MemoryPointer.new(:pointer, 128)

        rv = SLPFindScopes(@handle, pptr)

        if rv != SLP_OK
          raise SystemCallError.new('SLPFindScopes', rv)
        end

        arr = pptr.read_array_of_string
      ensure
        SLPFree(pptr.read_pointer)
        pptr.free
      end

      arr
    end

    # Issue a query for services.
    #
    # The +type+ argument is the service type string, including the authority
    # string, for the request. It may not be nil or empty.
    #
    # The +scope+ argument is a comma separated list of scope names. May be an
    # empty string if you wish to use scopes this machine is configured for.
    #
    # A query formulated of attribute pattern matching expressions in the
    # form of an LDAP search filter. The default is an empty string, which
    # will gather all services of the requested type.
    #
    def find_services(type, scope = '', filter = '')
      arr = []

      callback = Proc.new{ |hslp, url, life, err, cook|
        if err == SLP_OK
          arr << {url => life}
          true
        else
          false
        end
      }

      cookie = FFI::MemoryPointer.new(:void)

      rv = SLPFindSrvs(@handle, type, scope, filter, callback, cookie)

      if rv != SLP_OK
        raise SystemCallError.new('SLPFindSrvs', rv)
      end

      arr
    end

    # The find_service_types method issues an SLP service type request for
    # service types indicated by the scope parameter, limited by the naming
    # authority +auth+.
    #
    # The default naming authority is '*', i.e. all naming authorities. A
    # blank string will result in the default naming authority of IANA.
    #
    # A blank +scope+ parameter will use scopes this machine is configured for.
    #
    # Returns an array of service type names.
    #
    def find_service_types(auth = '*', scope = '')
      arr = []

      callback = Proc.new{ |hslp, types, err, cookie|
        if err == SLP_OK
          arr << types
          true
        else
          false
        end
      }

      cookie = FFI::MemoryPointer.new(:void)

      rv = SLPFindSrvTypes(@handle, auth, scope, callback, cookie)

      if rv != SLP_OK
        raise SystemCallError.new('SLPFindSrvs', rv)
      end

      arr
    end

    # Return a list of service attributes matching the +attrs+ for the
    # indicated service +url+ using the given +scope+.
    #
    # A blank +scope+ parameter will use scopes this machine is configured for.
    #
    def find_service_attributes(url, attrs = '', scope = '')
      arr = []

      callback = Proc.new{ |hslp, attrlist, err, cookie|
        if err == SLP_OK
          arr << attrlist
          true
        else
          false
        end
      }

      cookie = FFI::MemoryPointer.new(:void)

      rv = SLPFindAttrs(@handle, url, scope, attrs, callback, cookie)

      if rv != SLP_OK
        raise SystemCallError.new('SLPFindSrvs', rv)
      end

      arr
    end

    # Singleton methods

    # Returns the minimum refresh interval that will be accepted by all SLP
    # agents on the network. If no agents advertise a minimum refresh interval
    # then zero is returned, though that is not a valid lifetime parameter to
    # other methods.
    #
    def self.refresh_interval
      SLPGetRefreshInterval()
    end

    # Returns the value of the SLP property +name+.
    #
    def self.get_property(name)
      SLPGetProperty(name)
    end

    # Sets the value of the SLP property +prop+ to +name+.
    #
    def self.set_property(prop, name)
      SLPSetProperty(prop, name)
      name
    end

    # Parses a Service URL string and returns SLPSrvURL struct.
    #
    def self.parse_service_url(url)
      begin
        pptr = FFI::MemoryPointer.new(SLPSrvURL)

        if SLPParseSrvURL(url, pptr) != SLP_OK
          raise SystemCallError.new('SLPParseSrvURL', FFI.errno)
        end

        ptr = pptr.read_pointer
        struct = SLPSrvURL.new(ptr)
      ensure
        SLPFree(ptr)
      end

      struct
    end

    # Process the +string+ to escape any SLP reserved characters. If the
    # +istag_ parameter is true then it will look for bad tag characters.
    #
    def self.escape_reserved(string, istag = true)
      begin
        pptr = FFI::MemoryPointer.new(:pointer)

        if SLPEscape(string, pptr, istag) != SLP_OK
          raise SystemCallError.new('SLPEscape', FFI.errno)
        end

        str = pptr.read_pointer.read_string
      ensure
        SLPFree(pptr.read_pointer)
      end

      str
    end

    # Process the +string+ to unescape any SLP reserved characters. If the
    # +istag_ parameter is true then it will look for bad tag characters.
    #
    def self.unescape_reserved(string, istag = true)
      begin
        pptr = FFI::MemoryPointer.new(:pointer)

        if SLPUnescape(string, pptr, istag) != SLP_OK
          raise SystemCallError.new('SLPEscape', FFI.errno)
        end

        str = pptr.read_pointer.read_string
      ensure
        SLPFree(pptr.read_pointer)
      end

      str
    end
  end
end
