# frozen_string_literal: true
require 'set'

module Sprockets
  # Functional utilities for dealing with Processor functions.
  #
  # A Processor is a general function that may modify or transform an asset as
  # part of the pipeline. CoffeeScript to JavaScript conversion, Minification
  # or Concatenation are all implemented as separate Processor steps.
  #
  # Processors maybe any object that responds to call. So procs or a class that
  # defines a self.call method.
  #
  # For ergonomics, processors may return a number of shorthand values.
  # Unfortunately, this means that processors can not compose via ordinary
  # function composition. The composition helpers here can help.
  module ProcessorUtils
    extend self

    class CompositeProcessor < Struct.new(:processor_strategy, :param, :processors) # :nodoc:
      SINGULAR = lambda { |param, input| ProcessorUtils.call_processor param, input }
      PLURAL   = lambda { |param, input| ProcessorUtils.call_processors param, input }

      def self.create(processors)
        if processors.length == 1
          new SINGULAR, processors.first, processors
        else
          new PLURAL, processors, processors
        end
      end

      def call(input)
        processor_strategy.call param, input
      end

      def cache_key
        ProcessorUtils.processors_cache_keys(processors)
      end
    end

    # Public: Compose processors in right to left order.
    #
    # processors - Array of processors callables
    #
    # Returns a composed Proc.
    def compose_processors(*processors)
      CompositeProcessor.create processors
    end

    # Public: Invoke list of processors in right to left order.
    #
    # The right to left order processing mirrors standard function composition.
    # Think about:
    #
    #   bundle.call(uglify.call(coffee.call(input)))
    #
    # processors - Array of processor callables
    # input - Hash of input data to pass to each processor
    #
    # Returns a Hash with :data and other processor metadata key/values.
    def call_processors(processors, input)
      data = input[:data] || ""
      metadata = (input[:metadata] || {}).dup

      processors.reverse_each do |processor|
        result = call_processor(processor, input.merge(data: data, metadata: metadata))
        data = result.delete(:data)
        metadata.merge!(result)
      end

      metadata.merge(data: data)
    end

    # Public: Invoke processor.
    #
    # processor - Processor callables
    # input - Hash of input data to pass to processor
    #
    # Returns a Hash with :data and other processor metadata key/values.
    def call_processor(processor, input)
      metadata = (input[:metadata] || {}).dup
      metadata[:data] = input[:data]

      case result = processor.call({data: "", metadata: {}}.merge(input))
      when NilClass
        metadata
      when Hash
        metadata.merge(result)
      when String
        metadata.merge(data: result)
      else
        raise TypeError, "invalid processor return type: #{result.class}"
      end
    end

    # Internal: Get processor defined cached key.
    #
    # processor - Processor function
    #
    # Returns JSON serializable key or nil.
    def processor_cache_key(processor)
      processor.cache_key if processor.respond_to?(:cache_key)
    end

    # Internal: Get combined cache keys for set of processors.
    #
    # processors - Array of processor functions
    #
    # Returns Array of JSON serializable keys.
    def processors_cache_keys(processors)
      processors.map { |processor| processor_cache_key(processor) }
    end

    # Internal: Set of all "simple" value types allowed to be returned in
    # processor metadata.
    VALID_METADATA_VALUE_TYPES = Set.new([
      String,
      Symbol,
      TrueClass,
      FalseClass,
      NilClass,
      Integer
    ]).freeze

    # Internal: Set of all nested compound metadata types that can nest values.
    VALID_METADATA_COMPOUND_TYPES = Set.new([
      Array,
      Hash,
      Set
    ]).freeze

    # Internal: Hash of all "simple" value types allowed to be returned in
    # processor metadata.
    VALID_METADATA_VALUE_TYPES_HASH = VALID_METADATA_VALUE_TYPES.each_with_object({}) do |type, hash|
      hash[type] = true
    end.freeze

    # Internal: Hash of all nested compound metadata types that can nest values.
    VALID_METADATA_COMPOUND_TYPES_HASH = VALID_METADATA_COMPOUND_TYPES.each_with_object({}) do |type, hash|
      hash[type] = true
    end.freeze

    # Internal: Set of all allowed metadata types.
    VALID_METADATA_TYPES = (VALID_METADATA_VALUE_TYPES + VALID_METADATA_COMPOUND_TYPES).freeze

    # Internal: Validate returned result of calling a processor pipeline and
    # raise a friendly user error message.
    #
    # result - Metadata Hash returned from call_processors
    #
    # Returns result or raises a TypeError.
    def validate_processor_result!(result)
      if !result.instance_of?(Hash)
        raise TypeError, "processor metadata result was expected to be a Hash, but was #{result.class}"
      end

      if !result[:data].instance_of?(String)
        raise TypeError, "processor :data was expected to be a String, but as #{result[:data].class}"
      end

      result.each do |key, value|
        if !key.instance_of?(Symbol)
          raise TypeError, "processor metadata[#{key.inspect}] expected to be a Symbol"
        end
      end

      result
    end
  end
end
