require "./term_cursor"
require "./term_size"

module Reply
  # The `ExpressionEditor` allows to edit and display an expression.
  #
  # Its main task is to provide the display of the prompt and a multiline expression within
  # the term bounds, and ensure the correspondence between the cursor on screen and the cursor on the expression.
  #
  # Usage example:
  # ```
  # # new editor:
  # @editor = ExpressionEditor.new(
  #   prompt: ->(expr_line_number : Int32) { "prompt>" }
  # )
  #
  # # edit some code:
  # @editor.update do
  #   @editor << %(puts "World")
  #
  #   insert_new_line(indent: 1)
  #   @editor << %(puts "!")
  # end
  #
  # # move cursor:
  # @editor.move_cursor_up
  # 4.times { @editor.move_cursor_left }
  #
  # # edit:
  # @editor.update do
  #   @editor << "Hello "
  # end
  #
  # @editor.end_editing
  #
  # @editor.expression # => %(puts "Hello World"\n  puts "!")
  # puts "=> ok"
  #
  # # clear and restart edition:
  # @editor.prompt_next
  # ```
  #
  # The above displays:
  # ```
  # prompt>puts "Hello World"
  # prompt>  puts "!"
  # => ok
  # prompt>
  # ```
  #
  # Methods that modify the expression should be placed inside an `update` so the screen can be refreshed taking in account
  # the adding or removing of lines, and doesn't boilerplate the display.
  class ExpressionEditor
    getter lines : Array(String) = [""]
    getter expression : String? { lines.join('\n') }
    getter expression_height : Int32? { lines.sum { |l| line_height(l) } }
    property? color = true
    property output : IO = STDOUT

    # Tracks the cursor position relatively to the expression's lines, (y=0 corresponds to the first line and x=0 the first char)
    # This position is independent of text wrapping so its position will not match to real cursor on screen.
    #
    # `|` : cursor position
    #
    # ```
    # prompt>def very_looo
    # ooo|ng_name            <= wrapping
    # prompt>  bar
    # prompt>end
    # ```
    # For example here the cursor position is x=16, y=0, but real cursor is at x=3,y=1 from the beginning of expression.
    getter x = 0
    getter y = 0

    # The editor height, if not set (`nil`), equal to term height.
    setter height : Int32? = nil

    # The editor width, if not set (`nil`), equal to term width.
    setter width : Int32? = nil

    @prompt : Int32, Bool -> String
    @prompt_size : Int32

    @scroll_offset = 0
    @header_height = 0
    @footer_height = 0

    @header : IO, Int32 -> Int32 = ->(io : IO, previous_height : Int32) { 0 }
    @footer : IO, Int32 -> Int32 = ->(io : IO, previous_height : Int32) { 0 }
    @highlight = ->(code : String) { code }

    # The list of characters delimiting words.
    #
    # default: ` \n\t+-*/,;@&%<>"'^\\[](){}|.~:=!?`
    property word_delimiters : Array(Char) = {{" \n\t+-*/,;@&%<>\"'^\\[](){}|.~:=!?".chars}}

    # Creates a new `ExpressionEditor` with the given *prompt*.
    def initialize(&@prompt : Int32, Bool -> String)
      @prompt_size = @prompt.call(0, false).size # uncolorized size
      @prompt_size = 1 if @prompt_size == 0
    end

    # Sets a `Proc` allowing to display a header above the prompt. (used by auto-completion)
    #
    # *io*: The IO in which the header should be displayed.
    # *previous_height*: Previous header height, useful to keep a header size constant.
    # Should returns the exact *height* printed in the io.
    def set_header(&@header : IO, Int32 -> Int32)
    end

    # Sets a `Proc` allowing to display a footer under the prompt. (used by search)
    #
    # *io*: The IO in which the footer should be displayed.
    # *previous_height*: Previous footer height.
    # Should returns the exact *height* printed in the io.
    def set_footer(&@footer : IO, Int32 -> Int32)
    end

    # Sets the `Proc` to highlight the expression.
    def set_highlight(&@highlight : String -> String)
    end

    private def move_cursor(x, y)
      @x += x
      @y += y
    end

    private def move_real_cursor(x, y)
      @output.print Term::Cursor.move(x, -y)
    end

    private def move_abs_cursor(@x, @y)
    end

    private def reset_cursor
      @x = @y = 0
    end

    # Returns true is the char at *x*, *y* is a word char.
    private def word_char?(x)
      if x >= 0 && (ch = self.current_line[x]?)
        !ch.in? self.word_delimiters
      end
    end

    def current_line
      @lines[@y]
    end

    def previous_line?
      if @y > 0
        @lines[@y - 1]
      end
    end

    def next_line?
      @lines[@y + 1]?
    end

    # Returns the word under the cursor following `word_delimiters`.
    def current_word
      word_begin, word_end = self.current_word_begin_end

      self.current_line[word_begin..word_end]
    end

    # Returns begin and end position of `current_word`.
    def current_word_begin_end
      return 0, 0 if self.current_line.empty?

      word_begin = {@x - 1, 0}.max
      word_end = @x
      while word_char?(word_begin)
        word_begin -= 1
      end

      while word_char?(word_end)
        word_end += 1
      end

      {word_begin + 1, word_end - 1}
    end

    def empty?
      @lines.empty? || (@lines.size == 1 && @lines.first.empty?)
    end

    def cursor_on_last_line?
      (@y == @lines.size - 1)
    end

    def expression_before_cursor(x = @x, y = @y)
      String.build do |io|
        @lines[...y].each { |line| io << line << '\n' }
        io << @lines[y][...x]
      end
    end

    # Following functions modifies the expression, they should be called inside
    # an `update` block to see the changes in the screen: #

    # Should be called inside an `update`.
    def previous_line=(line)
      @lines[@y - 1] = line
      @expression = @expression_height = nil
    end

    # Should be called inside an `update`.
    def current_line=(line)
      @lines[@y] = line
      @expression = @expression_height = nil
    end

    # Should be called inside an `update`.
    def next_line=(line)
      @lines[@y + 1] = line
      @expression = @expression_height = nil
    end

    # Replaces the word under the cursor by *replacement*, then moves cursor at the end of *replacement*.
    # Should be called inside an `update`.
    def current_word=(replacement)
      word_begin, word_end = self.current_word_begin_end

      if word_begin == word_end == 0
        self.current_line = replacement
      else
        self.current_line = self.current_line.sub(word_begin..word_end, replacement)
      end

      move_abs_cursor(x: word_begin + replacement.size, y: @y)
    end

    # Should be called inside an `update`.
    def delete_line(y)
      @lines.delete_at(y)
      @expression = @expression_height = nil
    end

    # Should be called inside an `update`.
    def clear_expression
      @lines.clear << ""
      @expression = @expression_height = nil
    end

    # Should be called inside an `update`.
    #
    # If *char* is `\n` or `\r`, inserts a new line with indent 0.
    # Does nothing if the char is an `ascii_control?`.
    def <<(char : Char) : self
      return insert_new_line(0) if char.in? '\n', '\r'
      return self if char.ascii_control?

      if @x >= current_line.size
        self.current_line = current_line + char
      else
        self.current_line = current_line.insert(@x, char)
      end

      move_cursor(x: +1, y: 0)
      self
    end

    # Should be called inside an `update`.
    def <<(str : String) : self
      str.each_char do |ch|
        self << ch
      end
      self
    end

    # Should be called inside an `update`.
    def insert_new_line(indent)
      case @x
      when current_line.size
        @lines.insert(@y + 1, "  "*indent)
      when .< current_line.size
        @lines.insert(@y + 1, "  "*indent + current_line[@x..])
        self.current_line = current_line[...@x]
      end

      @expression = @expression_height = nil
      move_abs_cursor(x: indent*2, y: @y + 1)
      self
    end

    # Should be called inside an `update`.
    def delete
      case @x
      when current_line.size
        if next_line = next_line?
          self.current_line = current_line + next_line

          delete_line(@y + 1)
        end
      when .< current_line.size
        self.current_line = current_line.delete_at(@x)
      end
    end

    # Should be called inside an `update`.
    def back
      case @x
      when 0
        if prev_line = previous_line?
          self.previous_line = prev_line + current_line

          move_cursor(x: prev_line.size, y: -1)
          delete_line(@y + 1)
        end
      when .> 0
        self.current_line = current_line.delete_at(@x - 1)
        move_cursor(x: -1, y: 0)
      end
    end

    # Should be called inside an `update`.
    def delete_word
      self.delete if @x == current_line.size

      word_end = self.next_word_end
      self.current_line = current_line[...@x] + current_line[(word_end + 1)..]
    end

    # Should be called inside an `update`.
    def word_back
      self.back if @x == 0

      x = @x

      word_begin = self.previous_word_begin
      move_abs_cursor(x: word_begin, y: @y)

      self.current_line = current_line[...word_begin] + current_line[x..]
    end

    # Should be called inside an `update`.
    def delete_after_cursor
      if @x == current_line.size
        self.delete
      elsif !current_line.empty?
        self.current_line = current_line[...@x]
      end
    end

    # Should be called inside an `update`.
    def delete_before_cursor
      if @x == 0
        self.back
      elsif !current_line.empty?
        self.current_line = current_line[@x..]

        move_abs_cursor(x: 0, y: @y)
      end
    end

    # End modifying functions. #

    # Gives the size of the last part of the line when it's wrapped
    #
    # prompt>def very_looo
    # ooooooooong              <= last part
    # prompt>  bar
    # prompt>end
    #
    # e.g. here "ooooooooong".size = 10
    private def last_part_size(line_size)
      (@prompt_size + line_size) % self.width
    end

    # Returns the height of this line, (1 on common lines, more on wrapped lines):
    private def line_height(line)
      1 + (@prompt_size + line.size) // self.width
    end

    private def height_above_cursor(x = @x, y = @y)
      height = @lines.each.first(@y).sum { |l| line_height(l) }
      height += line_height(current_line[...@x]) - 1
      height
    end

    # The editor width, if not set (`nil`), equal to term width.
    def width
      @width || Term::Size.width
    end

    # The editor height, if not set (`nil`), equal to term height.
    def height
      @height || Term::Size.height
    end

    # Returns the max height that could take an expression on screen.
    #
    # The expression scrolls if it's higher than epression_max_height.
    private def epression_max_height
      self.height - @header_height - @footer_height
    end

    def move_cursor_left(allow_scrolling = true)
      case @x
      when 0
        # Wrap the cursor at the end of the previous line:
        #
        # `|`: cursor pos
        # `*`: wanted pos
        #
        # ```
        # prompt>def very_looo
        # ooooooooong*
        # prompt>| bar
        # prompt>end
        # ```
        if prev_line = previous_line?
          scroll_up_if_needed if allow_scrolling

          # Wrap real cursor:
          size_of_last_part = last_part_size(prev_line.size)
          move_real_cursor(x: -@prompt_size + size_of_last_part, y: -1)

          # Wrap cursor:
          move_cursor(x: prev_line.size, y: -1)
        end
      when .> 0
        # Move the cursor left, wrap the real cursor if needed:
        #
        # `|`: cursor pos
        # `*`: wanted pos
        #
        # ```
        # prompt>def very_looo*
        # |oooooooong
        # prompt>  bar
        # prompt>end
        # ```
        if last_part_size(@x) == 0
          scroll_up_if_needed if allow_scrolling
          move_real_cursor(x: self.width + 1, y: -1)
        else
          move_real_cursor(x: -1, y: 0)
        end
        move_cursor(x: -1, y: 0)
      end
    end

    def move_cursor_right(allow_scrolling = true)
      case @x
      when current_line.size
        # Wrap the cursor at the beginning of the next line:
        #
        # `|`: cursor pos
        # `*`: wanted pos
        #
        # ```
        # prompt>def very_looo
        # ooooooooong|
        # prompt>* bar
        # prompt>end
        # ```
        if next_line?
          scroll_down_if_needed if allow_scrolling

          # Wrap real cursor:
          size_of_last_part = last_part_size(current_line.size)
          move_real_cursor(x: -size_of_last_part + @prompt_size, y: +1)

          # Wrap cursor:
          move_cursor(x: -current_line.size, y: +1)
        end
      when .< current_line.size
        # Move the cursor right, wrap the real cursor if needed:
        #
        # `|`: cursor pos
        # `*`: wanted pos
        #
        # ```
        # prompt>def very_looo|
        # *oooooooong
        # prompt>  bar
        # prompt>end
        # ```
        if last_part_size(@x) == (self.width - 1)
          scroll_down_if_needed if allow_scrolling

          move_real_cursor(x: -self.width, y: +1)
        else
          move_real_cursor(x: +1, y: 0)
        end

        # move cursor right
        move_cursor(x: +1, y: 0)
      end
    end

    def move_cursor_up(allow_scrolling = true)
      scroll_up_if_needed if allow_scrolling

      if (@prompt_size + @x) >= self.width
        if @x >= self.width
          # Here, we have:
          # ```
          # prompt>def *very_looo
          # ooooooooooo|ooooooooo
          # ooooooooong
          # prompt>  bar
          # prompt>end
          # ```
          # So we need only to move real cursor up
          # and move back @x by term-width.
          #
          move_real_cursor(x: 0, y: -1)
          move_cursor(x: -self.width, y: 0)
        else
          # Here, we have:
          # ```
          # prompt>*def very_looo
          # ooo|ooooooooooooooooo
          # ooooooooong
          # prompt>  bar
          # prompt>end
          # ```
          #
          move_real_cursor(x: self.width - @x, y: -1)
          move_cursor(x: 0 - @x, y: 0)
        end

        true
      elsif prev_line = previous_line?
        # Here, there are a previous line in which we can move up, we want to
        # move on the last part of the previous line:
        size_of_last_part = last_part_size(prev_line.size)

        if size_of_last_part < @prompt_size + @x
          # ```
          # prompt>def very_looo
          # oooooooooooooooooooo
          # ong*                  <= last part
          # prompt>  ba|aar
          # prompt>end
          # ```
          move_real_cursor(x: -@x - @prompt_size + size_of_last_part, y: -1)
          move_abs_cursor(x: prev_line.size, y: @y - 1)
        else
          # ```
          # prompt>def very_looo
          # oooooooooooooooooooo
          # ooooooooooo*ooong    <= last part
          # prompt>  ba|aar
          # prompt>end
          # ```
          move_real_cursor(x: 0, y: -1)
          x = prev_line.size - size_of_last_part + @prompt_size + @x
          move_abs_cursor(x: x, y: @y - 1)
        end
        true
      else
        false
      end
    end

    def move_cursor_down(allow_scrolling = true)
      scroll_down_if_needed if allow_scrolling

      size_of_last_part = last_part_size(current_line.size)
      real_x = last_part_size(@x)

      remaining = current_line.size - @x

      if remaining > size_of_last_part
        # on middle
        if remaining > self.width
          # Here, there are enough remaining to just move down
          # ```
          # prompt>def very|_loooo
          # ooooooooooooooo*oooooo
          # ong
          # prompt>  bar
          # prompt>end
          # ```
          #
          move_real_cursor(x: 0, y: +1)
          move_cursor(x: self.width, y: 0)
        else
          # Here, we goes to end of current line:
          # ```
          # prompt>def very_loooo
          # ooooooooooooooo|ooooo
          # ong*
          # prompt>  bar
          # prompt>end
          # ```
          move_real_cursor(x: -real_x + size_of_last_part, y: +1)
          move_abs_cursor(x: current_line.size, y: @y)
        end
        true
      elsif next_line = next_line?
        case real_x
        when .< @prompt_size
          # Here, we are behind the prompt so we want goes to the beginning of the next line:
          # ```
          # prompt>def very_loooo
          # ooooooooooooooooooooo
          # ong|
          # prompt>* bar
          # prompt>end
          # ```
          move_real_cursor(x: -real_x + @prompt_size, y: +1)
          move_abs_cursor(x: 0, y: @y + 1)
        when .< @prompt_size + next_line.size
          # Here, we can just move down on the next line:
          # ```
          # prompt>def very_loooo
          # ooooooooooooooooooooo
          # ooooooooong|
          # prompt>  ba*r
          # prompt>end
          # ```
          move_real_cursor(x: 0, y: +1)
          move_abs_cursor(x: real_x - @prompt_size, y: @y + 1)
        else
          # Finally, here, we want to move at end of the next line:
          # ```
          # prompt>def very_loooo
          # ooooooooooooooooooooo
          # ooooooooooooooong|
          # prompt>  bar*
          # prompt>end
          # ```
          x = real_x - (@prompt_size + next_line.size)
          move_real_cursor(x: -x, y: +1)
          move_abs_cursor(x: next_line.size, y: @y + 1)
        end
        true
      else
        false
      end
    end

    def move_cursor_to(x, y, allow_scrolling = true)
      until @y == y
        (@y > y) ? move_cursor_up(allow_scrolling: false) : move_cursor_down(allow_scrolling: false)
      end

      until @x == x
        (@x > x) ? move_cursor_left(allow_scrolling: false) : move_cursor_right(allow_scrolling: false)
      end

      if allow_scrolling && update_scroll_offset
        update
      end
    end

    def move_cursor_to_begin(allow_scrolling = true)
      move_cursor_to(0, 0, allow_scrolling: allow_scrolling)
    end

    def move_cursor_to_end(allow_scrolling = true)
      y = @lines.size - 1

      move_cursor_to(@lines[y].size, y, allow_scrolling: allow_scrolling)
    end

    def move_cursor_to_end_of_line(y = @y, allow_scrolling = true)
      move_cursor_to(@lines[y].size, y, allow_scrolling: allow_scrolling)
    end

    def move_word_forward
      self.move_cursor_right if @x == self.current_line.size

      word_end = self.next_word_end
      self.move_cursor_to(x: word_end + 1, y: @y)
    end

    def move_word_backward
      self.move_cursor_left if @x == 0

      word_begin = self.previous_word_begin
      self.move_cursor_to(x: word_begin, y: @y)
    end

    private def next_word_end
      x = @x
      while word_char?(x) == false
        x += 1
      end

      while word_char?(x)
        x += 1
      end
      x - 1
    end

    private def previous_word_begin
      x = @x - 1
      while word_char?(x) == false
        x -= 1
      end

      while word_char?(x)
        x -= 1
      end
      x + 1
    end

    # Refresh the screen.
    #
    # It clears the display of the current expression,
    # then yields for modifications, and displays the new expression.
    #
    # if *force_full_view* is true, whole expression is displayed, even if it overflow the term width, otherwise
    # the expression is bound and can be scrolled.
    def update(force_full_view = false, &)
      height_to_clear = self.height_above_cursor
      with self yield

      @expression = @expression_height = nil

      # Updated expression can be smaller so we might need to adjust the cursor:
      @y = @y.clamp(0, @lines.size - 1)
      @x = @x.clamp(0, @lines[@y].size)

      print_expression_and_header(height_to_clear, force_full_view)
    end

    def update(force_full_view = false)
      height_to_clear = self.height_above_cursor

      print_expression_and_header(height_to_clear, force_full_view)
    end

    # Calls the header proc and saves the *header_height*
    private def update_header : String
      String.build do |io|
        @header_height = @header.call(io, @header_height)
      end
    end

    # Calls the footer proc and saves the *footer_height*
    private def update_footer : String
      String.build do |io|
        @footer_height = @footer.call(io, @footer_height)
      end
    end

    def replace(lines : Array(String))
      update { @lines = lines }
    end

    # Prints the full expression (without view bounds), and eventually replace it by *replacement*.
    def end_editing(replacement : Array(String)? = nil)
      if replacement
        update(force_full_view: true) do
          @lines = replacement
        end
      else
        update(force_full_view: true)
      end

      move_cursor_to_end(allow_scrolling: false)
      @output.puts
    end

    # Clears the expression and start a new prompt on a next line.
    def prompt_next
      @scroll_offset = 0
      @lines = [""]
      @expression = @expression_height = nil
      reset_cursor
      @prompt_size = 0
      print_prompt(@output, 0)
    end

    private def print_prompt(io, line_index)
      line_prompt_size = @prompt.call(line_index, false).size # uncolorized size
      line_prompt_size = 1 if line_prompt_size == 0

      @prompt_size = {line_prompt_size, @prompt_size}.max
      io.print @prompt.call(line_index, color?)

      # Padding to align the lines when the prompt size change
      (@prompt_size - line_prompt_size).times do
        io.print ' '
      end
    end

    def scroll_up
      if @scroll_offset < expression_height() - epression_max_height()
        @scroll_offset += 1
        update
      end
    end

    def scroll_down
      if @scroll_offset > 0
        @scroll_offset -= 1
        update
      end
    end

    private def scroll_up_if_needed
      if update_scroll_offset(y_shift: -1)
        update
      end
    end

    private def scroll_down_if_needed
      if update_scroll_offset(y_shift: +1)
        update
      end
    end

    # Updates the scroll offset in a way that (cursor + y_shift) is still between the view bounds
    # Returns true if the offset has been effectively modified.
    private def update_scroll_offset(y_shift = 0)
      start, end_ = self.view_bounds
      real_y = self.height_above_cursor + y_shift

      # case 1: cursor is before view start, we need to increase the scroll by the difference.
      if real_y < start
        @scroll_offset += start - real_y
        true

        # case 2: cursor is after view end, we need to decrease the scroll by the difference.
      elsif real_y > end_
        @scroll_offset -= real_y - end_
        true
      else
        false
      end
    end

    protected def expression_scrolled?
      expression_height() > epression_max_height()
    end

    # Returns y-start and end positions of the expression that should be displayed on the screen.
    # This take account of @scroll_offset, and the size start-end should never be greater than screen height.
    private def view_bounds
      end_ = expression_height() - 1

      start = {0, end_ + 1 - epression_max_height()}.max

      @scroll_offset = @scroll_offset.clamp(0, start) # @scroll_offset could not be greater than start.

      start -= @scroll_offset
      end_ -= @scroll_offset
      {start, end_}
    end

    private def print_line(io, colorized_line, line_index, line_size, prompt, first, is_last_part)
      if prompt
        io.puts unless first
        print_prompt(io, line_index)
      end
      io.print colorized_line

      # ```
      # prompt>begin                  |
      # prompt>  foooooooooooooooooooo|
      #                               | <- If the line size match exactly the screen width, we need to add a
      # prompt>  bar                  |    extra line feed, so computes based on `%` or `//` stay exact.
      # prompt>end                    |
      # ```
      io.puts if is_last_part && last_part_size(line_size) == 0
    end

    private def sync_output(&)
      if (output = @output).is_a?(IO::FileDescriptor) && output.tty?
        # Disallowing the synchronization reduce blinking on some terminal like vscode (#10)
        output.sync = false
        output.flush_on_newline = false
        output.print Term::Cursor.hide
        yield
        output.print Term::Cursor.show
        output.flush_on_newline = true
        output.sync = true
        output.flush
      else
        yield
      end
    end

    # Prints the colorized expression, this former is clipped if it's higher than screen.
    # The `header` is print just above.
    # The only displayed part of the expression is delimited by `view_bounds` and depend of the value of
    # `@scroll_offset`.
    # Lines that takes more than one line (if wrapped) are cut in consequence.
    #
    # *height_to_clear* is the height we have to clear between the previous cursor pos and the beginning of the prompt.
    # if *force_full_view* is true, all expression is dumped on screen, without clipping.
    private def print_expression_and_header(height_to_clear, force_full_view = false)
      height_to_clear += @header_height
      header = update_header()
      footer = update_footer()

      if force_full_view
        start, end_ = 0, Int32::MAX
      else
        update_scroll_offset()

        start, end_ = view_bounds()
      end

      first = true

      y = 0

      colorized_lines =
        if self.color?
          @highlight.call(self.expression).split('\n')
        else
          self.lines
        end

      # While printing, real cursor move, but @x/@y don't, so we track the moved cursor position to be able to
      # restore real cursor at @x/@y position.
      cursor_move_x = cursor_move_y = 0

      display = String.build do |io|
        # Iterate over the uncolored lines because we need to know the true size of each line:
        @lines.each_with_index do |line, line_index|
          line_height = line_height(line)

          break if y > end_
          if y + line_height <= start
            y += line_height
            next
          end

          if start <= y && y + line_height - 1 <= end_
            # The line can hold entirely between the view bounds, print it:
            print_line(io, colorized_lines[line_index], line_index, line.size, prompt: true, first: first, is_last_part: true)
            first = false

            cursor_move_x = line.size
            cursor_move_y = line_index

            y += line_height
          else
            # The line cannot holds entirely between the view bounds.
            # We need to cut the line into each part and display only parts that hold in the view
            colorized_parts = parts_from_colorized(colorized_lines[line_index])

            colorized_parts.each_with_index do |colorized_part, part_number|
              if start <= y <= end_
                # The part holds on the view, we can print it.
                print_line(io, colorized_part, line_index, line.size, prompt: part_number == 0, first: first, is_last_part: part_number == line_height - 1)
                first = false

                cursor_move_x = {line.size, (part_number + 1)*self.width - @prompt_size - 1}.min
                cursor_move_y = line_index
              end
              y += 1
            end
          end
        end
      end

      sync_output do
        # Rewind cursor from *height_to_clear*, then print the header and the clipped expression
        move_real_cursor(x: 0, y: -height_to_clear)
        @output.print Term::Cursor.column(1)
        @output.print Term::Cursor.clear_screen_down
        @output.print header
        @output.print display

        if @footer_height != 0
          # Display footer, then rewind cursor at the top left of the footer
          @output.puts
          @output.print footer
          @output.print Term::Cursor.column(1)
          move_real_cursor(x: @prompt_size, y: 1 - @footer_height)

          cursor_move_y += 1
          cursor_move_x = 0
        end

        # Retrieve the real cursor at its corresponding cursor position (`@x`, `@y`)
        x_save, y_save = @x, @y
        @y = cursor_move_y
        @x = cursor_move_x

        move_cursor_to(x_save, y_save, allow_scrolling: false)
      end
    end

    # Splits the given *line* (colorized) into parts delimited by wrapping.
    #
    #  Because *line* is colorized, it's hard to know when it's wrap based on its size (colors sequence might appear anywhere in the string)
    #  Here we does the following:
    #  * Create a `String::Builder` for the first part (`part_builder`)
    #  * Iterate over the *line*, parsing the color sequence
    #  * Count cursor `x` for each char unless color sequences
    #  * If count goes over term width:
    #    reset `x` to 0, and create a new `String::Builder` for next part.
    private def parts_from_colorized(line)
      parts = Array(String).new

      color_sequence = ""
      part_builder = String::Builder.new

      x = @prompt_size
      chars = line.each_char
      until (c = chars.next).is_a? Iterator::Stop
        # Parse color sequence:
        if c == '\e' && chars.next == '['
          color_sequence = String.build do |seq|
            seq << '\e' << '['

            until (c = chars.next) == 'm'
              break if c.is_a? Iterator::Stop
              seq << c
            end
            seq << 'm'
          end
          part_builder << color_sequence
        else
          part_builder << c
          x += 1
        end

        if x >= self.width
          # Wrapping: save part and create a new builder for next part
          part_builder << "\e[0m"
          parts << part_builder.to_s
          part_builder = String::Builder.new
          part_builder << color_sequence # We also add the previous color sequence because color need to be preserved.
          x = 0
        end
      end
      parts << part_builder.to_s
      parts
    end
  end
end
