require 'active_support/time_with_zone'

module Garage::Representer
  attr_accessor :params, :representer_attrs, :partial, :selector

  def partial?
    @partial
  end

  def render_hash(options={})
    obj = {}
    representer_attrs.each do |definition|
      if definition.options[:if]
        next unless definition.options[:if].call(self, options[:responder])
      end

      if definition.respond_to?(:encode)
        next unless handle_definition?(selector, definition, options)
        obj[definition.name] = definition.encode(self, options[:responder], selector[definition.name])
      else
        next if selector.excludes?('_links')
        obj['_links'] ||= {}
        obj['_links'][definition.rel.to_s] = { 'href' => definition.pathify(self) }
      end
    end
    obj
  end

  def handle_definition?(selector, definition, options)
    if definition.requires_select?
      # definition is not selected by default - it's opt-in
      selector.includes?(definition.name) && definition.selectable?(self, options[:responder])
    else
      # definition is selected by default - it's opt-out
      !selector.excludes?(definition.name)
    end
  end

  def default_url_options
    @default_url_options ||= {}
  end

  def represent!
    self.representer_attrs ||= []
    self.representer_attrs += self.class.representer_attrs
  end

  def self.representers
    @representers ||= []
  end

  def resource_class
    self.class
  end

  def to_resource(options = {})
    self
  end

  def link_path_for(rel)
    represent! unless representer_attrs
    representer_attrs.grep(Link).find { |link| link.rel === rel }.try(:pathify, self)
  end

  def self.included(base)
    self.representers << base

    base.class_eval do
      if Rails.application
        include Rails.application.routes.url_helpers
      end
      extend ClassMethods
    end
  end

  module ClassMethods
    attr_writer :representer_attrs

    def inherited(child)
      super
      child.representer_attrs = self.representer_attrs.clone
    end

    def representer_attrs
      @representer_attrs ||= []
    end

    def property(name, options={})
      representer_attrs << Definition.new(name, options)
    end

    def link(rel, options={}, &block)
      representer_attrs << Link.new(rel, options, block)
    end

    def collection(name, options={})
      representer_attrs << Collection.new(name, options)
    end

    def oauth_scope(scope)
      ->(resource, responder){
        # FIXME: this only works with User resource for now
        # partial representation will not render request scope-specific fields for better caching
        !resource.partial? && responder.controller.requested_by?(resource) && responder.controller.has_scope?(scope)
      }
    end

    def accessible(*args)
      ->(resource, responder){
        responder.controller.allow_access?(*args)
      }
    end

    # represents the representer's schema in JSON format
    def metadata
      {:definitions => representer_attrs.grep(Definition).map {|definition| definition.name},
       :links => representer_attrs.grep(Link).map {|link| link.options[:as] ? {link.rel => {'as' => link.options[:as]}} : link.rel}
      }
    end

    def param(*keys)
      keys.each {|key| params << key }
    end

    def params
      @params ||= []
    end
  end

  class NonEncodableValue < StandardError;  end

  class Definition
    attr_reader :options

    def initialize(name, options={})
      @name = name
      @options = options
    end

    def requires_select?
      @options[:selectable]
    end

    def selectable?(*args)
      if boolean?(@options[:selectable])
        @options[:selectable]
      else
        @options[:selectable].call(*args)
      end
    end

    def name
      (@options[:as] || @name).to_s
    end

    def encode(object, responder, selector = nil)
      value = object.send(@name)
      encode_value(value, responder, selector)
    end

    def encode_value(value, responder, selector)
      if value.is_a?(Garage::Representer)
        responder.encode_to_hash(value, partial: true, selector: selector)
      elsif primitive?(value)
        value
      else
        raise NonEncodableValue, "#{value.class} can not be encoded directly. Forgot to include Garage::Representer?"
      end
    end

    PRIMITIVE_CLASSES = [
      ActiveSupport::TimeWithZone,
      Date,
      Time,
      Integer,
      Float,
      Hash,
      Array,
      String,
      NilClass,
      TrueClass,
      FalseClass,
      Symbol,
    ]

    def primitive?(value)
      PRIMITIVE_CLASSES.any? {|k| value.is_a?(k) }
    end

    private

    def boolean?(value)
      value.is_a?(TrueClass) || value.is_a?(FalseClass)
    end
  end

  class Collection < Definition
    def encode(object, responder, selector = nil)
      value = object.send(@name)
      value.map do |item|
        encode_value(item, responder, selector)
      end
    end
  end

  class Link
    attr_reader :rel, :options, :block

    def initialize(rel, options, block)
      @rel, @options, @block = rel, options, block
    end

    def pathify(representer)
      representer.instance_exec(&@block)
    end
  end
end
