#!/usr/bin/env ruby

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

BIN_VERSION = "0.8.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
end

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

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'
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::Importable
  setlocale_lib = case Config::CONFIG['arch']
    when /darwin/; "libc.dylib"
    when /cygwin/; "cygwin1.dll"
    else; "libc.so.6"
  end

  Redwood::log "dynamically loading setlocale() from #{setlocale_lib}"
  begin
    dlload setlocale_lib
    extern "void setlocale(int, const char *)"
    Redwood::log "setting locale..."
    LibC.setlocale(6, "")  # LC_ALL == 6
  rescue RuntimeError => e
    Redwood::log "cannot dlload setlocale(); ncurses wide character support probably broken."
    Redwood::log "dlload error was #{e.class}: #{e.message}"
    if Config::CONFIG['arch'] =~ /bsd/
      Redwood::log "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.new
begin
  Index.lock
rescue Index::LockError => e
  require 'highline'

  h = HighLine.new
  h.wrap_at = :auto
  h.say Index.fancy_lock_error_message_for(e)

  case h.ask("Should I ask that process to kill itself? ")
  when /^\s*y(es)?\s*$/i
    h.say "Ok, suggesting seppuku..."
    FileUtils.touch Redwood::SUICIDE_FN
    sleep SuicideManager::DELAY * 2
    FileUtils.rm_f Redwood::SUICIDE_FN
    h.say "Let's try that again."
    retry
  else
    h.say <<EOS
Ok, giving up. If the process crashed and left a stale lockfile, you
can fix this by manually deleting #{Index.lockfile}.
EOS
    exit
  end
end

begin
  Redwood::start
  Index.load

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

  if(s = Index.source_for SentManager.source_name)
    SentManager.source = s
  else
    Redwood::log "no sent mail source, auto-adding..."
    Index.add_source SentManager.new_source
  end

  HookManager.run "startup"

  log "starting curses"
  start_cursing

  bm = BufferManager.new
  Colormap.new.populate_colormap

  log "initializing mail index buffer"
  imode = InboxMode.new
  ibuf = bm.spawn "Inbox", imode

  log "ready for interaction!"
  Logger.make_buf

  bm.draw_screen

  Index.usual_sources.each do |s|
    next unless s.respond_to? :connect
    reporting_thread("call #connect on #{s}") do
      begin
        s.connect
      rescue SourceError => e
        Redwood::log "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 { reporting_thread("poll after loading inbox") { sleep 1; PollManager.poll } unless $opts[:no_threads] || $opts[:no_initial_poll] }

  if $opts[:compose]
    ComposeMode.spawn_nicely :to_default => $opts[:compose]
  end

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

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

  until Redwood::exceptions.nonempty? || SuicideManager.die?
    c = 
       begin
         Ncurses.nonblocking_getch
       rescue Exception => e
         if e.is_a?(Interrupt)
           raise if BufferManager.ask_yes_or_no("Die ungracefully now?")
           bm.draw_screen
           nil
         end
       end
    next unless c
    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 :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 SuicideManager.die?
rescue Exception => e
  Redwood::record_exception e, "main"
ensure
  unless $opts[:no_threads]
    PollManager.stop if PollManager.instantiated?
    SuicideManager.stop if PollManager.instantiated?
    Index.stop_lock_update_thread
  end

  HookManager.run "shutdown"

  Redwood::finish
  stop_cursing
  Redwood::log "stopped cursing"

  if SuicideManager.instantiated? && SuicideManager.die?
    Redwood::log "I've been ordered to commit seppuku. I obey!"
  end

  if Redwood::exceptions.empty?
    Redwood::log "no fatal errors. good job, william."
    Index.save
  else
    Redwood::log "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 ~/.sup/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