require 'json'
require 'native'
require 'promise'
require 'opal/jquery/constants'

# {HTTP} is used to perform a `XMLHttpRequest` in ruby. It is a simple wrapper
# around jQuerys' `$.ajax` call. `XMLHttpRequest` is not wrapped directly as
# jquery provides some cross browser fixes.
#
# # Making requests
#
# To create a simple request, {HTTP} exposes class level methods to specify
# the HTTP action you wish to perform. Each action accepts the url for the
# request, as well as optional arguments passed as a hash:
#
#     HTTP.get("/users/1.json")
#     HTTP.post("/users", payload: data)
#
# The supported `HTTP` actions are:
#
# * {HTTP.get}
# * {HTTP.post}
# * {HTTP.put}
# * {HTTP.delete}
# * {HTTP.patch}
# * {HTTP.head}
#
# # Handling responses
#
# Responses can be handled using either a simple block callback, or using a
# {Promise} returned by the request.
#
# ## Using a block
#
# All HTTP action methods accept a block which can be used as a simple
# handler for the request. The block will be called for both successful as well
# as unsuccessful requests.
#
#     HTTP.get("/users/1") do |request|
#       puts "the request has completed!"
#     end
#
# This `request` object will simply be the instance of the {HTTP} class which
# wraps the native `XMLHttpRequest`. {HTTP#ok?} can be used to quickly determine
# if the request was successful.
#
#     HTTP.get("/users/1") do |request|
#       if request.ok?
#         puts "request was success"
#       else
#         puts "something went wrong with request"
#       end
#     end
#
# The {HTTP} instance will always be the only object passed to the block.
#
# ## Using a Promise
#
# If no block is given to one of the action methods, then a {Promise} is
# returned instead. See the standard library for more information on Promises.
#
#     HTTP.get("/users/1").then do |req|
#       puts "response ok!"
#     end.fail do |req|
#       puts "response was not ok"
#     end
#
# When using a {Promise}, both success and failure handlers will be passed the
# {HTTP} instance.
#
# # Accessing Response Data
#
# All data returned from an HTTP request can be accessed via the {HTTP} object
# passed into the block or promise handlers.
#
# - {#ok?} - returns `true` or `false`, if request was a success (or not).
# - {#body} - returns the raw text response of the request
# - {#status_code} - returns the raw {HTTP} status code as integer
# - {#json} - tries to convert the body response into a JSON object
class HTTP
  `var $ = #{JQUERY_SELECTOR.to_n}` # cache $ for SPEED

  # All valid {HTTP} action methods this class accepts.
  #
  # @see HTTP.get
  # @see HTTP.post
  # @see HTTP.put
  # @see HTTP.delete
  # @see HTTP.patch
  # @see HTTP.head
  ACTIONS = %w[get post put delete patch head]

  # @!method self.get(url, options = {}, &block)
  #
  # Create a {HTTP} `get` request.
  #
  # @example
  #   HTTP.get("/foo") do |req|
  #     puts "got data: #{req.data}"
  #   end
  #
  # @param url [String] url for request
  # @param options [Hash] any request options
  # @yield [self] optional block to handle response
  # @return [Promise, nil] optionally returns a promise

  # @!method self.post(url, options = {}, &block)
  #
  # Create a {HTTP} `post` request. Post data can be supplied using the
  # `payload` options. Usually this will be a hash which will get serialized
  # into a native javascript object.
  #
  # @example
  #   HTTP.post("/bar", payload: data) do |req|
  #     puts "got response"
  #   end
  #
  # @param url [String] url for request
  # @param options [Hash] optional request options
  # @yield [self] optional block to yield for response
  # @return [Promise, nil] returns a {Promise} unless block given

  # @!method self.put(url, options = {}, &block)

  # @!method self.delete(url, options = {}, &block)

  # @!method self.patch(url, options = {}, &block)

  # @!method self.head(url, options = {}, &block)

  ACTIONS.each do |action|
    define_singleton_method(action) do |url, options = {}, &block|
      new.send(action, url, options, block)
    end

    define_method(action) do |url, options = {}, &block|
      send(action, url, options, block)
    end
  end

  def self.setup
    Hash.new(`$.ajaxSetup()`)
  end

  def self.setup= settings
    `$.ajaxSetup(#{settings.to_n})`
  end

  attr_reader :body, :error_message, :method, :status_code, :url, :xhr

  def initialize
    @settings = {}
    @ok = true
  end

  def send(method, url, options, block)
    @method   = method
    @url      = url
    @payload  = options.delete :payload
    @handler  = block

    @settings.update options

    settings, payload = @settings.to_n, @payload.to_n

    %x{
      if (typeof(payload) === 'string' || settings.processData === false) {
        settings.data = payload;
      }
      else if (payload != nil) {
        settings.data = JSON.stringify(payload);
        settings.contentType = 'application/json';
      }

      settings.url  = #@url;
      settings.type = #{@method.upcase};

      settings.success = function(data, status, xhr) {
        return #{ succeed `data`, `status`, `xhr` };
      };

      settings.error = function(xhr, status, error) {
        return #{ fail `xhr`, `status`, `error` };
      };

      $.ajax(settings);
    }

    @handler ? self : promise
  end

  # Parses the http response body through json. If the response is not
  # valid JSON then an error will very likely be thrown.
  #
  # @example Getting JSON content
  #   HTTP.get("api.json") do |response|
  #     puts response.json
  #   end
  #
  #   # => {"key" => 1, "bar" => 2, ... }
  #
  # @return [Hash, Array] returns the parsed json
  def json
    @json ||= JSON.parse(@body)
  end

  # Returns true if the request succeeded, false otherwise.
  #
  # @example
  #   HTTP.get("/some/url") do |response|
  #     if response.ok?
  #       alert "Yay!"
  #     else
  #       alert "Aww :("
  #     end
  #
  # @return [true, false] true if request was successful
  def ok?
    @ok
  end

  # Returns the value of the specified response header.
  #
  # @param key [String] name of the header to get
  # @return [String] value of the header
  # @return [nil] if the header +key+ was not in the response
  def get_header(key)
    %x{
      var value = #@xhr.getResponseHeader(#{key});
      return (value === null) ? nil : value;
    }
  end

  def inspect
    "#<HTTP @url=#{@url} @method=#{@method}>"
  end

  private

  def promise
    return @promise if @promise

    @promise = Promise.new.tap { |promise|
      @handler = proc { |res|
        if res.ok?
          promise.resolve res
        else
          promise.reject res
        end
      }
    }
  end

  def succeed(data, status, xhr)
    %x{
      #@body = data;
      #@xhr  = xhr;
      #@status_code = xhr.status;

      if (typeof(data) === 'object') {
        #@json = #{ JSON.from_object `data` };
      }
    }

    @handler.call self if @handler
  end

  def fail(xhr, status, error)
    %x{
      #@body = xhr.responseText;
      #@xhr = xhr;
      #@status_code = xhr.status;
    }

    @ok = false
    @handler.call self if @handler
  end
end
