require "cgi"
require "multi_json"
require 'crack'
require 'crack/xml'

module Flexirest

  class Request
    include AttributeParsing
    include JsonAPIProxy
    include ActiveSupport::Inflector
    attr_accessor :post_params, :get_params, :url, :path, :headers, :method, :object, :body, :forced_url, :original_url, :retrying

    def initialize(method, object, params = {})
      @method                     = method
      @method[:options]           ||= {}
      @method[:options][:lazy]    ||= []
      @method[:options][:array]   ||= []
      @method[:options][:has_one] ||= {}
      @overridden_name            = @method[:options][:overridden_name]
      @object                     = object
      @response_delegate          = Flexirest::RequestDelegator.new(nil)
      @params                     = params
      @headers                    = HeadersList.new
      (@method[:options][:headers] || {}).each do |k,v|
        @headers[k] = v
      end
      @forced_url                 = nil
    end

    def object_is_class?
      !@object.respond_to?(:dirty?)
    end

    def class_name
      if object_is_class?
        @object.name
      else
        @object.class.name
      end
    end

    def original_object_class
      if object_is_class?
        @object
      else
        @object.class
      end
    end

    def base_url
      if object_is_class?
        url = @object.base_url
      else
        url = @object.class.base_url
      end
      if url.is_a?(Array)
        url = url.sample
      end
      url
    end

    def using_api_auth?
      if object_is_class?
        @object.using_api_auth?
      else
        @object.class.using_api_auth?
      end
    end

    def api_auth_access_id
      ret = nil
      if object_is_class?
        ret = @object.api_auth_access_id
        ret = ret.call if ret.respond_to?(:call)
      else
        ret = @object.class.api_auth_access_id
        ret = ret.call(@object) if ret.respond_to?(:call)
      end
      ret
    end

    def api_auth_secret_key
      ret = nil
      if object_is_class?
        ret = @object.api_auth_secret_key
        ret = ret.call if ret.respond_to?(:call)
      else
        ret = @object.class.api_auth_secret_key
        ret = ret.call(@object) if ret.respond_to?(:call)
      end
      ret
    end

    def api_auth_options
      if object_is_class?
        @object.api_auth_options
      else
        @object.class.api_auth_options
      end
    end

    def username
      ret = nil
      if object_is_class?
        ret = @object.username
        ret = ret.call if ret.respond_to?(:call)
      else
        ret = @object.class.username
        ret = ret.call(@object) if ret.respond_to?(:call)
      end
      ret
    end

    def password
      ret = nil
      if object_is_class?
        ret = @object.password
        ret = ret.call if ret.respond_to?(:call)
      else
       ret = @object.class.password
       ret = ret.call(@object) if ret.respond_to?(:call)
      end
      ret
    end

    def request_body_type
      if @method[:options][:request_body_type]
        @method[:options][:request_body_type]
      elsif @object.nil?
        nil
      elsif object_is_class?
        @object.request_body_type
      else
        @object.class.request_body_type
      end
    end

    def ignore_root
      if @method[:options][:ignore_root]
        @method[:options][:ignore_root]
      elsif @object.nil?
        nil
      elsif object_is_class?
        @object.ignore_root
      else
        @object.class.ignore_root
      end
    end

    def wrap_root
      if @method[:options][:wrap_root]
        @method[:options][:wrap_root]
      elsif @object.nil?
        nil
      elsif object_is_class?
        @object.wrap_root
      else
        @object.class.wrap_root
      end
    end

    def verbose?
      if object_is_class?
        @object.verbose
      else
        @object.class.verbose
      end
    end

    def translator
      if object_is_class?
        @object.translator
      else
        @object.class.translator
      end
    end

    def proxy
      if object_is_class?
        @object.proxy
      else
        @object.class.proxy
      end
    rescue
      nil
    end

    def http_method
      @method[:method]
    end

    def get?
      http_method == :get
    end

    def post?
      http_method == :post
    end

    def put?
      http_method == :put
    end

    def delete?
      http_method == :delete
    end

    def call(explicit_parameters=nil)
      @instrumentation_name = "#{class_name}##{@method[:name]}"
      result = nil
      cached = nil
      ActiveSupport::Notifications.instrument("request_call.flexirest", :name => @instrumentation_name) do
        @explicit_parameters = explicit_parameters
        @body = nil
        prepare_params
        prepare_url
        fake = @method[:options][:fake]
        if fake.present?
          if fake.respond_to?(:call)
            fake = fake.call(self)
          elsif @object.respond_to?(fake)
            fake = @object.send(fake)
          elsif @object.class.respond_to?(fake)
            fake = @object.class.send(fake)
          elsif @object.new.respond_to?(fake)
            fake = @object.new.send(fake)
          elsif @object.class.new.respond_to?(fake)
            fake = @object.class.new.send(fake)
          end
          Flexirest::Logger.debug "  \033[1;4;32m#{Flexirest.name}\033[0m #{@instrumentation_name} - Faked response found"
          content_type = @method[:options][:fake_content_type] || "application/json"
          return handle_response(OpenStruct.new(status:200, body:fake, response_headers:{"X-ARC-Faked-Response" => "true", "Content-Type" => content_type}))
        end
        if object_is_class?
          callback_result = @object.send(:_callback_request, :before, @method[:name], self)
        else
          callback_result = @object.class.send(:_callback_request, :before, @method[:name], self)
        end
        if callback_result == false
          return false
        end

        append_get_parameters
        prepare_request_body
        self.original_url = self.url
        cached = original_object_class.read_cached_response(self)
        if cached && !cached.is_a?(String)
          if cached.expires && cached.expires > Time.now
            Flexirest::Logger.debug "  \033[1;4;32m#{Flexirest.name}\033[0m #{@instrumentation_name} - Absolutely cached copy found"
            return handle_cached_response(cached)
          elsif cached.etag.to_s != "" #present? isn't working for some reason
            Flexirest::Logger.debug "  \033[1;4;32m#{Flexirest.name}\033[0m #{@instrumentation_name} - Etag cached copy found with etag #{cached.etag}"
            etag = cached.etag
          end
        end

        response = (
          if proxy && proxy.is_a?(Class)
            proxy.handle(self) do |request|
              request.do_request(etag)
            end
          else
            do_request(etag)
          end
        )

        # This block is called immediately when this request is not inside a parallel request block.
        # Otherwise this callback is called after the parallel request block ends.
        response.on_complete do |response_env|
          if verbose?
            Flexirest::Logger.debug "  Response"
            Flexirest::Logger.debug "  << Status : #{response_env.status}"
            response_env.response_headers.each do |k,v|
              Flexirest::Logger.debug "  << #{k} : #{v}"
            end
            Flexirest::Logger.debug "  << Body:\n#{response_env.body}"
          end

          if object_is_class? && @object.record_response?
            @object.record_response(self.url, response_env)
          end

          begin
            if object_is_class?
              callback_result = @object.send(:_callback_request, :after, @method[:name], response_env)
            else
              callback_result = @object.class.send(:_callback_request, :after, @method[:name], response_env)
            end
          rescue Flexirest::CallbackRetryRequestException
            if self.retrying != true
              self.retrying = true
              return call()
            end
          end

          result = handle_response(response_env, cached)
          @response_delegate.__setobj__(result)
          original_object_class.write_cached_response(self, response_env, result)
        end

        # If this was not a parallel request just return the original result
        return result if response.finished?
        # Otherwise return the delegate which will get set later once the call back is completed
        return @response_delegate
      end
    end

    def prepare_params
      if http_method == :post || http_method == :put || http_method == :patch
        params = (@object._attributes rescue {}).merge(@params || {}) rescue {}
      else
        params = @params || @object._attributes rescue {}
      end
      if params.is_a?(String) || params.is_a?(Integer)
        params = {id:params}
      end

      # Format includes parameter for jsonapi
      if proxy == :json_api
        JsonAPIProxy::Request::Params.translate(params, @object._include_associations)
        @object._reset_include_associations!
      end

      if @method[:options][:defaults].respond_to?(:call)
        default_params = @method[:options][:defaults].call(params)
      else
        default_params = @method[:options][:defaults] || {}
      end

      if @explicit_parameters
        params = @explicit_parameters
      end
      if http_method == :get
        @get_params = default_params.merge(params || {})
        @post_params = nil
      elsif http_method == :delete && @method[:options][:send_delete_body]
        @post_params = default_params.merge(params || {})
        @get_params = {}
      elsif params.is_a? String
        @post_params = params
        @get_params = {}
      else
        @post_params = (default_params || {}).merge(params || {})
        @get_params = {}
      end

      # Evaluate :only_changed
      if @method[:options][:only_changed]
        if http_method == :post or http_method == :put or http_method == :patch
          # we only ever mess with @post_params in here, because @get_params will/should never match our method criteria
          if @method[:options][:only_changed].is_a? Hash
            # only include the listed attributes marked 'true' when they are changed; attributed marked false are always included
            newPostHash = {}
            @method[:options][:only_changed].each_pair do |changed_attr_k,changed_attr_v|
              if changed_attr_v == false or @object.changes.has_key? changed_attr_k.to_sym
                newPostHash[changed_attr_k.to_sym] = @object[changed_attr_k.to_sym]
              end
            end
            @post_params = newPostHash
          elsif @method[:options][:only_changed].is_a? Array
            # only send these listed attributes, and only if they are changed
            newPostHash = {}
            @method[:options][:only_changed].each do |changed_attr|
              if @object.changes.has_key? changed_attr.to_sym
                newPostHash[changed_attr.to_sym] = @object[changed_attr.to_sym]
              end
            end
            @post_params = newPostHash
          else
            # only send attributes if they are changed, drop the rest
            newPostHash = {}
            @object.changed.each do |k|
              newPostHash[k] = @object[k]
            end
            @post_params = newPostHash
          end
        end
      end

      if @method[:options][:requires]
        requires = @method[:options][:requires].dup
        merged_params = @get_params.merge(@post_params || {})
        missing = []
        requires.each do |key|
          if merged_params[key.to_sym].blank? && ![true, false].include?(merged_params[key.to_sym])
            missing << key
          end
        end
        if missing.any?
          raise Flexirest::MissingParametersException.new("The following parameters weren't specifed: #{missing.join(", ")}")
        end
      end
    end

    def prepare_url
      missing = []
      if @forced_url && @forced_url.present?
        @url = @forced_url
      else
        @url = @method[:url].dup
        matches = @url.scan(/(:[a-z_-]+)/)
        @get_params ||= {}
        @post_params ||= {}
        matches.each do |token|
          token = token.first[1,999]
          # pull URL path variables out of @get_params/@post_params
          target = @get_params.delete(token.to_sym) || @post_params.delete(token.to_sym) || @get_params.delete(token.to_s) || @post_params.delete(token.to_s) || ""
          unless object_is_class?
            # it's possible the URL path variable may not be part of the request, in that case, try to resolve it from the object attributes
            target = @object._attributes[token.to_sym] || "" if target == ""
          end
          if target.to_s.blank?
            missing << token
          end
          @url.gsub!(":#{token}", URI.encode_www_form_component(target.to_s))
        end
      end

      if missing.present?
        raise Flexirest::MissingParametersException.new("The following parameters weren't specifed: #{missing.join(", ")}")
      end
    end

    def append_get_parameters
      if @get_params.any?
        if @method[:options][:params_encoder] == :flat
          @url += "?" + URI.encode_www_form(@get_params)
        else
          @url += "?" + @get_params.to_query
        end
      end
    end

    def prepare_request_body(params = nil)
      if proxy == :json_api
        if http_method == :get || (http_method == :delete && !@method[:options][:send_delete_body])
          @body = ""
        else
          headers["Content-Type"] ||= "application/vnd.api+json"
          @body = JsonAPIProxy::Request::Params.create(
            params || @post_params || {},
            object_is_class? ? @object.new : @object
          ).to_json
        end

        headers["Accept"] ||= "application/vnd.api+json"
        JsonAPIProxy::Headers.save(headers)
      elsif http_method == :get || (http_method == :delete && !@method[:options][:send_delete_body])
        if request_body_type == :form_encoded
          headers["Content-Type"] ||= "application/x-www-form-urlencoded; charset=utf-8"
        elsif request_body_type == :form_multipart
          headers["Content-Type"] ||= "multipart/form-data; charset=utf-8"
        elsif request_body_type == :json
          headers["Content-Type"] ||= "application/json; charset=utf-8"
        end
        @body = ""
      elsif request_body_type == :form_encoded
        @body ||= if params.is_a?(String)
          params
        elsif @post_params.is_a?(String)
          @post_params
        else
          p = (params || @post_params || {})
          if wrap_root.present?
            p = {wrap_root => p}
          end
          p.to_query
        end
        headers["Content-Type"] ||= "application/x-www-form-urlencoded"
      elsif request_body_type == :form_multipart
        headers["Content-Type"] ||= "multipart/form-data; charset=utf-8"
        @body ||= if params.is_a?(String)
          params
        elsif @post_params.is_a?(String)
          @post_params
        else
          p = (params || @post_params || {})
          if wrap_root.present?
            p = {wrap_root => p}
          end
          data, mp_headers = Flexirest::Multipart::Post.prepare_query(p)
          mp_headers.each do |k,v|
            headers[k] = v
          end
          data
        end
      elsif request_body_type == :json
        @body ||= if params.is_a?(String)
          params
        elsif @post_params.is_a?(String)
          @post_params
        else
          if wrap_root.present?
            {wrap_root => (params || @post_params || {})}.to_json
          else
            (params || @post_params || {}).to_json
          end
        end
        headers["Content-Type"] ||= "application/json; charset=utf-8"
      elsif request_body_type == :plain && @post_params[:body].present?
        @body = @post_params[:body]
        headers["Content-Type"] ||= "text/plain"
        headers["Content-Type"] = @post_params[:content_type] if @post_params[:content_type].present?
      end
    end

    def do_request(etag)
      http_headers = {}
      http_headers["If-None-Match"] = etag if etag
      http_headers["Accept"] = "application/hal+json, application/json;q=0.5"
      headers.each do |key,value|
        value = value.join(",") if value.is_a?(Array)
        http_headers[key] = value
      end
      if @method[:options][:url] || @forced_url
        @url = @method[:options][:url] || @method[:url]
        @url = @forced_url if @forced_url
        if connection = Flexirest::ConnectionManager.find_connection_for_url(@url)
          @url = @url.slice(connection.base_url.length, 255)
        else
          parts = @url.match(%r{^(https?://[a-z\d\.:-]+?)(/.*)}).to_a
          if (parts.empty?) # Not a full URL, so use hostname/protocol from existing base_url
            uri = URI.parse(base_url)
            @base_url = "#{uri.scheme}://#{uri.host}#{":#{uri.port}" if uri.port != 80 && uri.port != 443}"
            @url = "#{base_url}#{@url}".gsub(@base_url, "")
          else
            _, @base_url, @url = parts
          end
          base_url.gsub!(%r{//(.)}, "//#{username}:#{password}@\\1") if username && !base_url[%r{//[^/]*:[^/]*@}]
          connection = Flexirest::ConnectionManager.get_connection(base_url)
        end
      else
        parts = @url.match(%r{^(https?://[a-z\d\.:-]+?)(/.*)?$}).to_a
        if (parts.empty?) # Not a full URL, so use hostname/protocol from existing base_url
          uri = URI.parse(base_url)
          @base_url = "#{uri.scheme}://#{uri.host}#{":#{uri.port}" if uri.port != 80 && uri.port != 443}"
          @url = "#{base_url}#{@url}".gsub(@base_url, "")
          base_url = @base_url
        else
          base_url = parts[0]
        end
        base_url.gsub!(%r{//(.)}, "//#{username}:#{password}@\\1") if username && !base_url[%r{//[^/]*:[^/]*@}]
        connection = Flexirest::ConnectionManager.get_connection(base_url)
      end
      if @method[:options][:direct]
        Flexirest::Logger.info "  \033[1;4;32m#{Flexirest.name}\033[0m #{@instrumentation_name} - Requesting #{@url}"
      else
        Flexirest::Logger.info "  \033[1;4;32m#{Flexirest.name}\033[0m #{@instrumentation_name} - Requesting #{connection.base_url}#{@url}"
      end

      if verbose?
        Flexirest::Logger.debug "Flexirest Verbose Log:"
        Flexirest::Logger.debug "  Request"
        Flexirest::Logger.debug "  >> #{http_method.upcase} #{@url} HTTP/1.1"
        http_headers.each do |k,v|
          Flexirest::Logger.debug "  >> #{k} : #{v}"
        end
        Flexirest::Logger.debug "  >> Body:\n#{@body}"
      end

      request_options = {:headers => http_headers}
      if using_api_auth?
        request_options[:api_auth] = {
          :api_auth_access_id => api_auth_access_id,
          :api_auth_secret_key => api_auth_secret_key,
          :api_auth_options => api_auth_options
        }
      end
      if @method[:options][:timeout]
        request_options[:timeout] = @method[:options][:timeout]
      end

      case http_method
      when :get
        response = connection.get(@url, request_options)
      when :put
        response = connection.put(@url, @body, request_options)
      when :post
        response = connection.post(@url, @body, request_options)
      when :patch
        response = connection.patch(@url, @body, request_options)
      when :delete
        response = connection.delete(@url, @body, request_options)
      else
        raise InvalidRequestException.new("Invalid method #{http_method}")
      end

      response
    end

    def handle_cached_response(cached)
      if cached.result.is_a? Flexirest::ResultIterator
        cached.result
      else
        if object_is_class?
          cached.result
        else
          @object._copy_from(cached.result)
          @object
        end
      end
    end

    def handle_response(response, cached = nil)
      @response = response
      status = @response.status || 200
      if @response.body.blank?
        @response.response_headers['Content-Type'] = "application/json"
        @response.body = "{}"
      end

      if cached && response.status == 304
        Flexirest::Logger.debug "  \033[1;4;32m#{Flexirest.name}\033[0m #{@instrumentation_name}" +
          ' - Etag copy is the same as the server'
        return handle_cached_response(cached)
      end

      if (200..399).include?(status)
        if @method[:options][:plain]
          return @response = Flexirest::PlainResponse.from_response(@response)
        elsif is_json_response? || is_xml_response?
          if @response.respond_to?(:proxied) && @response.proxied
            Flexirest::Logger.debug "  \033[1;4;32m#{Flexirest.name}\033[0m #{@instrumentation_name} - Response was proxied, unable to determine size"
          else
            Flexirest::Logger.debug "  \033[1;4;32m#{Flexirest.name}\033[0m #{@instrumentation_name} - Response received #{@response.body.size} bytes"
          end
          result = generate_new_object(ignore_root: ignore_root, ignore_xml_root: @method[:options][:ignore_xml_root])
          # TODO: Cleanup when ignore_xml_root is removed
        else
          raise ResponseParseException.new(status:status, body:@response.body, headers: @response.headers)
        end
      else
        if is_json_response? || is_xml_response?
          error_response = generate_new_object(mutable: false, ignore_xml_root: @method[:options][:ignore_xml_root])
        else
          error_response = @response.body
        end
        if status == 400
          raise HTTPBadRequestClientException.new(status:status, result:error_response, raw_response: @response.body, url:@url, method: http_method)
        elsif status == 401
          raise HTTPUnauthorisedClientException.new(status:status, result:error_response, raw_response: @response.body, url:@url, method: http_method)
        elsif status == 403
          raise HTTPForbiddenClientException.new(status:status, result:error_response, raw_response: @response.body, url:@url, method: http_method)
        elsif status == 404
          raise HTTPNotFoundClientException.new(status:status, result:error_response, raw_response: @response.body, url:@url, method: http_method)
        elsif status == 405
          raise HTTPMethodNotAllowedClientException.new(status:status, result:error_response, raw_response: @response.body, url:@url, method: http_method)
        elsif status == 406
          raise HTTPNotAcceptableClientException.new(status:status, result:error_response, raw_response: @response.body, url:@url, method: http_method)
        elsif status == 408
          raise HTTPTimeoutClientException.new(status:status, result:error_response, raw_response: @response.body, url:@url, method: http_method)
        elsif status == 409
          raise HTTPConflictClientException.new(status:status, result:error_response, raw_response: @response.body, url:@url, method: http_method)
        elsif status == 429
          raise HTTPTooManyRequestsClientException.new(status:status, result:error_response, raw_response: @response.body, url:@url, method: http_method)
        elsif status == 500
          raise HTTPInternalServerException.new(status:status, result:error_response, raw_response: @response.body, url:@url, method: http_method)
        elsif status == 501
          raise HTTPNotImplementedServerException.new(status:status, result:error_response, raw_response: @response.body, url:@url, method: http_method)
        elsif status == 502
          raise HTTPBadGatewayServerException.new(status:status, result:error_response, raw_response: @response.body, url:@url, method: http_method)
        elsif status == 503
          raise HTTPServiceUnavailableServerException.new(status:status, result:error_response, raw_response: @response.body, url:@url, method: http_method)
        elsif status == 504
          raise HTTPGatewayTimeoutServerException.new(status:status, result:error_response, raw_response: @response.body, url:@url, method: http_method)
        elsif (400..499).include? status
          raise HTTPClientException.new(status:status, result:error_response, raw_response: @response.body, url:@url, method: http_method)
        elsif (500..599).include? status
          raise HTTPServerException.new(status:status, result:error_response, raw_response: @response.body, url:@url, method: http_method)
        elsif status == 0
          raise TimeoutException.new("Timed out getting #{response.url}")
        end
      end
      result
    end

    def new_object(attributes, name = nil, parent = nil, parent_attribute_name = nil)
      @method[:options][:has_many] ||= {}
      name = name.to_sym rescue nil
      if @method[:options][:has_many][name]
        overridden_name = name
        object = @method[:options][:has_many][name].new
      elsif @method[:options][:has_one][name]
        overridden_name = name
        object = @method[:options][:has_one][name].new
      else
        object = create_object_instance
      end

      object._parent = parent
      object._parent_attribute_name = parent_attribute_name

      if hal_response? && name.nil?
        attributes = handle_hal_links_embedded(object, attributes)
      end

      attributes.each do |k,v|
        if @method[:options][:rubify_names]
          k = rubify_name(k)
        else
          k = k.to_sym
        end
        overridden_name = select_name(k, overridden_name)
        set_corresponding_value(v, k, object, overridden_name)
      end
      object.clean! unless object_is_class?

      object
    end

    def set_corresponding_value(value, key = nil, object = nil, overridden_name = nil)
      optional_args = [key, object, overridden_name]
      value_from_object = optional_args.all? # trying to parse a JSON Hash value
      value_from_other_type = optional_args.none? # trying to parse anything else
      raise Flexirest::InvalidArgumentsException.new("Optional args need all to be filled or none") unless value_from_object || value_from_other_type
      k = key || :key
      v = value
      assignable_hash = value_from_object ? object._attributes : {}
      if value_from_object && @method[:options][:lazy].include?(k)
        assignable_hash[k] = Flexirest::LazyAssociationLoader.new(overridden_name, v, self, overridden_name:(overridden_name), parent: object, parent_attribute_name: k)
      elsif v.is_a? Hash
        assignable_hash[k] = new_object(v, overridden_name, object, k)
      elsif v.is_a? Array
        if @method[:options][:array].include?(k)
          assignable_hash[k] = Array.new
        else
          assignable_hash[k] = Flexirest::ResultIterator.new
        end
        v.each do |item|
          if item.is_a? Hash
            assignable_hash[k] << new_object(item, overridden_name)
          else
            assignable_hash[k] << set_corresponding_value(item)
          end
        end
      else
        parse_fields = [ @method[:options][:parse_fields], @object._date_fields ].compact.reduce([], :|)
        parse_fields = nil if parse_fields.empty?
        if (parse_fields && parse_fields.include?(k))
          assignable_hash[k] = parse_attribute_value(v)
        elsif parse_fields
          assignable_hash[k] = v
        elsif Flexirest::Base.disable_automatic_date_parsing
          assignable_hash[k] = v
        else
          assignable_hash[k] = parse_attribute_value(v)
        end
      end
      value_from_object ? object : assignable_hash[k]
    end

    def hal_response?
      _, content_type = @response.response_headers.detect{|k,v| k.downcase == "content-type"}
      faked_response = @response.response_headers.detect{|k,v| k.downcase == "x-arc-faked-response"}
      if content_type && content_type.respond_to?(:each)
        content_type.each do |ct|
          return true if ct[%r{application\/hal\+json}i]
          return true if ct[%r{application\/json}i]
        end
        faked_response
      elsif content_type && (content_type[%r{application\/hal\+json}i] || content_type[%r{application\/json}i]) || faked_response
        true
      else
        false
      end
    end

    def handle_hal_links_embedded(object, attributes)
      attributes["_links"] = attributes[:_links] if attributes[:_links]
      attributes["_embedded"] = attributes[:_embedded] if attributes[:_embedded]
      if attributes["_links"]
        attributes["_links"].each do |key, value|
          if value.is_a?(Array)
            object._attributes[key.to_sym] ||= Flexirest::ResultIterator.new
            value.each do |element|
              begin
                embedded_version = attributes["_embedded"][key].detect{|embed| embed["_links"]["self"]["href"] == element["href"]}
                object._attributes[key.to_sym] << new_object(embedded_version, key)
              rescue NoMethodError
                object._attributes[key.to_sym] << Flexirest::LazyAssociationLoader.new(key, element, self)
              end
            end
          else
            begin
              embedded_version = attributes["_embedded"][key]
              object._attributes[key.to_sym] = new_object(embedded_version, key)
            rescue NoMethodError
              object._attributes[key.to_sym] = Flexirest::LazyAssociationLoader.new(key, value, self)
            end
          end
        end
        attributes.delete("_links")
        attributes.delete("_embedded")
      end

      attributes
    end

    private

    def create_object_instance
      return object_is_class? ? @object.new : @object.class.new
    end

    def select_name(name, parent_name)
      if @method[:options][:has_many][name] || @method[:options][:has_one][name]
        return name
      end

      parent_name || name
    end

    def is_json_response?
      @response.response_headers['Content-Type'].nil? || @response.response_headers['Content-Type'].include?('json')
    end

    def is_json_api_response?
      @response.response_headers['Content-Type'] && @response.response_headers['Content-Type'].include?('application/vnd.api+json')
    end

    def is_xml_response?
      @response.response_headers['Content-Type'].include?('xml')
    end

    def generate_new_object(options={})
      if @response.body.is_a?(Array) || @response.body.is_a?(Hash)
        body = @response.body
      elsif is_json_response?
        begin
          body = @response.body.blank? ? {} : MultiJson.load(@response.body)
          body = {} if body.nil?
        rescue MultiJson::ParseError
          raise ResponseParseException.new(status:@response.status, body:@response.body, headers:@response.headers)
        end

        if is_json_api_response?
          body = JsonAPIProxy::Response.parse(body, @object)
        end

        if ignore_root
          [ignore_root].flatten.each do |key|
            body = body[key.to_s]
          end
        end
      elsif is_xml_response?
        body = @response.body.blank? ? {} : Crack::XML.parse(@response.body)
        if ignore_root
          [ignore_root].flatten.each do |key|
            body = body[key.to_s]
          end
        elsif options[:ignore_xml_root]
          Flexirest::Logger.warn("Using `ignore_xml_root` is deprecated, please switch to `ignore_root`")
          body = body[options[:ignore_xml_root].to_s]
        end
      end
      if translator
        body = begin
          @method[:name].nil? ? body : translator.send(@method[:name], body)
        rescue NoMethodError
          body
        end
      end
      if body.is_a? Array
        result = Flexirest::ResultIterator.new(@response)
        add_nested_body_to_iterator(result, body)
      else
        result = new_object(body, @overridden_name)
        result._status = @response.status
        result._headers = @response.response_headers
        result._etag = @response.response_headers['ETag']
        if !object_is_class? && options[:mutable] != false
          @object._copy_from(result)
          @object._clean!
          result = @object
        end
      end
      result
    end

    def add_nested_body_to_iterator(result, items)
      items.each do |json_object|
        if json_object.is_a? Hash
          result << new_object(json_object, @overridden_name)
        else
          result << set_corresponding_value(json_object)
        end
      end
    end

    def rubify_name(k)
      k.underscore.to_sym
    end
  end

  class RequestException < StandardError ; end
  class InvalidArgumentsException < StandardError ; end

  class InvalidRequestException < RequestException ; end
  class MissingParametersException < RequestException ; end
  class ResponseParseException < RequestException
    attr_accessor :status, :body, :headers
    def initialize(options)
      @status = options[:status]
      @body = options[:body]
      @headers = options[:headers]
    end
  end

  class HTTPException < RequestException
    attr_accessor :status, :result, :request_url, :body, :raw_response
    def initialize(options)
      @status = options[:status]
      @result = options[:result]
      @request_url = options[:url]
      @body = options[:raw_response]
      @method = options[:method]
    end
    alias_method :body, :raw_response

    def message
      method = @method.try(:upcase)
      "The #{method} to '#{@request_url}' returned a #{@status} status, which raised a #{self.class.to_s} with a body of: #{@body}"
    end

    def to_s
      message
    end
  end
  class HTTPClientException < HTTPException ; end
  class HTTPUnauthorisedClientException < HTTPClientException ; end
  class HTTPBadRequestClientException < HTTPClientException ; end
  class HTTPForbiddenClientException < HTTPClientException ; end
  class HTTPMethodNotAllowedClientException < HTTPClientException ; end
  class HTTPNotAcceptableClientException < HTTPClientException ; end
  class HTTPTimeoutClientException < HTTPClientException ; end
  class HTTPConflictClientException < HTTPClientException ; end
  class HTTPNotFoundClientException < HTTPClientException ; end
  class HTTPTooManyRequestsClientException < HTTPClientException ; end
  class HTTPServerException < HTTPException ; end
  class HTTPInternalServerException < HTTPServerException ; end
  class HTTPNotImplementedServerException < HTTPServerException ; end
  class HTTPBadGatewayServerException < HTTPServerException ; end
  class HTTPServiceUnavailableServerException < HTTPServerException ; end
  class HTTPGatewayTimeoutServerException < HTTPServerException ; end
end
