#### 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 "colorize"
require "math"

lib LibC
  {% if flag?(:unix) %}
    # Bare minimum for the ioctl stuff we need

    REMI_TIOCGWINSZ = 0x5413u32

    struct RemiWinsize
      ws_row    : UInt16
      ws_col    : UInt16
      ws_xpixel : UInt16
      ws_ypixel : UInt16
    end

    fun remi_ioctl = "ioctl"(fd : LibC::Int, what : LibC::ULong, ...) : LibC::Int
  {% end %}
end

module RemiLib::Console
  extend self

  # :nodoc:
  ANSI_CSI = "\e["

  # :nodoc:
  ANSI_RESET = "\e[m"

  # Used to tell `#cursor` the direction to move the cursor.
  enum Direction
    # Move the cursor up one line.
    Up

    # Move the cursor down one line.
    Down

    # Move the cursor forward one cell.
    Forward

    # Move the cursor back one cell.
    Backward

    # Move the cursor one line down, and to the first column.
    NextLine

    # Move the cursor one line up, and to the first column.
    PrevLine

    # Move the cursor to a specific column.
    ToColumn
  end

  # Changes how `#eraseScreen` behaves.
  enum EraseDisplay
    # Erases the screen from the cursor to the bottom of the screen.
    CursorToEnd

    # Erases the screen from the cursor to the top of the screen.
    CursorToStart

    # Erases the entire visible screen.
    WholeScreen

    # Erases the entire visible screen, plus any visible lines in the
    # scrollback.
    ScreenAndScrollback
  end

  # Changes how `#eraseLine` behaves.
  enum EraseLine
    # Erase text from the cursor to the end of the line.
    CursorToEnd

    # Erase text from the cursor to the start of the line.
    CursorToStart

    # Erase the entire line.
    WholeLine
  end

  enum Color
    Black
    Red
    Green
    Yellow
    Blue
    Magenta
    Cyan
    White
    BrightBlack
    BrightRed
    BrightGreen
    BrightYellow
    BrightBlue
    BrightMagenta
    BrightCyan
    BrightWhite
    Default

    def code : Tuple(UInt8, UInt8)
      case self
      in .black? then {30u8, 40u8}
      in .red? then {31u8, 41u8}
      in .green? then {32u8, 42u8}
      in .yellow? then {33u8, 43u8}
      in .blue? then {34u8, 44u8}
      in .magenta? then {35u8, 45u8}
      in .cyan? then {36u8, 46u8}
      in .white? then {37u8, 47u8}
      in .bright_black? then {90u8, 100u8}
      in .bright_red? then {91u8, 101u8}
      in .bright_green? then {92u8, 102u8}
      in .bright_yellow? then {93u8, 103u8}
      in .bright_blue? then {94u8, 104u8}
      in .bright_magenta? then {95u8, 105u8}
      in .bright_cyan? then {96u8, 106u8}
      in .bright_white? then {97u8, 107u8}
      in .default? then {39u8, 49u8}
      end
    end
  end

  # Moves the cursor by the given number of cells.
  def cursor(strm : IO, cells : UInt32, dir : Direction) : IO
    strm << ANSI_CSI << cells
    case dir
    when .up?        then strm << 'A'
    when .down?      then strm << 'B'
    when .forward?   then strm << 'C'
    when .backward?  then strm << 'D'
    when .next_line? then strm << 'E'
    when .prev_line? then strm << 'F'
    when .to_column? then strm << 'G'
    end
    strm
  end

  # Moves the cursor to the given column in the current row.
  def cursor(strm : IO, col : UInt32) : IO
    strm << ANSI_CSI << "#{col}G"
    strm
  end

  # Moves the cursor to the given row and column.
  def cursor(strm : IO, row : UInt32, col : UInt32) : IO
    strm << ANSI_CSI << "#{row};#{col}H"
    strm
  end

  # Erases the screen.  `from` dictates how the erasing is done.
  def eraseScreen(strm : IO, from : EraseDisplay = EraseDisplay::CursorToEnd)
    strm << ANSI_CSI
    case from
    when .cursor_to_end?         then strm << '0'
    when .cursor_to_start?       then strm << '1'
    when .whole_screen?          then strm << '2'
    when .screen_and_scrollback? then strm << '3'
    end
    strm << 'J'
    strm
  end

  # Erases text from the terminal.  `from` dictates how the erasing is done.
  def erase(strm : IO, from : EraseLine = EraseLine::CursorToEnd)
    strm << ANSI_CSI
    case from
    when .cursor_to_end?   then strm << '0'
    when .cursor_to_start? then strm << '1'
    when .whole_line?      then strm << '2'
    end
    strm << 'K'
    strm
  end

  # Scrolls the screen by the given number of lines.
  def scroll(strm : IO, lines : UInt32, down : Bool = false) : IO
    strm << ANSI_CSI << lines str << (down ? 'T' : 'S')
    strm
  end

  def auxCtrl(strm : IO, turnOn : Bool = true) : IO
    strm << ANSI_CSI << (turnOn ? '5' : '4') << 'i'
    strm
  end

  # Saves the cursor's position.  See `#restoreCursorPos`.
  def saveCursorPos(strm : IO) : IO
    strm << ANSI_CSI << 's'
    strm
  end

  # Restores the cursor's position.  See `#saveCursorPos`.
  def restoreCursorPos(strm : IO) : IO
    strm << ANSI_CSI << 'u'
    strm
  end

  # Hides the cursor.
  def hideCursor(strm : IO) : IO
    strm << ANSI_CSI << "?25l"
    strm
  end

  # Changes the cursor to visible.
  def showCursor(strm : IO) : IO
    strm << ANSI_CSI << "?25h"
    strm
  end

  # Resets the terminal.
  def reset(strm : IO) : IO
    strm << "\ec"
    strm
  end

  # Switches the terminal to its default font.
  def defaultFont(strm : IO) : IO
    strm << ANSI_CSI << "10m"
    strm
  end

  # Changes the terminal font.
  def font(strm : IO, fnt : UInt32) : IO
    raise "Font must be between 1 and 9, inclusive" unless fnt > 0 && fnt < 10
    strm << ANSI_CSI << "#{fnt + 10}m"
    strm
  end

  # Sets the terminal title.
  def setTitle(strm : IO, title : String) : IO
    strm << "\e]0; #{title}\e\\"
    strm
  end

  {% if flag?(:unix) %}
    # Gets the height and width of the terminal (in that order).
    def getWinSize : Tuple(UInt16, UInt16)
      thing = LibC::RemiWinsize.new
      LibC.remi_ioctl(STDOUT.fd, LibC::REMI_TIOCGWINSZ, pointerof(thing))
      {thing.ws_row, thing.ws_col}
    end
  {% end %}

  private macro defineAnsiOnOff(name, startNum, endNum)
    @[AlwaysInline]
    def start{{name.id}}(strm : IO) : IO
      strm << ANSI_CSI << {{startNum}} << "m"
    end

    @[AlwaysInline]
    def {{"end#{name}".id}}(strm : IO) : IO
      strm << ANSI_CSI << {{endNum}} << "m"
    end

    @[AlwaysInline]
    def with{{name.id}}(strm : IO, &block)
      start{{name.id}}(strm)
      yield
      {{"end#{name}".id}}(strm)
    end
  end

  defineAnsiOnOff(Bold, 1, 22)
  defineAnsiOnOff(Faint, 2, 22)
  defineAnsiOnOff(Italic, 3, 23)
  defineAnsiOnOff(Underline, 4, 24)
  defineAnsiOnOff(SlowBlink, 5, 25)
  defineAnsiOnOff(FastBlink, 6, 25)
  defineAnsiOnOff(ReverseVideo, 7, 7)
  defineAnsiOnOff(Conceal, 8, 28)
  defineAnsiOnOff(CrossedOut, 9, 29)
  defineAnsiOnOff(Fraktur, 20, 23)
  defineAnsiOnOff(Framed, 51, 54)
  defineAnsiOnOff(Encircled, 52, 54)
  defineAnsiOnOff(Overlined, 53, 55)

  @[AlwaysInline]
  def startColor(strm : IO, fg : Color, bg : Color = Color::Default) : IO
    fg, _ = fg.code
    _, bg = bg.code
    strm << ANSI_CSI << "#{fg};#{bg}m"
    strm
  end

  @[AlwaysInline]
  def endColor(strm : IO) : IO
    fg, bg = Color::Default.code
    strm << ANSI_CSI << "#{fg};#{bg}m"
    strm
  end

  @[AlwaysInline]
  def withColor(strm : IO, fg : Color, bg : Color = Color::Default, &block) : IO
    startColor(strm, fg, bg)
    yield
    endColor(strm)
  end
