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

module Contrast
  module Utils
    module Assess
      # This module will include all methods for some internal validations in the Tagged property module
      # and some other module methods from the same place, so we can ease the main module
      # This module includes simple methods for the tags like
      # adding tags, getting tags, deleting tags and similar
      module TaggedUtils
        # Given a tag name and range object, add a new tag to this
        # collection. If the given range touches an existing tag,
        # we'll combine the two, adjusting the existing one and
        # dropping this new one.
        #
        # @param label [String] the name of the tag
        # @param range [Range] the Range that the tag covers, inclusive to
        #   exclusive
        def add_tag label, range
          length = range.end - range.begin
          tag = Contrast::Agent::Assess::Tag.new(label, length, range.begin)
          existing = fetch_tag(label)
          tags[label] = Contrast::Utils::TagUtil.ordered_merge(existing, tag)
        end

        def set_tags label, tag_ranges
          tags[label] = tag_ranges
        end

        # Returns a list of all current tags.
        #
        # @return [Hash<Contrast::Agent::Assess::Tag>]
        def get_tags # rubocop:disable Naming/AccessorMethodName
          return Contrast::Utils::ObjectShare::EMPTY_HASH unless tracked?

          tags
        end

        # We'll use this as a helper method to retrieve tags from the hash.
        # Because the hash auto-populates an empty array when we try to
        # access a tag in it, we cannot use the [] method without side
        # effect. To get around this, we'll use a fetch work around.
        #
        # @param label [Symbol] the label to look up
        # @return [Array<Contrast::Agent::Assess::Tag>] all the tags with
        #   that label
        def fetch_tag label
          get_tags.fetch(label, nil) if tracked?
        end

        # Remove all tags with a given label
        def delete_tags label
          tags.delete(label) if tracked?
        end

        # Reset the tag hash
        def clear_tags
          tags.clear if tracked?
        end

        # Returns a list of all current tag labels, most likely to be used for
        # a splat operation
        def tag_keys
          return Contrast::Utils::ObjectShare::EMPTY_ARRAY unless tracked?

          tags.keys
        end

        # Calls merge to combine touching or overlapping tags
        # Deletes empty tags
        def cleanup_tags
          return unless tracked?

          Contrast::Utils::TagUtil.merge_tags(tags)
          tags.delete_if { |_, value| value.empty? }
        end

        # Find all of the ranges that span a given index. This is used
        # in propagation when we need to shift tags about. For instance, in
        # the append method when we need to see if any tag at the end needs
        # to be expanded out to the size of the new String.
        #
        # Note: Tags do not know their key, so this is only the range covered
        #
        # @param idx [Integer] the index to check for tags
        # @return [Array<Contrast::Agent::Assess::Tag>] a set of all the Tags
        #   covering the given index. This is only the ranges, not the names.
        def tags_at idx
          return Contrast::Utils::ObjectShare::EMPTY_ARRAY unless tracked?

          at = []
          tags.each_value do |tag_array|
            tag_array.each do |tag|
              if tag.covers?(idx)
                at << tag
              elsif tag.above?(idx)
                break
              end
            end
          end
          at
        end

        # given a range, select all tags in that range the selected tags are
        # shifted such that the start index of the new tag (0) aligns with
        # the given start index in the range
        #
        # current tags: 5-15
        # range       : 5-10
        # result      : 0-05
        #
        # Note that we disable Lint/DuplicateBranch in this branch in order
        # list out all tag range cases in the proper order to make this
        # easier to understand
        #
        # @param range [Range] the span to check, inclusive to exclusive
        # @return [Hash{String => Contrast::Agent::Assess::Tag}] the hash of
        #   key to tags
        def tags_at_range range
          return Contrast::Utils::ObjectShare::EMPTY_HASH unless tracked?

          at = Hash.new { |h, k| h[k] = [] }
          tags.each_pair do |key, value|
            add = nil
            value.each do |tag|
              within_range = resize_to_range(tag, range)
              if within_range
                add ||= []
                add << within_range
              end
            end
            next unless add&.any?

            at[key] = add
          end
          at
        end
      end
    end
  end
end