# Copyright (c) 2020 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details.
# frozen_string_literal: true

module Contrast
  module Utils
    # Utility methods for working with tag ranges
    class TagUtil
      class << self
        # Determine if the given array of tags is covered by the other
        # remaining_ranges: the tags left that haven't been covered by those given
        # ranges: the tags that are covering the first
        def covered? remaining_ranges, ranges
          return true unless remaining_ranges&.any?

          tag = remaining_ranges[0]
          ranges.each do |range|
            # If we covered the tag before using all the ranges, break
            break if tag.length <= 0

            relationship = tag.compare_range(range.start_idx, range.end_idx)
            case relationship
            when Contrast::Agent::Assess::Tag::BELOW
              # since the tags are ordered, if we're below, nope out
              return false
            when Contrast::Agent::Assess::Tag::LOW_SPAN
              # if we ever get a low span, that means a low part
              # won't be covered. there's no need to continue
              return false
            when Contrast::Agent::Assess::Tag::WITHOUT
              # if we ever get a without, that means a low part won't
              # be covered. there's no need to continue
              return false
            when Contrast::Agent::Assess::Tag::WITHIN
              # if we're within, then 0 out this tag since it is
              # completely covered
              tag.update_start(0)
              tag.update_end(0)
            when Contrast::Agent::Assess::Tag::HIGH_SPAN
              tag.update_start(range.end_idx)
            when Contrast::Agent::Assess::Tag::ABOVE
              # The tag's above where we are, it doesn't cover us but a later
              # one may
            end
          end
          return false unless tag.length <= 0

          remaining_ranges.shift
          covered?(remaining_ranges, ranges)
        end

        # Given an array of tags, add all new tags to that array
        #
        # The addition is done such that the new entry(ies)
        # are inserted so that the range they cover is in order
        # Any overlapping ranges are merged before returning
        #
        # arr: the array of tags to which we are adding
        # new_arr: misnomer. either an array of or a single Tag to be added
        def ordered_merge arr, new_arr
          # [Contrast::Agent::Assess::Tag, ...]
          if new_arr.is_a?(Array)
            return arr unless new_arr.any?
            return new_arr unless arr&.any?

            new_arr.each { |new_element| single_ordered_merge(arr, new_element) }
          # Contrast::Agent::Assess::Tag
          else
            return arr unless new_arr
            return [new_arr] unless arr

            single_ordered_merge(arr, new_arr)
          end
          smallerize(arr)
        end

        # Given a collection of tags, merge any tags that are continuous
        #
        # If tags is a hash, it should be in the format label => [tags]
        # The array of tags will each be merged
        #
        # If tags is an array in the format [tags], the array will be
        # merged
        #
        # The original object is returned, although setters should not be
        # necessary since tags is a collection in either case
        def merge_tags tags
          if tags.is_a?(Hash)
            tags.each_value { |value| smallerize(value) }
          else
            smallerize(tags)
          end
        end

        def size_aware_merge target_object, tags
          max_size = target_object.to_s.length
          tags = merge_tags(tags)
          tags.each do |tag|
            tag.update_end(max_size) if tag.extends_beyond_string_size?(max_size)
          end
        end

        private

        # Add one new element to the given array
        #
        # The addition is done such that the new entry(ies)
        # are inserted so that the range they cover is in order
        # Any overlapping ranges are merged before returning
        #
        # arr: the array to which the element is added
        # new_element: the element to be added to the array
        def single_ordered_merge arr, new_element
          idx = 0
          arr.each do |existing|
            break unless existing.start_idx < new_element.start_idx

            if existing.overlaps?(new_element.start_idx, new_element.end_idx)
              existing.merge(new_element)
              return # rubocop:disable Lint/NonLocalExitFromIterator
            end
            idx += 1
          end
          arr.insert(idx, new_element)
        end

        # Given an arry of tags, merge any that overlap
        # The tag that was higher up is removed from the
        # list of tags.
        # ranges like [0-3][3-6]-6-9] that should become [0-9]
        def smallerize tags
          smallered = []
          curr = nil
          tags.each do |tag|
            if curr.nil?
              curr = tag
              smallered << curr
            elsif tag.start_idx <= curr.end_idx
              curr.update_end(tag.end_idx) if tag.end_idx > curr.end_idx
            else
              curr = tag
              smallered << curr
            end
          end
          tags.delete_if { |tag| !smallered.include?(tag) }
        end
      end
    end
  end
end