class Ameba::Source
  # This class performs the heavy lifting in the source rewriting process.
  # It schedules code updates to be performed in the correct order.
  #
  # For simple cases, the resulting source will be obvious.
  #
  # Examples for more complex cases follow. Assume these examples are acting on
  # the source `puts(:hello, :world)`. The methods `#wrap`, `#remove`, etc.
  # receive a range as the first two arguments; for clarity, examples below use
  # English sentences and a string of raw code instead.
  #
  # ## Overlapping deletions:
  #
  # * remove `:hello, `
  # * remove `, :world`
  #
  # The overlapping ranges are merged and `:hello, :world` will be removed.
  #
  # ## Multiple actions at the same end points:
  #
  # Results will always be independent of the order they were given.
  # Exception: rewriting actions done on exactly the same range (covered next).
  #
  # Example:
  #
  # * replace `, ` by ` => `
  # * wrap `:hello, :world` with `{` and `}`
  # * replace `:world` with `:everybody`
  # * wrap `:world` with `[`, `]`
  #
  # The resulting string will be `puts({:hello => [:everybody]})`
  # and this result is independent of the order the instructions were given in.
  #
  # ## Multiple wraps on same range:
  #
  # * wrap `:hello` with `(` and `)`
  # * wrap `:hello` with `[` and `]`
  #
  # The wraps are combined in order given and results would be `puts([(:hello)], :world)`.
  #
  # ## Multiple replacements on same range:
  #
  # * replace `:hello` by `:hi`, then
  # * replace `:hello` by `:hey`
  #
  # The replacements are made in the order given, so the latter replacement
  # supersedes the former and `:hello` will be replaced by `:hey`.
  #
  # ## Swallowed insertions:
  #
  # * wrap `world` by `__`, `__`
  # * replace `:hello, :world` with `:hi`
  #
  # A containing replacement will swallow the contained rewriting actions
  # and `:hello, :world` will be replaced by `:hi`.
  #
  # ## Implementation
  #
  # The updates are organized in a tree, according to the ranges they act on
  # (where children are strictly contained by their parent).
  class Rewriter
    getter code : String

    def initialize(@code)
      @action_root = Rewriter::Action.new(0, code.size)
    end

    # Returns `true` if no (non trivial) update has been recorded
    def empty?
      @action_root.empty?
    end

    # Replaces the code of the given range with *content*.
    def replace(begin_pos, end_pos, content)
      combine begin_pos, end_pos,
        replacement: content.to_s
    end

    # Inserts the given strings before and after the given range.
    def wrap(begin_pos, end_pos, insert_before, insert_after)
      combine begin_pos, end_pos,
        insert_before: insert_before.to_s,
        insert_after: insert_after.to_s
    end

    # Shortcut for `replace(begin_pos, end_pos, "")`
    def remove(begin_pos, end_pos)
      replace(begin_pos, end_pos, "")
    end

    # Shortcut for `wrap(begin_pos, end_pos, content, nil)`
    def insert_before(begin_pos, end_pos, content)
      wrap(begin_pos, end_pos, content, nil)
    end

    # Shortcut for `wrap(begin_pos, end_pos, nil, content)`
    def insert_after(begin_pos, end_pos, content)
      wrap(begin_pos, end_pos, nil, content)
    end

    # Shortcut for `insert_before(pos, pos, content)`
    def insert_before(pos, content)
      insert_before(pos, pos, content)
    end

    # Shortcut for `insert_after(pos, pos, content)`
    def insert_after(pos, content)
      insert_after(pos, pos, content)
    end

    # Applies all scheduled changes and returns modified source as a new string.
    def process
      String.build do |io|
        last_end = 0
        @action_root.ordered_replacements.each do |begin_pos, end_pos, replacement|
          io << code[last_end...begin_pos] << replacement
          last_end = end_pos
        end
        io << code[last_end...code.size]
      end
    end

    protected def combine(begin_pos, end_pos, **attributes)
      check_range_validity(begin_pos, end_pos)

      action = Rewriter::Action.new(begin_pos, end_pos, **attributes)
      @action_root = @action_root.combine(action)
    end

    private def check_range_validity(begin_pos, end_pos)
      return unless begin_pos < 0 || end_pos > code.size

      raise IndexError.new(
        "The range #{begin_pos}...#{end_pos} is outside the bounds of " \
        "the source (0...#{code.size})"
      )
    end
  end
end
