module Hyperstack
  module Component
    # NativeLibrary handles importing JS libraries. Importing native components is handled
    # by the React::Base.  It also provides several methods used by auto-import.rb

    # A NativeLibrary is simply a wrapper that holds the name of the native js library.
    # It responds to const_missing and method_missing by looking up objects within the js library.
    # If the object is a react component it is wrapped by a reactrb component class, otherwise
    # a nested NativeLibrary is returned.

    # Two macros are provided: imports (for naming the native library) and renames which allows
    # the members of a library to be given different names within the ruby name space.

    # Public methods used by auto-import.rb are import_const_from_native and find_and_render_component
    class NativeLibrary
      class << self
        def imports(native_name)
          @__hyperstack_component_native_prefix = "#{native_name}."
          self
        end

        def rename(rename_list)
          # rename_list is a hash in the form: native_name => ruby_name, native_name => ruby_name
          rename_list.each do |js_name, ruby_name|
            native_name = lookup_native_name(js_name)
            if lookup_native_name(js_name)
              create_component_wrapper(self, native_name, ruby_name) ||
                create_library_wrapper(self, native_name, ruby_name)
            else
              raise "class #{name} < Hyperstack::Component::NativeLibrary could not import #{js_name}. "\
              "Native value #{scope_native_name(js_name)} is undefined."
            end
          end
        end

        def import_const_from_native(klass, const_name, create_library)
          native_name = lookup_native_name(const_name) ||
                        lookup_native_name(const_name[0].downcase + const_name[1..-1])
          native_name && (
            create_component_wrapper(klass, native_name, const_name) || (
              create_library &&
                create_library_wrapper(klass, native_name, const_name)))
        end

        def const_missing(const_name)
          import_const_from_native(self, const_name, true) || super
        end

        def method_missing(method, *args, &block)
          component_class = const_get(method) if const_defined?(method, false)
          component_class ||= import_const_from_native(self, method, false)
          raise 'could not import a react component named: '\
                "#{scope_native_name method}" unless component_class
          Hyperstack::Internal::Component::RenderingContext.render(component_class, *args, &block)
        end

        private

        def lookup_native_name(js_name)
          native_name = scope_native_name(js_name)
          `eval(#{native_name}) !== undefined && native_name`
        # rubocop:disable Lint/RescueException  # that is what eval raises in Opal >= 0.10.
        rescue Exception
          nil
          # rubocop:enable Lint/RescueException
        end

        def scope_native_name(js_name)
          "#{@__hyperstack_component_native_prefix}#{js_name}"
        end

        def create_component_wrapper(klass, native_name, ruby_name)
          if Hyperstack::Internal::Component::ReactWrapper.native_react_component?(native_name)
            new_klass = klass.const_set ruby_name, Class.new(NativeLibrary).imports(native_name)
            new_klass.class_eval do
              include Component
              imports native_name
            end
            new_klass
          end
        end

        def create_library_wrapper(klass, native_name, ruby_name)
          klass.const_set ruby_name, Class.new(NativeLibrary).imports(native_name)
        end
      end
      # handles the case of an application NativeLibrary called...
      # NativeLibrary !
      imports 'NativeLibrary'
    end
  end
end
