# frozen_string_literal: true

module Hanami
  class View
    # Decorates exposure values with matching parts.
    #
    # @api public
    # @since 2.1.0
    class PartBuilder
      class << self
        # Decorates an exposure value.
        #
        # @param name [Symbol] exposure name
        # @param value [Object] exposure value
        # @param as [Symbol, nil] alternative name to use for part class resolution
        #
        # @return [Hanami::View::Part] decorated value
        #
        # @api public
        # @since 2.1.0
        def call(name, value, as: nil, rendering:)
          builder = value.respond_to?(:to_ary) ? :build_collection_part : :build_part

          send(builder, name: name, value: value, as: as, rendering: rendering)
        end

        private

        def build_part(name:, value:, as:, rendering:)
          klass = part_class(name: name, as: as, rendering: rendering)

          klass.new(name: name, value: value, rendering: rendering)
        end

        def build_collection_part(name:, value:, as: nil, rendering:)
          item_name, item_as = collection_item_name_as(name, as, inflector: rendering.inflector)
          item_part_class = part_class(name: item_name, as: item_as, rendering: rendering)

          arr = value.to_ary.map { |item|
            item_part_class.new(name: item_name, value: item, rendering: rendering)
          }

          collection_as = as.is_a?(Array) ? as.first : nil
          build_part(name: name, value: arr, as: collection_as, rendering: rendering)
        end

        def collection_item_name_as(name, as, inflector:)
          singular_name = inflector.singularize(name).to_sym
          singular_as =
            if as.is_a?(Array)
              as.last if as.length > 1
            else
              as
            end

          if singular_as && !singular_as.is_a?(Class)
            singular_as = inflector.singularize(singular_as.to_s)
          end

          [singular_name, singular_as]
        end

        def part_class(name:, as:, rendering:)
          name = as || name

          if name.is_a?(Class)
            name
          else
            View.cache.fetch_or_store(:part_class, name, rendering.config) do
              resolve_part_class(name: name, rendering: rendering)
            end
          end
        end

        # rubocop:disable Metrics/PerceivedComplexity
        def resolve_part_class(name:, rendering:)
          namespace = rendering.config.part_namespace
          return rendering.config.part_class unless namespace

          name = rendering.inflector.camelize(name.to_s)

          # Give autoloaders a chance to act
          begin
            klass = namespace.const_get(name)
          rescue NameError # rubocop:disable Lint/HandleExceptions
          end

          if !klass && namespace.const_defined?(name, false)
            klass = namespace.const_get(name)
          end

          if klass && klass < Part
            klass
          else
            rendering.config.part_class
          end
        end
        # rubocop:enable Metrics/PerceivedComplexity
      end
    end
  end
end
