module Reply
  class History
    getter history = Deque(Array(String)).new
    getter max_size = 10_000
    getter index = 0

    # Hold the history lines being edited, always contains one element more than @history
    # because it can also contain the current line (not yet in history)
    @edited_history = [nil] of Array(String)?

    def size
      @history.size
    end

    def <<(lines)
      lines = lines.dup # make history elements independent

      if l = @history.delete_element(lines)
        # re-insert duplicate elements at the end
        @history.push(l)
      else
        # delete oldest entries until history size is `max_size`
        while @history.size >= max_size
          @history.delete_at(0)
        end

        @history.push(lines)
      end
      set_to_last
    end

    def clear
      @history.clear
      @edited_history.clear.push(nil)
      @index = 0
    end

    def up(current_edited_lines : Array(String))
      unless @index == 0
        @edited_history[@index] = current_edited_lines

        @index -= 1
        (@edited_history[@index]? || @history[@index]).dup
      end
    end

    def down(current_edited_lines : Array(String))
      unless @index == @history.size
        @edited_history[@index] = current_edited_lines

        @index += 1
        (@edited_history[@index]? || @history[@index]? || [""]).dup
      end
    end

    def go_to(index)
      if 0 <= index < @history.size
        @index = index

        @history[@index].dup
      end
    end

    def max_size=(max_size)
      @max_size = max_size.clamp 1..
    end

    def load(file : Path | String)
      File.touch(file) unless File.exists?(file)
      File.open(file, "r") { |f| load(f) }
    end

    def load(io : IO)
      str = io.gets_to_end
      if str.empty?
        @history = Deque(Array(String)).new
      else
        history =
          str.gsub(/(\\\n|\\\\)/, {
            "\\\n": '\e', # replace temporary `\\n` by `\e` because we first split by `\n` but want to avoid `\\n`. (`\e` could not exist in a history line)
            "\\\\": '\\', # replace `\\` by `\`.
          })
            .split('\n')        # split each expression
            .map(&.split('\e')) # split each expression by lines

        @history = Deque(Array(String)).new(history)
      end

      @edited_history.clear
      (@history.size + 1).times do
        @edited_history << nil
      end
      @index = @history.size
    end

    def save(file : Path | String)
      File.open(file, "w") { |f| save(f) }
    end

    def save(io : IO)
      @history.join(io, '\n') do |entry, io2|
        entry.join(io2, "\\\n") do |line, io3|
          io3 << line.gsub('\\', "\\\\")
        end
      end
    end

    # Sets the index to last added value
    protected def set_to_last
      @index = @history.size
      @edited_history.fill(nil).push(nil)
    end
  end
end

class Deque(T)
  # Add this method because unlike `Array`, `Deque#delete` return a Boolean instead of the element.
  def delete_element(obj) : T?
    internal_delete { |i| i == obj }
  end
end
