require_relative 'component/version'
require_relative 'component/renderer'
require 'cgi'

module Rack
  # Subclass Rack::Component to compose functional, declarative responses to
  # HTTP requests.
  # @example Subclass Rack::Component to compose functional, declarative
  # responses to HTTP requests.
  #   class Greeter < Rack::Component
  #     render { "Hi, #{env[:name]" }
  #   end
  class Component
    # @example If you don't want to subclass, you can extend
    # Rack::Component::Methods instead.
    #   class POROGreeter
    #     extend Rack::Component::Methods
    #     render { "Hi, #{env[:name]" }
    #   end
    module Methods
      def self.extended(base)
        base.include(InstanceMethods)
      end

      def render(opts = {})
        block_given? ? configure_block(Proc.new) : configure_template(opts)
      end

      def call(env = {}, &children)
        new(env).render(&children)
      end

      # Instances of Rack::Component come with these methods.
      # :reek:ModuleInitialize
      module InstanceMethods
        # +env+ is Rack::Component's version of React's +props+ hash.
        def initialize(env)
          @env = env
        end

        # +env+ can be an empty hash, but cannot be nil
        # @return [Hash]
        def env
          @env || {}
        end

        # +h+ removes HTML characters from strings via +CGI.escapeHTML+.
        # @return [String]
        def h(obj)
          CGI.escapeHTML(obj.to_s)
        end
      end

      private

      # :reek:TooManyStatements
      # :reek:DuplicateMethodCall
      def configure_block(block)
        # Convert the block to an instance method, because instance_exec
        # doesn't allow passing an &child param, and because it's faster.
        define_method :_rc_render, &block
        private :_rc_render

        # Now that the block is a method, it must be called with the correct
        # number of arguments. Ruby's +arity+ method is unreliable when keyword
        # args are involved, so we count arity by hand.
        arity = block.parameters.reject { |type, _| type == :block }.length

        # Reek hates this DuplicateMethodCall, but fixing it would mean checking
        # arity at runtime, rather than when the render macro is called.
        if arity.zero?
          define_method(:render) { |&child| _rc_render(&child) }
        else
          define_method(:render) { |&child| _rc_render(env, &child) }
        end
      end

      def configure_template(options)
        renderer = Renderer.new(options)
        define_method(:render) { |&child| renderer.call(self, &child) }
      end
    end

    extend Methods
  end
end
