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

cs__scoped_require 'contrast/agent/assess/properties'
cs__scoped_require 'contrast/agent/assess/frozen_properties'
cs__scoped_require 'contrast/agent/assess/insulator'

module Contrast
  module CoreExtensions
    module Assess
      # This module is responsible for maintaining the data we need to
      # construct a trace event for the object in which it is included. Rather
      # than have this code all over the place, any class that wants to use
      # dataflow features should be sent
      # 'include Contrast::CoreExtensions::Assess::AssessExtension'
      module AssessExtension
        include Contrast::Utils::ScopeUtil

        # Lazily build properties object. Only objects that have been tracked
        # will have the @_cs__properties, but all will respond to the
        # cs__properties method call. You should only call this method if you
        # either intend to start tracking an object or you have already checked
        # cs__tracked? and it is true.
        def cs__properties
          # If this object was tracked before being frozen, it'll have
          # mutable properties we need inside of the insulator @_cs__properties
          if cs__frozen?
            if instance_variable_defined?(:@_cs__properties)
              @_cs__properties.properties
            else
              Contrast::Agent::Assess::Insulator.generate_frozen.properties
            end
          else
            @_cs__properties ||= Contrast::Agent::Assess::Insulator.generate
            @_cs__properties.properties
          end
        end

        def cs__properties?
          instance_variable_defined?(:@_cs__properties)
        end

        # This is a way to check if we are already tracking an object without
        # adding tracking to it. If the object already has been tracked we will
        # return the tracking state of its properties. If the object hasn't
        # already been tracked we will return false without starting to track
        # it
        def cs__tracked?
          cs__properties? && cs__properties.tracked?
        end

        def cs__reset_properties
          return unless cs__properties?

          @_cs__properties = nil
        end

        # copy tags and info from object to self if object support methods
        # obj: the object from which to copy tags and events
        # shift: how far to shift the tags, negative moves left
        # skip_tags: array of tags to skip copying
        def cs__copy_from obj, shift = 0, skip_tags = nil
          return if obj.equal?(self)
          return unless Contrast::Utils::DuckUtils.quacks_to?(obj,
                                                              :cs__tracked?)
          return unless obj.cs__tracked?
          return if cs__properties == Contrast::Agent::Assess::Insulator.generate_frozen.properties

          # This was fun to find...
          # the clone and dup methods don't apply to instance variables in the
          # cloned/ duped thing, so the arrays in the properties were the same.
          # The most infinite of infinite loops ensued.
          # DO NOT TAKE THIS OUT!
          cs__reset_properties if obj.cs__properties == cs__properties

          obj.cs__properties.events.each do |event|
            cs__properties.events << event
          end

          obj.cs__properties.tag_keys.each do |key|
            next if skip_tags&.include?(key)

            new_tags = []
            value = obj.cs__properties.fetch_tag(key)
            value.each do |tag|
              new_tags << tag.copy_modified(shift)
            end
            existing = cs__properties.fetch_tag(key)
            if existing
              existing.concat(new_tags)
            else
              cs__properties.set_tags(key, new_tags)
            end
          end
        end

        # Some propagation occurred, but we're not sure what the
        # exact transformation was. To be safe, we just explode
        # all the tags from the source to the return.
        #
        # If the return already had that tag, the existing tag
        # range is recycled to save us an object.
        def cs__splat_tags ret, source = self
          return unless Contrast::Utils::DuckUtils.quacks_to?(
              ret,
              :cs__tracked?)

          splat_source = Contrast::Utils::DuckUtils.quacks_to?(
              source,
              :cs__tracked?)
          splat_source &&= source.cs__tracked?
          if splat_source
            length = Contrast::Utils::StringUtils.ret_length(ret)
            source.cs__properties.tag_keys.each do |key|
              existing = ret.cs__properties.fetch_tag(key)
              # if the tag already exists, drop all but the first range
              # then change that range to cover the entire return
              if existing
                existing.drop(existing.length - 1)
                range = existing[0]
                range.repurpose(0, length)
              else
                span = Contrast::Agent::Assess::AdjustedSpan.new(0, length)
                ret.cs__properties.add_tag(key, span)
              end
            end
          end
          return unless ret.cs__tracked?

          length ||= Contrast::Utils::StringUtils.ret_length(ret)
          ret.cs__properties.tag_keys.each do |key|
            next unless key

            existing = ret.cs__properties.fetch_tag(key)
            next unless existing

            existing.each do |range|
              range.update_end(length) if range.end_idx > length
            end
          end
        end
      end
    end
  end
end