#!/usr/local/bin/ruby18

version_number = "1.3.1"

# Loads the ncurses-ruby module and imports "Ncurses" into the
# current namespace. It stops the program if loading the
# ncurses-ruby module fails.
def load_ncurses
  begin
    require "rubygems"
    require "ncurses"
    include Ncurses
  rescue LoadError
    $stderr.print <<EOF
  There is no Ncurses-Ruby package installed which is needed by TPP.
  You can download it on: http://ncurses-ruby.berlios.de/
EOF
    Kernel.exit(1)
  end
end

# Maps color names to constants and indexes.
class ColorMap

  # Maps color name _color_ to a constant
  def ColorMap.get_color(color)
    colors = { "white" => COLOR_WHITE,
               "yellow" => COLOR_YELLOW,
               "red" => COLOR_RED,
               "green" => COLOR_GREEN,
               "blue" => COLOR_BLUE,
               "cyan" => COLOR_CYAN,
               "magenta" => COLOR_MAGENTA,
               "black" => COLOR_BLACK,
               "default" => -1 }
    colors[color]
  end

  # Maps color name to a color pair index
  def ColorMap.get_color_pair(color)
    colors = { "white" => 1,
               "yellow" => 2,
               "red" => 3,
               "green" => 4,
               "blue" => 5,
               "cyan" => 6,
               "magenta" => 7,
               "black" => 8, 
               "default" =>-1}
    colors[color]
  end

end

# Opens a TPP source file, and splits it into the different pages.
class FileParser

  def initialize(filename)
    @filename = filename
    @pages = []
  end

  # Parses the specified file and returns an array of Page objects
  def get_pages
    begin
      f = File.open(@filename)
    rescue
      $stderr.puts "Error: couldn't open file: #{$!}"
      Kernel.exit(1)
    end

    number_pages = 0

    cur_page = Page.new("slide " + (number_pages + 1).to_s)

    f.each_line do |line|
      line.chomp!
      case line
        when /^--##/ # ignore comments
        when /^--newpage/
          @pages << cur_page
          number_pages += 1
          name = line.sub(/^--newpage/,"")
          if name == "" then
            name = "slide " + (number_pages+1).to_s
          else
            name.strip!
          end
          cur_page = Page.new(name)
        else
          cur_page.add_line(line)
      end # case
    end # each
    @pages << cur_page
  end
end # class FileParser


# Represents a page (aka `slide') in TPP. A page consists of a title and one or 
# more lines.
class Page

  def initialize(title)
    @lines = []
    @title = title
    @cur_line = 0
    @eop = false
  end

  # Appends a line to the page, but only if _line_ is not null
  def add_line(line)
    @lines << line if line
  end

  # Returns the next line. In case the last line is hit, then the end-of-page marker is set.
  def next_line
    line = @lines[@cur_line]
    @cur_line += 1
    if @cur_line >= @lines.size then
      @eop = true
    end
    return line
  end

  # Returns whether end-of-page has been reached.
  def eop?
    @eop
  end

  # Resets the end-of-page marker and sets the current line marker to the first line
  def reset_eop
    @cur_line = 0
    @eop = false
  end

  # Returns all lines in the page.
  def lines
    @lines
  end

  # Returns the page's title
  def title
    @title
  end
end



