module Steep
  class Project
    class Target
      attr_reader :name
      attr_reader :options

      attr_reader :source_patterns
      attr_reader :ignore_patterns
      attr_reader :signature_patterns

      attr_reader :source_files
      attr_reader :signature_files

      attr_reader :status

      SignatureSyntaxErrorStatus = Struct.new(:timestamp, :errors, keyword_init: true)
      SignatureValidationErrorStatus = Struct.new(:timestamp, :errors, keyword_init: true)
      SignatureOtherErrorStatus = Struct.new(:timestamp, :error, keyword_init: true)
      TypeCheckStatus = Struct.new(:environment, :subtyping, :type_check_sources, :timestamp, keyword_init: true)

      def initialize(name:, options:, source_patterns:, ignore_patterns:, signature_patterns:)
        @name = name
        @options = options
        @source_patterns = source_patterns
        @ignore_patterns = ignore_patterns
        @signature_patterns = signature_patterns

        @source_files = {}
        @signature_files = {}
      end

      def add_source(path, content = "")
        file = SourceFile.new(path: path)

        if block_given?
          file.content = yield
        else
          file.content = content
        end

        source_files[path] = file
      end

      def remove_source(path)
        source_files.delete(path)
      end

      def update_source(path, content = nil)
        file = source_files[path]
        if block_given?
          file.content = yield(file.content)
        else
          file.content = content || file.content
        end
      end

      def add_signature(path, content = "")
        file = SignatureFile.new(path: path)
        if block_given?
          file.content = yield
        else
          file.content = content
        end
        signature_files[path] = file
      end

      def remove_signature(path)
        signature_files.delete(path)
      end

      def update_signature(path, content = nil)
        file = signature_files[path]
        if block_given?
          file.content = yield(file.content)
        else
          file.content = content || file.content
        end
      end

      def source_file?(path)
        source_files.key?(path)
      end

      def signature_file?(path)
        signature_files.key?(path)
      end

      def possible_source_file?(path)
        self.class.test_pattern(source_patterns, path, ext: ".rb") &&
          !self.class.test_pattern(ignore_patterns, path, ext: ".rb")
      end

      def possible_signature_file?(path)
        self.class.test_pattern(signature_patterns, path, ext: ".rbs")
      end

      def self.test_pattern(patterns, path, ext:)
        patterns.any? do |pattern|
          p = pattern.end_with?(File::Separator) ? pattern : pattern + File::Separator
          p.delete_prefix!('./')
          (path.to_s.start_with?(p) && path.extname == ext) || File.fnmatch(pattern, path.to_s)
        end
      end

      def type_check(target_sources: source_files.values, validate_signatures: true)
        Steep.logger.tagged "target#type_check(target_sources: [#{target_sources.map(&:path).join(", ")}], validate_signatures: #{validate_signatures})" do
          Steep.measure "load signature and type check" do
            load_signatures(validate: validate_signatures) do |env, check, timestamp|
              Steep.measure "type checking #{target_sources.size} files" do
                run_type_check(env, check, timestamp, target_sources: target_sources)
              end
            end
          end
        end
      end

      def self.construct_env_loader(options:)
        repo = RBS::Repository.new(no_stdlib: options.vendor_path)
        options.repository_paths.each do |path|
          repo.add(path)
        end

        loader = RBS::EnvironmentLoader.new(
          core_root: options.vendor_path ? nil : RBS::EnvironmentLoader::DEFAULT_CORE_ROOT,
          repository: repo
        )
        loader.add(path: options.vendor_path) if options.vendor_path
        options.libraries.each do |lib|
          name, version = lib.split(/:/, 2)
          loader.add(library: name, version: version)
        end

        loader
      end

      def environment
        @environment ||= RBS::Environment.from_loader(Target.construct_env_loader(options: options))
      end

      def load_signatures(validate:)
        timestamp = case status
                    when TypeCheckStatus
                      status.timestamp
                    end
        now = Time.now

        updated_files = []

        signature_files.each_value do |file|
          if !timestamp || file.content_updated_at >= timestamp
            updated_files << file
            file.load!()
          end
        end

        if signature_files.each_value.all? {|file| file.status.is_a?(SignatureFile::DeclarationsStatus) }
          if status.is_a?(TypeCheckStatus) && updated_files.empty?
            yield status.environment, status.subtyping, status.timestamp
          else
            begin
              env = environment.dup

              signature_files.each_value do |file|
                if file.status.is_a?(SignatureFile::DeclarationsStatus)
                  file.status.declarations.each do |decl|
                    env << decl
                  end
                end
              end

              env = env.resolve_type_names

              definition_builder = RBS::DefinitionBuilder.new(env: env)
              factory = AST::Types::Factory.new(builder: definition_builder)
              check = Subtyping::Check.new(factory: factory)

              if validate
                validator = Signature::Validator.new(checker: check)
                validator.validate()

                if validator.no_error?
                  yield env, check, now
                else
                  @status = SignatureValidationErrorStatus.new(
                    errors: validator.each_error.to_a,
                    timestamp: now
                  )
                end
              else
                yield env, check, Time.now
              end
            rescue RBS::DuplicatedDeclarationError => exn
              @status = SignatureValidationErrorStatus.new(
                errors: [
                  Signature::Errors::DuplicatedDefinitionError.new(
                    name: exn.name,
                    location: exn.decls[0].location
                  )
                ],
                timestamp: now
              )
            rescue => exn
              Steep.log_error exn
              @status = SignatureOtherErrorStatus.new(error: exn, timestamp: now)
            end
          end

        else
          errors = signature_files.each_value.with_object([]) do |file, errors|
            if file.status.is_a?(SignatureFile::ParseErrorStatus)
              errors << file.status.error
            end
          end

          @status = SignatureSyntaxErrorStatus.new(
            errors: errors,
            timestamp: Time.now
          )
        end
      end

      def run_type_check(env, check, timestamp, target_sources: source_files.values)
        type_check_sources = []

        target_sources.each do |file|
          Steep.logger.tagged("path=#{file.path}") do
            if file.type_check(check, timestamp)
              type_check_sources << file
            end
          end
        end

        @status = TypeCheckStatus.new(
          environment: env,
          subtyping: check,
          type_check_sources: type_check_sources,
          timestamp: timestamp
        )
      end

      def no_error?
        source_files.all? do |_, file|
          file.status.is_a?(Project::SourceFile::TypeCheckStatus)
        end
      end

      def errors
        case status
        when TypeCheckStatus
          source_files.each_value.flat_map(&:errors).select { |error | options.error_to_report?(error) }
        else
          []
        end
      end
    end
  end
end