module Consul
  module Controller

    def self.included(base)
      base.send :include, InstanceMethods
      base.send :extend, ClassMethods
      if ensure_power_initializer_present?
        Util.before_action(base, :ensure_power_initializer_present)
      end
    end

    private

    def self.ensure_power_initializer_present?
      ['development', 'test', 'cucumber', 'in_memory'].include?(Rails.env)
    end

    module ClassMethods

      def current_power_initializer
        @current_power_initializer || (superclass.respond_to?(:current_power_initializer) && superclass.current_power_initializer)
      end

      def current_power_initializer=(initializer)
        @current_power_initializer = initializer
      end

      private

      def require_power_check(options = {})
        Util.before_action(self, :unchecked_power, options)
      end

      # This is badly named, since it doesn't actually skip the :check_power filter
      def skip_power_check(options = {})
        Util.skip_before_action(self, :unchecked_power, options)
      end

      def current_power(&initializer)
        self.current_power_initializer = initializer
        Util.around_action(self, :with_current_power)

        if respond_to?(:helper_method)
          helper_method :current_power
        end
      end

      def power(*args)
        guard = Consul::Guard.new(*args)
        controller = self

        # One .power directive will skip the check for all actions, even
        # if that .power directive has :only or :except options.
        skip_power_check

        # Store arguments for testing
        consul_power_args << args

        Util.before_action(self, guard.filter_options) do |controller|
          guard.ensure!(controller, controller.action_name)
        end

        if guard.direct_access_method
          consul_features_module.module_eval do
            # It's dangerous to re-define direct access methods like this:
            #
            #     power :one, as: :my_power
            #     power :two, as: :my_power
            #
            # The method would always check the last power only.
            # To prevent this we're raising an error.
            if method_defined?(guard.direct_access_method)
              raise DuplicateMethod, "Method #{direct_access_method} is already defined on #{controller.name}"
            end

            define_method guard.direct_access_method do
              guard.power_value(self, action_name)
            end

            private guard.direct_access_method
          end
        end

      end

      # Instead of using define_method on the controller we're enhancing,
      # we define dynamic method in a module and have the controller include that.
      # This way the controller can override our generated method and access
      # the original implenentation with super().
      #
      # See https://thepugautomatic.com/2013/07/dsom/ for more examples on this
      # technique.
      def consul_features_module
        name = :ConsulFeatures
        # Each controller class should get its own FeatureModule, even when
        # we already inherit one from our parent.
        if const_defined?(name, (_search_ancestors = false))
          const_get(name, _search_ancestors = false)
        else
          mod = Module.new
          const_set(name, mod)
          include(mod)
          mod
        end
      end

      # On first access we inherit .consul_power_args from our ancestor classes.
      # We also copy inherited args so we don't change our parent's .consul_power_args
      def consul_power_args
        unless @consul_power_args_initialized
          if superclass && superclass.respond_to?(:consul_power_args, true)
            @consul_power_args = superclass.send(:consul_power_args).dup
          else
            @consul_power_args = []
          end
          @consul_power_args_initialized = true
        end
        @consul_power_args
      end

    end

    module InstanceMethods

      private

      def unchecked_power
        raise Consul::UncheckedPower, "This controller does not check against a power"
      end

      def current_power
        @current_power_class && @current_power_class.current
      end

      def with_current_power(&action)
        power = instance_eval(&self.class.current_power_initializer) or raise Consul::Error, 'current_power initializer returned nil'
        @current_power_class = power.class
        @current_power_class.current = power
        action.call
      ensure
        if @current_power_class
          @current_power_class.current = nil
        end
      end

      def ensure_power_initializer_present
        unless self.class.current_power_initializer.present?
          raise Consul::UnreachablePower, 'You included Consul::Controller but forgot to define a power using current_power do ... end'
        end
      end

    end

  end

end