# Implements a generic visualizer from which all other visualizers need to be 
# derived. If Ruby supported abstract methods, all the do_* methods would be 
# abstract.
class TppVisualizer

  def initialize
    # nothing
  end

  # Splits a line into several lines, where each of the result lines is at most 
  # _width_ characters long, caring about word boundaries, and returns an array 
  # of strings.
  def split_lines(text,width)
    lines = []
    if text then
      begin
        i = width
        if text.length <= i then # text length is OK -> add it to array and stop splitting
          lines << text
          text = ""
        else
          # search for word boundary (space actually)
          while i > 0 and text[i] != ' '[0] do
            i -= 1
          end
          # if we can't find any space character, simply cut it off at the maximum width
          if i == 0 then
            i = width
          end
          # extract line
          x = text[0..i-1]
          # remove extracted line
          text = text[i+1..-1]
          # added line to array
          lines << x
        end
      end while text.length > 0
    end
    return lines
  end

  def do_footer(footer_text)
    $stderr.puts "Error: TppVisualizer#do_footer has been called directly."
    Kernel.exit(1)
  end
  
  def do_header(header_text)
    $stderr.puts "Error: TppVisualizer#do_header has been called directly."
    Kernel.exit(1)
  end

    
  def do_refresh
    $stderr.puts "Error: TppVisualizer#do_refresh has been called directly."
    Kernel.exit(1)
  end

  def new_page
    $stderr.puts "Error: TppVisualizer#new_page has been called directly."
    Kernel.exit(1)
  end

  def do_heading(text)
    $stderr.puts "Error: TppVisualizer#do_heading has been called directly."
    Kernel.exit(1)
  end

  def do_withborder
    $stderr.puts "Error: TppVisualizer#do_withborder has been called directly."
    Kernel.exit(1)
  end

  def do_horline
    $stderr.puts "Error: TppVisualizer#do_horline has been called directly."
    Kernel.exit(1)
  end

  def do_color(text)
    $stderr.puts "Error: TppVisualizer#do_color has been called directly."
    Kernel.exit(1)
  end

  def do_center(text)
    $stderr.puts "Error: TppVisualizer#do_center has been called directly."
    Kernel.exit(1)
  end

  def do_right(text)
    $stderr.puts "Error: TppVisualizer#do_right has been called directly."
    Kernel.exit(1)
  end

  def do_exec(cmdline)
    $stderr.puts "Error: TppVisualizer#do_exec has been called directly."
    Kernel.exit(1)
  end

  def do_wait
    $stderr.puts "Error: TppVisualizer#do_wait has been called directly."
    Kernel.exit(1)
  end

  def do_beginoutput
    $stderr.puts "Error: TppVisualizer#do_beginoutput has been called directly."
    Kernel.exit(1)
  end

  def do_beginshelloutput
    $stderr.puts "Error: TppVisualizer#do_beginshelloutput has been called directly."
    Kernel.exit(1)
  end

  def do_endoutput
    $stderr.puts "Error: TppVisualizer#do_endoutput has been called directly."
    Kernel.exit(1)
  end

  def do_endshelloutput
    $stderr.puts "Error: TppVisualizer#do_endshelloutput has been called directly."
    Kernel.exit(1)
  end

  def do_sleep(time2sleep)
    $stderr.puts "Error: TppVisualizer#do_sleep has been called directly."
    Kernel.exit(1)
  end

  def do_boldon
    $stderr.puts "Error: TppVisualizer#do_boldon has been called directly."
    Kernel.exit(1)
  end

  def do_boldoff
    $stderr.puts "Error: TppVisualizer#do_boldoff has been called directly."
    Kernel.exit(1)
  end

  def do_revon
    $stderr.puts "Error: TppVisualizer#do_revon has been called directly."
    Kernel.exit(1)
  end

  def do_revoff
    $stderr.puts "Error: TppVisualizer#do_revoff has been called directly."
    Kernel.exit(1)
  end

  def do_ulon
    $stderr.puts "Error: TppVisualizer#do_ulon has been called directly."
    Kernel.exit(1)
  end

  def do_uloff
    $stderr.puts "Error: TppVisualizer#do_uloff has been called directly."
    Kernel.exit(1)
  end

  def do_beginslideleft
    $stderr.puts "Error: TppVisualizer#do_beginslideleft has been called directly."
    Kernel.exit(1)
  end

  def do_endslide
    $stderr.puts "Error: TppVisualizer#do_endslide has been called directly."
    Kernel.exit(1)
  end

  def do_command_prompt
    $stderr.puts "Error: TppVisualizer#do_command_prompt has been called directly."
    Kernel.exit(1)
  end
  
  def do_beginslideright
    $stderr.puts "Error: TppVisualizer#do_beginslideright has been called directly."
    Kernel.exit(1)
  end

  def do_beginslidetop
    $stderr.puts "Error: TppVisualizer#do_beginslidetop has been called directly."
    Kernel.exit(1)
  end

  def do_beginslidebottom
    $stderr.puts "Error: TppVisualizer#do_beginslidebottom has been called directly."
    Kernel.exit(1)
  end

  def do_sethugefont
    $stderr.puts "Error: TppVisualizer#do_sethugefont has been called directly."
    Kernel.exit(1)
  end

  def do_huge(text)
    $stderr.puts "Error: TppVisualizer#do_huge has been called directly."
    Kernel.exit(1)
  end

  def print_line(line)
    $stderr.puts "Error: TppVisualizer#print_line has been called directly."
    Kernel.exit(1)
  end

  def do_title(title)
    $stderr.puts "Error: TppVisualizer#do_title has been called directly."
    Kernel.exit(1)
  end

  def do_author(author)
    $stderr.puts "Error: TppVisualizer#do_author has been called directly."
    Kernel.exit(1)
  end

  def do_date(date)
    $stderr.puts "Error: TppVisualizer#do_date has been called directly."
    Kernel.exit(1)
  end

  def do_bgcolor(color)
    $stderr.puts "Error: TppVisualizer#do_bgcolor has been called directly."
    Kernel.exit(1)
  end

  def do_fgcolor(color)
    $stderr.puts "Error: TppVisualizer#do_fgcolor has been called directly."
    Kernel.exit(1)
  end

  def do_color(color)
    $stderr.puts "Error: TppVisualizer#do_color has been called directly."
    Kernel.exit(1)
  end

  # Receives a _line_, parses it if necessary, and dispatches it 
  # to the correct method which then does the correct processing.
  # It returns whether the controller shall wait for input.
  def visualize(line,eop)
    case line
      when /^--heading /
        text = line.sub(/^--heading /,"")
        do_heading(text)
      when /^--withborder/
        do_withborder
      when /^--horline/
        do_horline
      when /^--color /
        text = line.sub(/^--color /,"")
        text.strip!
        do_color(text)
      when /^--center /
        text = line.sub(/^--center /,"")
        do_center(text)
      when /^--right /
        text = line.sub(/^--right /,"")
        do_right(text)
      when /^--exec /
        cmdline = line.sub(/^--exec /,"")
        do_exec(cmdline)
      when /^---/
        do_wait
        return true
      when /^--beginoutput/
        do_beginoutput
      when /^--beginshelloutput/
        do_beginshelloutput
      when /^--endoutput/
        do_endoutput
      when /^--endshelloutput/
        do_endshelloutput
      when /^--sleep /
        time2sleep = line.sub(/^--sleep /,"")
        do_sleep(time2sleep)
      when /^--boldon/
        do_boldon
      when /^--boldoff/
        do_boldoff
      when /^--revon/
        do_revon
      when /^--revoff/
        do_revoff
      when /^--ulon/
        do_ulon
      when /^--uloff/
        do_uloff
      when /^--beginslideleft/
        do_beginslideleft
      when /^--endslideleft/, /^--endslideright/, /^--endslidetop/, /^--endslidebottom/
        do_endslide
      when /^--beginslideright/
        do_beginslideright
      when /^--beginslidetop/
        do_beginslidetop
      when /^--beginslidebottom/
        do_beginslidebottom
      when /^--sethugefont /
        params = line.sub(/^--sethugefont /,"")
        do_sethugefont(params.strip)
      when /^--huge /
        figlet_text = line.sub(/^--huge /,"")
        do_huge(figlet_text)
      when /^--footer /
        @footer_txt = line.sub(/^--footer /,"")
        do_footer(@footer_txt) 
      when /^--header /
        @header_txt = line.sub(/^--header /,"")
        do_header(@header_txt) 
      when /^--title /
        title = line.sub(/^--title /,"")
        do_title(title)
      when /^--author /
        author = line.sub(/^--author /,"")
        do_author(author)
      when /^--date /
        date = line.sub(/^--date /,"")
        if date == "today" then
          date = Time.now.strftime("%b %d %Y")
        elsif date =~ /^today / then
          date = Time.now.strftime(date.sub(/^today /,""))
        end
        do_date(date)
      when /^--bgcolor /
        color = line.sub(/^--bgcolor /,"").strip
        do_bgcolor(color)
      when /^--fgcolor /
        color = line.sub(/^--fgcolor /,"").strip
        do_fgcolor(color)
      when /^--color /
        color = line.sub(/^--color /,"").strip
        do_color(color)
    else
      print_line(line)
    end

    return false
  end

  def close
    # nothing
  end

