module NicePartials
  class Partial
    autoload :Content, "nice_partials/partial/content"
    autoload :Section, "nice_partials/partial/section"
    autoload :Stack, "nice_partials/partial/stack"

    attr_reader :outer_partial

    attr_reader :local_assigns
    alias_method :locals, :local_assigns

    def initialize(view_context, local_assigns = nil)
      @view_context, @local_assigns = view_context, local_assigns
      @outer_partial = view_context.partial # Capture the existing outer partial we're rendering within, if any.
    end

    def yield(*arguments, &block)
      if arguments.empty?
        @captured_buffer
      else
        content_for(*arguments, &block)
      end
    end

    def helpers(&block)
      Helpers.class_eval(&block)
    end

    # `translate` is a shorthand to set `content_for` with content that's run through
    # the view's `translate`/`t` context.
    #
    #   partial.t :title                       # => partial.content_for :title, t(".title")
    #   partial.t title: :section              # => partial.content_for :title, t(".section")
    #   partial.t title: "some.custom.key"     # => partial.content_for :title, t("some.custom.key")
    #   partial.t :description, title: :header # Mixing is supported too.
    #
    # Note that `partial.t "some.custom.key"` can't derive a `content_for` name, so an explicit
    # name must be provided e.g. `partial.t title: "some.custom.key"`.
    def translate(*names, **renames)
      names.chain(renames).each do |name, key = name|
        content_for name, @view_context.t(key.is_a?(String) ? key : ".#{key}")
      end
    end
    alias t translate

    # Allows an inner partial to copy content from an outer partial.
    #
    # Additionally a hash of keys to rename in the new partial context can be passed.
    #
    #   First, an outer partial gets some content set:
    #   <% partial.title "Hello there" %>
    #   <% partial.byline "Somebody" %>
    #
    #   Second, a new partial is rendered, but we want to extract the title, byline content but rename the byline key too:
    #   <%= render "shared/title" do |cp| %>
    #     <% cp.content_from partial, :title, byline: :name %>
    #   <% end %>
    #
    #   # Third, the contents with any renames are accessible in shared/_title.html.erb:
    #   <%= partial.title %> # => "Hello there"
    #   <%= partial.name %> # => "Somebody"
    def content_from(partial, *names, **renames)
      names.chain(renames).each { |key, new_key = key| public_send new_key, partial.public_send(key).to_s }
    end

    # Similar to Rails' built-in `content_for` except it defers any block execution
    # and lets you pass arguments into it, like so:
    #
    #   # Here we store a block with some eventual content…
    #   <% partial.title { |tag| tag.h1 } %>
    #
    #  # …which we can then yield into with some predefined options later.
    #  <%= partial.title.yield tag.with_options(class: "text-bold") %>
    def section(name, content = nil, **options, &block)
      section_from(name).then { _1.write?(content, **options, &block) ? nil : _1 }
    end

    def section?(name)
      section_from(name).present?
    end
    alias content_for? section?

    def content_for(...)
      section(...).presence&.to_s
    end

    def slice(*keys)
      keys.index_with { content_for _1 }
    end

    def capture(*arguments, &block)
      @captured_buffer = @view_context.capture(*arguments, self, &block)
    end

    private

    def section_from(name)
      @sections ||= {} and @sections[name] ||= Section.new(@view_context, @local_assigns&.dig(name))
    end

    def respond_to_missing?(meth, include_private = false)
      meth != :to_ary # Avoid creating a section when doing `*partial`.
    end

    def method_missing(meth, *arguments, **keywords, &block)
      define_accessor meth and public_send(meth, *arguments, **keywords, &block)
    end

    def define_accessor(name)
      name = name.to_s.chomp("?").to_sym
      self.class.define_method(name) { |content = nil, **options, &block| section(name, content, **options, &block) }
      self.class.define_method("#{name}?") { section?(name) }
    end

    def helpers_context
      @helpers_context ||= Helpers.new(@view_context, self)
    end

    class Helpers < SimpleDelegator
      def self.method_added(name)
        super

        unless name == :initialize || name == :partial
          NicePartials::Partial.delegate name, to: :helpers_context
        end
      end

      attr_reader :partial

      def initialize(view_context, partial)
        super(view_context)
        @partial = partial
      end
    end
  end
end
