require_relative "version"
require_relative "hook"
require_relative "config"
require_relative "trace"
require_relative "class_map"
require_relative "metadata"
require_relative "util"
require_relative "open"

# load extension
require_relative "appmap"
require_relative "detect_enabled"

module AppMap
  class << self
    @configuration = nil
    @configuration_file_path = nil

    # Gets the configuration. If there is no configuration, the default
    # configuration is initialized.
    def configuration
      @configuration ||= initialize_configuration
    end

    # Sets the configuration. This is only expected to happen once per
    # Ruby process.
    def configuration=(config)
      warn "AppMap is already configured" if @configuration && config

      @configuration = config
    end

    def output_dir
      ENV["APPMAP_OUTPUT_DIR"] || DEFAULT_APPMAP_DIR
    end

    def default_config_file_path
      ENV["APPMAP_CONFIG_FILE"] || "appmap.yml"
    end

    # Configures AppMap for recording. Default behavior is to configure from
    # APPMAP_CONFIG_FILE, or 'appmap.yml'. If no config file is available, a
    # configuration will be automatically generated and used - and the user is prompted
    # to create the config file.
    #
    # This method also activates the code hooks which record function calls as trace events.
    # Call this function before the program code is loaded by the Ruby VM, otherwise
    # the load events won't be seen and the hooks won't activate.
    def initialize_configuration(config_file_path = default_config_file_path)
      Util.startup_message "Configuring AppMap from path #{config_file_path}"
      Config.load_from_file(config_file_path).tap do |configuration|
        self.configuration = configuration
        Hook.new(configuration).enable
      end
    end

    def info(msg)
      if defined?(::Rails) && defined?(::Rails.logger)
        ::Rails.logger.info msg
      else
        warn msg
      end
    end

    def tracing_enabled?(thread: nil)
      return false unless @tracing

      @tracing.enabled?(thread_id: thread&.object_id)
    end

    # Used to start tracing, stop tracing, and record events.
    def tracing
      @tracing ||= Trace::Tracing.new
    end

    # Records the events which occur while processing a block, and returns an AppMap as a Hash.
    # Recording may optionally capture only a single thread.
    def record(thread: nil)
      tracer = tracing.trace(thread: thread)
      tracer.enable
      begin
        yield
      ensure
        tracing.delete(tracer)
      end

      events = [].tap do |event_list|
        event_list << tracer.next_event.to_h while tracer.event?
      end
      {
        "version" => AppMap::APPMAP_FORMAT_VERSION,
        "metadata" => detect_metadata,
        "classMap" => class_map(tracer.event_methods),
        "events" => events
      }
    end

    # Uploads an AppMap to the AppLand website and displays it.
    def open(appmap = nil, &block)
      appmap ||= AppMap.record(&block)
      AppMap::Open.new(appmap).perform
    end

    # Builds a class map from a config and a list of Ruby methods.
    def class_map(methods)
      ClassMap.build_from_methods(methods)
    end

    # Returns default metadata detected from the Ruby system and from the
    # filesystem.
    def detect_metadata
      @metadata ||= Metadata.detect.freeze
      Util.deep_dup(@metadata)
    end

    def parameter_schema?
      ENV["APPMAP_PARAMETER_SCHEMA"] == "true"
    end

    def explain_queries?
      ENV["APPMAP_EXPLAIN_QUERIES"] == "true"
    end

    def recording_enabled?(recording_method = nil)
      DetectEnabled.new(recording_method).enabled?
    end
  end
end
