#!/usr/bin/env ruby

require 'rubygems'

no_ncursesw = false
begin
  require 'ncursesw'
rescue LoadError
  require 'ncurses'
  no_ncursesw = true
end

no_gpgme = false
begin
  require 'gpgme'
rescue LoadError
  no_gpgme = true
end

require 'fileutils'
require 'trollop'
require "sup"; Redwood::check_library_version_against "git"

if ENV['SUP_PROFILE']
  require 'ruby-prof'
  RubyProf.start
end

if no_ncursesw
  info "No 'ncursesw' gem detected. Install it for wide character support."
end

if no_gpgme
  info "No 'gpgme' gem detected. Install it for email encryption, decryption and signatures."
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.start
  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 :poll_unusual, "Poll for new messages from unusual sources", '{'
  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 :clear_hooks, "Clear all hooks", 'H'
  k.add :show_console, "Show the Console buffer", '~'

  ## Submap for less often used keybindings
  k.add_multi "reload (c)olors, rerun (k)eybindings hook", 'O' do |kk|
    kk.add :reload_colors, "Reload colors", 'c'
    kk.add :run_keybindings_hook, "Rerun keybindings hook", 'k'
  end
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
  Index.start_sync_worker unless $opts[:no_threads]

  $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"
  Redwood::Keymap.run_hook global_keymap

  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
    IdleManager.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

    IdleManager.ping

    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 (enter for saved searches): "
      unless query.nil?
        if query.empty?
          bm.spawn_unless_exists("Saved searches") { SearchListMode.new }
        else
          SearchResultsMode.spawn_from_query query
        end
      end
    when :search_unread
      SearchResultsMode.spawn_from_query "is:unread"
    when :list_labels
      labels = LabelManager.all_labels.map { |l| LabelManager.string_for l }
      labels = labels.each { |l| l.force_encoding 'UTF-8' if l.methods.include?(:encoding) }

      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 :poll_unusual
      if BufferManager.ask_yes_or_no "Really poll unusual sources?"
        reporting_thread("user-invoked unusual poll") { PollManager.poll_unusual }
      end
    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 :clear_hooks
      HookManager.clear
    when :show_console
      b, new = bm.spawn_unless_exists("Console", :system => true) { ConsoleMode.new }
      b.mode.run
    when :reload_colors
      Colormap.reset
      Colormap.populate_colormap
      bm.completely_redraw_screen
      bm.flash "reloaded colors"
    when :run_keybindings_hook
      HookManager.clear_one 'keybindings'
      Keymap.run_hook global_keymap
      bm.flash "keybindings hook 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?
    IdleManager.stop if IdleManager.instantiated?
    Index.stop_lock_update_thread
  end

  HookManager.run "shutdown"

  Index.stop_sync_worker
  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

  if (fn = ENV['SUP_PROFILE'])
    result = RubyProf.stop
    File.open(fn, 'w') { |io| RubyProf::CallTreePrinter.new(result).print(io) }
  end
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. Please submit the contents of
#{BASE_DIR}/exception-log.txt and a brief report of the
circumstances to http://masanjin.net/sup-bugs/ 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
