module Garage
  class TokenScope
    def self.configure(&block)
      @config = Config.new
      @config.instance_eval(&block)
    end

    def self.configuration
      @config or raise "Garage::TokenScope.configure must be called in initializer"
    end

    def self.all_scopes
      configuration.scopes.values
    end

    def self.optional_scopes
      configuration.scopes.values.select(&:optional?)
    end

    def self.revealed_scopes
      configuration.scopes.values.reject(&:hidden?)
    end

    def self.hidden_scopes
      configuration.scopes.values.select(&:hidden?)
    end

    def self.ability(user, scopes)
      scopes = scopes.map(&:to_sym)
      scopes = [:public] if scopes.empty? # backward compatiblity for scopes without any scope, assuming public
      Ability.new(user, configuration.scopes.slice(*scopes).values)
    end

    class Ability
      def initialize(user, scopes = [])
        @user = user
        @access = []
        load_scopes(scopes)
      end

      def load_scopes(scopes)
        scopes.each do |scope|
          load_scope(scope)
        end
      end

      def load_scope(scope)
        scope = TokenScope.configuration.scopes[scope] if scope.is_a?(Symbol)
        @access.concat(scope.accessible_resources)
      end

      def missing_scopes(klass, action)
        TokenScope.configuration.required_scopes(klass, action)
      end

      def access!(klass, action)
        allow?(klass, action) or raise MissingScopeError.new(@user, action, klass, :forbidden, missing_scopes(klass, action))
      end

      def allow?(klass, action)
        @access.include?([klass.to_s, action])
      end
    end


    class Config
      def namespace(ns, &block)
        @ns = ns
        instance_eval(&block)
        @ns = nil
      end

      def scopes
        @scopes ||= {}
      end

      def required_scopes(klass, action)
        @required_scopes ||= {}
        @required_scopes[[klass.to_s, action]] ||= []
      end

      def register(scope_symbol, options={}, &block)
        if options[:namespace] || @ns
          scope_symbol = [options[:namespace] || @ns, scope_symbol].join(".").to_sym
        end
        scope = Scope.new(scope_symbol, options)
        scope.instance_eval(&block) if block_given?
        unless scope.hidden?
          scope.accessible_resources.each do |klass, action|
            required_scopes(klass, action) << scope.to_sym
          end
        end

        scopes[scope_symbol] = scope
      end
    end

    class Scope
      attr_reader :description

      def initialize(sym, options={})
        @sym = sym
        @access = []
        @hidden = options[:hidden]
        @description = options[:desc]
      end

      def name
        @sym.to_s
      end
      alias_method :to_s, :name

      def access(action, klass)
        @access << [klass.to_s, action]
      end

      def accessible_resources
        @access
      end

      def to_sym
        @sym
      end

      def hidden?
        !!@hidden
      end

      def optional?
        @sym != :public && !hidden?
      end
    end
  end
end
