module Hobo
  
  module Permissions
    
    def self.enable
      Hobo::Permissions::Associations.enable
    end
    
    def self.included(klass)
      klass.class_eval do
        extend ClassMethods
        
        alias_method_chain :create, :hobo_permission_check
        alias_method_chain :update, :hobo_permission_check
        alias_method_chain :destroy, :hobo_permission_check

        attr_accessor :acting_user, :origin, :origin_attribute

        bool_attr_accessor :exempt_from_edit_checks
        
        define_callbacks :after_user_new
      end
    end
    
    def self.find_aliased_name(klass, method_name)
      # The method +method_name+ will have been aliased. We jump through some hoops to figure out
      # what it's new name is
      method_name = method_name.to_s
      method = klass.instance_method method_name
      methods = klass.private_instance_methods + klass.instance_methods
      new_name = methods.select {|m| klass.instance_method(m) == method }.find { |m| m != method_name }
    end
    
    module ClassMethods
      
      def user_find(user, *args)
        record = find(*args)
        yield(record) if block_given?
        record.user_view user
        record
      end


      def user_new(user, attributes={})
        new(attributes) do |r|
          r.set_creator user
          yield r if block_given? 
          r.user_view(user)
          r.send :callback, :after_user_new
        end
      end


      def user_create(user, attributes={}, &block)
        if attributes.is_a?(Array)
          attributes.map { |attrs| user_create(user, attrs) }
        else
          record = user_new(user, attributes, &block)
          record.user_save(user)
          record
        end
      end


      def user_create!(user, attributes={}, &block)
        if attributes.is_a?(Array)
          attributes.map { |attrs| user_create(user, attrs) }
        else
          record = user_new(user, attributes, &block)
          record.user_save!(user)
          record
        end
      end
      
      def viewable_by?(user, attribute=nil)
        new.viewable_by?(user, attribute)
      end
      
    end
    
    
    # --- Hook ActiveRecord CRUD actions --- #
    
    
    def permission_check_required?
      # Lifecycle steps are exempt from permission checks
      acting_user && !(self.class.has_lifecycle? && lifecycle.active_step)
    end
        
    def create_with_hobo_permission_check(*args, &b)
      if permission_check_required?
        create_permitted? or raise PermissionDeniedError, "#{self.class.name}#create"
      end
      create_without_hobo_permission_check(*args, &b)
    end

    def update_with_hobo_permission_check(*args)
      if permission_check_required?
        update_permitted? or raise PermissionDeniedError, "#{self.class.name}#update"
      end
      update_without_hobo_permission_check(*args)
    end

    def destroy_with_hobo_permission_check
      if permission_check_required?
        destroy_permitted? or raise PermissionDeniedError, "#{self.class.name}#.destroy"
      end
      
      destroy_without_hobo_permission_check
    end
    
    # -------------------------------------- #
    

    # --- Permissions API --- #
    
    
    def with_acting_user(user)
      old = acting_user
      self.acting_user = user
      result = yield
      self.acting_user = old
      result
    end
    
    def user_save(user)
      with_acting_user(user) { save }
    end
        
    def user_save!(user)
      with_acting_user(user) { save! }
    end

    def user_destroy(user)
      with_acting_user(user) { destroy }
    end
        
    def user_view(user, attribute=nil)
      raise PermissionDeniedError unless viewable_by?(user, attribute)
    end
        
    def user_update_attributes(user, attributes)
      with_acting_user(user) do
        self.attributes = attributes
        save
      end
    end

    def user_update_attributes!(user, attributes)
      with_acting_user(user) do
        self.attributes = attributes
        save!
      end
    end
    
    def creatable_by?(user)
      with_acting_user(user) { create_permitted? }
    end

    def updatable_by?(user)
      with_acting_user(user) { update_permitted? }
    end
    
    def destroyable_by?(user)
      with_acting_user(user) { destroy_permitted? }
    end
    
    def method_callable_by?(user, method)
      permission_method = "#{method}_permitted?"
      respond_to?(permission_method) && with_acting_user(user) { send(permission_method) }
    end
    
    def viewable_by?(user, attribute=nil)
      if attribute
        attribute = attribute.to_s.sub(/\?$/, '').to_sym
        return false if attribute && self.class.never_show?(attribute)
      end
      with_acting_user(user) { view_permitted?(attribute) }
    end
    
    
    def editable_by?(user, attribute=nil)
      return false if attribute_protected?(attribute)
      
      return true if exempt_from_edit_checks?

      # Can't view implies can't edit
      return false unless viewable_by?(user, attribute)
      
      if attribute
        attribute = attribute.to_s.sub(/\?$/, '').to_sym

        # Try the attribute-specic edit-permission method if there is one
        if has_hobo_method?(meth = "#{attribute}_edit_permitted?")
          with_acting_user(user) { send(meth) } 
        end
      
        # No setter = no edit permission
        return false if !respond_to?("#{attribute}=")

        refl = self.class.reflections[attribute.to_sym]
        if refl && refl.macro != :belongs_to # a belongs_to is handled the same as a regular attribute
          return association_editable_by?(user, refl)
        end
      end

      with_acting_user(user) { edit_permitted?(attribute) }
    end
    
    
    def attribute_protected?(attribute)
      attribute = attribute.to_s
      
      return true if attributes_protected_by_default.include? attribute
      
      if self.class.accessible_attributes
        return true if !self.class.accessible_attributes.include?(attribute)
      elsif self.class.protected_attributes
        return true if self.class.protected_attributes.include?(attribute)
      end
      
      # Readonly attributes can be set on creation but not thereafter
      return self.class.readonly_attributes.include?(attribute) if !new_record? && self.class.readonly_attributes
      
      false
    end
    
    
    def association_editable_by?(user, reflection)      
      # has_one and polymorphic associations are not editable (for now)
      return false if reflection.macro == :has_one || reflection.options[:polymorphic]
      
      return false unless reflection.options[:accessible]
            
      record = if (through = reflection.through_reflection)
                 # For edit permission on a has_many :through,
                 # the user needs create+destroy permission on the join model
                 send(through.name).new_candidate
               else
                 # For edit permission on a regular has_many,
                 # the user needs create/destroy permission on the member model
                 send(reflection.name).new_candidate
               end
      record.creatable_by?(user) && record.destroyable_by?(user)
    end
    
    # ----------------------- #
    
    
    # --- Permission Declaration Helpers --- #
    
    def only_changed?(*attributes)
      attributes = attributes.map do |attr|
        with_attribute_or_belongs_to_keys(attr) { |a, ftype| ftype ? [a, ftype] : a }
      end.flatten
      
      changed.all? { |attr| attributes.include?(attr) }
    end
    
    def none_changed?(*attributes)
      attributes = attributes.map do |attr|
        with_attribute_or_belongs_to_keys(attr) { |a, ftype| ftype ? [a, ftype] : a }
      end.flatten
      
      attributes.all? { |attr| !changed.include?(attr) }
    end

    def any_changed?(*attributes)
      attributes.any? do |attr|
        with_attribute_or_belongs_to_keys(attr) do |a, ftype|
          if ftype
            changed.include?(a) || changed.include?(ftype)
          else
            changed.include?(a)
          end
        end
      end
    end

    def all_changed?(*attributes)
      attributes = prepare_attributes_for_change_helpers(attributes)
      attributes.all? do |attr|
        with_attribute_or_belongs_to_keys(attr) do |a, ftype|
          if ftype
            changed.include?(a) || changed.include?(ftype)
          else
            changed.include?(a)
          end
        end
      end
    end    
    
    def with_attribute_or_belongs_to_keys(attribute)
      if (refl = self.class.reflections[attribute.to_sym]) && refl.macro == :belongs_to
        if refl.options[:polymorphic]
          yield refl.primary_key_name, refl.options[:foreign_type]
        else
          yield refl.primary_key_name, nil
        end
      else
        yield attribute.to_s, nil
      end
    end
      
    
    
    # -------------------------------------- #
    
    
    # --- Default *_permitted? methods --- #
    
    # Conservative default permissions #
    def create_permitted?;  false end
    def update_permitted?;  false end
    def destroy_permitted?; false end
    
    # Allow viewing by default
    def view_permitted?(attribute) true end
      
    # By default, attempt to derive edit permission from create/update permission
    def edit_permitted?(attribute)
      unknownify_attribute(attribute) if attribute
      new_record? ? create_permitted? : update_permitted?
    rescue Hobo::UndefinedAccessError
      # The permission is dependent on the unknown value
      # so this attribute is not editable
      false
    ensure
      deunknownify_attribute(attribute) if attribute
    end
  
  
    # Add some singleton methods to +record+ so give the effect that +attribute+ is unknown. That is,
    # attempts to access the attribute will result in a Hobo::UndefinedAccessError
    def unknownify_attribute(attr)
      metaclass.class_eval do
        define_method attr do
          raise Hobo::UndefinedAccessError
        end
      end
      
      if (refl = self.class.reflections[attr.to_sym]) && refl.macro == :belongs_to
        # A belongs_to -- also unknownify the underlying fields
        unknownify_attribute refl.primary_key_name
        unknownify_attribute refl.options[:foreign_type] if refl.options[:polymorphic]
      else
        # A regular field -- hack the dirty tracking methods
        
        metaclass.class_eval do
        
          define_method "#{attr}_change" do
            raise Hobo::UndefinedAccessError
          end
        
          define_method "#{attr}_was" do
            read_attribute attr
          end
        
          define_method "#{attr}_changed?" do
            true
          end

          def changed?
            true
          end
      
          define_method :changed do
            changed_attributes.keys | [attr.to_s]
          end
      
          def changes
            raise Hobo::UndefinedAccessError
          end
        
        end
      end
    end
    
    # Best. Name. Ever
    def deunknownify_attribute(attr)
      attr = attr.to_sym
      
      metaclass.send :remove_method, attr
      
      if (refl = self.class.reflections[attr]) && refl.macro == :belongs_to
        # A belongs_to -- restore the underlying fields
        deunknownify_attribute refl.primary_key_name
        deunknownify_attribute refl.options[:foreign_type] if refl.options[:polymorphic]
      else
        # A regular field -- restore the dirty tracking methods
        ["#{attr}_change", "#{attr}_was", "#{attr}_changed?", :changed?, :changed, :changes].each do |m|
          metaclass.send :remove_method, m.to_sym
        end
      end
    end
  end
  
end