module Ruby
  module Signature
    class EnvironmentLoader
      class UnknownLibraryNameError < StandardError
        attr_reader :name

        def initialize(name:)
          @name = name
          super "Cannot find a library or gem: `#{name}`"
        end
      end

      LibraryPath = Struct.new(:name, :path, keyword_init: true)
      GemPath = Struct.new(:name, :version, :path, keyword_init: true)

      attr_reader :paths
      attr_reader :stdlib_root
      attr_reader :gem_vendor_path

      STDLIB_ROOT = Pathname(__dir__) + "../../../stdlib"

      def self.gem_sig_path(name, version)
        Pathname(Gem::Specification.find_by_name(name, version).gem_dir) + "sig"
      rescue Gem::MissingSpecError
        nil
      end

      def initialize(stdlib_root: STDLIB_ROOT, gem_vendor_path: nil)
        @stdlib_root = stdlib_root
        @gem_vendor_path = gem_vendor_path
        @paths = []
        @no_builtin = false
      end

      def add(path: nil, library: nil)
        case
        when path
          @paths << path
        when library
          name, version = self.class.parse_library(library)

          case
          when !version && path = stdlib?(name)
            @paths << LibraryPath.new(name: name, path: path)
          when (path = gem?(name, version))
            @paths << GemPath.new(name: name, version: version, path: path)
          else
            raise UnknownLibraryNameError.new(name: library)
          end
        end
      end

      def self.parse_library(lib)
        lib.split(/:/)
      end

      def stdlib?(name)
        if stdlib_root
          path = stdlib_root + name
          if path.directory?
            path
          end
        end
      end

      def gem?(name, version)
        if gem_vendor_path
          # Try vendored RBS first
          gem_dir = gem_vendor_path + name
          if gem_dir.directory?
            return gem_dir
          end
        end

        # Try ruby gem library
        self.class.gem_sig_path(name, version)
      end

      def each_signature(path = nil, immediate: true, &block)
        if block_given?
          if path
            case
            when path.file?
              if path.extname == ".rbs" || immediate
                yield path
              end
            when path.directory?
              path.children.each do |child|
                each_signature child, immediate: false, &block
              end
            end
          else
            paths.each do |path|
              case path
              when Pathname
                each_signature path, immediate: immediate, &block
              when LibraryPath
                each_signature path.path, immediate: immediate, &block
              when GemPath
                each_signature path.path, immediate: immediate, &block
              end
            end
          end
        else
          enum_for :each_signature, path, immediate: immediate
        end
      end

      def no_builtin!
        @no_builtin = true
      end

      def no_builtin?
        @no_builtin
      end

      def load(env:)
        signature_files = []

        unless no_builtin?
          signature_files.push(*each_signature(stdlib_root + "builtin"))
        end

        each_signature do |path|
          signature_files.push path
        end

        signature_files.each do |file|
          buffer = Buffer.new(name: file.to_s, content: file.read)
          env.buffers.push(buffer)
          Parser.parse_signature(buffer).each do |decl|
            env << decl
          end
        end
      end
    end
  end
end