#!/usr/bin/env ruby

require 'uri'
require 'rubygems'
require 'trollop'
require "sup"; Redwood::check_library_version_against "git"

PROGRESS_UPDATE_INTERVAL = 15 # seconds

class Float
  def to_s; sprintf '%.2f', self; end
  def to_time_s; infinite? ? "unknown" : super end
end

class Numeric
  def to_time_s
    i = to_i
    sprintf "%d:%02d:%02d", i / 3600, (i / 60) % 60, i % 60
  end
end

class Set
  def to_s; to_a * ',' end
end

def time
  startt = Time.now
  yield
  Time.now - startt
end

opts = Trollop::options do
  version "sup-sync (sup #{Redwood::VERSION})"
  banner <<EOS
Synchronizes the Sup index with one or more message sources by adding
messages, deleting messages, or changing message state in the index as
appropriate.

"Message state" means read/unread, archived/inbox, starred/unstarred,
and all user-defined labels on each message.

"Default source state" refers to any state that a source itself has
keeps about a message. Sup-sync uses this information when adding a
new message to the index. The source state is typically limited to
read/unread, archived/inbox status and a single label based on the
source name. Messages using the default source state are placed in
the inbox (i.e. not archived) and unstarred.

Usage:
  sup-sync [options] <source>*

where <source>* is zero or more source URIs. If no sources are given,
sync from all usual sources. Supported source URI schemes can be seen
by running "sup-add --help".

Options controlling HOW message state is altered:
EOS
  opt :asis, "If the message is already in the index, preserve its state. Otherwise, use default source state. (Default.)", :short => :none
  opt :restore, "Restore message state from a dump file created with sup-dump. If a message is not in this dumpfile, act as --asis.", :type => String, :short => :none
  opt :discard, "Discard any message state in the index and use the default source state. Dangerous!", :short => :none
  opt :archive, "When using the default source state, mark messages as archived.", :short => "-x"
  opt :read, "When using the default source state, mark messages as read."
  opt :extra_labels, "When using the default source state, also apply these user-defined labels (a comma-separated list)", :default => "", :short => :none

text <<EOS

Other options:
EOS
  opt :verbose, "Print message ids as they're processed."
  opt :optimize, "As the final operation, optimize the index."
  opt :all_sources, "Scan over all sources.", :short => :none
  opt :dry_run, "Don't actually modify the index. Probably only useful with --verbose.", :short => "-n"
  opt :version, "Show version information", :short => :none

  conflicts :asis, :restore, :discard
end

op = [:asis, :restore, :discard].find { |x| opts[x] } || :asis

Redwood::start
index = Redwood::Index.init

restored_state = if opts[:restore]
  dump = {}
  puts "Loading state dump from #{opts[:restore]}..."
  IO.foreach opts[:restore] do |l|
    l =~ /^(\S+) \((.*?)\)$/ or raise "Can't read dump line: #{l.inspect}"
    mid, labels = $1, $2
    dump[mid] = labels.to_set_of_symbols
  end
  puts "Read #{dump.size} entries from dump file."
  dump
else
  {}
end

seen = {}
index.lock_interactively or exit
begin
  index.load

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

  sources = if opts[:all_sources]
    Redwood::SourceManager.sources
  elsif ARGV.empty?
    Redwood::SourceManager.usual_sources
  else
    ARGV.map do |uri|
      Redwood::SourceManager.source_for uri or Trollop::die "Unknown source: #{uri}. Did you add it with sup-add first?"
    end
  end

  sources.each do |source|
    puts "Scanning #{source}..."
    num_added = num_updated = num_deleted = num_scanned = num_restored = 0
    last_info_time = start_time = Time.now

    Redwood::PollManager.poll_from source do |action,m,old_m,progress|
      num_scanned += 1
      if action == :delete
        num_deleted += 1
        puts "Deleting #{m.id}" if opts[:verbose]
      elsif action == :add
        seen[m.id] = true

        ## tweak source labels according to commandline arguments if necessary
        m.labels.delete :inbox if opts[:archive]
        m.labels.delete :unread if opts[:read]
        m.labels += opts[:extra_labels].to_set_of_symbols(",")

        ## decide what to do based on message labels and the operation we're performing
        dothis = case
        when (op == :restore) && restored_state[m.id]
          if old_m && (old_m.labels != restored_state[m.id])
            num_restored += 1
            m.labels = restored_state[m.id]
            :update_message_state
          elsif old_m.nil?
            num_restored += 1
            m.labels = restored_state[m.id]
            :add_message
          else
            # labels are the same; don't do anything
          end
        when op == :discard
          if old_m && (old_m.labels != m.labels)
            :update_message_state
          else
            # labels are the same; don't do anything
          end
        else
          if old_m
            :update_message
          else
            :add_message
          end
        end

        ## now, actually do the operation
        case dothis
        when :add_message
          puts "Adding new message #{source}##{m.source_info} with labels #{m.labels}" if opts[:verbose]
          num_added += 1
        when :update_message
          puts "Updating message #{source}##{m.source_info}; labels #{old_m.labels} => #{m.labels}; offset #{old_m.source_info} => #{m.source_info}" if opts[:verbose]
          num_updated += 1
        when :update_message_state
          puts "Changing flags for #{source}##{m.source_info} from #{old_m.labels} to #{m.labels}" if opts[:verbose]
          num_updated += 1
        end
      else fail
      end

      if Time.now - last_info_time > PROGRESS_UPDATE_INTERVAL
        last_info_time = Time.now
        elapsed = last_info_time - start_time
        pctdone = progress * 100.0
        remaining = (100.0 - pctdone) * (elapsed.to_f / pctdone)
        printf "## scanned %dm (~%.0f%%) @ %.1fm/s. %s elapsed, ~%s remaining\n", num_scanned, pctdone, num_scanned / elapsed, elapsed.to_time_s, remaining.to_time_s
      end
      next if opts[:dry_run]
    end

    puts "Scanned #{num_scanned}, added #{num_added}, updated #{num_updated}, deleted #{num_deleted} messages from #{source}."
    puts "Restored state on #{num_restored} (#{100.0 * num_restored / num_scanned}%) messages." if num_restored > 0
  end

  index.save

  if opts[:optimize]
    puts "Optimizing index..."
    optt = time { index.optimize unless opts[:dry_run] }
    puts "Optimized index of size #{index.size} in #{optt}s."
  end
rescue Redwood::FatalSourceError => e
  $stderr.puts "Sorry, I couldn't communicate with a source: #{e.message}"
rescue Exception => e
  File.open("sup-exception-log.txt", "w") { |f| f.puts e.backtrace }
  raise
ensure
  Redwood::finish
  index.unlock
end
