require "hyperstack/internal/component"

module Hyperstack
  module Component
    module IsomorphicHelpers
      def self.included(base)
        base.extend(ClassMethods)
      end

      if RUBY_ENGINE != 'opal'
        def self.load_context(ctx, controller, name = nil)
          @context = Context.new("#{controller.object_id}-#{Time.now.to_i}", ctx, controller, name)
          @context.load_opal_context
          ::Rails.logger.debug "************************** React Server Context Initialized #{name} #{Time.now.to_f} *********************************************"
          @context
        end
      else
        def self.load_context(unique_id = nil, name = nil)
          # can be called on the client to force re-initialization for testing purposes
          if !unique_id || !@context || @context.unique_id != unique_id
            message =
              if on_opal_server?
                `console.history = []` rescue nil
                "************************ React Prerendering Context Initialized #{name} ***********************"
              else
                '************************ React Browser Context Initialized ****************************'
              end

            log(message)

            @context = Context.new(unique_id)
          end

          # True is returned here because this method is evaluated by MiniRacer,
          #   and can cause TypeError: Converting circular structure to JSON to raise
          true
        end
      end

      def self.context
        @context
      end

      def self.log(message, message_type = :info)
        message = [message] unless message.is_a? Array

        if (message_type == :info || message_type == :warning) && Hyperstack.env.production?
          return
        end

        if message_type == :info
          if on_opal_server?
            style = 'background: #00FFFF; color: red'
          else
            style = 'background: #222; color: #bada55'
          end
          message = ["%c" + message[0], style]+message[1..-1]
          `console.log.apply(console, message)`
        elsif message_type == :warning
          `console.warn.apply(console, message)`
        else
          `console.error.apply(console, message)`
        end
      end

      if RUBY_ENGINE != 'opal'
        def self.on_opal_server?
          false
        end

        def self.on_opal_client?
          false
        end
      else
        def self.on_opal_server?
          `typeof Opal.global.document === 'undefined'`
        end

        def self.on_opal_client?
          !on_opal_server?
        end
      end

      def log(*args)
        IsomorphicHelpers.log(*args)
      end

      def on_opal_server?
        self.class.on_opal_server?
      end

      def on_opal_client?
        self.class.on_opal_client?
      end

      def self.prerender_footers(controller = nil)
        footer = Context.prerender_footer_blocks.collect { |block| block.call controller }.join("\n")
        if RUBY_ENGINE != 'opal'
          footer = (footer + @context.send_to_opal(:prerender_footers).to_s) if @context
          footer = footer.html_safe
        end
        footer
      end

      class Context
        attr_reader :controller
        attr_reader :unique_id

        def self.define_isomorphic_method(method_name, &block)
          @@ctx_methods ||= {}
          @@ctx_methods[method_name] = block
        end

        def self.before_first_mount_blocks
          @before_first_mount_blocks ||= []
        end

        def self.prerender_footer_blocks
          @prerender_footer_blocks ||= []
        end

        def initialize(unique_id, ctx = nil, controller = nil, cname = nil)
          @unique_id = unique_id
          @cname = cname
          if RUBY_ENGINE != 'opal'
            @controller = controller
            @ctx = ctx
            if defined? @@ctx_methods
              @@ctx_methods.each do |method_name, block|
                @ctx.attach("ServerSideIsomorphicMethod.#{method_name}", proc{|args| block.call(args.to_json)})
              end
            end
          end
          Hyperstack::Application::Boot.run(context: self)
          self.class.before_first_mount_blocks.each { |block| block.call(self) }
        end

        def load_opal_context
          send_to_opal(:load_context, @unique_id, @cname)
        end

        def eval(js)
          @ctx.eval(js) if @ctx
        end

        def send_to_opal(method_name, *args)
          return unless @ctx
          Hyperstack::Internal::Component::Rails::ComponentLoader.new(@ctx).load!
          method_args = args.collect do |arg|
            quarg = "#{arg}".tr('"', "'")
            "\"#{quarg}\""
          end.join(', ')
          @ctx.eval("Opal.Hyperstack.$const_get('Component').$const_get('IsomorphicHelpers').$#{method_name}(#{method_args})")
        end

        def self.register_before_first_mount_block(&block)
          before_first_mount_blocks << block
        end

        def self.register_prerender_footer_block(&block)
          prerender_footer_blocks << block
        end
      end

      class IsomorphicProcCall

        attr_reader :context

        def result
          @result.first if @result
        end

        def initialize(name, block, context, *args)
          @name = name
          @context = context
          block.call(self, *args)
          @result ||= send_to_server(*args)
        end

        def when_on_client(&block)
          @result = [block.call] if IsomorphicHelpers.on_opal_client?
        end

        def send_to_server(*args)
          if IsomorphicHelpers.on_opal_server?
            method_string = "ServerSideIsomorphicMethod." + @name + "(" + args.to_json + ")"
            @result = [JSON.parse(`eval(method_string)`)]
          end
        end

        def when_on_server(&block)
          @result = [block.call.to_json] unless IsomorphicHelpers.on_opal_client? || IsomorphicHelpers.on_opal_server?
        end
      end

      module ClassMethods
        def on_opal_server?
          IsomorphicHelpers.on_opal_server?
        end

        def on_opal_client?
          IsomorphicHelpers.on_opal_client?
        end

        def log(*args)
          IsomorphicHelpers.log(*args)
        end

        def controller
          IsomorphicHelpers.context.controller
        end

        def before_first_mount(&block)
          IsomorphicHelpers::Context.register_before_first_mount_block(&block)
        end

        def prerender_footer(&block)
          IsomorphicHelpers::Context.register_prerender_footer_block(&block)
        end

        if RUBY_ENGINE != 'opal'
          def isomorphic_method(name, &block)
            IsomorphicHelpers::Context.send(:define_isomorphic_method, name) do |args_as_json|
              IsomorphicHelpers::IsomorphicProcCall.new(name, block, self, *JSON.parse(args_as_json)).result
            end
          end
        else
          def isomorphic_method(name, &block)
            self.class.send(:define_method, name) do | *args |
              IsomorphicHelpers::IsomorphicProcCall.new(name, block, self, *args).result
            end
          end
        end

      end
    end
  end
end
