# frozen_string_literal: true

require_relative '../value_equality'

module RubyTerraform
  module Models
    class PathSet
      class << self
        def empty
          new([])
        end

        def extract_from(object)
          empty.add_paths_from(object)
        end
      end

      extend Forwardable

      include ValueEquality

      def_delegators(:@paths, :empty?)

      attr_reader(:paths)

      def initialize(paths)
        @paths = paths
      end

      def add_paths_from(object)
        self.class.new(paths + extract_paths_from(object))
      end

      def gaps
        initial_context = { last: Path.new([]), complete: [] }
        result = paths.sort.inject(initial_context) do |acc, path|
          current_path = path
          last_path = acc[:last]
          missing_paths = determine_missing_paths(last_path, current_path)
          updated_paths = acc[:complete] + missing_paths

          { last: current_path, complete: updated_paths }
        end

        self.class.new(result[:complete])
      end

      def state
        [paths]
      end

      private

      def extract_paths_from(
        object,
        current = Path.new([]),
        accumulator = []
      )
        normalised = normalise(object)
        if normalised.is_a?(Enumerable)
          normalised.inject(accumulator) do |a, e|
            extract_paths_from(e[0], current.append(e[1]), a)
          end
        else
          accumulator + [current]
        end
      end

      def normalise(object)
        case object
        when Array then object.each_with_index.to_a
        when Hash
          object.to_a.map do |e|
            [e[1], e[0].to_sym]
          end
        else
          object
        end
      end

      # rubocop:disable Metrics/MethodLength
      def determine_missing_paths(last_path, current_path)
        last_indices = resolve_last_indices(last_path, current_path)
        current_indices = current_path.list_indices

        current_indices.inject([]) do |acc, current_index|
          current_location, current_element = current_index
          last_index =
            last_indices.find { |index| index[0] == current_location }
          last_element = last_index[1]

          next(acc) unless current_element.positive?

          start_element = last_element.nil? ? 0 : last_element + 1
          next(acc) if start_element == current_element

          acc + create_missing_paths(
            start_element, current_element, current_path, current_location
          )
        end
      end
      # rubocop:enable Metrics/MethodLength

      # rubocop:disable Metrics/MethodLength
      def resolve_last_indices(last_path, current_path)
        last_indices = last_path.list_indices
        current_indices = current_path.list_indices

        current_indices.collect do |current_entry|
          location, current_element = current_entry
          last_entry = last_indices.find { |index| index[0] == location }
          last_element = last_entry&.slice(1)
          reset_entry = [location, nil]

          next(reset_entry) unless last_element
          next(reset_entry) if current_element < last_element

          current_sub_path = current_path.to_location(location)
          last_sub_path = last_path.to_location(location)

          unless current_sub_path.same_parent_collection?(last_sub_path)
            next(reset_entry)
          end

          last_entry
        end
      end
      # rubocop:enable Metrics/MethodLength

      def create_missing_paths(from, to, path, location)
        (from...to).collect do |element|
          path.before_location(location).append(element)
        end
      end
    end
  end
end