end

abstract class ::IO
  @[AlwaysInline]
  def startColor(fg : RemiLib::Console::Color, bg : RemiLib::Console::Color = RemiLib::Console::Color::Default) : self
    RemiLib::Console.startColor(self, fg, bg)
  end

  @[AlwaysInline]
  def endColor : self
    RemiLib::Console.endColor(self)
  end

  @[AlwaysInline]
  def withColor(fg : RemiLib::Console::Color, bg : RemiLib::Console::Color = RemiLib::Console::Color::Default,
                &block) : self
    self.startColor(fg, bg)
    yield
    self.endColor
  end

  private macro defineAnsiOnOff(name, startNum, endNum)
    @[AlwaysInline]
    def start{{name.id}} : self
      RemiLib::Console.start{{name.id}}(self)
    end

    @[AlwaysInline]
    def {{"end#{name}".id}} : self
      RemiLib::Console.end{{name.id}}(self)
    end

    @[AlwaysInline]
    def with{{name.id}}(&block)
      self.start{{name.id}}
      yield
      self.end{{name.id}}
    end
  end

  defineAnsiOnOff(Bold, 1, 22)
  defineAnsiOnOff(Faint, 2, 22)
  defineAnsiOnOff(Italic, 3, 23)
  defineAnsiOnOff(Underline, 4, 24)
  defineAnsiOnOff(SlowBlink, 5, 25)
  defineAnsiOnOff(FastBlink, 6, 25)
  defineAnsiOnOff(ReverseVideo, 7, 7)
  defineAnsiOnOff(Conceal, 8, 28)
  defineAnsiOnOff(CrossedOut, 9, 29)
  defineAnsiOnOff(Fraktur, 20, 23)
  defineAnsiOnOff(Framed, 51, 54)
  defineAnsiOnOff(Encircled, 52, 54)
  defineAnsiOnOff(Overlined, 53, 55)
end
