module V8
  # capture 99 stack frames on exception with normal details.
  # You can adjust these values for performance or turn of stack capture entirely
  V8::C::V8::SetCaptureStackTraceForUncaughtExceptions(true, 99, V8::C::StackTrace::kOverview)
  class Error < StandardError
    include Enumerable

    # @!attribute [r] value
    # @return [Object] the JavaScript value passed to the `throw` statement
    attr_reader :value

    # @!attribute [r] cause
    # @return [Exception] the underlying error (if any) that triggered this error to be raised
    attr_reader :cause

    # @!attribute [r] javascript_backtrace
    # @return [V8::StackTrace] the complete JavaScript stack at the point this error was thrown
    attr_reader :javascript_backtrace

    # keep an alias to the StandardError#backtrace method so that we can capture
    # just ruby backtrace frames
    alias_method :standard_error_backtrace, :backtrace

    def initialize(message, value, javascript_backtrace, cause = nil)
      super(message)
      @value = value
      @cause = cause
      @javascript_backtrace = javascript_backtrace
    end

    def causes
      [].tap do |causes|
        current = self
        until current.nil? do
          causes.push current
          current = current.respond_to?(:cause) ? current.cause : nil
        end
      end
    end

    def backtrace(*modifiers)
      return unless super()
      trace_framework = modifiers.include?(:framework)
      trace_ruby = modifiers.length == 0 || modifiers.include?(:ruby)
      trace_javascript = modifiers.length == 0 || modifiers.include?(:javascript)
      bilingual_backtrace(trace_ruby, trace_javascript).tap do |trace|
        trace.reject! {|frame| frame =~  %r{(lib/v8/.*\.rb|ext/v8/.*\.cc)}} unless modifiers.include?(:framework)
      end
    end

    def root_cause
      causes.last
    end

    def in_javascript?
      causes.last.is_a? self.class
    end

    def in_ruby?
      !in_javascript?
    end

    def bilingual_backtrace(trace_ruby = true, trace_javascript = true)
      backtrace = causes.reduce(:backtrace => [], :ruby => -1, :javascript => -1) { |accumulator, cause|
        accumulator.tap do
          if trace_ruby
            backtrace_selector = cause.respond_to?(:standard_error_backtrace) ? :standard_error_backtrace : :backtrace
            ruby_frames = cause.send(backtrace_selector)[0..accumulator[:ruby]]
            accumulator[:backtrace].unshift *ruby_frames
            accumulator[:ruby] -= ruby_frames.length
          end
          if trace_javascript && cause.respond_to?(:javascript_backtrace)
            javascript_frames = cause.javascript_backtrace.to_a[0..accumulator[:javascript]].map(&:to_s)
            accumulator[:backtrace].unshift *javascript_frames
            accumulator[:javascript] -= javascript_frames.length
          end
        end
      }[:backtrace]
    end

    module Try
      def try
        V8::C::TryCatch() do |trycatch|
          result = yield
          if trycatch.HasCaught()
            raise V8::Error(trycatch)
          else
            result
          end
        end
      end
    end

    module Protect
      def protect
        yield
      rescue Exception => e
        error = V8::C::Exception::Error(e.message)
        error.SetHiddenValue("rr::Cause", V8::C::External::New(e))
        V8::C::ThrowException(error)
      end
    end

  end

  # Convert the result of a triggered JavaScript try/catch block into
  # a V8::Error
  #
  # This is a bit of a yak-shave because JavaScript let's you throw all
  # kinds of things. We do our best to make sure that the message property
  # of the resulting V8::Error is as helpful as possible, and that it
  # contains as much source location information as we can put onto it.
  #
  # For example:
  #
  #    throw 4
  #    throw 'four'
  #    throw {number: 4}
  #
  # are all valid cases, none of which actually reference an exception object
  # with a stack trace and a message. only with something like:
  #
  #    throw new Error('fail!')
  #
  # do you get the a proper stacktrace and a message property. However a lot of
  # times JavaScript library authors are lazy and do this:
  #
  #     throw {message: 'foo', otherMetadata: 'bar'}
  #
  # It's common enough so we do the courtesy of having the resulting V8::Error
  # have as its message in ruby land the 'message' property of the value object
  #
  # To further complicate things, SyntaxErrors do not have a JavaScript stack
  # (even if they occur during js execution). This can make debugging a nightmare
  # so we copy in the source location of the syntax error into the message of
  # the resulting V8::Error
  #
  # @param [V8::C::TryCatch] native trycatch object that has been triggered
  # @return [V8::Error] the error generated by this try/catch
  def self.Error(trycatch)
    exception = trycatch.Exception()
    value = exception.to_ruby
    cause = nil
    javascript_backtrace = V8::StackTrace.new(trycatch.Message().GetStackTrace())
    message = if !exception.kind_of?(V8::C::Value)
      exception.to_s
    elsif exception.IsNativeError()
      if cause = exception.GetHiddenValue("rr::Cause")
        cause = cause.Value()
      end
      if value['constructor'] == V8::Context.current['SyntaxError']
        info = trycatch.Message()
        resource_name = info.GetScriptResourceName().to_ruby
        "#{value['message']} at #{resource_name}:#{info.GetLineNumber()}:#{info.GetStartColumn() + 1}"
      else
        exception.Get("message").to_ruby
      end
    elsif exception.IsObject()
      value['message'] || value.to_s
    else
      value.to_s
    end
    V8::Error.new(message, value, javascript_backtrace, cause)
  end
  const_set :JSError, Error
end