# frozen_string_literal: true

require_relative 'subset'

module DeepCover
  class Analyser::Ruby25LikeBranch < Analyser
    def self.human_name
      'Ruby25 branches'
    end
    include Analyser::Subset
    SUBSET_CLASSES = [Node::Case, Node::Csend, Node::If, Node::ShortCircuit,
                      Node::Until, Node::UntilPost, Node::While, Node::WhilePost,
                     ].freeze

    def initialize(*args)
      super
      @loc_index = 0
    end

    def results
      extractor = NodeCoverageExtrator.new
      each_node.map do |node|
        extractor.branch_coverage(node)
      end.to_h
    end

    # This is the class doing the work. Since everything is about the node, the class delegates
    # missing methods to the node, simplifying the code.
    class NodeCoverageExtrator < SimpleDelegator
      def initialize(node = nil)
        self.node = node
        @loc_index = 0
      end

      alias_method :node=, :__setobj__
      alias_method :node, :__getobj__

      def branch_coverage(node)
        self.node = node
        case node
        when Node::Case
          handle_case
        when Node::Csend
          handle_csend
        when Node::If
          handle_if
        when Node::ShortCircuit
          handle_short_circuit
        when Node::Until, Node::While, Node::UntilPost, Node::WhilePost
          handle_until_while
        end
      end

      def handle_case
        cond_info = [:case, *node_loc_infos]

        sub_keys = [:when] * (branches.size - 1) + [:else]
        empty_fallbacks = whens.map { |w| wrap_rwhitespace_and_comments_if_ruby25(w.loc_hash[:begin] || w.loc_hash[:expression]).end }
        empty_fallbacks.map!(&:begin)

        if loc_hash[:else]
          empty_fallbacks << wrap_rwhitespace_and_comments_if_ruby25(loc_hash[:else]).end
        else
          # DeepCover manually inserts a `else` for Case when there isn't one for tracker purposes.
          # The normal behavior of ruby25's branch coverage when there is no else is to return the loc of the node
          # So we sent that fallback.
          empty_fallbacks << expression
        end

        branches_locs = whens.map do |when_node|
          next when_node.body if when_node.body.is_a?(Node::EmptyBody)

          start_at = when_node.loc_hash[:begin]
          start_at = start_at.wrap_rwhitespace_and_comments.end if start_at
          start_at ||= when_node.body.expression.begin

          end_at = when_node.body.expression.end
          start_at.with(end_pos: end_at.end_pos)
        end

        branches_locs << node.else
        clauses_infos = infos_for_branches(branches_locs, sub_keys, empty_fallbacks, execution_counts: branches.map(&:execution_count))

        [cond_info, clauses_infos]
      end

      def handle_csend
        # csend wraps the comment but not the newlines
        node_range = wrap_rwhitespace_and_comments_if_ruby25(node.expression, whitespaces: /\A[ \t\r\f]+/)
        cond_info = [:"&.", *node_loc_infos(node_range)]
        false_branch, true_branch = branches
        [cond_info, {[:then, *node_loc_infos(node_range)] => true_branch.execution_count,
                     [:else, *node_loc_infos(node_range)] => false_branch.execution_count,
                    },
        ]
      end

      def handle_if
        key = style == :unless ? :unless : :if

        node_range = extend_elsif_range
        cond_info = [key, *node_loc_infos(node_range)]

        sub_keys = [:then, :else]
        if style == :ternary
          empty_fallback_locs = [nil, nil]
        else
          first_clause_fallback = wrap_rwhitespace_and_comments_if_ruby25(loc_hash[:begin]) if loc_hash[:begin]
          first_clause_fallback ||= wrap_rwhitespace_and_comments_if_ruby25(condition.expression)
          # Ruby26 wraps the comments but only on the same line
          # No need for condition since Ruby25 wraps all of them
          first_clause_fallback = first_clause_fallback.wrap_final_comment.end
          else_loc = loc_hash[:else]
          if else_loc
            second_clause_fallback = wrap_rwhitespace_and_comments_if_ruby25(else_loc).end
          elsif !modifier?
            second_clause_fallback = root_if_node.loc_hash[:end].begin
          end

          empty_fallback_locs = [first_clause_fallback, second_clause_fallback]
        end
        # loc can be nil if the clause can't be empty, such as ternary and modifer if/unless

        if key == :unless
          sub_keys.reverse!
          empty_fallback_locs.reverse!
        end

        branches_locs = branches
        execution_counts = branches_locs.map(&:execution_count)
        if modifier?
          branches_locs = branches_locs.map do |branch|
            if branch.is_a?(Node::Kwbegin)
              if branch.instructions.empty?
                wrap_rwhitespace_and_comments_if_ruby25(branch.loc_hash[:begin]).end
              elsif branch.instructions.first.is_a?(Node::Ensure)
                # Kernel.binding.pry
                end_pos = wrap_rwhitespace_and_comments_if_ruby25(branch.instructions.last.expression).end_pos
                wrap_rwhitespace_and_comments_if_ruby25(branch.loc_hash[:begin]).end.with(end_pos: end_pos)
              else
                end_pos = branch.instructions.last.expression.end_pos
                branch.loc_hash[:begin].wrap_rwhitespace_and_comments.end.with(end_pos: end_pos)
              end
            else
              branch
            end
          end
        end

        branches_locs[1] = extend_elsif_range(branches_locs[1])

        clauses_infos = infos_for_branches(branches_locs, sub_keys, empty_fallback_locs, execution_counts: execution_counts, node_range: node_range)
        [cond_info, clauses_infos]
      end

      def handle_short_circuit
        cond_info = [operator, *node_loc_infos]
        sub_keys = [:then, :else]
        sub_keys.reverse! if node.is_a?(Node::Or)

        [cond_info, infos_for_branches(branches, sub_keys, [nil, nil])]
      end

      def handle_until_while
        key = loc_hash[:keyword].source.to_sym
        base_info = [key, *node_loc_infos]
        body_node = if node.is_a?(Node::WhilePost) || node.is_a?(Node::UntilPost)
                      if !body.instructions.empty?
                        end_pos = body.instructions.last.expression.end_pos
                        body.instructions.first.expression.with(end_pos: end_pos)
                      else
                        wrap_rwhitespace_and_comments_if_ruby25(body.loc_hash[:begin]).end
                      end
                    elsif body.is_a?(Node::Begin) && !body.expressions.empty?
                      end_pos = body.expressions.last.expression.end_pos
                      body.expressions.first.expression.with(end_pos: end_pos)
                    elsif body.is_a?(Node::EmptyBody)
                      wrap_rwhitespace_and_comments_if_ruby25(condition.loc_hash[:expression]).end
                    else
                      body
                    end

        [base_info, {[:body, *node_loc_infos(body_node)] => body.execution_count}]
      end

      protected

      # If the actual else clause (final one) of an if...elsif...end is empty, then Ruby25 wraps the final whitespace
      def extend_elsif_range(possible_elsif = node)
        return possible_elsif unless possible_elsif.is_a?(Node::If) && possible_elsif.style == :elsif
        deepest_if = possible_elsif.deepest_elsif_node
        if deepest_if.false_branch.is_a?(Node::EmptyBody)
          return wrap_rwhitespace_and_comments_if_ruby25(possible_elsif.expression)
        end
        possible_elsif
      end

      def infos_for_branch(branch, key, empty_fallback_loc, execution_count: nil, node_range: node)
        if !branch.is_a?(Node::EmptyBody)
          loc = branch
        elsif branch.expression
          # There is clause, but it is empty
          loc = empty_fallback_loc
        else
          # There is no clause
          loc = node_range
        end

        execution_count ||= branch.execution_count
        [[key, *node_loc_infos(loc)], execution_count]
      end

      def infos_for_branches(branches, keys, empty_fallback_locs, execution_counts: [], node_range: node)
        branches_infos = branches.map.with_index do |branch, i|
          infos_for_branch(branch, keys[i], empty_fallback_locs[i], execution_count: execution_counts[i], node_range: node_range)
        end
        branches_infos.to_h
      end

      def node_loc_infos(node_or_range = node)
        source_range = node_or_range.is_a?(Node) ? node_or_range.expression : node_or_range

        @loc_index += 1
        [@loc_index, source_range.line, source_range.column, source_range.last_line, source_range.last_column]
      end

      def wrap_rwhitespace_and_comments_if_ruby25(expression, whitespaces: /\A\s+/)
        if RUBY_VERSION.start_with?('2.5')
          expression.wrap_rwhitespace_and_comments(whitespaces: whitespaces)
        else
          expression
        end
      end
    end
  end
end
