# frozen_string_literal: true

module Unparser
  module Writer
    class DynamicString
      include Writer, Adamantium

      PATTERNS_2 =
        [
          %i[str_empty begin].freeze,
          %i[begin str_nl].freeze
        ].freeze

      PATTERNS_3 =
        [
          %i[begin str_nl_eol str_nl_eol].freeze,
          %i[str_nl_eol begin str_nl_eol].freeze,
          %i[str_ws begin str_nl_eol].freeze
        ].freeze

      FLAT_INTERPOLATION = %i[ivar cvar gvar nth_ref].to_set.freeze

      private_constant(*constants(false))

      def emit_heredoc_reminder
        return unless heredoc?

        emit_heredoc_body
        emit_heredoc_footer
      end

      def dispatch
        if heredoc?
          emit_heredoc_header
        else
          emit_dstr
        end
      end

    private

      def heredoc_header
        '<<-HEREDOC'
      end

      def heredoc?
        !children.empty? && (nl_last_child? && heredoc_pattern?)
      end

      def emit_heredoc_header
        write(heredoc_header)
      end

      def emit_heredoc_body
        nl
        emit_normal_heredoc_body
      end

      def emit_heredoc_footer
        write('HEREDOC')
      end

      def classify(node)
        if n_str?(node)
          classify_str(node)
        else
          node.type
        end
      end

      def classify_str(node)
        if str_nl?(node)
          :str_nl
        elsif node.children.first.end_with?("\n")
          :str_nl_eol
        elsif str_ws?(node)
          :str_ws
        elsif str_empty?(node)
          :str_empty
        end
      end

      def str_nl?(node)
        node.eql?(s(:str, "\n"))
      end

      def str_empty?(node)
        node.eql?(s(:str, ''))
      end

      def str_ws?(node)
        /\A( |\t)+\z/.match?(node.children.first)
      end

      def heredoc_pattern?
        heredoc_pattern_2? || heredoc_pattern_3?
      end

      def heredoc_pattern_3?
        children.each_cons(3).any? do |group|
          PATTERNS_3.include?(group.map(&method(:classify)))
        end
      end

      def heredoc_pattern_2?
        children.each_cons(2).any? do |group|
          PATTERNS_2.include?(group.map(&method(:classify)))
        end
      end

      def nl_last_child?
        last = children.last
        n_str?(last) && last.children.first[-1].eql?("\n")
      end

      def emit_normal_heredoc_body
        buffer.root_indent do
          children.each do |child|
            if n_str?(child)
              write(escape_dynamic(child.children.first))
            else
              emit_dynamic(child)
            end
          end
        end
      end

      def escape_dynamic(string)
        string.gsub('#', '\#')
      end

      def emit_dynamic(child)
        if FLAT_INTERPOLATION.include?(child.type)
          write('#')
          visit(child)
        elsif n_dstr?(child)
          emit_body(child.children)
        else
          write('#{')
          emit_dynamic_component(child.children.first)
          write('}')
        end
      end

      def emit_dynamic_component(node)
        visit(node) if node
      end

      def emit_dstr
        if children.empty?
          write('%()')
        else
          segments.each_with_index do |children, index|
            emit_segment(children, index)
          end
        end
      end

      def breakpoint?(child, current)
        last_type = current.last&.type

        [
          n_str?(child) && last_type.equal?(:str) && current.none?(&method(:n_begin?)),
          last_type.equal?(:dstr),
          n_dstr?(child) && last_type
        ].any?
      end

      def segments
        segments = []

        segments << current = []

        children.each do |child|
          if breakpoint?(child, current)
            segments << current = []
          end

          current << child
        end

        segments
      end

      def emit_segment(children, index)
        write(' ') unless index.zero?

        write('"')
        emit_body(children)
        write('"')
      end

      def emit_body(children)
        buffer.root_indent do
          children.each_with_index do |child, index|
            if n_str?(child)
              string = child.children.first
              if string.eql?("\n") && children.fetch(index.pred).type.equal?(:begin)
                write("\n")
              else
                write(string.inspect[1..-2])
              end
            else
              emit_dynamic(child)
            end
          end
        end
      end
    end # DynamicString
  end # Writer
end # Unparser
