# frozen_string_literal: true
module Curl
  class Easy
    class << self
      def deferred_multi_close_mutex
        @deferred_multi_close_mutex ||= Mutex.new
      end

      def deferred_multi_closes
        deferred_multi_close_mutex.synchronize do
          (@deferred_multi_closes ||= []).dup
        end
      end

      def release_deferred_multi_close(multi, easy)
        if easy && multi.requests[easy.object_id]
          begin
            multi.remove(easy)
          rescue StandardError
            # Deferred cleanup only applies to implicit single-easy multis, so
            # clear any stale Ruby bookkeeping and continue closing the handle.
            multi.instance_variable_set(:@requests, {})
          end
        else
          multi.instance_variable_set(:@requests, {})
        end

        multi.instance_variable_set(:@deferred_close, false)
        multi._close
        true
      rescue StandardError
        false
      end

      def defer_multi_close(multi, easy, owner: Thread.current)
        deferred_multi_close_mutex.synchronize do
          @deferred_multi_closes ||= []
          return if @deferred_multi_closes.any? { |entry| entry[:multi].equal?(multi) }

          multi.instance_variable_set(:@deferred_close, true)
          @deferred_multi_closes << { multi: multi, easy: easy, owner: owner }
        end
      end

      def flush_deferred_multi_closes(all_threads: false)
        pending = deferred_multi_close_mutex.synchronize do
          @deferred_multi_closes ||= []

          if all_threads
            @deferred_multi_closes.shift(@deferred_multi_closes.length)
          else
            owner = Thread.current
            remaining = []
            current = []

            @deferred_multi_closes.each do |entry|
              if entry[:owner].equal?(owner)
                current << entry
              else
                remaining << entry
              end
            end

            @deferred_multi_closes = remaining
            current
          end
        end

        pending.each do |entry|
          multi = entry[:multi]
          easy = entry[:easy]

          unless release_deferred_multi_close(multi, easy)
            defer_multi_close(multi, easy, owner: entry[:owner])
          end
        end
      end
    end

    at_exit do
      flush_deferred_multi_closes(all_threads: true)
    end
    
    alias_method :_curb_native_close, :close
    alias_method :_curb_native_multi_set, :multi=
    
    def close
      previous_multi = self.multi
      result = _curb_native_close
      previous_multi.__send__(:__unregister_idle_easy_reference, self) if previous_multi
      result
    end
    
    def multi=(multi)
      previous_multi = self.multi
      return multi if previous_multi.equal?(multi)
    
      result = _curb_native_multi_set(multi)
      previous_multi.__send__(:__unregister_idle_easy_reference, self) if previous_multi
      multi.__send__(:__register_idle_easy_reference, self) if multi
      result
    end
    
    alias post http_post
    alias put http_put
    alias body body_str
    alias head header_str

    class Error < StandardError
      attr_accessor :message, :code
      def initialize(code, msg)
        self.message = msg
        self.code = code
      end
    end

    #
    # call-seq:
    #   easy.status  => String
    #
    def status
      # Matches the last HTTP Status - following the HTTP protocol specification 'Status-Line = HTTP-Version SP Status-Code SP (Opt:)Reason-Phrase CRLF'
      statuses = self.header_str.to_s.scan(/HTTP\/\d(\.\d)?\s(\d+\s.*)\r\n/).map {|match| match[1] }
      statuses.last.strip if statuses.length > 0
    end

    #
    # call-seq:
    #   easy.set :sym|Fixnum, value
    #
    # set options on the curl easy handle see http://curl.haxx.se/libcurl/c/curl_easy_setopt.html
    #
    def set(opt,val)
      if opt.is_a?(Symbol)
        option = sym2curl(opt)
      else
        option = opt.to_i
      end

      begin
        setopt(option, val)
      rescue TypeError
        raise TypeError, "Curb doesn't support setting #{opt} [##{option}] option"
      end
    end

    #
    # call-seq:
    #   easy.sym2curl :symbol => Fixnum
    #
    #  translates ruby symbols to libcurl options
    #
    def sym2curl(opt)
      Curl.const_get("CURLOPT_#{opt.to_s.upcase}")
    end

    #
    # call-seq:
    #   easy.perform                                     => true
    #
    # Transfer the currently configured URL using the options set for this
    # Curl::Easy instance. If this is an HTTP URL, it will be transferred via
    # the configured HTTP Verb.
    #
    def perform
      self.class.flush_deferred_multi_closes

      if Curl.scheduler_active? && self.multi.nil?
        ret = Curl.perform_with_scheduler(self)
      else
        multi = self.multi
        created_multi = multi.nil?
        raised = false

        if created_multi
          multi = Curl::Multi.new
          self.multi = multi
        end

        begin
          multi.add(self)
          ret = multi.perform
          multi.remove(self) if self.multi == multi
        rescue Exception
          raised = true
          raise
        ensure
          if created_multi
            if raised
              unless self.class.release_deferred_multi_close(multi, self)
                self.class.defer_multi_close(multi, self)
              end
              self.multi = nil if self.multi == multi
            elsif Curl::Multi.autoclose
              multi.__send__(:_autoclose)
              self.multi = nil if self.multi == multi
            else
              self.multi = multi
            end
          elsif Curl::Multi.autoclose
            multi.__send__(:_autoclose)
            self.multi = nil if self.multi == multi
          else
            self.multi = multi
          end
        end
      end

      if (callback_error = _take_callback_error)
        raise callback_error
      end

      if self.last_result != 0 && self.on_failure.nil?
        err_class, err_summary = Curl::Easy.error(self.last_result)
        err_detail = self.last_error
        raise err_class.new([err_summary, err_detail].compact.join(": "))
      end

      ret
    end

    #
    # call-seq:

    #
    # easy = Curl::Easy.new
    # easy.nosignal = true
    #
    def nosignal=(onoff)
      set :nosignal, !!onoff
    end

    #
    # call-seq:
    #   easy = Curl::Easy.new("url") do|c|
    #    c.delete = true
    #   end
    #   easy.perform
    #
    def delete=(onoff)
      set :customrequest, onoff ? 'DELETE' : nil
      onoff
    end
    #
    # call-seq:
    #
    #  easy = Curl::Easy.new("url")
    #  easy.version = Curl::HTTP_2_0
    #  easy.http_version = Curl::HTTP_1_1
    #  easy.http_version = Curl::HTTP_1_0
    #  easy.http_version = Curl::HTTP_NONE
    #
    def version=(http_version)
      self.http_version = http_version
    end

    def version
      http_version
    end

    #
    # call-seq:
    #   easy.url = "http://some.url/"                    => "http://some.url/"
    #
    # Set the URL for subsequent calls to +perform+. It is acceptable
    # (and even recommended) to reuse Curl::Easy instances by reassigning
    # the URL between calls to +perform+.
    #
    def url=(u)
      set :url, u
    end

    #
    # call-seq:
    #   easy.proxy_url = string                          => string
    #
    # Set the URL of the HTTP proxy to use for subsequent calls to +perform+.
    # The URL should specify the the host name or dotted IP address. To specify
    # port number in this string, append :[port] to the end of the host name.
    # The proxy string may be prefixed with [protocol]:// since any such prefix
    # will be ignored. The proxy's port number may optionally be specified with
    # the separate option proxy_port .
    #
    # When you tell the library to use an HTTP proxy, libcurl will transparently
    # convert operations to HTTP even if you specify an FTP URL etc. This may have
    # an impact on what other features of the library you can use, such as
    # FTP specifics that don't work unless you tunnel through the HTTP proxy. Such
    # tunneling is activated with proxy_tunnel = true.
    #
    # libcurl respects the environment variables *http_proxy*, *ftp_proxy*,
    # *all_proxy* etc, if any of those is set. The proxy_url option does however
    # override any possibly set environment variables.
    #
    # Starting with libcurl 7.14.1, the proxy host string given in environment
    # variables can be specified the exact same way as the proxy can be set with
    # proxy_url, including protocol prefix (http://) and embedded user + password.
    #
    def proxy_url=(url)
      set :proxy, url
    end

    #
    # call-seq:
    #   easy.request_target = string                    => string
    #
    # Set the request-target used in the HTTP request line (libcurl CURLOPT_REQUEST_TARGET).
    # Useful for absolute-form request targets (e.g., when speaking to proxies) or
    # special targets like "*" (OPTIONS *). Requires libcurl with CURLOPT_REQUEST_TARGET support.
    #
    def request_target=(value)
      if Curl.const_defined?(:CURLOPT_REQUEST_TARGET)
        set :request_target, value
      else
        raise NotImplementedError, "CURLOPT_REQUEST_TARGET is not supported by this libcurl"
      end
    end

    def ssl_verify_host=(value)
      value = 1 if value.class == TrueClass
      value = 0 if value.class == FalseClass
      self.ssl_verify_host_integer=value
    end

    #
    # call-seq:
    #   easy.ssl_verify_host?                            => boolean
    #
    # Deprecated: call easy.ssl_verify_host instead
    # can be one of [0,1,2]
    #
    # Determine whether this Curl instance will verify that the server cert
    # is for the server it is known as.
    #
    def ssl_verify_host?
      ssl_verify_host.nil? ? false : (ssl_verify_host > 0)
    end

    #
    # call-seq:
    #   easy.interface = string                          => string
    #
    # Set the interface name to use as the outgoing network interface.
    # The name can be an interface name, an IP address or a host name.
    #
    def interface=(value)
      set :interface, value
    end

    #
    # call-seq:
    #   easy.userpwd = string                            => string
    #
    # Set the username/password string to use for subsequent calls to +perform+.
    # The supplied string should have the form "username:password"
    #
    def userpwd=(value)
      set :userpwd, value
    end

    #
    # call-seq:
    #   easy.proxypwd = string                           => string
    #
    # Set the username/password string to use for proxy connection during
    # subsequent calls to +perform+. The supplied string should have the
    # form "username:password"
    #
    def proxypwd=(value)
      set :proxyuserpwd, value
    end

    #
    # call-seq:
    #   easy.cookies = "name1=content1; name2=content2;" => string
    #
    # Set the manual Cookie request header for this Curl::Easy instance.
    # The format of the string should be NAME=CONTENTS, where NAME is the cookie name and
    # CONTENTS is what the cookie should contain. Set multiple cookies in one string like this:
    # "name1=content1; name2=content2;".
    #
    # Notes:
    # - This only affects the outgoing Cookie header (libcurl CURLOPT_COOKIE) and does NOT
    #   alter the internal libcurl cookie engine (which stores cookies from Set-Cookie).
    # - To change cookies stored in the engine, use {#cookielist} / {#cookielist=} or
    #   {#set} with :cookielist.
    # - To clear a previously set manual Cookie header, assign an empty string ('').
    #   Assigning +nil+ has no effect in current curb versions.
    #
    def cookies=(value)
      set :cookie, value
    end

    #
    # call-seq:
    #   easy.cookiefile = string                         => string
    #
    # Set a file that contains cookies to be sent in subsequent requests by this Curl::Easy instance.
    #
    # *Note* that you must set enable_cookies true to enable the cookie
    # engine, or this option will be ignored.
    #
    # Note: assigning +nil+ has no effect; pass a path string to use a cookie file.
    #
    def cookiefile=(value)
      set :cookiefile, value
    end

    #
    # call-seq:
    #   easy.cookiejar = string                          => string
    #
    # Set a cookiejar file to use for this Curl::Easy instance. Cookies from the response
    # will be written into this file.
    #
    # *Note* that you must set enable_cookies true to enable the cookie
    # engine, or this option will be ignored.
    #
    # Note: assigning +nil+ has no effect; pass a path string to persist cookies to a file.
    #
    def cookiejar=(value)
      set :cookiejar, value
    end

    #
    # call-seq:
    #   easy.cookielist = string                         => string
    #
    # Modify cookies in libcurl's internal cookie engine (CURLOPT_COOKIELIST).
    # Accepts a Set-Cookie style string, one or more lines in Netscape cookie file format,
    # or one of the special commands: "ALL" (clear), "SESS" (remove session cookies),
    # "FLUSH" (write to jar), "RELOAD" (reload from file).
    #
    # Examples:
    #   easy.cookielist = "Set-Cookie: session=42; Domain=example.com; Path=/;"
    #   easy.cookielist = [
    #     ['.example.com', 'TRUE', '/', 'FALSE', 0, 'c1', 'v1'].join("\t"),
    #     ['.example.com', 'TRUE', '/', 'FALSE', 0, 'c2', 'v2'].join("\t"),
    #     ''
    #   ].join("\n")
    #   easy.cookielist = 'ALL'   # clear all cookies in the engine
    #
    def cookielist=(value)
      set :cookielist, value
    end

    #
    # call-seq:
    #  easy = Curl::Easy.new("url") do|c|
    #   c.head = true
    #  end
    #  easy.perform
    #
    def head=(onoff)
      set :nobody, onoff
    end

    #
    # call-seq:
    #   easy.follow_location = boolean                   => boolean
    #
    # Configure whether this Curl instance will follow Location: headers
    # in HTTP responses. Redirects will only be followed to the extent
    # specified by +max_redirects+.
    #
    def follow_location=(onoff)
      set :followlocation, onoff
    end

    #
    # call-seq:
    #   easy.http_head                                   => true
    #
    # Request headers from the currently configured URL using the HEAD
    # method and current options set for this Curl::Easy instance. This
    # method always returns true, or raises an exception (defined under
    # Curl::Err) on error.
    #
    def http_head
      set :nobody, true
      ret = self.perform
      set :nobody, false
      ret
    end

    #
    # call-seq:
    #   easy.http_get                                    => true
    #
    # GET the currently configured URL using the current options set for
    # this Curl::Easy instance. This method always returns true, or raises
    # an exception (defined under Curl::Err) on error.
    #
    def http_get
      set :httpget, true
      http :GET
    end
    alias get http_get

    #
    # call-seq:
    #   easy.http_delete
    #
    # DELETE the currently configured URL using the current options set for
    # this Curl::Easy instance. This method always returns true, or raises
    # an exception (defined under Curl::Err) on error.
    #
    def http_delete
      self.http :DELETE
    end
    alias delete http_delete

    class << self

      #
      # call-seq:
      #   Curl::Easy.perform(url) { |easy| ... }           => #<Curl::Easy...>
      #
      # Convenience method that creates a new Curl::Easy instance with
      # the specified URL and calls the general +perform+ method, before returning
      # the new instance. For HTTP URLs, this is equivalent to calling +http_get+.
      #
      # If a block is supplied, the new instance will be yielded just prior to
      # the +http_get+ call.
      #
      def perform(*args)
        c = Curl::Easy.new(*args)
        yield c if block_given?
        c.perform
        c
      end

      #
      # call-seq:
      #   Curl::Easy.http_get(url) { |easy| ... }          => #<Curl::Easy...>
      #
      # Convenience method that creates a new Curl::Easy instance with
      # the specified URL and calls +http_get+, before returning the new instance.
      #
      # If a block is supplied, the new instance will be yielded just prior to
      # the +http_get+ call.
      #
      def http_get(*args)
        c = Curl::Easy.new(*args)
        yield c if block_given?
        c.http_get
        c
      end

      #
      # call-seq:
      #   Curl::Easy.http_head(url) { |easy| ... }         => #<Curl::Easy...>
      #
      # Convenience method that creates a new Curl::Easy instance with
      # the specified URL and calls +http_head+, before returning the new instance.
      #
      # If a block is supplied, the new instance will be yielded just prior to
      # the +http_head+ call.
      #
      def http_head(*args)
        c = Curl::Easy.new(*args)
        yield c if block_given?
        c.http_head
        c
      end

      #
      # call-seq:
      #   Curl::Easy.http_put(url, data) {|c| ... }
      #   Curl::Easy.http_put(url, "some=urlencoded%20form%20data&and=so%20on") => true
      #   Curl::Easy.http_put(url, "some=urlencoded%20form%20data", "and=so%20on", ...) => true
      #   Curl::Easy.http_put(url, "some=urlencoded%20form%20data", Curl::PostField, "and=so%20on", ...) => true
      #   Curl::Easy.http_put(url, Curl::PostField, Curl::PostField ..., Curl::PostField) => true
      #
      # see easy.http_put
      #
      def http_put(*args)
        url = args.shift
        c = Curl::Easy.new(url)
        yield c if block_given?
        c.http_put(*args)
        c
      end

      #
      # call-seq:
      #   Curl::Easy.http_patch(url, data) {|c| ... }
      #   Curl::Easy.http_patch(url, "some=urlencoded%20form%20data&and=so%20on") => true
      #   Curl::Easy.http_patch(url, "some=urlencoded%20form%20data", "and=so%20on", ...) => true
      #   Curl::Easy.http_patch(url, "some=urlencoded%20form%20data", Curl::PostField, "and=so%20on", ...) => true
      #   Curl::Easy.http_patch(url, Curl::PostField, Curl::PostField ..., Curl::PostField) => true
      #
      # see easy.http_patch
      #
      def http_patch(*args)
        url = args.shift
        c = Curl::Easy.new(url)
        yield c if block_given?
        c.http_patch(*args)
        c
      end

      #
      # call-seq:
      #   Curl::Easy.http_post(url, "some=urlencoded%20form%20data&and=so%20on") => true
      #   Curl::Easy.http_post(url, "some=urlencoded%20form%20data", "and=so%20on", ...) => true
      #   Curl::Easy.http_post(url, "some=urlencoded%20form%20data", Curl::PostField, "and=so%20on", ...) => true
      #   Curl::Easy.http_post(url, Curl::PostField, Curl::PostField ..., Curl::PostField) => true
      #
      # POST the specified formdata to the currently configured URL using
      # the current options set for this Curl::Easy instance. This method
      # always returns true, or raises an exception (defined under
      # Curl::Err) on error.
      #
      # If you wish to use multipart form encoding, you'll need to supply a block
      # in order to set multipart_form_post true. See #http_post for more
      # information.
      #
      def http_post(*args)
        url = args.shift
        c = Curl::Easy.new url
        yield c if block_given?
        c.http_post(*args)
        c
      end

      #
      # call-seq:
      #   Curl::Easy.http_delete(url) { |easy| ... }       => #<Curl::Easy...>
      #
      # Convenience method that creates a new Curl::Easy instance with
      # the specified URL and calls +http_delete+, before returning the new instance.
      #
      # If a block is supplied, the new instance will be yielded just prior to
      # the +http_delete+ call.
      #
      def http_delete(*args)
        c = Curl::Easy.new(*args)
        yield c if block_given?
        c.http_delete
        c
      end

      # call-seq:
      #   Curl::Easy.download(url, filename = url.split(/\?/).first.split(/\//).last) { |curl| ... }
      #
      # Stream the specified url (via perform) and save the data directly to the
      # supplied filename (defaults to the last component of the URL path, which will
      # usually be the filename most simple urls).
      #
      # If a block is supplied, it will be passed the curl instance prior to the
      # perform call.
      #
      # *Note* that the semantics of the on_body handler are subtly changed when using
      # download, to account for the automatic routing of data to the specified file: The
      # data string is passed to the handler *before* it is written
      # to the file, allowing the handler to perform mutative operations where
      # necessary. As usual, the transfer will be aborted if the on_body handler
      # returns a size that differs from the data chunk size - in this case, the
      # offending chunk will *not* be written to the file, the file will be closed,
      # and a Curl::Err::AbortedByCallbackError will be raised.
      def download(url, filename = url.split(/\?/).first.split(/\//).last, &blk)
        curl = Curl::Easy.new(url, &blk)

        output = if filename.is_a? IO
          filename.binmode if filename.respond_to?(:binmode)
          filename
        else
          File.open(filename, 'wb')
        end

        begin
          old_on_body = curl.on_body do |data|
            result = old_on_body ?  old_on_body.call(data) : data.length
            output << data if result == data.length
            result
          end
          curl.perform
        ensure
          output.close rescue IOError
        end

        return curl
      end
    end

    # Allow the incoming cert string to be file:password
    # but be careful to not use a colon from a windows file path
    # as the split point. Mimic what curl's main does
    if respond_to?(:cert=)
    alias_method :native_cert=, :cert=
    def cert=(cert_file)
      pos = cert_file.rindex(':')
      if pos && pos > 1
        self.native_cert= cert_file[0..pos-1]
        self.certpassword= cert_file[pos+1..-1]
      else
        self.native_cert= cert_file
      end
      self.cert
    end
    end

  end
end
