# frozen_string_literal: true

module RSpecTracer
  class CoverageReporter
    COVERAGE_MODE = {
      array: 'array',
      hash: 'hash'
    }.freeze

    attr_reader :mode, :coverage, :coverage_stat, :examples_coverage

    def initialize
      @mode = if !RSpecTracer.simplecov? || ::Coverage.peek_result.first.last.is_a?(Array)
                COVERAGE_MODE[:array]
              else
                COVERAGE_MODE[:hash]
              end

      @examples_coverage = Hash.new do |examples, example_id|
        examples[example_id] = Hash.new do |files, file_path|
          files[file_path] = {}
        end
      end
    end

    def record_coverage
      @coverage = peek_coverage
    end

    def compute_diff(example_id)
      peek_coverage.each_pair do |file_path, current_stats|
        unless @coverage.key?(file_path)
          missing_file_diff_coverage(example_id, file_path, current_stats)

          next
        end

        next if current_stats == @coverage[file_path]

        existing_file_diff_coverage(example_id, file_path, current_stats)
      end
    end

    def generate_final_examples_coverage
      filtered_files = Set.new

      @examples_coverage.each_key do |example_id|
        @examples_coverage[example_id].select! do |file_path|
          next false if filtered_files.include?(file_path)

          file_name = RSpecTracer::SourceFile.file_name(file_path)

          if RSpecTracer.coverage_filters.any? { |filter| filter.match?(file_name: file_name) }
            filtered_files << file_path

            false
          else
            true
          end
        end
      end
    end

    def merge_coverage(missed_coverage)
      record_coverage

      missed_coverage.each_pair do |file_path, line_coverage|
        line_coverage_dup = if @coverage.key?(file_path)
                              @coverage[file_path].dup
                            else
                              line_stub(file_path)
                            end

        line_coverage.each_pair do |line_number, strength|
          line_coverage_dup[line_number.to_i] += strength
        end

        @coverage[file_path] = line_coverage_dup.freeze
      end
    end

    def generate_final_coverage
      return if @coverage_stat

      all_files = final_coverage_files
      @coverage = @coverage.slice(*all_files)

      all_files.each do |file_path|
        @coverage[file_path] ||= line_stub(file_path).freeze
      end
    end

    private

    def existing_file_diff_coverage(example_id, file_path, coverage_stats)
      coverage_stats.zip(@coverage[file_path])
        .each_with_index do |(c_stat, p_stat), line_number|
          next if c_stat.nil? || p_stat.nil? || c_stat == p_stat

          @examples_coverage[example_id][file_path][line_number] = c_stat - p_stat
        end
    end

    def missing_file_diff_coverage(example_id, file_path, coverage_stats)
      coverage_stats.each_with_index do |stat, line_number|
        next if stat.nil? || stat.zero?

        @examples_coverage[example_id][file_path][line_number] = stat
      end
    end

    def final_coverage_files
      all_files = @coverage.keys.to_set

      if RSpecTracer.coverage_tracked_files
        tracked_files = Dir[RSpecTracer.coverage_tracked_files].map do |file_name|
          RSpecTracer::SourceFile.file_path(file_name)
        end

        all_files |= tracked_files
      end

      all_files.select! do |file_path|
        file_name = RSpecTracer::SourceFile.file_name(file_path)

        RSpecTracer.coverage_filters.none? { |filter| filter.match?(file_name: file_name) }
      end

      all_files.sort
    end

    def peek_coverage
      data = ::Coverage.peek_result.select do |file_path, _|
        file_path.start_with?(RSpecTracer.root)
      end

      return data if @mode == COVERAGE_MODE[:array]

      data.transform_values { |stats| stats[:lines] }
    end

    def line_stub(file_path)
      case RUBY_ENGINE
      when 'ruby'
        ruby_line_stub(file_path)
      when 'jruby'
        jruby_line_stub(file_path)
      end
    end

    def ruby_line_stub(file_path)
      lines = File.foreach(file_path).map { nil }
      iseqs = [::RubyVM::InstructionSequence.compile_file(file_path)]

      until iseqs.empty?
        iseq = iseqs.pop

        iseq.trace_points.each { |line_number, type| lines[line_number - 1] = 0 if type == :line }
        iseq.each_child { |child| iseqs << child }
      end

      lines
    end

    def jruby_line_stub(file_path)
      lines = File.foreach(file_path).map { nil }
      root_node = ::JRuby.parse(File.read(file_path))

      visitor = org.jruby.ast.visitor.NodeVisitor.impl do |_name, node|
        if node.newline?
          if node.respond_to?(:position)
            lines[node.position.line] = 0
          else
            lines[node.line] = 0
          end
        end

        node.child_nodes.each { |child| child&.accept(visitor) }
      end

      root_node.accept(visitor)

      lines
    end
  end
end
