#!/usr/bin/env ruby

require 'rubygems'
require 'ncurses'
require 'curses'
require 'fileutils'
require 'trollop'
require "sup"

BIN_VERSION = "0.9.1"

unless Redwood::VERSION == BIN_VERSION
  $stderr.puts <<EOS

Error: version mismatch!
The sup executable is at version #{BIN_VERSION.inspect}.
The sup libraries are at version #{Redwood::VERSION.inspect}.

Is your development environment conflicting with rubygems?
EOS
  exit(-1)
end

$opts = Trollop::options do
  version "sup v#{Redwood::VERSION}"
  banner <<EOS
Sup is a curses-based email client.

Usage:
  sup [options]

Options are:
EOS
  opt :list_hooks, "List all hooks and descriptions, and quit."
  opt :no_threads, "Turn off threading. Helps with debugging. (Necessarily disables background polling for new messages.)"
  opt :no_initial_poll, "Don't poll for new messages when starting."
  opt :search, "Search for this query upon startup", :type => String
  opt :compose, "Compose message to this recipient upon startup", :type => String
  opt :subject, "When composing, use this subject", :type => String, :short => "j"
end

Trollop::die :subject, "requires --compose" if $opts[:subject] && !$opts[:compose]

Redwood::HookManager.register "startup", <<EOS
Executes at startup
No variables.
No return value.
EOS

Redwood::HookManager.register "shutdown", <<EOS 
Executes when sup is shutting down. May be run when sup is crashing,
so don\'t do anything too important. Run before the label, contacts,
and people are saved.
No variables.
No return value.
EOS

if $opts[:list_hooks]
  Redwood::HookManager.print_hooks
  exit
end

Thread.abort_on_exception = true # make debugging possible
Thread.current.priority = 1 # keep ui responsive

module Redwood

global_keymap = Keymap.new do |k|
  k.add :quit_ask, "Quit Sup, but ask first", 'q'
  k.add :quit_now, "Quit Sup immediately", 'Q'
  k.add :help, "Show help", '?'
  k.add :roll_buffers, "Switch to next buffer", 'b'
  k.add :roll_buffers_backwards, "Switch to previous buffer", 'B'
  k.add :kill_buffer, "Kill the current buffer", 'x'
  k.add :list_buffers, "List all buffers", ';'
  k.add :list_contacts, "List contacts", 'C'
  k.add :redraw, "Redraw screen", :ctrl_l
  k.add :search, "Search all messages", '\\', 'F'
  k.add :search_unread, "Show all unread messages", 'U'
  k.add :list_labels, "List labels", 'L'
  k.add :poll, "Poll for new messages", 'P'
  k.add :compose, "Compose new message", 'm', 'c'
  k.add :nothing, "Do nothing", :ctrl_g
  k.add :recall_draft, "Edit most recent draft message", 'R'
  k.add :show_inbox, "Show the Inbox buffer", 'I'
  k.add :show_console, "Show the Console buffer", '~'
end

## the following magic enables wide characters when used with a ruby
## ncurses.so that's been compiled against libncursesw. (note the w.) why
## this works, i have no idea. much like pretty much every aspect of
## dealing with curses.  cargo cult programming at its best.
##
## BSD users: if libc.so.6 is not found, try installing compat6x.
require 'dl/import'
module LibC
  extend DL.const_defined?(:Importer) ? DL::Importer : DL::Importable
  setlocale_lib = case Config::CONFIG['arch']
    when /darwin/; "libc.dylib"
    when /cygwin/; "cygwin1.dll"
    else; "libc.so.6"
  end

  debug "dynamically loading setlocale() from #{setlocale_lib}"
  begin
    dlload setlocale_lib
    extern "void setlocale(int, const char *)"
    debug "setting locale..."
    LibC.setlocale(6, "")  # LC_ALL == 6
  rescue RuntimeError => e
    warn "cannot dlload setlocale(); ncurses wide character support probably broken."
    warn "dlload error was #{e.class}: #{e.message}"
    if Config::CONFIG['arch'] =~ /bsd/
      warn "BSD variant detected. You may have to install a compat6x package to acquire libc."
    end
  end
