# frozen_string_literal: true

require "active_support/dependencies"

module AbstractController
  module Helpers
    extend ActiveSupport::Concern

    included do
      class_attribute :_helpers, default: Module.new
      class_attribute :_helper_methods, default: Array.new
    end

    class MissingHelperError < LoadError
      def initialize(error, path)
        @error = error
        @path  = "helpers/#{path}.rb"
        set_backtrace error.backtrace

        if error.path =~ /^#{path}(\.rb)?$/
          super("Missing helper file helpers/%s.rb" % path)
        else
          raise error
        end
      end
    end

    module ClassMethods
      # When a class is inherited, wrap its helper module in a new module.
      # This ensures that the parent class's module can be changed
      # independently of the child class's.
      def inherited(klass)
        helpers = _helpers
        klass._helpers = Module.new { include helpers }
        klass.class_eval { default_helper_module! } unless klass.anonymous?
        super
      end

      # Declare a controller method as a helper. For example, the following
      # makes the +current_user+ and +logged_in?+ controller methods available
      # to the view:
      #   class ApplicationController < ActionController::Base
      #     helper_method :current_user, :logged_in?
      #
      #     def current_user
      #       @current_user ||= User.find_by(id: session[:user])
      #     end
      #
      #     def logged_in?
      #       current_user != nil
      #     end
      #   end
      #
      # In a view:
      #  <% if logged_in? -%>Welcome, <%= current_user.name %><% end -%>
      #
      # ==== Parameters
      # * <tt>method[, method]</tt> - A name or names of a method on the controller
      #   to be made available on the view.
      def helper_method(*meths)
        meths.flatten!
        self._helper_methods += meths

        meths.each do |meth|
          _helpers.class_eval <<-ruby_eval, __FILE__, __LINE__ + 1
            def #{meth}(*args, &blk)                               # def current_user(*args, &blk)
              controller.send(%(#{meth}), *args, &blk)             #   controller.send(:current_user, *args, &blk)
            end                                                    # end
          ruby_eval
        end
      end

      # The +helper+ class method can take a series of helper module names, a block, or both.
      #
      # ==== Options
      # * <tt>*args</tt> - Module, Symbol, String
      # * <tt>block</tt> - A block defining helper methods
      #
      # When the argument is a module it will be included directly in the template class.
      #   helper FooHelper # => includes FooHelper
      #
      # When the argument is a string or symbol, the method will provide the "_helper" suffix, require the file
      # and include the module in the template class. The second form illustrates how to include custom helpers
      # when working with namespaced controllers, or other cases where the file containing the helper definition is not
      # in one of Rails' standard load paths:
      #   helper :foo             # => requires 'foo_helper' and includes FooHelper
      #   helper 'resources/foo'  # => requires 'resources/foo_helper' and includes Resources::FooHelper
      #
      # Additionally, the +helper+ class method can receive and evaluate a block, making the methods defined available
      # to the template.
      #
      #   # One line
      #   helper { def hello() "Hello, world!" end }
      #
      #   # Multi-line
      #   helper do
      #     def foo(bar)
      #       "#{bar} is the very best"
      #     end
      #   end
      #
      # Finally, all the above styles can be mixed together, and the +helper+ method can be invoked with a mix of
      # +symbols+, +strings+, +modules+ and blocks.
      #
      #   helper(:three, BlindHelper) { def mice() 'mice' end }
      #
      def helper(*args, &block)
        modules_for_helpers(args).each do |mod|
          add_template_helper(mod)
        end

        _helpers.module_eval(&block) if block_given?
      end

      # Clears up all existing helpers in this class, only keeping the helper
      # with the same name as this class.
      def clear_helpers
        inherited_helper_methods = _helper_methods
        self._helpers = Module.new
        self._helper_methods = Array.new

        inherited_helper_methods.each { |meth| helper_method meth }
        default_helper_module! unless anonymous?
      end

      # Returns a list of modules, normalized from the acceptable kinds of
      # helpers with the following behavior:
      #
      # String or Symbol:: :FooBar or "FooBar" becomes "foo_bar_helper",
      # and "foo_bar_helper.rb" is loaded using require_dependency.
      #
      # Module:: No further processing
      #
      # After loading the appropriate files, the corresponding modules
      # are returned.
      #
      # ==== Parameters
      # * <tt>args</tt> - An array of helpers
      #
      # ==== Returns
      # * <tt>Array</tt> - A normalized list of modules for the list of
      #   helpers provided.
      def modules_for_helpers(args)
        args.flatten.map! do |arg|
          case arg
          when String, Symbol
            file_name = "#{arg.to_s.underscore}_helper"
            begin
              require_dependency(file_name)
            rescue LoadError => e
              raise AbstractController::Helpers::MissingHelperError.new(e, file_name)
            end

            mod_name = file_name.camelize
            begin
              mod_name.constantize
            rescue LoadError
              # dependencies.rb gives a similar error message but its wording is
              # not as clear because it mentions autoloading. To the user all it
              # matters is that a helper module couldn't be loaded, autoloading
              # is an internal mechanism that should not leak.
              raise NameError, "Couldn't find #{mod_name}, expected it to be defined in helpers/#{file_name}.rb"
            end
          when Module
            arg
          else
            raise ArgumentError, "helper must be a String, Symbol, or Module"
          end
        end
      end

      private
        # Makes all the (instance) methods in the helper module available to templates
        # rendered through this controller.
        #
        # ==== Parameters
        # * <tt>module</tt> - The module to include into the current helper module
        #   for the class
        def add_template_helper(mod)
          _helpers.module_eval { include mod }
        end

        def default_helper_module!
          module_name = name.sub(/Controller$/, "".freeze)
          module_path = module_name.underscore
          helper module_path
        rescue LoadError => e
          raise e unless e.is_missing? "helpers/#{module_path}_helper"
        rescue NameError => e
          raise e unless e.missing_name? "#{module_name}Helper"
        end
    end
  end
end
