# frozen_string_literal: true

module Lumberjack
  # Provides isolated fiber or thread local storage for thread-safe data access.
  module ContextLocals
    # Lightweight structure to hold context-local data.
    #
    # @api private
    class Data
      attr_accessor :context, :cleared

      def initialize(copy = nil)
        @context = copy&.context
        @cleared = copy&.cleared
      end
    end

    # Set the isolation level for context locals.
    #
    # @param value [Symbol] The isolation level, either :fiber or :thread.
    # @return [void]
    def isolation_level=(value)
      value = value&.to_sym
      value = :fiber unless [:fiber, :thread].include?(value)
      @isolation_level = value
    end

    # Get the isolation level for context locals.
    #
    # @return [Symbol] The isolation level, either :fiber or :thread.
    def isolation_level
      @isolation_level ||= :fiber
    end

    private

    def new_context_locals(&block)
      init_context_locals! unless defined?(@context_locals)

      if isolation_level == :fiber
        set_context_locals(&block)
      else
        set_context_locals_thread_id do
          set_context_locals(&block)
        end
      end
    end

    def set_context_locals(&block)
      scope_id = context_locals_scope_id
      current = nil
      data = nil

      begin
        @context_locals_mutex.synchronize do
          current = @context_locals[scope_id] if scope_id
          data = Data.new(current)
          @context_locals[scope_id] = data
        end
        yield data
      ensure
        @context_locals_mutex.synchronize do
          if current.nil?
            @context_locals.delete(scope_id)
          else
            @context_locals[scope_id] = current
          end
        end
      end
    end

    def current_context_locals
      return nil unless defined?(@context_locals)

      # Fast path that avoids the mutex when no context locals are set anywhere. This is
      # safe without synchronization: Hash#empty? only reads the size and cannot raise or
      # return garbage during a concurrent update, so the worst case is a stale value. A
      # stale false falls through to the synchronized lookup, which is always correct. A
      # stale true cannot hide an entry for the current scope because entries for the
      # current scope are only ever written from code running in that same scope, so any
      # such write is already visible here by program order.
      return nil if @context_locals.empty?

      scope_id = context_locals_scope_id
      @context_locals_mutex.synchronize do
        @context_locals[scope_id]
      end
    end

    # Initialize the context locals storage and mutex.
    def init_context_locals!
      @context_locals ||= {}
      @context_locals_mutex ||= Mutex.new
      @isolation_level ||= Lumberjack.isolation_level
    end

    def context_locals_scope_id
      if isolation_level == :fiber
        Fiber.current.object_id
      else
        Thread.current.thread_variable_get(:lumberjack_context_locals_thread_id)
      end
    end

    # Create a consistent thread ID for context locals. We can't use Thread.current.object_id
    # directly because it may change during execution (e.g., in JRuby when threads are
    # migrated between native threads). Instead we store a unique ID in a thread variable.
    def set_context_locals_thread_id
      thread_id = Thread.current.thread_variable_get(:lumberjack_context_locals_thread_id)
      return yield if thread_id

      thread_id = Object.new.object_id
      begin
        Thread.current.thread_variable_set(:lumberjack_context_locals_thread_id, thread_id)
        yield
      ensure
        Thread.current.thread_variable_set(:lumberjack_context_locals_thread_id, nil)
      end
    end
  end
end