end

def start_cursing
  Ncurses.initscr
  Ncurses.noecho
  Ncurses.cbreak
  Ncurses.stdscr.keypad 1
  Ncurses.use_default_colors
  Ncurses.curs_set 0
  Ncurses.start_color
  $cursing = true
end

def stop_cursing
  return unless $cursing
  Ncurses.curs_set 1
  Ncurses.echo
  Ncurses.endwin
end
module_function :start_cursing, :stop_cursing

Index.init
Index.lock_interactively or exit

begin
  Redwood::start
  Index.load

  $die = false
  trap("TERM") { |x| $die = true }
  trap("WINCH") { |x| BufferManager.sigwinch_happened! }

  if(s = Redwood::SourceManager.source_for DraftManager.source_name)
    DraftManager.source = s
  else
    debug "no draft source, auto-adding..."
    Redwood::SourceManager.add_source DraftManager.new_source
  end

  if(s = Redwood::SourceManager.source_for SentManager.source_uri)
    SentManager.source = s
  else
    Redwood::SourceManager.add_source SentManager.default_source
  end

  HookManager.run "startup"

  debug "starting curses"
  Redwood::Logger.remove_sink $stderr
  start_cursing

  bm = BufferManager.init
  Colormap.new.populate_colormap

  debug "initializing log buffer"
  lmode = Redwood::LogMode.new "system log"
  lmode.on_kill { Logger.clear! }
  Logger.add_sink lmode
  Logger.force_message "Welcome to Sup! Log level is set to #{Logger.level}."
  if Logger::LEVELS.index(Logger.level) > 0
    Logger.force_message "For more verbose logging, restart with SUP_LOG_LEVEL=#{Logger::LEVELS[Logger::LEVELS.index(Logger.level)-1]}."
  end

  debug "initializing inbox buffer"
  imode = InboxMode.new
  ibuf = bm.spawn "Inbox", imode

  debug "ready for interaction!"

  bm.draw_screen

  Redwood::SourceManager.usual_sources.each do |s|
    next unless s.respond_to? :connect
    reporting_thread("call #connect on #{s}") do
      begin
        s.connect
      rescue SourceError => e
        error "fatal error loading from #{s}: #{e.message}"
      end
    end
  end unless $opts[:no_initial_poll]

  imode.load_threads :num => ibuf.content_height, :when_done => lambda { |num| reporting_thread("poll after loading inbox") { sleep 1; PollManager.poll } unless $opts[:no_threads] || $opts[:no_initial_poll] }

  if $opts[:compose]
    to = Person.from_address_list $opts[:compose]
    mode = ComposeMode.new :to => to, :subj => $opts[:subject]
    BufferManager.spawn "New Message", mode
    mode.edit_message
  end

  unless $opts[:no_threads]
    PollManager.start
    Index.start_lock_update_thread
  end

  if $opts[:search]
    SearchResultsMode.spawn_from_query $opts[:search]
  end

  until Redwood::exceptions.nonempty? || $die
    c = begin
      Ncurses.nonblocking_getch
    rescue Interrupt
      raise if BufferManager.ask_yes_or_no "Die ungracefully now?"
      BufferManager.draw_screen
      nil
    end

    if c.nil?
      if BufferManager.sigwinch_happened?
        debug "redrawing screen on sigwinch"
        BufferManager.completely_redraw_screen
      end
      next
    end

    if c == 410
      ## this is ncurses's way of telling us it's detected a refresh.
      ## since we have our own sigwinch handler, we don't do anything.
      next
    end

    bm.erase_flash

    action = begin
      if bm.handle_input c
        :nothing
      else
        bm.resolve_input_with_keymap c, global_keymap
      end
    rescue InputSequenceAborted
      :nothing
    end
    case action
    when :quit_now
      break if bm.kill_all_buffers_safely
    when :quit_ask
      if bm.ask_yes_or_no "Really quit?"
        break if bm.kill_all_buffers_safely
      end
    when :help
      curmode = bm.focus_buf.mode
      bm.spawn_unless_exists("<help for #{curmode.name}>") { HelpMode.new curmode, global_keymap }
    when :roll_buffers
      bm.roll_buffers
    when :roll_buffers_backwards
      bm.roll_buffers_backwards
    when :kill_buffer
      bm.kill_buffer_safely bm.focus_buf
    when :list_buffers
      bm.spawn_unless_exists("buffer list", :system => true) { BufferListMode.new }
    when :list_contacts
      b, new = bm.spawn_unless_exists("Contact List") { ContactListMode.new }
      b.mode.load_in_background if new
    when :search
      query = BufferManager.ask :search, "search all messages: "
      next unless query && query !~ /^\s*$/
      SearchResultsMode.spawn_from_query query
    when :search_unread
      SearchResultsMode.spawn_from_query "is:unread"
    when :list_labels
      labels = LabelManager.all_labels.map { |l| LabelManager.string_for l }
      user_label = bm.ask_with_completions :label, "Show threads with label (enter for listing): ", labels
      unless user_label.nil?
        if user_label.empty?
          bm.spawn_unless_exists("Label list") { LabelListMode.new } if user_label && user_label.empty?
        else
          LabelSearchResultsMode.spawn_nicely user_label
        end
      end
    when :compose
      ComposeMode.spawn_nicely
    when :poll
      reporting_thread("user-invoked poll") { PollManager.poll }
    when :recall_draft
      case Index.num_results_for :label => :draft
      when 0
        bm.flash "No draft messages."
      when 1
        m = nil
        Index.each_id_by_date(:label => :draft) { |mid, builder| m = builder.call }
        r = ResumeMode.new(m)
        BufferManager.spawn "Edit message", r
        r.edit_message
      else
        b, new = BufferManager.spawn_unless_exists("All drafts") { LabelSearchResultsMode.new [:draft] }
        b.mode.load_threads :num => b.content_height if new
      end
    when :show_inbox
      BufferManager.raise_to_front ibuf
    when :show_console
      b, new = bm.spawn_unless_exists("Console", :system => true) { ConsoleMode.new }
      b.mode.run
    when :nothing, InputSequenceAborted
    when :redraw
      bm.completely_redraw_screen
    else
      bm.flash "Unknown keypress '#{c.to_character}' for #{bm.focus_buf.mode.name}."
    end

    bm.draw_screen
  end

  bm.kill_all_buffers if $die