end

# Implements an interactive visualizer which builds on top of ncurses.
class NcursesVisualizer < TppVisualizer

  def initialize
    @figletfont = "standard"
    Ncurses.initscr
    Ncurses.curs_set(0)
    Ncurses.cbreak # unbuffered input
    Ncurses.noecho # turn off input echoing
    Ncurses.stdscr.intrflush(false)
    Ncurses.stdscr.keypad(true)
    @screen = Ncurses.stdscr
    setsizes
    Ncurses.start_color()
    Ncurses.use_default_colors()
    do_bgcolor("black")
    #do_fgcolor("white")
    @fgcolor = ColorMap.get_color_pair("white")
    @voffset = 5
    @indent = 3
    @cur_line = @voffset
    @output = @shelloutput = false
  end

  def get_key
    ch = @screen.getch
    case ch
      when Ncurses::KEY_RIGHT
        return :keyright
      when Ncurses::KEY_DOWN
        return :keydown
      when Ncurses::KEY_LEFT
        return :keyleft
      when Ncurses::KEY_UP
        return :keyup
      when Ncurses::KEY_RESIZE
        return :keyresize
      else
        return ch
      end
  end

  def clear
    @screen.clear
    @screen.refresh
  end


  def setsizes
    @termwidth = Ncurses.getmaxx(@screen)
    @termheight = Ncurses.getmaxy(@screen)
  end

  def do_refresh
    @screen.refresh
  end

  def do_withborder
    @withborder = true
    draw_border
  end
  
  def do_command_prompt()
    message = "Press any key to continue :)"
    cursor_pos = 0
    max_len = 50
    prompt = "tpp@localhost:~ $ "
    string = ""
    window = @screen.dupwin
    Ncurses.overwrite(window,@screen) # overwrite @screen with window
    Ncurses.curs_set(1)
    Ncurses.echo
    window.move(@termheight/4,1)
    window.clrtoeol()
    window.clrtobot()
    window.mvaddstr(@termheight/4,1,prompt) # add the prompt string

    loop do
      window.mvaddstr(@termheight/4,1+prompt.length,string) # add the code
      window.move(@termheight/4,1+prompt.length+cursor_pos) # move cursor to the end of code
      ch = window.getch
      case ch
        when Ncurses::KEY_ENTER, ?\n, ?\r
          Ncurses.curs_set(0)
          Ncurses.noecho
          rc = Kernel.system(string)
          if not rc then
            @screen.mvaddstr(@termheight/4+1,1,"Error: exec \"#{string}\" failed with error code #{$?}")
            @screen.mvaddstr(@termheight-2,@termwidth/2-message.length/2,message)
          end
          if rc then
            @screen.mvaddstr(@termheight-2,@termwidth/2-message.length/2,message)
            ch = Ncurses.getch()
            @screen.refresh
          end
          return
		when Ncurses::KEY_LEFT
          cursor_pos = [0, cursor_pos-1].max # jump one character to the left
        when Ncurses::KEY_RIGHT
          cursor_pos = [0, cursor_pos+1].max # jump one character to the right
        when Ncurses::KEY_BACKSPACE
          string = string[0...([0, cursor_pos-1].max)] + string[cursor_pos..-1]
          cursor_pos = [0, cursor_pos-1].max
          window.mvaddstr(@termheight/4, 1+prompt.length+string.length, " ")
        when " "[0]..255
          if (cursor_pos < max_len)
            string[cursor_pos,0] = ch.chr
            cursor_pos += 1
          else
            Ncurses.beep
          end
      else
          Ncurses.beep
      end
    end
    Ncurses.curs_set(0)

  end

  def draw_border
    @screen.move(0,0)
    @screen.addstr(".")
    (@termwidth-2).times { @screen.addstr("-") }; @screen.addstr(".")
    @screen.move(@termheight-2,0)
    @screen.addstr("`")
    (@termwidth-2).times { @screen.addstr("-") }; @screen.addstr("'")
    1.upto(@termheight-3) do |y|
      @screen.move(y,0)
      @screen.addstr("|") 
    end
    1.upto(@termheight-3) do |y|
      @screen.move(y,@termwidth-1)
      @screen.addstr("|") 
    end
  end

  def new_page
    @cur_line = @voffset
    @output = @shelloutput = false
    setsizes
    @screen.clear
  end

  def do_heading(line)
    @screen.attron(Ncurses::A_BOLD)
    print_heading(line)
    @screen.attroff(Ncurses::A_BOLD)
  end

  def do_horline
    @screen.attron(Ncurses::A_BOLD)
    @termwidth.times do |x|
      @screen.move(@cur_line,x)
      @screen.addstr("-")
    end
    @screen.attroff(Ncurses::A_BOLD)
  end

  def print_heading(text)
    width = @termwidth - 2*@indent
    lines = split_lines(text,width)
    lines.each do |l|
      @screen.move(@cur_line,@indent)
      x = (@termwidth - l.length)/2
      @screen.move(@cur_line,x)
      @screen.addstr(l)
      @cur_line += 1
    end
  end

  def do_center(text)
    width = @termwidth - 2*@indent
    if @output or @shelloutput then
      width -= 2
    end
    lines = split_lines(text,width)
    lines.each do |l|
      @screen.move(@cur_line,@indent)
      if @output or @shelloutput then
        @screen.addstr("| ")
      end
      x = (@termwidth - l.length)/2
      @screen.move(@cur_line,x)
      @screen.addstr(l)
      if @output or @shelloutput then
        @screen.move(@cur_line,@termwidth - @indent - 2)
        @screen.addstr(" |")
      end
      @cur_line += 1
    end
  end

  def do_right(text)
    width = @termwidth - 2*@indent
    if @output or @shelloutput then
      width -= 2
    end
    lines = split_lines(text,width)
    lines.each do |l|
      @screen.move(@cur_line,@indent)
      if @output or @shelloutput then
        @screen.addstr("| ")
      end
      x = (@termwidth - l.length - 5)
      @screen.move(@cur_line,x)
      @screen.addstr(l)
      if @output or @shelloutput then
        @screen.addstr(" |")
      end
      @cur_line += 1
    end
  end

  def show_help_page
    help_text = [ "tpp help", 
                  "",
                  "space bar ............................... display next entry within page",
                  "space bar, cursor-down, cursor-right .... display next page",
                  "b, cursor-up, cursor-left ............... display previous page",
                  "q, Q .................................... quit tpp",
                  "j, J .................................... jump directly to page",
                  "l, L .................................... reload current file",
                  "s, S .................................... jump to the first page",
                  "e, E .................................... jump to the last page",
                  "c, C .................................... start command line",
                  "?, h .................................... this help screen" ]
    @screen.clear
    y = @voffset
    help_text.each do |line|
      @screen.move(y,@indent)
      @screen.addstr(line)
      y += 1
    end
    @screen.move(@termheight - 2, @indent)
    @screen.addstr("Press any key to return to slide")
    @screen.refresh
  end

  def do_exec(cmdline)
    if $execok then
      rc = Kernel.system(cmdline)
      if not rc then
        # @todo: add error message
      end
    else
      @screen.addstr("--exec disabled by default for security reasons. Use option -x to enable it.")
    end
  end

  def do_wait
    # nothing
  end

  def do_beginoutput
    @screen.move(@cur_line,@indent)
    @screen.addstr(".")
    (@termwidth - @indent*2 - 2).times { @screen.addstr("-") }
    @screen.addstr(".")
    @output = true
    @cur_line += 1
  end

  def do_beginshelloutput
    @screen.move(@cur_line,@indent)
    @screen.addstr(".")
    (@termwidth - @indent*2 - 2).times { @screen.addstr("-") }
    @screen.addstr(".")
    @shelloutput = true
    @cur_line += 1
  end

  def do_endoutput
    if @output then
      @screen.move(@cur_line,@indent)
      @screen.addstr("`")
      (@termwidth - @indent*2 - 2).times { @screen.addstr("-") }
      @screen.addstr("'")
      @output = false
      @cur_line += 1
    end
  end

  def do_title(title)
    do_boldon
    do_center(title)
    do_boldoff
    do_center("")
  end

  def do_footer(footer_txt)
    @screen.move(@termheight - 3, (@termwidth - footer_txt.length)/2)
    @screen.addstr(footer_txt)
  end
 
 def do_header(header_txt)
    @screen.move(@termheight - @termheight+1, (@termwidth - header_txt.length)/2)
    @screen.addstr(header_txt)
  end

  def do_author(author)
    do_center(author)
    do_center("")
  end

  def do_date(date)
    do_center(date)
    do_center("")
  end

  def do_endshelloutput
    if @shelloutput then
      @screen.move(@cur_line,@indent)
      @screen.addstr("`")
      (@termwidth - @indent*2 - 2).times { @screen.addstr("-") }
      @screen.addstr("'")
      @shelloutput = false
      @cur_line += 1
    end
  end

  def do_sleep(time2sleep)
    Kernel.sleep(time2sleep.to_i)
  end

  def do_boldon
    @screen.attron(Ncurses::A_BOLD)
  end

  def do_boldoff
    @screen.attroff(Ncurses::A_BOLD)
  end

  def do_revon
    @screen.attron(Ncurses::A_REVERSE)
  end

  def do_revoff
    @screen.attroff(Ncurses::A_REVERSE)
  end

  def do_ulon
    @screen.attron(Ncurses::A_UNDERLINE)
  end

  def do_uloff
    @screen.attroff(Ncurses::A_UNDERLINE)
  end

  def do_beginslideleft
    @slideoutput = true
    @slidedir = "left"
  end

  def do_endslide
    @slideoutput = false
  end

  def do_beginslideright
    @slideoutput = true
    @slidedir = "right"
  end

  def do_beginslidetop
    @slideoutput = true
    @slidedir = "top"
  end

  def do_beginslidebottom
    @slideoutput = true
    @slidedir = "bottom"
  end

  def do_sethugefont(params)
    @figletfont = params
  end

  def do_huge(figlet_text)
    output_width = @termwidth - @indent
    output_width -= 2 if @output or @shelloutput
    op = IO.popen("figlet -f #{@figletfont} -w #{output_width} -k \"#{figlet_text}\"","r")
    op.readlines.each do |line|
      print_line(line)
    end
    op.close
  end

  def do_bgcolor(color)
    bgcolor = ColorMap.get_color(color) or COLOR_BLACK
    Ncurses.init_pair(1, COLOR_WHITE, bgcolor)
    Ncurses.init_pair(2, COLOR_YELLOW, bgcolor)
    Ncurses.init_pair(3, COLOR_RED, bgcolor)
    Ncurses.init_pair(4, COLOR_GREEN, bgcolor)
    Ncurses.init_pair(5, COLOR_BLUE, bgcolor)
    Ncurses.init_pair(6, COLOR_CYAN, bgcolor)
    Ncurses.init_pair(7, COLOR_MAGENTA, bgcolor)
    Ncurses.init_pair(8, COLOR_BLACK, bgcolor)
    if @fgcolor then
      Ncurses.bkgd(Ncurses.COLOR_PAIR(@fgcolor))
    else
      Ncurses.bkgd(Ncurses.COLOR_PAIR(1))
    end
  end

  def do_fgcolor(color)
    @fgcolor = ColorMap.get_color_pair(color)
    Ncurses.attron(Ncurses.COLOR_PAIR(@fgcolor))
  end

  def do_color(color)
    num = ColorMap.get_color_pair(color)
    Ncurses.attron(Ncurses.COLOR_PAIR(num))
  end

  def type_line(l)
    l.each_byte do |x|
      @screen.addstr(x.chr)
      @screen.refresh()
      r = rand(20)
      time_to_sleep = (5 + r).to_f / 250;
      # puts "#{time_to_sleep} #{r}"
      Kernel.sleep(time_to_sleep)
    end
  end

  def slide_text(l)
    return if l == ""
    case @slidedir
    when "left"
      xcount = l.length-1
      while xcount >= 0
        @screen.move(@cur_line,@indent)
        @screen.addstr(l[xcount..l.length-1])
        @screen.refresh()
        time_to_sleep = 1.to_f / 20
        Kernel.sleep(time_to_sleep)
        xcount -= 1
      end  
    when "right"
      (@termwidth - @indent).times do |pos|
        @screen.move(@cur_line,@termwidth - pos - 1)
        @screen.clrtoeol()
        maxpos = (pos >= l.length-1) ? l.length-1 : pos
        @screen.addstr(l[0..pos])
        @screen.refresh()
        time_to_sleep = 1.to_f / 20
        Kernel.sleep(time_to_sleep)
      end # do
    when "top"
      # ycount = @cur_line
      new_scr = @screen.dupwin
      1.upto(@cur_line) do |i|
        Ncurses.overwrite(new_scr,@screen) # overwrite @screen with new_scr
        @screen.move(i,@indent)
        @screen.addstr(l)
        @screen.refresh()
        Kernel.sleep(1.to_f / 10)
      end
    when "bottom"
      new_scr = @screen.dupwin
      (@termheight-1).downto(@cur_line) do |i|
        Ncurses.overwrite(new_scr,@screen)
        @screen.move(i,@indent)
        @screen.addstr(l)
        @screen.refresh()
        Kernel.sleep(1.to_f / 10)
      end
    end
  end

  def print_line(line)
    width = @termwidth - 2*@indent
    if @output or @shelloutput then
      width -= 2
    end
    lines = split_lines(line,width)
    lines.each do |l|
      @screen.move(@cur_line,@indent)
      if (@output or @shelloutput) and ! @slideoutput then
        @screen.addstr("| ")
      end
      if @shelloutput and (l =~ /^\$/ or l=~ /^%/ or l =~ /^#/) then # allow sh and csh style prompts
        type_line(l)
      elsif @slideoutput then
        slide_text(l)
      else
        @screen.addstr(l)
      end
      if (@output or @shelloutput) and ! @slideoutput then
        @screen.move(@cur_line,@termwidth - @indent - 2)
        @screen.addstr(" |")
      end
      @cur_line += 1
    end
  end

  def close
    Ncurses.nocbreak
    Ncurses.endwin
  end

  def read_newpage(pages,current_page)
    page = []
    @screen.clear()
    col = 0
    line = 2
    pages.each_index do |i|
      @screen.move(line,col*15 + 2)
      if current_page == i then
        @screen.printw("%2d %s <=",i+1,pages[i].title[0..80])
      else  
        @screen.printw("%2d %s",i+1,pages[i].title[0..80])
      end
      line += 1
      if line >= @termheight - 3 then
        line = 2
        col += 1
      end
    end
    prompt = "jump to slide: "
    prompt_indent = 12
    @screen.move(@termheight - 2, @indent + prompt_indent)
    @screen.addstr(prompt)
    # @screen.refresh();
    Ncurses.echo
    @screen.scanw("%d",page)
    Ncurses.noecho
    @screen.move(@termheight - 2, @indent + prompt_indent)
    (prompt.length + page[0].to_s.length).times { @screen.addstr(" ") }
    if page[0] then
      return page[0] - 1
    end
    return -1 # invalid page
  end

  def store_screen
    @screen.dupwin
  end

  def restore_screen(s)
    Ncurses.overwrite(s,@screen)
  end

  def draw_slidenum(cur_page,max_pages,eop)
    @screen.move(@termheight - 2, @indent)
    @screen.attroff(Ncurses::A_BOLD) # this is bad
    @screen.addstr("[slide #{cur_page}/#{max_pages}]")
	if @footer_txt.to_s.length > 0 then
	  do_footer(@footer_txt)
	end
	if @header_txt.to_s.length > 0 then
	  do_header(@header_txt)
	end

    if eop then
      draw_eop_marker
    end
  end

  def draw_eop_marker
    @screen.move(@termheight - 2, @indent - 1)
    @screen.attron(A_BOLD)
    @screen.addstr("*")
    @screen.attroff(A_BOLD)
  end

end


# Implements a visualizer which converts TPP source to LaTeX-beamer source (http://latex-beamer.sf.net/
class LatexVisualizer < TppVisualizer

  def initialize(outputfile)
    @filename = outputfile
    begin
      @f = File.open(@filename,"w+")
    rescue
      $stderr.print "Error: couldn't open file: #{$!}"
      Kernel.exit(1)
    end
    @slide_open = false
    @verbatim_open = false
    @width = 50
    @title = @date = @author = false
    @begindoc = false
    @f.puts '% Filename:      tpp.tex
% Purpose:       template file for tpp latex export
% Authors:       (c) Andreas Gredler, Michael Prokop http://grml.org/
% License:       This file is licensed under the GPL v2.
% Latest change: Fre Apr 15 20:34:37 CEST 2005
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

\documentclass{beamer}

\mode<presentation>
{
  \usetheme{Montpellier}
  \setbeamercovered{transparent}
}

\usepackage[german]{babel}
\usepackage{umlaut}
\usepackage[latin1]{inputenc}
\usepackage{times}
\usepackage[T1]{fontenc}

'
  end

  def do_footer(footer_text)
  end
  
  def do_header(header_text)
  end

  def do_refresh
  end

  def try_close
    if @verbatim_open then
      @f.puts '\end{verbatim}'
      @verbatim_open = false
    end
    if @slide_open then
      @f.puts '\end{frame}'
      @slide_open = false
    end
  end

  def new_page
    try_close
  end

  def do_heading(text)
    try_close
    @f.puts "\\section{#{text}}"
  end

  def do_withborder
  end

  def do_horline
  end

  def do_color(text)
  end

  def do_center(text)
    print_line(text)
  end

  def do_right(text)
    print_line(text)
  end

  def do_exec(cmdline)
  end

  def do_wait
  end

  def do_beginoutput
    # TODO: implement output stuff
  end

  def do_beginshelloutput
  end

  def do_endoutput
  end

  def do_endshelloutput
  end

  def do_sleep(time2sleep)
  end

  def do_boldon
  end

  def do_boldoff
  end

  def do_revon
  end
 
  def do_command_prompt
  end
  def do_revoff
  end

  def do_ulon
  end

  def do_uloff
  end

  def do_beginslideleft
  end

  def do_endslide
  end
  
  def do_beginslideright
  end

  def do_beginslidetop
  end

  def do_beginslidebottom
  end

  def do_sethugefont(text)
  end

  def do_huge(text)
  end

  def try_open
    if not @begindoc then
      @f.puts '\begin{document}'
      @begindoc = true
    end
    if not @slide_open then
      @f.puts '\begin{frame}[fragile]'
      @slide_open = true
    end
    if not @verbatim_open then
      @f.puts '\begin{verbatim}'
      @verbatim_open = true
    end
  end

  def try_intro
    if @author and @title and @date and not @begindoc then
      @f.puts '\begin{document}'
      @begindoc = true
    end
    if @author and @title and @date then
      @f.puts '\begin{frame}
        \titlepage
      \end{frame}'
    end
  end

  def print_line(line)
    try_open
    split_lines(line,@width).each do |l|
      @f.puts "#{l}"
    end
  end

  def do_title(title)
    @f.puts "\\title[#{title}]{#{title}}"
    @title = true
    try_intro
  end

  def do_author(author)
    @f.puts "\\author{#{author}}"
    @author = true
    try_intro
  end

  def do_date(date)
    @f.puts "\\date{#{date}}"
    @date = true
    try_intro
  end

  def do_bgcolor(color)
  end

  def do_fgcolor(color)
  end

  def do_color(color)
  end

  def close
    try_close
    @f.puts '\end{document}
    %%%%% END OF FILE %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%'
    @f.close
  end

end


# Implements a generic controller from which all other controllers need to be derived.
class TppController

  def initialize
    $stderr.puts "Error: TppController.initialize has been called directly!"
    Kernel.exit(1)
  end

  def close
    $stderr.puts "Error: TppController.close has been called directly!"
    Kernel.exit(1)
  end

  def run
    $stderr.puts "Error: TppController.run has been called directly!"
    Kernel.exit(1)
  end

end

# Implements a non-interactive controller for ncurses. Useful for displaying
# unattended presentation.
class AutoplayController < TppController

  def initialize(filename,secs,visualizer_class)
    @filename = filename
    @vis = visualizer_class.new
    @seconds = secs
    @cur_page = 0
  end

  def close
    @vis.close
  end

  def run
    begin
      @reload_file = false
      parser = FileParser.new(@filename)
      @pages = parser.get_pages
      if @cur_page >= @pages.size then
        @cur_page = @pages.size - 1
      end
      @vis.clear
      @vis.new_page
      do_run
    end while @reload_file
  end

  def do_run
    loop do
      wait = false
      @vis.draw_slidenum(@cur_page + 1, @pages.size, false)
      # read and visualize lines until the visualizer says "stop" or we reached end of page
      begin
        line = @pages[@cur_page].next_line
        eop = @pages[@cur_page].eop?
        wait = @vis.visualize(line,eop)
      end while not wait and not eop
      # draw slide number on the bottom left and redraw:
      @vis.draw_slidenum(@cur_page + 1, @pages.size, eop)
      @vis.do_refresh

      if eop then
        if @cur_page + 1 < @pages.size then
          @cur_page += 1
        else
          @cur_page = 0
        end
        @pages[@cur_page].reset_eop
        @vis.new_page
      end

      Kernel.sleep(@seconds)
    end # loop
  end

end

# Implements an interactive controller which feeds the visualizer until it is 
# told to stop, and then reads a key press and executes the appropiate action.
class InteractiveController < TppController

  def initialize(filename,visualizer_class)
    @filename = filename
    @vis = visualizer_class.new
    @cur_page = 0
  end

  def close
    @vis.close
  end

  def run
    begin
      @reload_file = false
      parser = FileParser.new(@filename)
      @pages = parser.get_pages
      if @cur_page >= @pages.size then
        @cur_page = @pages.size - 1
      end
      @vis.clear
      @vis.new_page
      do_run
    end while @reload_file
  end

  def do_run
    loop do
      wait = false
      @vis.draw_slidenum(@cur_page + 1, @pages.size, false)
      # read and visualize lines until the visualizer says "stop" or we reached end of page
      begin
        line = @pages[@cur_page].next_line
        eop = @pages[@cur_page].eop?
        wait = @vis.visualize(line,eop)
      end while not wait and not eop
      # draw slide number on the bottom left and redraw:
      @vis.draw_slidenum(@cur_page + 1, @pages.size, eop)
      @vis.do_refresh

      # read a character from the keyboard
      # a "break" in the when means that it breaks the loop, i.e. goes on with visualizing lines
      loop do
        ch = @vis.get_key
        case ch
          when 'q'[0], 'Q'[0] # 'Q'uit
            return
          when 'r'[0], 'R'[0] # 'R'edraw slide
            changed_page = true # @todo: actually implement redraw
          when 'e'[0], 'E'[0]
            @cur_page = @pages.size - 1
            break
          when 's'[0], 'S'[0]
            @cur_page = 0
            break
          when 'j'[0], 'J'[0] # 'J'ump to slide
            screen = @vis.store_screen
            p = @vis.read_newpage(@pages,@cur_page)
            if p >= 0 and p < @pages.size
              @cur_page = p
              @pages[@cur_page].reset_eop
              @vis.new_page
            else
              @vis.restore_screen(screen)
            end
            break
          when 'l'[0], 'L'[0] # re'l'oad current file
            @reload_file = true
            return
          when 'c'[0], 'C'[0] # command prompt
            screen = @vis.store_screen
            @vis.do_command_prompt
            @vis.clear
            @vis.restore_screen(screen)
          when '?'[0], 'h'[0]
            screen = @vis.store_screen
            @vis.show_help_page
            ch = @vis.get_key
            @vis.clear
            @vis.restore_screen(screen)
          when :keyright, :keydown, ' '[0]
            if @cur_page + 1 < @pages.size and eop then
              @cur_page += 1
              @pages[@cur_page].reset_eop
              @vis.new_page
            end
            break
          when 'b'[0], 'B'[0], :keyleft, :keyup
            if @cur_page > 0 then
              @cur_page -= 1
              @pages[@cur_page].reset_eop
              @vis.new_page
            end
            break
          when :keyresize
            @vis.setsizes
        end
      end
    end # loop
  end

end


# Implements a visualizer which converts TPP source to a nicely formatted text 
# file which can e.g. be used as handout.
class TextVisualizer < TppVisualizer

  def initialize(outputfile)
    @filename = outputfile
    begin
      @f = File.open(@filename,"w+")
    rescue
      $stderr.print "Error: couldn't open file: #{$!}"
      Kernel.exit(1)
    end
    @output_env = false
    @title = @author = @date = false
    @figletfont = "small"
    @width = 80
  end

  def do_footer(footer_text)
  end
  
  def do_header(header_text)
  end

  def do_refresh
  end

  def new_page
    @f.puts "--------------------------------------------"
  end

  def do_heading(text)
    @f.puts "\n"
    split_lines(text,@width).each do |l|
      @f.puts "#{l}\n"
    end
    @f.puts "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"
  end

  def do_withborder
  end

  def do_horline
    @f.puts "********************************************"
  end

  def do_color(text)
  end

  def do_exec(cmdline)
  end

  def do_wait
  end

  def do_beginoutput
    @f.puts ".---------------------------"
    @output_env = true
  end

  def do_beginshelloutput
    do_beginoutput
  end

  def do_endoutput
    @f.puts "`---------------------------"
    @output_env = false
  end

  def do_endshelloutput
    do_endoutput
  end

  def do_sleep(time2sleep)
  end

  def do_boldon
  end

  def do_boldoff
  end

  def do_revon
  end
 
  def do_command_prompt
  end
  def do_revoff
  end

  def do_ulon
  end

  def do_uloff
  end

  def do_beginslideleft
  end

  def do_endslide
  end
  
  def do_beginslideright
  end

  def do_beginslidetop
  end

  def do_beginslidebottom
  end

  def do_sethugefont(text)
    @figletfont = text
  end

  def do_huge(text)
    output_width = @width
    output_width -= 2 if @output_env
    op = IO.popen("figlet -f #{@figletfont} -w @output_width -k \"#{text}\"","r")
    op.readlines.each do |line|
      print_line(line)
    end
    op.close
  end

  def print_line(line)
    lines = split_lines(line,@width)
    lines.each do |l|
      if @output_env then
        @f.puts "| #{l}"
      else
        @f.puts "#{l}"
      end
    end
  end

  def do_center(text)
    lines = split_lines(text,@width)
    lines.each do |line|
      spaces = (@width - line.length) / 2
      spaces = 0 if spaces < 0
      spaces.times { line = " " + line }
      print_line(line)
    end
  end

  def do_right(text)
    lines = split_lines(text,@width)
    lines.each do |line|
      spaces = @width - line.length
      spaces = 0 if spaces < 0
      spaces.times { line = " " + line }
      print_line(line)
    end
  end

  def do_title(title)
    @f.puts "Title: #{title}"
    @title = true
    if @title and @author and @date then
      @f.puts "\n\n"
    end
  end

  def do_author(author)
    @f.puts "Author: #{author}"
    @author = true
    if @title and @author and @date then
      @f.puts "\n\n"
    end
  end

  def do_date(date)
    @f.puts "Date: #{date}"
    @date = true
    if @title and @author and @date then
      @f.puts "\n\n"
    end
  end

  def do_bgcolor(color)
  end

  def do_fgcolor(color)
  end

  def do_color(color)
  end

  def close
    @f.close
  end

end

# Implements a non-interactive controller to control non-interactive 
# visualizers (i.e. those that are used for converting TPP source code into 
# another format)
class ConversionController < TppController

  def initialize(input,output,visualizer_class)
    parser = FileParser.new(input)
    @pages = parser.get_pages
    @vis = visualizer_class.new(output)
  end

  def run
    @pages.each do |p|
      begin
        line = p.next_line
        eop = p.eop?
        @vis.visualize(line,eop)
      end while not eop
    end
  end

  def close
    @vis.close
  end

end

# Prints a nicely formatted usage message.
def usage
  $stderr.puts "usage: #{$0} [-t <type> -o <file>] <file>\n"
  $stderr.puts "\t -t <type>\tset filetype <type> as output format"
  $stderr.puts "\t -o <file>\twrite output to file <file>"
  $stderr.puts "\t -s <seconds>\twait <seconds> seconds between slides (with -t autoplay)"
  $stderr.puts "\t -x\t\tallow parsing of --exec in input files"
  $stderr.puts "\t --version\tprint the version"
  $stderr.puts "\t --help\t\tprint this help"
  $stderr.puts "\n\t currently available types: ncurses (default), autoplay, latex, txt"
  Kernel.exit(1)
end



################################
# Here starts the main program #
################################

input = nil
output = nil
type = "ncurses"
time = 1
$execok = nil

skip_next = false

ARGV.each_index do |i|
  if skip_next then
    skip_next = false
  else
    if ARGV[i] == '-v' or ARGV[i] == '--version' then
      printf "tpp - text presentation program %s\n", version_number
      Kernel.exit(1)
    elsif ARGV[i] == '-h' or ARGV[i] == '--help' then
      usage
    elsif ARGV[i] == '-t' then
      type = ARGV[i+1]
      skip_next = true
    elsif ARGV[i] == '-o' then
      output = ARGV[i+1]
      skip_next = true
    elsif ARGV[i] == "-s" then
      time = ARGV[i+1].to_i
      skip_next = true
    elsif ARGV[i] == "-x" then
      $execok = 1
    elsif input == nil then
      input = ARGV[i]
    end
    if output!=nil and output==input then
      $stderr.puts "Don't use the input file name as the output filename to prevent overwriting it. \n"
      Kernel.exit(1)
    end
  end
end

if input == nil then
  usage
end

ctrl = nil

case type
  when "ncurses"
    load_ncurses
    ctrl = InteractiveController.new(input,NcursesVisualizer)
  when "autoplay"
    load_ncurses
    ctrl = AutoplayController.new(input,time,NcursesVisualizer)
  when "txt"
    if output == nil then
      usage
    else
      ctrl = ConversionController.new(input,output,TextVisualizer)
    end
  when "latex"
    if output == nil then
      usage
    else
      ctrl = ConversionController.new(input,output,LatexVisualizer)
    end
else
  usage
end # case

ctrl.run
ctrl.close
