# frozen_string_literal: true

require "set"

module RubyNext
  module Core
    # Patch contains the extension implementation
    # and meta information (e.g., Ruby version).
    class Patch
      attr_reader :refineables, :name, :mod, :method_name, :version, :body, :singleton, :core_ext, :supported, :native, :location

      # Create a new patch for module/class (mod)
      # with the specified uniq name
      #
      # `core_ext` defines the strategy for core extensions:
      #    - :patch — extend class directly
      #    - :prepend — extend class by prepending a module (e.g., when needs `super`)
      def initialize(mod = nil, method:, name: nil, version:, supported: nil, native: nil, location: nil, refineable: mod, core_ext: :patch, singleton: nil)
        @mod = mod
        @method_name = method
        @version = version
        if method_name && mod
          @supported = supported.nil? ? mod.method_defined?(method_name) : supported
          # define whether running Ruby has a native implementation for this method
          # for that, we check the source_location (which is nil for C defined methods)
          @native = native.nil? ? (supported? && mod.instance_method(method_name).source_location.nil?) : native
        end
        @singleton = singleton
        @refineables = Array(refineable)
        @body = yield
        @core_ext = core_ext
        @location = location || build_location(caller_locations(1, 5))
        @name = name || build_module_name
      end

      def prepend?
        core_ext == :prepend
      end

      def core_ext?
        !mod.nil?
      end

      alias supported? supported
      alias native? native
      alias singleton? singleton

      def to_module
        Module.new.tap do |ext|
          ext.module_eval(body, *location)

          RubyNext::Core.const_set(name, ext)
        end
      end

      private

      def build_module_name
        mod_name = singleton? ? singleton.name : mod.name
        camelized_method_name = method_name.to_s.split("_").map(&:capitalize).join

        "#{mod_name}#{camelized_method_name}".gsub(/\W/, "")
      end

      def build_location(trace_locations)
        # The caller_locations behaviour depends on implementaion,
        # e.g. in JRuby https://github.com/jruby/jruby/issues/6055
        while trace_locations.first.label != "patch"
          trace_locations.shift
        end

        trace_location = trace_locations[1]

        [trace_location.absolute_path, trace_location.lineno + 2]
      end
    end

    # Registry for patches
    class Patches
      attr_reader :extensions, :refined

      def initialize
        @names = Set.new
        @extensions = Hash.new { |h, k| h[k] = [] }
        @refined = Hash.new { |h, k| h[k] = [] }
      end

      # Register new patch
      def <<(patch)
        raise ArgumentError, "Patch already registered: #{patch.name}" if @names.include?(patch.name)
        @names << patch.name
        @extensions[patch.mod] << patch if patch.core_ext?
        patch.refineables.each { |r| @refined[r] << patch } unless patch.native?
      end
    end

    class << self
      STRATEGIES = %i[refine core_ext].freeze

      attr_reader :strategy

      def strategy=(val)
        raise ArgumentError, "Unknown strategy: #{val}. Available: #{STRATEGIES.join(",")}" unless STRATEGIES.include?(val)
        @strategy = val
      end

      def refine?
        strategy == :refine
      end

      def core_ext?
        strategy == :core_ext
      end

      def patch(*args, **kwargs, &block)
        patches << Patch.new(*args, **kwargs, &block)
      end

      # Inject `using RubyNext` at the top of the source code
      def inject!(contents)
        if contents.frozen?
          contents = contents.sub(/^(\s*[^#\s].*)/, 'using RubyNext;\1')
        else
          contents.sub!(/^(\s*[^#\s].*)/, 'using RubyNext;\1')
        end
        contents
      end

      def patches
        @patches ||= Patches.new
      end
    end

    # Use refinements by default
    self.strategy = :refine
  end
end

require_relative "core/kernel/then"

require_relative "core/proc/compose"

require_relative "core/enumerable/tally"
require_relative "core/enumerable/filter"
require_relative "core/enumerable/filter_map"

require_relative "core/enumerator/produce"

require_relative "core/array/difference_union_intersection"

require_relative "core/hash/merge"

require_relative "core/string/split"

require_relative "core/unboundmethod/bind_call"

require_relative "core/time/floor"
require_relative "core/time/ceil"

# Core extensions required for pattern matching
# Required for pattern matching with refinements
unless defined?(NoMatchingPatternError)
  class NoMatchingPatternError < RuntimeError
  end
end

require_relative "core/constants/no_matching_pattern_error"
require_relative "core/array/deconstruct"
require_relative "core/hash/deconstruct_keys"
require_relative "core/struct/deconstruct"
require_relative "core/struct/deconstruct_keys"

# Generate refinements
RubyNext.module_eval do
  RubyNext::Core.patches.refined.each do |mod, patches|
    refine mod do
      patches.each do |patch|
        module_eval(patch.body, *patch.location)
      end
    end
  end
end