# For more information on what the possible values of fields that are passed to the RETS server can be, see {http://www.rets.org/documentation}.
module RETS
  module Base
    class Core
      GET_OBJECT_DATA = ["object-id", "description", "content-id", "content-description", "location", "content-type", "preferred"]

      # Can be called after any {RETS::Base::Core} call that hits the RETS Server.
      # @return [String] How big the request was
      attr_reader :request_size

      # Can be called after any {RETS::Base::Core} call that hits the RETS Server.
      # @return [String] SHA1 hash of the request
      attr_reader :request_hash

      # Can be called after any {#get_object} or {#search} call that hits the RETS Server.
      # @return [Float] How long the request took
      attr_reader :request_time

      # Can be called after any {RETS::Base::Core} call that hits the RETS Server.
      # @return [Hash]
      #   Gives access to the miscellaneous RETS data, such as reply text, code, delimiter, count and so on depending on the API call made.
      #   * *text* (String) - Reply text from the server
      #   * *code* (String) - Reply code from the server
      attr_reader :rets_data

      def initialize(http, urls)
        @http = http
        @urls = urls
      end

      ##
      # Attempts to logout of the RETS server.
      #
      # @raise [RETS::CapabilityNotFound]
      # @raise [RETS::APIError]
      # @raise [RETS::HTTPError]
      def logout
        unless @urls[:logout]
          raise RETS::CapabilityNotFound.new("No Logout capability found for given user.")
        end

        @http.request(:url => @urls[:logout])

        nil
      end

      ##
      # Whether the RETS server has the requested capability.
      #
      # @param [Symbol] type Lowercase of the capability, "getmetadata", "getobject" and so on
      # @return [Boolean]
      def has_capability?(type)
        @urls.has_key?(type)
      end

      ##
      # Requests metadata from the RETS server.
      #
      # @param [Hash] args
      # @option args [String] :type Metadata to request, the same value if you were manually making the request, "METADATA-SYSTEM", "METADATA-CLASS" and so on
      # @option args [String] :id Filter the data returned by ID, "*" would return all available data
      # @option args [Integer, Optional] :read_timeout How many seconds to wait before giving up
      #
      # @yield For every group of metadata downloaded
      # @yieldparam [String] :type Type of data that was parsed with "METADATA-" stripped out, for "METADATA-SYSTEM" this will be "SYSTEM"
      # @yieldparam [Hash] :attrs Attributes of the data, generally *Version*, *Date* and *Resource* but can vary depending on what metadata you requested
      # @yieldparam [Array] :metadata Array of hashes with metadata info
      #
      # @raise [RETS::CapabilityNotFound]
      # @raise [RETS::APIError]
      # @raise [RETS::HTTPError]
      # @see #rets_data
      # @see #request_size
      # @see #request_hash
      def get_metadata(args, &block)
        raise ArgumentError, "No block passed" unless block_given?

        unless @urls[:getmetadata]
          raise RETS::CapabilityNotFound.new("No GetMetadata capability found for given user.")
        end

        @request_size, @request_hash, @request_time, @rets_data = nil, nil, nil, nil
        @http.request(:url => @urls[:getmetadata], :read_timeout => args[:read_timeout], :params => {:Format => :COMPACT, :Type => args[:type], :ID => args[:id]}) do |response|
          stream = RETS::StreamHTTP.new(response)
          sax = RETS::Base::SAXMetadata.new(block)

          Nokogiri::XML::SAX::Parser.new(sax).parse_io(stream)

          @request_size, @request_hash = stream.size, stream.hash
          @rets_data = sax.rets_data
        end

        nil
      end

      ##
      # Requests an object from the RETS server.
      #
      # @param [Hash] args
      # @option args [String] :resource Resource to load, typically *Property*
      # @option args [String] :type Type of object you want, usually *Photo*
      # @option args [String] :id What objects to return
      # @option args [Array, Optional] :accept Array of MIME types to accept, by default this is *image/png*, *image/gif* and *image/jpeg*
      # @option args [Boolean, Optional] :location Return the location of the object rather than the contents of it
      # @option args [Integer, Optional] :read_timeout How many seconds to wait before timing out
      #
      # @yield For every object downloaded
      # @yieldparam [Hash] :headers Object headers
      #     * *object-id* (String) - Objects ID
      #     * *content-id* (String) - Content ID
      #     * *content-type* (String) - MIME type of the content
      #     * *description* (String, Optional) - A description of the object
      #     * *location* (String, Optional) - Where the file is located, only returned is *location* is true
      # @yieldparam [String, Optional] :content Content for the object, not called when *location* is set
      #
      # @raise [RETS::CapabilityNotFound]
      # @raise [RETS::APIError]
      # @raise [RETS::HTTPError]
      # @see #rets_data
      # @see #request_size
      # @see #request_hash
      def get_object(args, &block)
        raise ArgumentError, "No block passed" unless block_given?

        unless @urls[:getobject]
          raise RETS::CapabilityNotFound.new("No GetObject capability found for given user.")
        end

        req = {:url => @urls[:getobject], :read_timeout => args[:read_timeout], :headers => {}}
        req[:params] = {:Resource => args[:resource], :Type => args[:type], :Location => (args[:location] ? 1 : 0), :ID => args[:id]}
        if args[:accept].is_a?(Array)
          req[:headers]["Accept"] = args[:accept].join(",")
        else
          req[:headers]["Accept"] = "image/png,image/gif,image/jpeg"
        end

        # Will get swapped to a streaming call rather than a download-and-parse later, easy to do as it's called with a block now
        start = Time.now.utc.to_f

        @request_size, @request_hash, @request_time, @rets_data = nil, nil, nil, nil
        @http.request(req) do |response|
          body = response.read_body

          @request_time = Time.now.utc.to_f - start
          @request_size, @request_hash = body.length, Digest::SHA1.hexdigest(body)

          # Make sure we aren't erroring
          if body =~ /(<RETS (.+)\>)/
            # RETSIQ (and probably others) return a <RETS> tag on a location request without any error inside
            # since parsing errors out of full image data calls is a tricky pain. We're going to keep the
            # loose error checking, but will confirm that it has an actual error code
            code, text = @http.get_rets_response(Nokogiri::XML($1).xpath("//RETS").first)
            unless code == "0"
              @rets_data = {:code => code, :text => text}

              if code == "20403"
                return
              else
                raise RETS::APIError.new("#{code}: #{text}", code, text)
              end
            end
          end

          # Using a wildcard somewhere
          if response.content_type == "multipart/parallel"
            boundary = response.type_params["boundary"]
            boundary.gsub!(/^"|"$/, "")

            parts = body.split("--#{boundary}\r\n")
            parts.last.gsub!("\r\n--#{boundary}--", "")
            parts.each do |part|
              part.strip!
              next if part == ""

              headers, content = part.split("\r\n\r\n", 2)

              parsed_headers = {}
              headers.split("\r\n").each do |line|
                name, value = line.split(":", 2)
                next unless value and value != ""

                parsed_headers[name.downcase] = value.strip
              end

              # Check off the first children because some Rap Rets seems to use RETS-Status
              # and it will include it with an object while returning actual data.
              # It only does this for multipart requests, single image pulls will use <RETS> like it should.
              if parsed_headers["content-type"] == "text/xml"
                code, text = @http.get_rets_response(Nokogiri::XML(content).children.first)
                next if code == "20403"
              end

              if block.arity == 1
                yield parsed_headers
              else
                yield parsed_headers, content
              end

            end

          # Either text (error) or an image of some sorts, which is irrelevant for this
          else
            headers = {}
            GET_OBJECT_DATA.each do |field|
              next unless response.header[field] and response.header[field] != ""
              headers[field] = response.header[field].strip
            end

            if block.arity == 1
              yield headers
            else
              yield headers, body
            end
          end
        end

        nil
      end

      ##
      # Searches the RETS server for data.
      #
      # @param [Hash] args
      # @option args [String] :search_type What to search on, typically *Property*, *Office* or *Agent*
      # @option args [String] :class What class of data to return, varies between RETS implementations and can be anything from *1* to *ResidentialProperty*
      # @option args [String] :query How to filter data, should be unescaped as CGI::escape will be called on the string
      # @option args [Symbol, Optional] :count_mode Either *:only* to return just the total records found or *:both* to get count and records returned
      # @option args [Integer, Optional] :limit Limit total records returned
      # @option args [Integer, Optional] :offset Offset to start returning records from
      # @option args [Array, Optional] :select Restrict the fields the RETS server returns
      # @option args [Boolean, Optional] :standard_names Whether to use standard names for all fields
      # @option args [String, Optional] :restricted String to show in place of a field value for any restricted fields the user cannot see
      # @option args [Integer, Optional] :read_timeout How long to wait for data from the socket before giving up
      # @option args [Boolean, Optional] :disable_stream Disables the streaming setup for data and instead loads it all and then parses
      #
      # @yield Called for every <DATA></DATA> group from the RETS server
      # @yieldparam [Hash] :data One record of data from the RETS server
      #
      # @raise [RETS::CapabilityNotFound]
      # @raise [RETS::APIError]
      # @raise [RETS::HTTPError]
      # @see #rets_data
      # @see #request_size
      # @see #request_hash
      def search(args, &block)
        if !block_given? and args[:count_mode] != :only
          raise ArgumentError, "No block found"
        end

        unless @urls[:search]
          raise RETS::CapabilityNotFound.new("Cannot find URL for Search call")
        end

        req = {:url => @urls[:search], :read_timeout => args[:read_timeout]}
        req[:params] = {:Format => "COMPACT-DECODED", :SearchType => args[:search_type], :QueryType => "DMQL2", :Query => args[:query], :Class => args[:class], :Limit => args[:limit], :Offset => args[:offset], :RestrictedIndicator => args[:restricted]}
        req[:params][:Select] = args[:select].join(",") if args[:select].is_a?(Array)
        req[:params][:StandardNames] = 1 if args[:standard_names]

        if args[:count] == 1
          req[:params][:Count] = 2
        elsif args[:count_mode] == :both
          req[:params][:Count] = 1
        end

        @request_size, @request_hash, @request_time, @rets_data = nil, nil, nil, {}

        start = Time.now.utc.to_f
        @http.request(req) do |response|
          if args[:disable_stream]
            stream = StringIO.new(response.body)
            @request_time = Time.now.utc.to_f - start
          else
            stream = RETS::StreamHTTP.new(response)
          end

          sax = RETS::Base::SAXSearch.new(@rets_data, block)
          Nokogiri::XML::SAX::Parser.new(sax).parse_io(stream)

          if args[:disable_stream]
            @request_size, @request_hash = response.body.length, Digest::SHA1.hexdigest(response.body)
          else
            @request_size, @request_hash = stream.size, stream.hash
          end
        end

        nil
      end
    end
  end
end