require 'sprockets/engines'
require 'sprockets/file_reader'
require 'sprockets/legacy_proc_processor'
require 'sprockets/legacy_tilt_processor'
require 'sprockets/mime'
require 'sprockets/processor_utils'
require 'sprockets/uri_utils'
require 'sprockets/utils'

module Sprockets
  # `Processing` is an internal mixin whose public methods are exposed on
  # the `Environment` and `CachedEnvironment` classes.
  module Processing
    include ProcessorUtils, URIUtils, Utils

    def pipelines
      config[:pipelines]
    end

    def register_pipeline(name, proc = nil, &block)
      proc ||= block

      self.config = hash_reassoc(config, :pipelines) do |pipelines|
        pipelines.merge(name.to_sym => proc)
      end
    end

    # Preprocessors are ran before Postprocessors and Engine
    # processors.
    def preprocessors
      config[:preprocessors]
    end
    alias_method :processors, :preprocessors

    # Postprocessors are ran after Preprocessors and Engine processors.
    def postprocessors
      config[:postprocessors]
    end

    # Registers a new Preprocessor `klass` for `mime_type`.
    #
    #     register_preprocessor 'text/css', Sprockets::DirectiveProcessor
    #
    # A block can be passed for to create a shorthand processor.
    #
    #     register_preprocessor 'text/css', :my_processor do |context, data|
    #       data.gsub(...)
    #     end
    #
    def register_preprocessor(*args, &block)
      register_config_processor(:preprocessors, *args, &block)
    end
    alias_method :register_processor, :register_preprocessor

    # Registers a new Postprocessor `klass` for `mime_type`.
    #
    #     register_postprocessor 'application/javascript', Sprockets::DirectiveProcessor
    #
    # A block can be passed for to create a shorthand processor.
    #
    #     register_postprocessor 'application/javascript', :my_processor do |context, data|
    #       data.gsub(...)
    #     end
    #
    def register_postprocessor(*args, &block)
      register_config_processor(:postprocessors, *args, &block)
    end

    # Remove Preprocessor `klass` for `mime_type`.
    #
    #     unregister_preprocessor 'text/css', Sprockets::DirectiveProcessor
    #
    def unregister_preprocessor(*args)
      unregister_config_processor(:preprocessors, *args)
    end
    alias_method :unregister_processor, :unregister_preprocessor

    # Remove Postprocessor `klass` for `mime_type`.
    #
    #     unregister_postprocessor 'text/css', Sprockets::DirectiveProcessor
    #
    def unregister_postprocessor(*args)
      unregister_config_processor(:postprocessors, *args)
    end

    # Bundle Processors are ran on concatenated assets rather than
    # individual files.
    def bundle_processors
      config[:bundle_processors]
    end

    # Registers a new Bundle Processor `klass` for `mime_type`.
    #
    #     register_bundle_processor  'application/javascript', Sprockets::DirectiveProcessor
    #
    # A block can be passed for to create a shorthand processor.
    #
    #     register_bundle_processor 'application/javascript', :my_processor do |context, data|
    #       data.gsub(...)
    #     end
    #
    def register_bundle_processor(*args, &block)
      register_config_processor(:bundle_processors, *args, &block)
    end

    # Remove Bundle Processor `klass` for `mime_type`.
    #
    #     unregister_bundle_processor 'application/javascript', Sprockets::DirectiveProcessor
    #
    def unregister_bundle_processor(*args)
      unregister_config_processor(:bundle_processors, *args)
    end

    # Public: Register bundle metadata reducer function.
    #
    # Examples
    #
    #   Sprockets.register_bundle_metadata_reducer 'application/javascript', :jshint_errors, [], :+
    #
    #   Sprockets.register_bundle_metadata_reducer 'text/css', :selector_count, 0 { |total, count|
    #     total + count
    #   }
    #
    # mime_type - String MIME Type. Use '*/*' applies to all types.
    # key       - Symbol metadata key
    # initial   - Initial memo to pass to the reduce funciton (default: nil)
    # block     - Proc accepting the memo accumulator and current value
    #
    # Returns nothing.
    def register_bundle_metadata_reducer(mime_type, key, *args, &block)
      case args.size
      when 0
        reducer = block
      when 1
        if block_given?
          initial = args[0]
          reducer = block
        else
          initial = nil
          reducer = args[0].to_proc
        end
      when 2
        initial = args[0]
        reducer = args[1].to_proc
      else
        raise ArgumentError, "wrong number of arguments (#{args.size} for 0..2)"
      end

      self.config = hash_reassoc(config, :bundle_reducers, mime_type) do |reducers|
        reducers.merge(key => [initial, reducer])
      end
    end

    protected
      def resolve_processors_cache_key_uri(uri)
        params = parse_uri_query_params(uri[11..-1])
        params[:engine_extnames] = params[:engines] ? params[:engines].split(',') : []
        processors = processors_for(params[:type], params[:file_type], params[:engine_extnames], params[:pipeline])
        processors_cache_keys(processors)
      end

      def build_processors_uri(type, file_type, engine_extnames, pipeline)
        engines = engine_extnames.join(',') if engine_extnames.any?
        query = encode_uri_query_params(
          type: type,
          file_type: file_type,
          engines: engines,
          pipeline: pipeline
        )
        "processors:#{query}"
      end

      def processors_for(type, file_type, engine_extnames, pipeline)
        pipeline ||= :default
        config[:pipelines][pipeline.to_sym].call(self, type, file_type, engine_extnames)
      end

      def default_processors_for(type, file_type, engine_extnames)
        bundled_processors = config[:bundle_processors][type]
        if bundled_processors.any?
          bundled_processors
        else
          self_processors_for(type, file_type, engine_extnames)
        end
      end

      def self_processors_for(type, file_type, engine_extnames)
        processors = []

        processors.concat config[:postprocessors][type]

        if type != file_type && processor = config[:transformers][file_type][type]
          processors << processor
        end

        processors.concat engine_extnames.map { |ext| engines[ext] }
        processors.concat config[:preprocessors][file_type]

        if processors.any? || mime_type_charset_detecter(type)
          processors << FileReader
        end

        processors
      end

    private
      def register_config_processor(type, mime_type, klass, proc = nil, &block)
        proc ||= block
        processor = wrap_processor(klass, proc)

        self.config = hash_reassoc(config, type, mime_type) do |processors|
          processors.unshift(processor)
          processors
        end

        compute_transformers!
      end

      def unregister_config_processor(type, mime_type, klass)
        if klass.is_a?(String) || klass.is_a?(Symbol)
          klass = config[type][mime_type].detect do |cls|
            cls.respond_to?(:name) && cls.name == "Sprockets::LegacyProcProcessor (#{klass})"
          end
        end

        self.config = hash_reassoc(config, type, mime_type) do |processors|
          processors.delete(klass)
          processors
        end

        compute_transformers!
      end

      def deprecate_legacy_processor_interface(interface)
        msg = "You are using a deprecated processor interface #{ interface.inspect }.\n" +
        "Please update your processor interface:\n" +
        "https://github.com/rails/sprockets/blob/master/guides/extending_sprockets.md#supporting-all-versions-of-sprockets-in-processors\n"

        Deprecation.new([caller[3]]).warn msg
      end

      def wrap_processor(klass, proc)
        if !proc
          if klass.respond_to?(:call)
            klass
          else
            deprecate_legacy_processor_interface(klass)
            LegacyTiltProcessor.new(klass)
          end
        elsif proc.respond_to?(:arity) && proc.arity == 2
          deprecate_legacy_processor_interface(proc)
          LegacyProcProcessor.new(klass.to_s, proc)
        else
          proc
        end
      end
  end
end
