# frozen_string_literal: true

module Micro
  module Observers

    class Manager
      EqualTo = -> (observer) do
        -> item { item[0] == :observer && item[1] == observer }
      end

      def self.for(subject)
        new(subject)
      end

      def initialize(subject, list = nil)
        @subject = subject

        @list = Utils.compact_array(list.kind_of?(Array) ? list : [])
      end

      def included?(observer)
        @list.any?(&EqualTo[observer])
      end

      def attach(*args)
        options = args.last.is_a?(Hash) ? args.pop : Utils::EMPTY_HASH

        Utils.compact_array(args).each do |observer|
          if options[:allow_duplication] || !included?(observer)
            @list << [:observer, observer, options[:data]]
          end
        end

        self
      end

      def on(options = Utils::EMPTY_HASH)
        event, callable, with = options[:event], options[:call], options[:with]

        return self unless event.is_a?(Symbol) && callable.respond_to?(:call)

        @list << [:callable, event, [callable, with]]
      end

      def detach(*args)
        Utils.compact_array(args).each do |observer|
          @list.delete_if(&EqualTo[observer])
        end

        self
      end

      def notify(*events)
        broadcast(EventsOrActions[events])

        self
      end

      def call(options = Utils::EMPTY_HASH)
        broadcast(EventsOrActions.fetch_actions(options))

        self
      end

      private

        def broadcast(evts_or_acts)
          evts_or_acts.each do |evt_or_act|
            @list.each do |strategy, observer, data|
              call!(observer, strategy, data, with: evt_or_act)
            end
          end
        end

        def call!(observer, strategy, data, with:)
          return call_callable(data) if strategy == :callable && observer == with

          return call_observer(observer, with, data) if strategy == :observer
        end

        def call_callable(data)
          callable, arg = data[0], data[1]

          callable_arg = arg.is_a?(Proc) ? arg.call(@subject) : (arg || @subject)

          callable.call(callable_arg)
        end

        def call_observer(observer, method_name, data)
          return unless observer.respond_to?(method_name)

          handler = observer.method(method_name)

          handler.arity == 1 ? handler.call(@subject) : handler.call(@subject, data)
        end

      private_constant :EqualTo
    end

  end
end