module Hyperstack
  module State
    module Observable
      def self.bulk_update(&block)
        Internal::State::Mapper.bulk_update(&block)
      end

      def self.included(base)
        base.include Internal::AutoUnmount
        %i[singleton_method method].each do |kind|
          base.send(:"define_#{kind}", :receives) do |*args, &block|
            Internal::Receiver.mount(self, *args, &block)
          end
          base.send(:"define_#{kind}", :observe) do |*args, &block|
            result = block ? block.call : args.last
            Internal::State::Mapper.observed! self
            result
          end
          base.send(:"define_#{kind}", :mutate) do |*args, &block|
            # any args will be ignored thus allowing us to say `mutate @foo = 123, @bar[:x] = 7` etc
            result = block ? block.call : args.last
            Internal::State::Mapper.mutated! self
            result
          end
          base.send(:"define_#{kind}", :toggle) do |var|
            # @var = !@var
            var = "@#{var}"
            result = instance_variable_set(var, !instance_variable_get(var))
            Internal::State::Mapper.mutated! self
            result
          end
          if RUBY_ENGINE == 'opal'
            base.send(:"define_#{kind}", :set) do |var|
              if Hyperstack.naming_convention == :prefix_state
                var = "_#{var}" if var !~ /^_/
              elsif Hyperstack.naming_convention != :none && var !~ /^[a-z]/
                dont_mutate = true
              end
              lambda do |val|
                `self[#{var}] = #{val}`
                mutate unless dont_mutate
              end
            end
          else
            base.send(:"define_#{kind}", :set) do |var|
              if Hyperstack.naming_convention == :prefix_state
                var = "_#{var}" if var !~ /^_/
              elsif Hyperstack.naming_convention != :none && var !~ /^[a-z]/
                dont_mutate = true
              end
              lambda do |val|
                instance_variable_set(:"@#{var}", val)
                mutate unless dont_mutate
              end
            end
          end
          base.singleton_class.send(:"define_#{kind}", :observer) do |name, &block|
            define_method(name) do |*args|
              instance_exec(*args, &block).tap do
                Internal::State::Mapper.observed! self
              end
            end
          end
          base.singleton_class.send(:"define_#{kind}", :mutator) do |name, &block|
            define_method(name) do |*args|
              instance_exec(*args, &block).tap do
                Internal::State::Mapper.mutated! self
              end
            end
          end
          base.singleton_class.send(:"define_#{kind}", :state_reader) do |*names|
            names.each do |name|
              var_name = if Hyperstack.naming_convention == :prefix_state && name !~ /^_/
                           "_#{name}"
                         else
                           name
                         end
              define_method(name) do
                instance_variable_get(:"@#{var_name}").tap { Internal::State::Mapper.observed!(self) }
              end
            end
          end
          base.singleton_class.send(:"define_#{kind}", :state_writer) do |*names|
            names.each do |name|
              var_name = if Hyperstack.naming_convention == :prefix_state && name !~ /^_/
                           "_#{name}"
                         else
                           name
                         end
              define_method(:"#{name}=") do |x|
                instance_variable_set(:"@#{var_name}", x).tap { Internal::State::Mapper.mutated!(self) }
              end
            end
          end
          base.singleton_class.send(:"define_#{kind}", :state_accessor) do |*names|
            state_reader(*names)
            state_writer(*names)
          end
        end
        # receives is defined at the class an instance level above, but for
        # convenience we also define it as singleton_method on the singleton class allowing this:
        # class Foo
        #   class << self
        #     receives ...
        #   end
        # end
        base.singleton_class.define_singleton_method(:receives) do |*args, &block|
          Internal::Receiver.mount(base, *args, &block)
        end

        unless base.respond_to? :__hyperstack_state_observer_included
          base.receives Hyperstack::Application::Boot do
            ObjectSpace.each_object(Class) do |klass|
              next unless klass <= base
              next unless klass.respond_to?(:initialize)
              next unless klass.method(:initialize).arity.zero?
              klass.initialize
            end
          end
          base.singleton_class.attr_reader :__hyperstack_state_observer_included
        end
      end
    end
  end
end
