#### libremiliacr
#### Copyright(C) 2020-2024 Remilia Scarlet <remilia@posteo.jp>
####
#### This program is free software: you can redistribute it and/or modify it
#### under the terms of the GNU General Public License as published the Free
#### Software Foundation, either version 3 of the License, or (at your option)
#### any later version.
####
#### This program is distributed in the hope that it will be useful, but WITHOUT
#### ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
#### FITNESS FOR A PARTICULAR PURPOSE.See the GNU General Public License for
#### more details.
####
#### You should have received a copy of the GNU General Public License along
#### with this program.If not, see<http:####www.gnu.org/licenses/.>
require "math"

module RemiLib
  extend self

  # :nodoc:
  SANITIZE_CHARS = {
    '\e' => "ESC",
    '\u{07}' => "BEL",
    '\0' => "NUL",
    '^' => "CFLEX",
    '\u{08}' => "BS",
    '\u{09}' => "HT",
    '\u{0B}' => "VT",
    '\u{0C}' => "FF",
    '\u{1A}' => "SUB",
    '\u{01}' => "SOH",
    '\u{02}' => "STX",
    '\u{03}' => "ETX",
    '\u{04}' => "EOT",
    '\u{05}' => "ENQ",
    '\u{06}' => "ACK",
    '\u{0E}' => "SO",
    '\u{0F}' => "SI",
    '\u{7F}' => "DEL"
  }

  # :nodoc:
  SANITIZE_EXTRA_CHARS = {
    '\u{0A}' => "LF",
    '\u{0D}' => "CR"
  }

  # Sanitizes *str* so that it's somewhat safer to print into a terminal.  This
  # essentially strips out meta characters (`ESC`, `SUB`, `BEL`, etc.),
  # replacing them with textual representations.
  #
  # If *leaveExtras* is `true`, then this will **NOT** remove additional
  # characters such as Carriage Return and Line Feed.
  @[AlwaysInline]
  def sanitize(str : String, leaveExtras : Bool = false) : String
    ret = str.gsub(SANITIZE_CHARS)
    ret = ret.gsub(SANITIZE_EXTRA_CHARS) unless leaveExtras
    ret
  end

  # Trims a null-padded string.
  #
  # If *fromFirst* is `false`, this removes all trailing null characters from a
  # string.  If *fromFirst* is `true`, all characters (null or not) are removed
  # starting from the first null character found.
  def trimNulls(str : String, fromFirst : Bool = true) : String
    if fromFirst
      first = str.index('\u{0}')
      if first.nil?
        str
      else
        str.[0...first]
      end
    else
      str.rstrip('\u{0}')
    end
  end

  # Returns a new `String` that contains *str*.  If the length of *str* is less
  # than *minLength*, then additional null bytes are added until the length is
  # equal to *minLength*.
  def padString(str : String, minLength : Int) : String
    String.build(minLength) do |ret|
      ret << str
      (minLength - str.size).times { |_| ret << '\u{0}' }
    end
  end

  # Writes *str* to *dest*.  If the length of *str* is less than *minLength*,
  # then additional null bytes are written to *dest* until the length is equal
  # to *minLength*.
  def writePaddedString(str : String, dest : IO, length : Int) : Nil
    if str.size >= length
      dest.write_string(str[0...length].to_slice)
    else
      dest.write_string(str.to_slice)
      (length - str.size).times do |_|
        dest.write_byte(0)
      end
    end
  end

  # Prints *text* to *str*, wrapping the text as it goes.  *indent* is the
  # number of *indentChar*s to write at the beginning of each line.  *maxWidth*
  # is the maximum length of a line of text.
  #
  # The first line can be shorter by applying an additional offset of
  # *firstLineOffset*.  Thus the first line will be at most
  # `maxWidth - indent - firstLineOffset` long.
  def buildWrapped(text : String, str : IO, *, indent : Int = 0, firstLineOffset : Int = 0,
                   maxWidth : Int = 80, indentChar : Char = ' ') : Nil
    pos : Int32 = 0
    maxLineWidth : Int32 = 0
    inc : Int32 = 0
    block : String = ""
    indentStr : String = indentChar.to_s
    firstLine : Bool = true

    while pos < text.size
      # We have extra to do if we're not on the first line (or indentFirstLine
      # is true)
      if firstLine
        maxLineWidth = maxWidth - indent - firstLineOffset
      else
        str << indentStr * indent
        maxLineWidth = maxWidth - indent
      end

      # See if we can write out the entire string on this first line or not
      if (pos + maxLineWidth) >= text.size
        str << text[pos..].strip
        return
      end

      # Get a block of text
      block = text[pos, maxLineWidth]

      # If this block doesn't contain a space, just write out the entire block.
      # If it wraps, it wraps... we don't handle hyphenation.
      if !block.includes?(' ')
        str << block.strip
        str << '\n'
        pos += maxLineWidth

      elsif block[0] == ' '
        # The only space is the very first character
        str << block[1..]
        pos += maxLineWidth

      elsif block[-1] == ' '
        # The space is the last character in the block, so just write the block minus the space
        str << block.strip
        str << '\n'
        pos += block.size

      else
        if text[pos + block.size] == ' '
          # Special case, we're at a word boundary
          str << block
          str << '\n'
          pos += block.size + 1
        else
          # The space is somewhere in the middle of `block`
          # Find the position of the final space in the block
          inc = block.rindex(' ') || raise "Space suddenly missing"
          str << block[...inc]
          str << '\n'
          pos += inc + 1
        end
      end

      firstLine = false
    end
  end

  # Prints *text* to a new string, wrapping the text as it goes, then returns
  # the new string.  *indent* is the number of *indentChar*s to write at the
  # beginning of each line.  *maxWidth* is the maximum length of a line of text.
  #
  # The first line can be shorter by applying an _additional_ offset
  # of *firstLineOffset*.  Thus the first line will be at most
  # `maxWidth - indent - firstLineOffset` long.
  @[Deprecated("Use #buildWrapped without an IO")]
  def makeWrapped(text : String, *, indent : Int = 0, maxWidth : Int = 80,
                  firstLineOffset : Int = 0, indentChar : Char = ' ') : String
    buildWrapped(text, indent: indent, maxWidth: maxWidth, firstLineOffset: firstLineOffset,
                 indentChar: indentChar)
  end

  def buildWrapped(text : String, *, indent : Int = 0, maxWidth : Int = 80,
                   firstLineOffset : Int = 0, indentChar : Char = ' ') : String
    String.build do |str|
      buildWrapped(text, str, indent: indent, maxWidth: maxWidth, firstLineOffset: firstLineOffset,
                   indentChar: indentChar)
    end
  end

  # Splits `text` at each space, taking into account quoted strings.  This is
  # similar to how most (all?) shells split a command line into individual
  # arguments.
  #
  # For example, calling `splitAsArgs(%{This "is a test" string})` would produce
  # an array with these elements:
  #
  # 1. This
  # 2. "is a test"
  # 3. string
  #
  # If *removeQuotes* is `true`, then the outer quotes are removed from all
  # returned strings.
  def splitAsArgs(text : String, *, removeQuotes : Bool = false) : Array(String)
    ret : Array(String) = text.split(Regex.new(%q{("[^"]+")|(\S+)}), remove_empty: true)
    ret.delete(" ")

    if removeQuotes
      ret.map! do |str|
        if str[0] == '"' && str[-1] == '"'
          str[1...-1]
        else
          str
        end
      end
    end

    ret
  end
end