rescue Exception => e
  Redwood::record_exception e, "main"
ensure
  unless $opts[:no_threads]
    PollManager.stop if PollManager.instantiated?
    Index.stop_lock_update_thread
  end

  HookManager.run "shutdown"

  Redwood::finish
  stop_cursing
  Redwood::Logger.remove_all_sinks!
  Redwood::Logger.add_sink $stderr, false
  debug "stopped cursing"

  if $die
    info "I've been ordered to commit seppuku. I obey!"
  end

  if Redwood::exceptions.empty?
    debug "no fatal errors. good job, william."
    Index.save
  else
    error "oh crap, an exception"
  end

  Index.unlock
end

unless Redwood::exceptions.empty?
  File.open(File.join(BASE_DIR, "exception-log.txt"), "w") do |f|
    Redwood::exceptions.each do |e, name|
      f.puts "--- #{e.class.name} from thread: #{name}"
      f.puts e.message, e.backtrace
    end
  end
  $stderr.puts <<EOS
----------------------------------------------------------------
I'm very sorry. It seems that an error occurred in Sup. Please
accept my sincere apologies. If you don't mind, please send the
contents of #{BASE_DIR}/exception-log.txt and a brief report of the
circumstances to sup-talk at rubyforge dot orgs so that I might
address this problem. Thank you!

Sincerely,
William
----------------------------------------------------------------
EOS
  Redwood::exceptions.each do |e, name|
    puts "--- #{e.class.name} from thread: #{name}"
    puts e.message, e.backtrace
  end
end

end