# gem
require 'rest_client'

# stdlib
require 'digest/md5'
require 'openssl'

require 'cgi'

# optional gem
begin
  require 'rack'
rescue LoadError; end

# pick a json gem if available
%w[ yajl/json_gem json json_pure ].each{ |json|
  begin
    require json
    break
  rescue LoadError
  end
}

# the data structure used in RestGraph
RestGraphStruct = Struct.new(:auto_decode,
                             :graph_server, :old_server,
                             :accept, :lang,
                             :app_id, :secret,
                             :data, :cache,
                             :error_handler,
                             :log_handler) unless defined?(RestGraphStruct)

class RestGraph < RestGraphStruct
  class Error < RuntimeError; end
  class Event < Struct.new(:duration, :url); end
  class Event::Requested < Event; end
  class Event::CacheHit  < Event; end

  Attributes = RestGraphStruct.members.map(&:to_sym)

  # honor default attributes
  Attributes.each{ |name|
    module_eval <<-RUBY
      def #{name}
        (r = super).nil? ? (self.#{name} = self.class.default_#{name}) : r
      end
    RUBY
  }

  # setup defaults
  module DefaultAttributes
    extend self
    def default_auto_decode ; true                         ; end
    def default_graph_server; 'https://graph.facebook.com/'; end
    def default_old_server  ; 'https://api.facebook.com/'  ; end
    def default_accept      ; 'text/javascript'            ; end
    def default_lang        ; 'en-us'                      ; end
    def default_app_id      ; nil                          ; end
    def default_secret      ; nil                          ; end
    def default_data        ; {}                           ; end
    def default_cache       ; nil                          ; end
    def default_error_handler
      lambda{ |error| raise ::RestGraph::Error.new(error) }
    end
    def default_log_handler
      lambda{ |event| }
    end
  end
  extend DefaultAttributes

  def initialize o={}
    (Attributes + [:access_token]).each{ |name|
      send("#{name}=", o[name]) if o.key?(name)
    }
  end

  def access_token
    data['access_token']
  end

  def access_token= token
    data['access_token'] = token
  end

  def authorized?
    !!access_token
  end

  def url path, query={}, server=graph_server
    "#{server}#{path}#{build_query_string(query)}"
  end

  def get    path, query={}, opts={}
    request(:get   , url(path, query, graph_server), opts)
  end

  def delete path, query={}, opts={}
    request(:delete, url(path, query, graph_server), opts)
  end

  def post   path, payload, query={}, opts={}
    request(:post  , url(path, query, graph_server), opts, payload)
  end

  def put    path, payload, query={}, opts={}
    request(:put   , url(path, query, graph_server), opts, payload)
  end

  # cookies, app_id, secrect related below

  def parse_rack_env! env
    env['HTTP_COOKIE'].to_s =~ /fbs_#{app_id}=([^\;]+)/
    self.data = parse_fbs!($1)
  end

  def parse_cookies! cookies
    self.data = parse_fbs!(cookies["fbs_#{app_id}"])
  end

  def parse_fbs! fbs
    self.data = check_sig_and_return_data(
      # take out facebook sometimes there but sometimes not quotes in cookies
      Rack::Utils.parse_query(fbs.to_s.gsub('"', '')))
  end

  def parse_json! json
    self.data = json &&
      check_sig_and_return_data(JSON.parse(json))
  rescue JSON::ParserError
  end

  def fbs
    "#{fbs_without_sig(data).join('&')}&sig=#{calculate_sig(data)}"
  end

  # facebook's new signed_request...

  def parse_signed_request! request
    sig_encoded, json_encoded = request.split('.')
    sig,  json = [sig_encoded, json_encoded].map{ |str|
      "#{str.tr('-_', '+/')}==".unpack('m').first
    }
    self.data = JSON.parse(json) if
      secret && OpenSSL::HMAC.digest('sha256', secret, json_encoded) == sig
  rescue JSON::ParserError
  end

  # oauth related

  def authorize_url opts={}
    query = {:client_id => app_id, :access_token => nil}.merge(opts)
    "#{graph_server}oauth/authorize#{build_query_string(query)}"
  end

  def authorize! opts={}
    query = {:client_id => app_id, :client_secret => secret}.merge(opts)
    self.data = Rack::Utils.parse_query(
                  request(:get, url('oauth/access_token', query),
                          :suppress_decode => true))
  end

  # old rest facebook api, i will definitely love to remove them someday

  def old_rest path, query={}, opts={}
    request(
      :get,
      url("method/#{path}", {:format => 'json'}.merge(query), old_server),
      opts)
  end

  def exchange_sessions opts={}
    query = {:client_id => app_id, :client_secret => secret,
             :type => 'client_cred'}.merge(opts)
    request(:post, url('oauth/exchange_sessions', query))
  end

  def fql code, query={}, opts={}
    old_rest('fql.query', {:query => code}.merge(query), opts)
  end

  def fql_multi codes, query={}, opts={}
    c = if codes.respond_to?(:to_json)
           codes.to_json
        else
          middle = codes.inject([]){ |r, (k, v)|
                     r << "\"#{k}\":\"#{v.gsub('"','\\"')}\""
                   }.join(',')
          "{#{middle}}"
        end
    old_rest('fql.multiquery', {:queries => c}.merge(query), opts)
  end

  private
  def request meth, uri, opts={}, payload=nil
    start_time = Time.now
    post_request(cache_get(uri) || fetch(meth, uri, payload),
                 opts[:suppress_decode])
  rescue RestClient::Exception => e
    post_request(e.http_body, opts[:suppress_decode])
  ensure
    log_handler.call(Event::Requested.new(Time.now - start_time, uri))
  end

  def build_query_string query={}
    qq = access_token ? {:access_token => access_token}.merge(query) : query
    q  = qq.select{ |k, v| v }
    return '' if q.empty?
    return '?' + q.map{ |(k, v)| "#{k}=#{CGI.escape(v.to_s)}" }.join('&')
  end

  def build_headers
    headers = {}
    headers['Accept']          = accept if accept
    headers['Accept-Language'] = lang   if lang
    headers
  end

  def post_request result, suppress_decode=nil
    if auto_decode && !suppress_decode
      check_error(JSON.parse(result))
    else
      result
    end
  end

  def check_sig_and_return_data cookies
    cookies if secret && calculate_sig(cookies) == cookies['sig']
  end

  def check_error hash
    if error_handler && hash.kind_of?(Hash) &&
       (hash['error'] ||    # from graph api
        hash['error_code']) # from fql
      error_handler.call(hash)
    else
      hash
    end
  end

  def calculate_sig cookies
    Digest::MD5.hexdigest(fbs_without_sig(cookies).join + secret)
  end

  def fbs_without_sig cookies
    cookies.reject{ |(k, v)| k == 'sig' }.sort.map{ |a| a.join('=') }
  end

  def cache_key uri
    Digest::MD5.hexdigest(uri)
  end

  def cache_get uri
    return unless cache
    start_time = Time.now
    cache[cache_key(uri)].tap{ |result|
      log_handler.call(Event::CacheHit.new(Time.now - start_time, uri)) if
        result
    }
  end

  def fetch meth, uri, payload
    RestClient::Request.execute(:method => meth, :url => uri,
                                :headers => build_headers,
                                :payload => payload).
      tap{ |result|
        cache[cache_key(uri)] = result if cache
      }
  end
end