# frozen_string_literal: true

require "ffi"

module Appsignal
  class Extension
    # JRuby extension wrapper
    #
    # Only loaded if the system is detected as JRuby.
    #
    # @api private
    module Jruby
      extend FFI::Library

      # JRuby extension String helpers.
      #
      # Based on the make_appsignal_string and make_ruby_string helpers from the
      # AppSignal C-extension in `ext/appsignal_extension.c`.
      module StringHelpers
        class AppsignalString < FFI::Struct
          layout :len, :size_t,
            :buf, :pointer
        end

        def make_appsignal_string(ruby_string)
          raise ArgumentError, "argument is not a string" unless ruby_string.is_a?(String)

          AppsignalString.new.tap do |appsignal_string|
            appsignal_string[:len] = ruby_string.bytesize
            appsignal_string[:buf] = FFI::MemoryPointer.from_string(ruby_string)
          end
        end

        def make_ruby_string(appsignal_string)
          appsignal_string[:buf].read_string(appsignal_string[:len]).tap do |ruby_string|
            ruby_string.force_encoding(Encoding::UTF_8)
          end
        end
      end
      include StringHelpers

      def self.lib_extension
        if Appsignal::System.agent_platform.include?("darwin")
          "dylib"
        else
          "so"
        end
      end

      begin
        begin
          # RubyGems will install the extension in the gem's lib directory.
          ffi_lib File.join(File.dirname(__FILE__), "../../../lib/libappsignal.#{lib_extension}")
        rescue LoadError
          ffi_lib File.join(File.dirname(__FILE__), "../../../ext/libappsignal.#{lib_extension}")
        end
        typedef AppsignalString.by_value, :appsignal_string

        attach_function :appsignal_start, [], :void
        attach_function :appsignal_stop, [], :void
        attach_function :appsignal_diagnose, [], :appsignal_string
        attach_function :appsignal_get_server_state,
          [:appsignal_string],
          :appsignal_string
        attach_function :appsignal_running_in_container, [], :bool
        attach_function :appsignal_set_environment_metadata,
          [:appsignal_string, :appsignal_string],
          :void

        # Metrics methods
        attach_function :appsignal_set_gauge,
          [:appsignal_string, :double, :pointer],
          :void
        attach_function :appsignal_increment_counter,
          [:appsignal_string, :double, :pointer],
          :void
        attach_function :appsignal_add_distribution_value,
          [:appsignal_string, :double, :pointer],
          :void

        # Logging methods
        attach_function :appsignal_log,
          [:appsignal_string, :int32, :int32, :appsignal_string, :pointer],
          :void

        # Transaction methods
        attach_function :appsignal_free_transaction,
          [:pointer],
          :void
        attach_function :appsignal_start_transaction,
          [:appsignal_string, :appsignal_string, :long],
          :pointer
        attach_function :appsignal_start_event,
          [:pointer, :long],
          :void
        attach_function :appsignal_finish_event,
          [:pointer, :appsignal_string, :appsignal_string, :appsignal_string, :int64,
           :long],
          :void
        attach_function :appsignal_finish_event_data,
          [:pointer, :appsignal_string, :appsignal_string, :pointer, :int64, :long],
          :void
        attach_function :appsignal_record_event,
          [:pointer, :appsignal_string, :appsignal_string, :appsignal_string, :int64,
           :long, :long],
          :void
        attach_function :appsignal_record_event_data,
          [:pointer, :appsignal_string, :appsignal_string, :pointer, :int64, :long,
           :long],
          :void
        attach_function :appsignal_set_transaction_error,
          [:pointer, :appsignal_string, :appsignal_string, :pointer],
          :void
        attach_function :appsignal_set_transaction_action,
          [:pointer, :appsignal_string],
          :void
        attach_function :appsignal_set_transaction_namespace,
          [:pointer, :appsignal_string],
          :void
        attach_function :appsignal_set_transaction_sample_data,
          [:pointer, :appsignal_string, :pointer],
          :void
        attach_function :appsignal_set_transaction_queue_start,
          [:pointer, :long],
          :void
        attach_function :appsignal_set_transaction_metadata,
          [:pointer, :appsignal_string, :appsignal_string],
          :void
        attach_function :appsignal_finish_transaction,
          [:pointer, :long],
          :bool
        attach_function :appsignal_complete_transaction,
          [:pointer],
          :void
        attach_function :appsignal_duplicate_transaction,
          [:pointer, :appsignal_string],
          :pointer
        attach_function :appsignal_transaction_to_json,
          [:pointer],
          :appsignal_string

        # Span methods
        attach_function :appsignal_create_root_span,
          [:appsignal_string],
          :pointer
        attach_function :appsignal_create_root_span_with_timestamp,
          [:appsignal_string, :int64, :int64],
          :pointer
        attach_function :appsignal_create_child_span,
          [:pointer],
          :pointer
        attach_function :appsignal_create_child_span_with_timestamp,
          [:pointer, :int64, :int64],
          :pointer
        attach_function :appsignal_create_span_from_traceparent,
          [:appsignal_string],
          :pointer
        attach_function :appsignal_span_id,
          [:pointer],
          :appsignal_string
        attach_function :appsignal_span_to_json,
          [:pointer],
          :appsignal_string
        attach_function :appsignal_set_span_name,
          [:pointer, :appsignal_string],
          :void
        attach_function :appsignal_set_span_namespace,
          [:pointer, :appsignal_string],
          :void
        attach_function :appsignal_add_span_error,
          [:pointer, :appsignal_string, :appsignal_string, :pointer],
          :void
        attach_function :appsignal_set_span_sample_data,
          [:pointer, :appsignal_string, :pointer],
          :void
        attach_function :appsignal_set_span_attribute_string,
          [:pointer, :appsignal_string, :appsignal_string],
          :void
        attach_function :appsignal_set_span_attribute_sql_string,
          [:pointer, :appsignal_string, :appsignal_string],
          :void
        attach_function :appsignal_set_span_attribute_int,
          [:pointer, :appsignal_string, :int64],
          :void
        attach_function :appsignal_set_span_attribute_bool,
          [:pointer, :appsignal_string, :bool],
          :void
        attach_function :appsignal_set_span_attribute_double,
          [:pointer, :appsignal_string, :double],
          :void
        attach_function :appsignal_close_span,
          [:pointer],
          :void
        attach_function :appsignal_close_span_with_timestamp,
          [:pointer, :int64, :int64],
          :void
        attach_function :appsignal_free_span,
          [:pointer],
          :void

        # Data struct methods
        attach_function :appsignal_free_data, [:pointer], :void
        attach_function :appsignal_data_map_new, [], :pointer
        attach_function :appsignal_data_array_new, [], :pointer
        attach_function :appsignal_data_map_set_string,
          [:pointer, :appsignal_string, :appsignal_string],
          :void
        attach_function :appsignal_data_map_set_integer,
          [:pointer, :appsignal_string, :int64],
          :void
        attach_function :appsignal_data_map_set_float,
          [:pointer, :appsignal_string, :double],
          :void
        attach_function :appsignal_data_map_set_boolean,
          [:pointer, :appsignal_string, :bool],
          :void
        attach_function :appsignal_data_map_set_null,
          [:pointer, :appsignal_string],
          :void
        attach_function :appsignal_data_map_set_data,
          [:pointer, :appsignal_string, :pointer],
          :void
        attach_function :appsignal_data_array_append_string,
          [:pointer, :appsignal_string],
          :void
        attach_function :appsignal_data_array_append_integer,
          [:pointer, :int64],
          :void
        attach_function :appsignal_data_array_append_float,
          [:pointer, :double],
          :void
        attach_function :appsignal_data_array_append_boolean,
          [:pointer, :bool],
          :void
        attach_function :appsignal_data_array_append_null,
          [:pointer],
          :void
        attach_function :appsignal_data_array_append_data,
          [:pointer, :pointer],
          :void
        attach_function :appsignal_data_equal,
          [:pointer, :pointer],
          :bool
        attach_function :appsignal_data_to_json,
          [:pointer],
          :appsignal_string

        Appsignal.extension_loaded = true if Appsignal.respond_to? :extension_loaded=
      rescue LoadError => error
        error_message = "ERROR: AppSignal failed to load extension. " \
          "Please run `appsignal diagnose` and email us at support@appsignal.com\n" \
          "#{error.class}: #{error.message}"
        Appsignal.internal_logger.error(error_message) if Appsignal.respond_to? :internal_logger
        Kernel.warn error_message
        Appsignal.extension_loaded = false if Appsignal.respond_to? :extension_loaded=
        raise error if ENV["_APPSIGNAL_EXTENSION_INSTALL"] == "true"
      end

      def start
        appsignal_start
      end

      def stop
        appsignal_stop
      end

      def diagnose
        make_ruby_string(appsignal_diagnose)
      end

      def get_server_state(key)
        state = appsignal_get_server_state(make_appsignal_string(key))
        make_ruby_string state if state[:len] > 0
      end

      def log(group, level, format, message, attributes)
        appsignal_log(
          make_appsignal_string(group),
          level,
          format,
          make_appsignal_string(message),
          attributes.pointer
        )
      end

      def start_transaction(transaction_id, namespace, gc_duration_ms)
        transaction = appsignal_start_transaction(
          make_appsignal_string(transaction_id),
          make_appsignal_string(namespace),
          gc_duration_ms
        )

        return if !transaction || transaction.null?

        Transaction.new(transaction)
      end

      def data_map_new
        Data.new(appsignal_data_map_new)
      end

      def data_array_new
        Data.new(appsignal_data_array_new)
      end

      def running_in_container?
        appsignal_running_in_container
      end

      def set_environment_metadata(key, value)
        appsignal_set_environment_metadata(
          make_appsignal_string(key),
          make_appsignal_string(value)
        )
      end

      def set_gauge(key, value, tags)
        appsignal_set_gauge(make_appsignal_string(key), value, tags.pointer)
      end

      def increment_counter(key, value, tags)
        appsignal_increment_counter(make_appsignal_string(key), value, tags.pointer)
      end

      def add_distribution_value(key, value, tags)
        appsignal_add_distribution_value(make_appsignal_string(key), value, tags.pointer)
      end

      class Transaction
        include StringHelpers

        attr_reader :pointer

        def initialize(pointer)
          @pointer = FFI::AutoPointer.new(
            pointer,
            Extension.method(:appsignal_free_transaction)
          )
        end

        def start_event(gc_duration_ms)
          Extension.appsignal_start_event(pointer, gc_duration_ms)
        end

        def finish_event(name, title, body, body_format, gc_duration_ms)
          case body
          when String
            method = :appsignal_finish_event
            body_arg = make_appsignal_string(body)
          when Data
            method = :appsignal_finish_event_data
            body_arg = body.pointer
          else
            raise ArgumentError,
              "body argument should be a String or Appsignal::Extension::Data"
          end
          Extension.public_send(
            method,
            pointer,
            make_appsignal_string(name),
            make_appsignal_string(title),
            body_arg,
            body_format,
            gc_duration_ms
          )
        end

        def record_event(name, title, body, body_format, duration, gc_duration_ms) # rubocop:disable Metrics/ParameterLists
          case body
          when String
            method = :appsignal_record_event
            body_arg = make_appsignal_string(body)
          when Data
            method = :appsignal_record_event_data
            body_arg = body.pointer
          else
            raise ArgumentError,
              "body argument should be a String or Appsignal::Extension::Data"
          end
          Extension.public_send(
            method,
            pointer,
            make_appsignal_string(name),
            make_appsignal_string(title),
            body_arg,
            body_format,
            duration,
            gc_duration_ms
          )
        end

        def set_error(name, message, backtrace)
          Extension.appsignal_set_transaction_error(
            pointer,
            make_appsignal_string(name),
            make_appsignal_string(message),
            backtrace.pointer
          )
        end

        def set_action(action_name) # rubocop:disable Naming/AccessorMethodName
          Extension.appsignal_set_transaction_action(
            pointer,
            make_appsignal_string(action_name)
          )
        end

        def set_namespace(namespace) # rubocop:disable Naming/AccessorMethodName
          Extension.appsignal_set_transaction_namespace(
            pointer,
            make_appsignal_string(namespace)
          )
        end

        def set_sample_data(key, payload)
          Extension.appsignal_set_transaction_sample_data(
            pointer,
            make_appsignal_string(key),
            payload.pointer
          )
        end

        def set_queue_start(time) # rubocop:disable Naming/AccessorMethodName
          Extension.appsignal_set_transaction_queue_start(pointer, time)
        end

        def set_metadata(key, value)
          Extension.appsignal_set_transaction_metadata(
            pointer,
            make_appsignal_string(key),
            make_appsignal_string(value)
          )
        end

        def finish(gc_duration_ms)
          Extension.appsignal_finish_transaction(pointer, gc_duration_ms)
        end

        def duplicate(new_transaction_id)
          duplicate_transaction = Extension.appsignal_duplicate_transaction(
            pointer,
            make_appsignal_string(new_transaction_id)
          )

          return if !duplicate_transaction || duplicate_transaction.null?

          Transaction.new(duplicate_transaction)
        end

        def complete
          Extension.appsignal_complete_transaction(pointer)
        end

        def to_json # rubocop:disable Lint/ToJSON
          json = Extension.appsignal_transaction_to_json(pointer)
          make_ruby_string(json) if json[:len] > 0
        end
      end

      class Span
        include StringHelpers
        extend StringHelpers

        attr_reader :pointer

        def initialize(pointer)
          @pointer = FFI::AutoPointer.new(
            pointer,
            Extension.method(:appsignal_free_span)
          )
        end

        def self.root(namespace)
          namespace = make_appsignal_string(namespace)
          Span.new(Extension.appsignal_create_root_span(namespace))
        end

        def child
          Span.new(Extension.appsignal_create_child_span(pointer))
        end

        def add_error(name, message, backtrace)
          Extension.appsignal_add_span_error(
            pointer,
            make_appsignal_string(name),
            make_appsignal_string(message),
            backtrace.pointer
          )
        end

        def set_sample_data(key, payload)
          Extension.appsignal_set_span_sample_data(
            pointer,
            make_appsignal_string(key),
            payload.pointer
          )
        end

        def set_name(name) # rubocop:disable Naming/AccessorMethodName
          Extension.appsignal_set_span_name(
            pointer,
            make_appsignal_string(name)
          )
        end

        def set_attribute_string(key, value)
          Extension.appsignal_set_span_attribute_string(
            pointer,
            make_appsignal_string(key),
            make_appsignal_string(value)
          )
        end

        def set_attribute_int(key, value)
          Extension.appsignal_set_span_attribute_int(
            pointer,
            make_appsignal_string(key),
            value
          )
        end

        def set_attribute_bool(key, value)
          Extension.appsignal_set_span_attribute_bool(
            pointer,
            make_appsignal_string(key),
            value
          )
        end

        def set_attribute_double(key, value)
          Extension.appsignal_set_span_attribute_double(
            pointer,
            make_appsignal_string(key),
            value
          )
        end

        def to_json # rubocop:disable Lint/ToJSON
          json = Extension.appsignal_span_to_json(pointer)
          make_ruby_string(json) if json[:len] > 0
        end

        def close
          Extension.appsignal_close_span(pointer)
        end
      end

      class Data
        include StringHelpers
        attr_reader :pointer

        def initialize(pointer)
          @pointer = FFI::AutoPointer.new(
            pointer,
            Extension.method(:appsignal_free_data)
          )
        end

        def set_string(key, value)
          Extension.appsignal_data_map_set_string(
            pointer,
            make_appsignal_string(key),
            make_appsignal_string(value)
          )
        end

        def set_integer(key, value)
          Extension.appsignal_data_map_set_integer(
            pointer,
            make_appsignal_string(key),
            value
          )
        end

        def set_float(key, value)
          Extension.appsignal_data_map_set_float(
            pointer,
            make_appsignal_string(key),
            value
          )
        end

        def set_boolean(key, value)
          Extension.appsignal_data_map_set_boolean(
            pointer,
            make_appsignal_string(key),
            value
          )
        end

        def set_nil(key) # rubocop:disable Naming/AccessorMethodName
          Extension.appsignal_data_map_set_null(
            pointer,
            make_appsignal_string(key)
          )
        end

        def set_data(key, value)
          Extension.appsignal_data_map_set_data(
            pointer,
            make_appsignal_string(key),
            value.pointer
          )
        end

        def append_string(value)
          Extension.appsignal_data_array_append_string(
            pointer,
            make_appsignal_string(value)
          )
        end

        def append_integer(value)
          Extension.appsignal_data_array_append_integer(pointer, value)
        end

        def append_float(value)
          Extension.appsignal_data_array_append_float(pointer, value)
        end

        def append_boolean(value)
          Extension.appsignal_data_array_append_boolean(pointer, value)
        end

        def append_nil
          Extension.appsignal_data_array_append_null(pointer)
        end

        def append_data(value)
          Extension.appsignal_data_array_append_data(pointer, value.pointer)
        end

        def ==(other)
          Extension.appsignal_data_equal(pointer, other.pointer)
        end

        def to_s
          make_ruby_string Extension.appsignal_data_to_json(pointer)
        end
      end
    end
  end
end
