require "execjs/runtime"

module ExecJS
  class GraalJSRuntime < Runtime
    class Context < Runtime::Context
      def initialize(runtime, source = "", options = {})
        @context = Polyglot::InnerContext.new
        @context.eval('js', 'delete this.console')
        @js_object = @context.eval('js', 'Object')

        source = source.encode(Encoding::UTF_8)
        unless source.empty?
          translate do
            eval_in_context(source)
          end
        end
      end

      def exec(source, options = {})
        source = source.encode(Encoding::UTF_8)
        source = "(function(){#{source}})()" if /\S/.match?(source)

        translate do
          eval_in_context(source)
        end
      end

      def eval(source, options = {})
        source = source.encode(Encoding::UTF_8)
        source = "(#{source})" if /\S/.match?(source)

        translate do
          eval_in_context(source)
        end
      end

      def call(source, *args)
        source = source.encode(Encoding::UTF_8)
        source = "(#{source})" if /\S/.match?(source)

        translate do
          function = eval_in_context(source)
          function.call(*convert_ruby_to_js(args))
        end
      end

      private

      ForeignException = defined?(Polyglot::ForeignException) ? Polyglot::ForeignException : ::RuntimeError

      def translate
        convert_js_to_ruby yield
      rescue ForeignException => e
        if e.message && e.message.start_with?('SyntaxError:')
          error_class = ExecJS::RuntimeError
        else
          error_class = ExecJS::ProgramError
        end

        backtrace = (e.backtrace || []).map { |line| line.sub('(eval)', '(execjs)') }
        raise error_class, e.message, backtrace
      end

      def convert_js_to_ruby(value)
        case value
        when true, false, Integer, Float
          value
        else
          if value.nil?
            nil
          elsif value.respond_to?(:call)
            nil
          elsif value.respond_to?(:to_str)
            value.to_str
          elsif value.respond_to?(:to_ary)
            value.to_ary.map do |e|
              if e.respond_to?(:call)
                nil
              else
                convert_js_to_ruby(e)
              end
            end
          else
            object = value
            h = {}
            object.instance_variables.each do |member|
              v = object[member]
              unless v.respond_to?(:call)
                h[member.to_s] = convert_js_to_ruby(v)
              end
            end
            h
          end
        end
      end

      def convert_ruby_to_js(value)
        case value
        when nil, true, false, Integer, Float
          value
        when String, Symbol
          Truffle::Interop.as_truffle_string value
        when Array
          value.map { |e| convert_ruby_to_js(e) }
        when Hash
          h = @js_object.new
          value.each_pair do |k,v|
            h[convert_ruby_to_js(k)] = convert_ruby_to_js(v)
          end
          h
        else
          raise TypeError, "Unknown how to convert to JS: #{value.inspect}"
        end
      end

      class_eval <<-'RUBY', "(execjs)", 1
        def eval_in_context(code); @context.eval('js', code); end
      RUBY
    end

    def name
      "GraalVM (Graal.js)"
    end

    def available?
      return @available if defined?(@available)

      unless RUBY_ENGINE == "truffleruby"
        return @available = false
      end

      unless defined?(Polyglot::InnerContext)
        warn "TruffleRuby #{RUBY_ENGINE_VERSION} does not have support for inner contexts, use a more recent version", uplevel: 0 if $VERBOSE
        return @available = false
      end

      unless Polyglot.languages.include? "js"
        warn "The language 'js' is not available, you likely need to `export TRUFFLERUBYOPT='--jvm --polyglot'`", uplevel: 0 if $VERBOSE
        warn "You also need to install the 'js' component, see https://github.com/oracle/truffleruby/blob/master/doc/user/polyglot.md#installing-other-languages", uplevel: 0 if $VERBOSE
        return @available = false
      end

      @available = true
    end
  end
end
