#!/usr/local/bin/perl

use strict;
use DBI;
use Curses;
use Curses::UI;
use Term::ReadKey;
use Net::Twitter;
use Data::Dumper;
use Text::Wrap;
use Getopt::GUI::Long;
use strict;
use Date::Parse;

our $VERSION = "0.42";
our $dbh;
our $DEBUG = 0;

my $DBVERSION = 1;

my %prefwidgets;
my @preferences =
  ( name => 'Twitter Name',
    pass => 'Twitter Password',
    undef,
    fetchunread => 'Tweets to show when nothing new',
    max_tweets   => 'Maximum number of new tweets to fetch',
    undef,
    do_debugging => 'Log debugging to ~/.twitmail.debug',
  );

my %prefdefaults =
  (fetchunread => 100,
   max_tweets => 200,
   do_debugging => 0,
  );

my @helptexts =
  (" (g)et new tweets | (TAB) move to post field | (>) more help",
   " (r)eply | (f)orward/retweet | (o)pen URL | (^) Show Parent |  (p)references | (>) more help ",
   " (/) Search Tweets | (ctrl-q) Quit | (>) more help "
   );
my $helpnum = -1;

Getopt::GUI::Long::Configure(qw(display_help no_ignore_case allow_zero));

my %opts = (m => 'friends,replies',
	    I => 4);

GetOptions(\%opts,
	   ["l|list",     "List new tweets"],
	   ["a|show-all", "Show all messages, not just recent"],
	   ["m|modes=s",  "Comma separated modes to use: friends,replies"],
	   ["u|update",   "Set your status from arguments"],
	   ["r|reply=i",  "Reply to message number INTEGER with text from arguments"],
	   ["f|follow=s", "Follow the user named STRING"],
	   ["S|no-save",  "Don't save the config file back"],
	   ["I|indent-depth", "Amount of spaces to use when indenting"],
	   ["n|count=i",  "Number of messages to return (max)"],
	   ["d|dump",     "Dumper the results instead of nicely printing"],
	   ["D|debug",    "output DEBUG to ~/.twitmail.debug"],
	  );

$DEBUG = 1 if ($opts{'D'});

my %config;

read_config();
init_dbh();

DEBUG("user: " . (username => $config{'user'} || get_config('name')));
DEBUG("pass: " . (username => $config{'password'} || get_config('pass')));

my $twit;
init_twit();

#$twit->credentials($config{'user'}, $config{'password'});

# set our update status
if ($opts{'u'}) {
    $twit->update({status => format_message()});
}

# reply
if ($opts{'r'}) {
    $opts{'r'} = sprintf("%02.2d", $opts{'r'});
    if (!exists($config{'msgnum' . $opts{'r'}})) {
	die "No such message number: $opts{'r'}\n";
    }
    $twit->update({status =>
		   format_message('@' . $config{'usernum' . $opts{'r'}}),
		   in_reply_to_status_id => $config{'msgnum' . $opts{'r'}}});
    exit;
}

# add a friend if needed
if ($opts{'f'}) {
    $twit->create_friend({id => $opts{'f'}, follow => '1'});
    exit;
}

if (!$opts{'l'}) {
    do_curses();
    exit;
}

# display mail
my @modes = split(/\s*,\s*/,$opts{'m'});

my $msgnum = 0;

foreach my $mode (@modes) {

    my %args;
    if (!$opts{'a'} && defined($config{$mode . '_since_id'})) {
	$args{'since_id'} = $config{$mode . '_since_id'};
    }

    if ($opts{'n'}) {
	$args{'count'} = $opts{'n'};
    }

    if ($mode eq 'replies') {
	print_results($twit->replies(\%args), $mode, 'since_id', 'id');
    }

    if ($mode eq 'friends') {
	print_results($twit->friends_timeline(\%args), $mode, 'since_id', 'id');
    }

    if ($mode eq 'public') {
	print_results($twit->public_timeline(\%args), $mode);
    }
}

save_config();

sub print_results {
    my ($data, $mode, $setarg, $setfrom) = @_;
    my @newdata;
    my %ids;

    if ($opts{'d'}) {
	print Dumper($_[0]);
	return;
    }

    if ($twit->http_message() ne 'OK') {
	print "ERROR!\n";
	print "  ", $twit->http_code(),"\n";
	print "  ", $twit->http_message(),"\n";
	print "  ", $twit->get_error()->{'error'},"\n";
	exit(1);
    }

    return if ($#$data == -1);
    print "---- $mode ------------------------------------------------------------\n";

    foreach my $dat (@$data) {
	$ids{$dat->{'id'}} = $dat;
    }
    foreach my $dat (@$data) {
	if ($dat->{'in_reply_to_status_id'} &&
	    exists($ids{$dat->{'in_reply_to_status_id'}})) {
	    push @{$ids{$dat->{'in_reply_to_status_id'}}{'replies'}}, $dat;
	} else {
	    push @newdata, $dat;
	}
    }
    print_each_result(\@newdata, $mode, $setarg, $setfrom, 0, "", "");
}

sub print_each_result {
    my @lines = sprint_each_result(@_);
    print @lines;
}

sub sprint_each_result {
    my ($data, $mode, $setarg, $setfrom, $indent, $lastday, $lastuser,
	$nowrap, $includestatus, $ids)
      = @_;
    my @results;

    $indent ||= 0;

    my $intext;
    if ($indent) {
	$intext = " " . " " x (($indent-1) * $opts{'I'}) . "-" x $opts{'I'} ;
	$intext =~ s/-/|/;
	$intext =~ s/-$/ /;
    } else {
	$intext = " ";
    }
    my $intextlen = length($intext);
    my $intextspaces = " " x $intextlen;

    foreach my $dat (@$data) {

	$msgnum++;

	# set the time stamp from zulu time to localtime
	$dat->{'created_at'} = localtime(str2time($dat->{'created_at'}));

	# truncate for prettiness
	$dat->{'created_at'} =~ s/^(...)......./$1/; # only use day of week
	my $thisday = $1;
	$dat->{'created_at'} =~ s/:\d\d//;           # drop seconds
	$dat->{'created_at'} =~ s/^$lastday/   / if ($lastday);
	$lastday = $thisday;

	# create the results
	my $line = sprintf("%-2.2s %-9.9s %-10.10s  $intext%s", 
			   ($includestatus ? $dat->{'twitmailstatus'} : $msgnum),
			   $dat->{'created_at'},
			   ($lastuser eq $dat->{'user'}{'screen_name'})
			   ? "" : $dat->{'user'}{'screen_name'},
			   $dat->{'text'});
	$line = wrap("", 
		     sprintf("%2.2s %-9.9s %-10.10s  $intextspaces","", "",""),
		     $line) unless($nowrap);
	$line .= "\n";
	$lastuser = $dat->{'user'}{'screen_name'};
	push @results, $line;
	push @$ids, $dat->{'id'} if ($ids);

	my $formattednum = sprintf("%02.2d", $msgnum);

	$config{'msgnum' . $formattednum} = 
	  $dat->{'id'} if (exists($dat->{'id'}));
	$config{'usernum' . $formattednum} =
	  $dat->{'user'}{'screen_name'} if (exists($dat->{'user'}));

	if (exists($dat->{'replies'})) {
	    my @newlines = 
	      sprint_each_result($dat->{'replies'}, $mode, $setarg, $setfrom,
				 $indent+1, $lastday, $lastuser, $nowrap,
				 $includestatus, $ids);
	    push @results, @newlines;
	}

    }

#    print "  ", Dumper($twit->get_error()),"\n";

    # XXX: only if error
    if (defined($mode) && defined($setarg)) {
	$config{"${mode}_${setarg}"} = $data->[0]{$setfrom || $setarg};
    }
    return @results;
}

sub read_config {
    open(I, $ENV{'HOME'} . "/.twitmailrc");
    while (<I>) {
	my @data = /^([^\s]+)\s+(.*)/;
	$config{$data[0]} = $data[1];
    }
}

sub save_config {
    return if ($opts{'S'});
    open(O, ">" . $ENV{'HOME'} . "/.twitmailrc");
    foreach my $key (sort keys(%config)) {
	print O "$key\t$config{$key}\n";
    }
}

sub format_message {
    my $output;
    $output .= join(" ",@_, @ARGV);
    if (length($output) > 140) {
	die "message too long (" . length($output) . " > 140); sorry\n";
    }
    return $output;
}

##############################################################################
# CURSES VARIABLES
#   (foiled again)
#

my $count = 0;
my $tweets;
my $subjectListBox;
my %subjects;
my $twitedit;

my %msginfo;
my %labels;
my $cui;
my $statusbar;

my $mainwin;
my $prefwin;
my $helpbar;
my $statusbar;
my $msgwin;

sub show_msg {
    my $num = $_[0]->get_active_value();
    my $tweet = $tweets->{$num};
    $msginfo{'from'}->text($tweet->{'fromuser'});
    $msginfo{'fromname'}->text($tweet->{'fromname'});
    $msginfo{'date'}->text($tweet->{'created_at'});
    $msginfo{'reply'}->text($tweet->{'inreplytousername'} || "");
    $msginfo{'text'}->text($tweet->{'tweet'});

    my $text = $labels{$num};
    $_[0]->add_labels($num => $text) if ($text =~ s/^ N//);

    if ($tweet->{'twitmailstatus'} eq 'N') {
	DEBUG("marking $tweet->{id} as read");
	mark_tweet_as($tweet, '');
    }
    $msgwin->draw();
}

sub thread_tweets {
    my ($tweets) = @_;
    my @returnlist;

    foreach my $tweetid (sort keys(%{$tweets})) {
	my $tweet = $tweets->{$tweetid};
	my $replyid = $tweet->{'in_reply_to_status_id'} || 
	  $tweet->{'inreplytoid'};
	if ($replyid &&
	    exists($tweets->{$replyid})) {
	    push @{$tweets->{$replyid}{'replies'}},
	      $tweet;
	} else {
	    push @returnlist, $tweet;
	}
    }
    return @returnlist;
}

sub check_for_new {
    $cui->status("Fetching Tweets");
    collect_tweets();
    $cui->nostatus();
    gather_default_tweets(); #XXX just merge in new
}

sub merge_in_tweets {
    my @newtweets = @_;

    DEBUG("merging: starting=", Dumper($tweets));

    foreach my $newtweetset (@newtweets) {
	foreach my $tweetid (keys(%$newtweetset)) {
	    $tweets->{$tweetid} = $newtweetset->{$tweetid};
	}
    }

    DEBUG("after merge: ", Dumper($tweets));
}

sub gather_default_tweets {
    $tweets = get_tweets();

    my @keys = keys(%$tweets);
    my $isnew = 1;

    DEBUG("here: keys=" . $#keys);

    if ($#keys == -1) {
	# no new tweets; get last 100

	$tweets = get_tweets("", "limit " . get_config('fetchunread',100));
	$isnew = 0;
	# $tweets = get_tweets("where id = 2515551407 or id = 2515608455 or id = 2517039242","");
    } else {
	# merge in the last unread
	merge_in_tweets(get_tweets("where twitmailstatus <> 'N'", "limit 1"));
    }

    my $ids = set_listbox();

    if (! $isnew && $#$ids > -1) {
	DEBUG("selecting $ids->[$#$ids] instead of $ids->[0]\n");
	$subjectListBox->set_selection($ids->[$#$ids]);
    }
}

sub set_listbox {
    my (@ids);

    my @threaded = thread_tweets($tweets);
    DEBUG("THREADED OUTPUT:");
    DEBUG(Dumper(\@threaded));
    my @lines = sprint_each_result(\@threaded, undef, undef, undef, 0,
				   undef, undef, 1, 1, \@ids);

    DEBUG(Dumper(\@lines));

    for (my $num = 0; $num <= $#lines; $num++) {
	$labels{$ids[$num]} = $lines[$num];
    }
    $subjectListBox->labels(\%labels);
    $subjectListBox->values(\@ids);
    set_status("Showing " . ($#ids + 1) . " tweets");
    return \@ids;
}

my $inreplyto;
my $starttext;

sub set_twit_text {
    my ($text) = @_;
    $twitedit->text($text);
    $twitedit->draw();
    update_count();
    $twitedit->focus();
}

sub set_status{
    my $text = " Status: " . join(" ",@_);
    DEBUG("setting status: $text");
    $text .= " " x (78-length($text));
    $statusbar->text($text);
}

######################################################################
# COMMANDS
#

sub reply_to {
    my ($widget) = @_[0];
    return if (!$widget);
    $inreplyto = $widget->get_active_value();;
    $starttext = '@' . $tweets->{$inreplyto}{'fromuser'} . " ";
    set_twit_text($starttext);
}

sub retweet {
    my ($widget) = @_[0];
    return if (!$widget);
    $inreplyto = $widget->get_active_value();;
    $starttext = 'RT @' . $tweets->{$inreplyto}{'fromuser'} . " " .
      $tweets->{$inreplyto}{'tweet'};
    set_twit_text($starttext);
}

sub openurl {
    my ($widget) = @_[0];
    return if (!$widget);
    my $tweetid = $widget->get_active_value();;
    my $tweet = $tweets->{$tweetid}{'tweet'};
    $tweet =~ s/.*http//;
    $tweet =~ s/\s+.*//;
    $cui->status("Opening $tweet");
    # XXX: security quote
    system("firefox '$tweet' &");
    $cui->nostatus();
}

sub submit_twit {
    my $text = $twitedit->get();
    my @args;

    if ($inreplyto && $text =~ /^$starttext/) {
	@args = (in_reply_to_status_id => $inreplyto);
	mark_tweet_as($inreplyto, 'R');
    }

    my $len = length($text);
    if ($len > 140) {
	# xxx!!!
    } else {
	$cui->status("Submitting tweet");
	my $status = $twit->update({status => $text, @args});
	$cui->nostatus;
	if (!defined($status)) {
	    $cui->error("Submitting tweet failed");
	}
    }
    $inreplyto = '';
    $starttext = '';
    set_twit_text($starttext);
    $subjectListBox->focus();
}

sub fetch_parent {
    my ($widget) = @_[0];
    # merge in the last unread
    my $currentid = $widget->get_active_value();;
    my $parentid  = $tweets->{$currentid}{'inreplytoid'};
    if (!$parentid) {
	$cui->status("This tweet is not a reply and doesn't have a parent tweet");
	sleep(2);
	$cui->nostatus;
	return;
    }
    merge_in_tweets(get_tweets("where id = ?", "", $parentid));
    set_listbox();
}

my $twitcount;
sub update_count {
    my $count = length($twitedit->get());
    my $text = $count || "";
    $twitcount->text($text);
    if ($count > 140) {
	$twitcount->reverse(1);
    } else {
	$twitcount->reverse(0);
    }
}

sub save_prefs {
    foreach my $pref (keys(%prefwidgets)) {
	set_config($pref, $prefwidgets{$pref}->get());
    }
}

sub rotate_help {
    $helpnum++;
    $helpnum = 0 if ($helpnum > $#helptexts);
    my $help = $helptexts[$helpnum];
    $help .= " " x (78-length($help));
    DEBUG("setting help to #$helpnum '$help'");
    $helpbar->text($help);
    $helpbar->draw();
}

sub search_text {
    my $answer =
      $cui->question(-question => "Enter text to search past tweets for:",
		     -title => "Search For Text");
    if ($answer) {
	$answer = "%$answer%";
	$tweets = get_tweets("where tweet like ?", "", $answer);
	my @keys = keys(%$tweets);
	if ($#keys == -1) {
	    $cui->error("no tweets found for that search");
	} else {
	    set_listbox();
	}
    } else {
	set_status("No search text entered; cancelling search...");
    }
}


######################################################################
# CURSES SETUP
#

sub do_curses {
#    import  Curses::UI;
    my ($cols, $lines) = GetTerminalSize();
    my $twitline = ($lines || 24) - 19;
    my $beyondheaders = 10;

    $cui = new Curses::UI( -color_support => 1 );


    $mainwin = $cui->add('win', 'Window',
			 -border => 1,
			 -y      => 1, # eventually a menu
			 -bfg    => 'blue');
    my $win = $mainwin;

    $subjectListBox = $win->add("lb", "Listbox",
				-values => [],
				-labels => {},
				-vscrollbar => 'left',
				-onselchange => \&show_msg,
				-width => -1,
				-height => $twitline);
    $subjectListBox->focus();
    $subjectListBox->set_binding(\&reply_to ,      "r");
    $subjectListBox->set_binding(\&fetch_parent ,  "^");
    $subjectListBox->set_binding(\&retweet  ,      "f");
    $subjectListBox->set_binding(\&openurl  ,      "o");
    $subjectListBox->set_binding(\&check_for_new , "g");
    $subjectListBox->set_binding(\&rotate_help ,   ">");
    $subjectListBox->set_binding(\&search_text ,   "/");
    $subjectListBox->set_binding(sub {$prefwin->focus();} , "p");
    $subjectListBox->set_binding(sub {exit} , "q");

    $msgwin = $mainwin->add('win', 'Window',
			       -border => 1,
			       -width => 78,
			       -height => 9,
			       -y => $twitline,
			       -focusable => 0,
			       -bfg => 'blue');
    my $msgline = 0;

    #
    # From fields
    #
    $msgwin->add("fromlabel", "Label",
	      -text => "From: ",
	      -y => $msgline,
	      -bg => 'yellow',
	      -bfg => 'yellow',
	      -fg => 'black',
	      -x => 1, -width => $beyondheaders-1, -height => 1);
    $msginfo{'from'} = 
      $msgwin->add("from", "Label",
		-y => $msgline,
		-x => $beyondheaders, -width => 14, -height => 1,
	       );

    $msginfo{'fromname'} = 
      $msgwin->add("fromname", "Label",
		-y => $msgline,
		-x => $beyondheaders + 15, -width => 30, -height => 1,
	       );

    #
    # in reply to fields
    #
    $msgwin->add("replylabel", "Label",
	      -text => "Reply:",
	      -bg => 'yellow',
	      -bfg => 'yellow',
	      -fg => 'black',
	      -y => $msgline+1,
	      -x => 1, -width => $beyondheaders-1, -height => 1);

    $msginfo{'reply'} = 
      $msgwin->add("reply", "Label",
		-y => $msgline+1,
		-x => $beyondheaders, -width => 10, -height => 1,
	       );

    $msginfo{'replyname'} = 
      $msgwin->add("replyname", "Label",
		-y => $msgline+1,
		-x => $beyondheaders + 11, -width => 30, -height => 1,
	       );

    #
    # in reply to fields
    #
    $msgwin->add("datelabel", "Label",
	      -text => "Sent: ",
	      -bg => 'yellow',
	      -bfg => 'yellow',
	      -fg => 'black',
	      -y => $msgline+2,
	      -x => 1, -width => $beyondheaders-1, -height => 1);

    $msginfo{'date'} = 
      $msgwin->add("date", "Label",
		-y => $msgline+2,
		-x => $beyondheaders, -width => 40, -height => 1,
	       );


    $msginfo{'text'} = $msgwin->add("tl", "TextEditor",
				 -readonly => 1, -wrapping => 1,
				 -text => "",
				 -y => $msgline + 4,
				 -x => 3, -width => 76, -height => 4,
				);
    $msginfo{'text'}->focusable(0);

    #
    # outgoing twit editor
    #
    $twitedit = $win->add("twit", "TextEditor",
			  -wrapping => 1,
			  -bfg => 'green',
			  -border => 1,
			  -text => "",
			  -y => $twitline+10, -x => 0,
			  -width => 78, -height => 4,
			  -onchange => \&update_count,
		     );
    $twitedit->set_binding(\&submit_twit , KEY_ENTER);

    $twitcount = $win->add("twitcount", "Label",
			   -y => $twitline+9, -x => 74, -width => 4,
			   -height => 1);
    $twitcount->focusable(0);

    #
    # help bar
    #
    $helpbar = $win->add("help", "Label",
			 -text => "",
			 -y => -1,
			 -x => 0,
			 -bg => 'blue',
			 -bfg => 'blue',
			 -fg => 'black',
			 -width => 78, -height => 1);
    rotate_help();

    #
    # status bar
    #
    $statusbar = $win->add("status", "Label",
			   -text => "",
			   -y => -2,
			   -x => 0,
			   -bg => 'blue',
			   -bfg => 'blue',
			   -fg => 'black',
			   -width => 78, -height => 1);
    set_status("initializing...");

    #    my $text = $win->add("text1", "TextEditor");
    #    $text->focus();

    # boot stap the list values
    gather_default_tweets();
    set_listbox();

    $cui->set_binding(sub {exit} , "\cQ");

    ############################################################
    # Preferences
    #
    $prefwin = $cui->add('prefwin', 'Window',
			 -border => 1,
			 -y      => 1, # eventually a menu
			 -bfg    => 'blue');

    #
    # build the list of preference widgets
    #
    my $prefcount = 1;
    DEBUG("num prefs: " . $#preferences);
    for (my $i = 0; $i <= $#preferences; $i++) {
	$prefcount++;
	DEBUG("pref: " . $preferences[$i]);
	if (defined($preferences[$i])) {
	    DEBUG("pref text: " . $preferences[$i+1]);
	    $prefwin->add($preferences[$i] . 'lab', "Label",
			  -x => 1, -y => $prefcount,
			  -text => $preferences[$i+1] . ":");
	    $prefwidgets{$preferences[$i]} =
	      $prefwin->add($preferences[$i] . 'wid', "TextEntry",
			    -x => 40, -y => $prefcount, -width => 20,
			    -text => (get_config($preferences[$i]) ||
				      $prefdefaults{$preferences[$i]} || ""),
			    -underline => 1);
	    $i++;
	}
    }

    $prefwin->add('ok','Buttonbox', -x => 1, -y => $prefcount+2,
		  -buttons => [
			       { -label => "Save",
				 -onpress => sub { save_prefs();
						   init_twit();
						   $mainwin->focus(); }},
			       { -label => "Cancel",
				 -onpress => sub { $mainwin->focus(); }}
			      ]);

    $cui->set_binding(sub {$mainwin->focus();} , "\cC");

    if (!$twit) {
	$prefwin->focus();
    }

    $cui->mainloop();
}

my $instweeth;
my $deltweeth;
my $gettweeth;
sub remember_tweets {
    my @tweets = @_;
    if (!$instweeth) {
	$deltweeth = $dbh->prepare_cached("delete from tweets where id = ?");
	$gettweeth = $dbh->prepare_cached("select id from tweets where id = ?");
	$instweeth =
	  $dbh->prepare_cached("insert into tweets(id, fromid, fromuser,
                                                   fromname, fromdesc, fromurl,
                                                   created_at, inreplytoid,
                                                   inreplytouserid,
                                                   inreplytousername,
                                                   tweet, twitmailstatus)
                                values(?, ?, ?, ?, ?, ?, ?, ?, ? ,?, ?, ?)");
	die "failed to create insert to tweets statement $! $@"
	  if (!$instweeth);
    }
    foreach my $tweet (@tweets) {
	$gettweeth->execute($tweet->{'id'});
	if (! $gettweeth->fetchrow_array()) {
	    # doesn't exist yet, so insert it
	    $instweeth->execute($tweet->{'id'},
				$tweet->{'user'}{'id'},
				$tweet->{'user'}{'screen_name'},
				$tweet->{'user'}{'name'},
				$tweet->{'user'}{'desc'},
				$tweet->{'user'}{'url'},
				$tweet->{'created_at'},
				$tweet->{'in_reply_to_status_id'},
				$tweet->{'in_reply_to_user_id'},
				$tweet->{'in_reply_to_screen_name'},
				$tweet->{'text'},
				'N');
	} else {
	    DEBUG($tweet->{'id'} . " is a duplicate tweet");
	}
	$gettweeth->finish();
    }
}

my $markh;
sub mark_tweet_as {
    my ($tweet, $status) = @_;
    if (!$markh) {
	$markh = $dbh->prepare_cached("update tweets set twitmailstatus = ?
                                        where id = ?");
    }
    $tweet = $tweet->{'id'} if (ref($tweet) eq 'HASH');
    DEBUG(" ...marking $tweet as '$status'");
    $markh->execute($status, $tweet);
}

sub get_tweets {
    my $clause = shift;
    $clause = "where twitmailstatus = 'N'" if (!defined($clause));
    my $limiter = shift;
    $limiter || "";
    my $gettweeth;
    DEBUG("getting tweets: clause=$clause, limit=$limiter");
    $gettweeth =
      $dbh->prepare_cached("select * from tweets   $clause
                             order by id desc      $limiter ");
    $gettweeth->execute(@_);
    my $results = $gettweeth->fetchall_hashref('id');

    # map some common data names
    foreach my $key (keys(%$results)) {
	my $tweet = $results->{$key};
	$tweet->{'user'}{'screen_name'} = $tweet->{'fromname'};
	$tweet->{'text'} = $tweet->{'tweet'};
	$tweet->{'in_reply_to_status_id'} = $tweet->{'inreplytoid'};
	$tweet->{'in_reply_to_screen_name'} = $tweet->{'in_reply_to_username'};
	$tweet->{'in_reply_to_user_id'} = $tweet->{'inreplytouserid'};
    }

    return $results;
}

sub collect_tweets {
    init_dbh();
    my $lastid = get_config('last_id') || 1;
    my $max_pages = get_config('max_tweets')/20 || 200;

    DEBUG("collecting since $lastid");

    my $friend_data = $twit->friends_timeline({
					       since_id => $lastid
					      });
    my $lowid = get_min_id_from_array($friend_data);
    DEBUG(" lowest id collected in page 1: $lowid");
    my $count = 1;
    while ($count < $max_pages && $#$friend_data > -1 && $lowid > $lastid) {
	$count++;
	my $new_friend_data = $twit->friends_timeline({since_id => $lastid,
						       page => $count});
	if ($new_friend_data) {
	    push @$friend_data, @$new_friend_data;
	} else {
	    last;
	}
	$lowid = get_min_id_from_array($friend_data);
	DEBUG(" lowest id collected in page $count: $lowid");
    }
    DEBUG("lastid:      $lastid");
    DEBUG("collectedid: $lowid");
    DEBUG("collected $#$friend_data friend tweets ($count pages)");


    my $reply_data = $twit->replies({since_id => $lastid});

    my @alldata;
    push @alldata, @$friend_data if (defined($friend_data));
    push @alldata, @$reply_data if (defined($reply_data));

    # don't assume perfectly sorted
    $lastid = get_max_id_from_array(\@alldata) if ($#alldata > -1);
    set_config('last_id', $lastid);
    set_config('last_poll', time());

    DEBUG("collected: " . ($#alldata+1) . "; new lastid = $lastid");

    remember_tweets(@alldata);
}

sub get_max_id_from_array {
    my $maxid;
    my ($tweets) = @_;
    foreach my $tweet (@$tweets) {
	$maxid = $tweet->{'id'} if (!$maxid || $maxid < $tweet->{'id'});
    }
    return $maxid;
}

sub get_min_id_from_array {
    my ($tweets) = @_;
    my $minid;
    foreach my $tweet (@$tweets) {
	if (ref($tweet) ne 'HASH') {
	    DEBUG("ack: not a hash: " . Dumper($tweet));
	    exit;
	}
	$minid = $tweet->{'id'} if (!$minid || $minid > $tweet->{'id'});
    }
    return $minid;
}

my $paramconfig;
sub get_config {
    my ($name, $default) = @_;
    # check the DB version
    if (!$paramconfig) {
	$paramconfig = $dbh->prepare_cached("select value from twitmail where name = ?");
    }
    my $result = '';
    if ($paramconfig) {
	$paramconfig->execute($name);
	$result = $paramconfig->fetchrow_arrayref();
	$result = $result->[0] if (ref($result) eq 'ARRAY');
	$paramconfig->finish(); # required to work around a bug.
    }
    return $result if ($result || !$default);
    return $default;
}

my $setparamconfig;
my $delparamconfig;
sub set_config {
    my ($name, $value) = @_;
    # check the DB version
    if (!$setparamconfig) {
	$setparamconfig = $dbh->prepare_cached("insert into twitmail(name, value) values(?, ?)");
	$delparamconfig = $dbh->prepare_cached("delete from twitmail where name = ?");
    }
    $delparamconfig->execute($name);
    $setparamconfig->execute($name, $value);
}

sub init_dbh {
    # set up the DB
    $dbh = DBI->connect("DBI:SQLite:dbname=" . $ENV{'HOME'} . "/.twitmaildb");
    $dbh->{'sqlite_handle_binary_nulls'} = 1;
    $dbh->{'unicode'} = 1;

    # check the DB version
    my $ver = get_config('dbversion');
    DEBUG("DATABASE VERSION: $ver");
    return if ($ver eq $DBVERSION);
    setup_database();
}

sub init_twit {
    my $user = $config{'user'} || get_config('name');
    my $pass = $config{'password'} || get_config('pass');

    if ($user && $pass) {
	$twit = Net::Twitter->new(username => $user,
			    password => $pass,
			    source => 'twitmail');
    } else {
	$twit = undef;
    }

    $DEBUG = get_config('do_debugging');
}


######################################################################
# DATABASE
#

sub setup_database {

    print "CREATING DATABASE\n";
    # create the DB
    $dbh->do("CREATE table twitmail (
              name       varchar(255),
              value      varchar(4096)
              )");
    $dbh->do("INSERT into twitmail(name, value) values('dbversion','" . $DBVERSION . "')");

    $dbh->do("CREATE table tweets (
              id                                 int,
              fromid                             int,
              fromuser                           varchar(4096),
              fromname                           varchar(4096),
              fromdesc                           varchar(4096),
              fromurl                            varchar(4096),
              created_at                         varchar(4096),
              inreplytoid                        int,
              inreplytouserid                    int,
              inreplytousername                  varchar(4096),
              tweet                              varchar(4096),
              twitmailstatus                     varchar(4096)
             )");
    $dbh->do("INSERT into tweets values('1', '14429718', 'hardaker', 'twitmail author', '', '', '', '', '', '', 'Welcome to twitmail!  Use up/down to change messages.  Press the > key to show more help.', 'N')");
    $dbh->do("INSERT into tweets values('2', '14429718', 'hardaker', 'twitmail author', '', '', '', '', '', '', 'Press the \"g\" key to fetch new twitter messages to read.', 'N')");
}

my $fh;
sub DEBUG {
    return if (!$DEBUG);
    if (!$fh) {
	$fh = new IO::File;
	$fh->open(">" . $ENV{'HOME'} . "/.twitmail.debug");
    }
    $fh->print(@_,"\n");
}

=pod

=head1 NAME

twitmail - Because some tweets you just can't afford to miss

=head1 SYNOPSIS

=head2 Curses Interface

Simply run "twitmail" and fill out the preferences and get started!

=head2 Command Line

Or you can use the command line interface:

Read new twits:

  # twitmail -l
  ---- friends ---------------------------------------------------------
  01 Tue 18:33 airsax     woohoo finally my DSP board plays nice with my
                          macbook!!!  i've spent on and off the last 3
                          or 4 weeks working on this!
  02     18:38 canusis    how does it always get to be 7pm, and I haven't
                          even gotten started on any work yet?
                          tomorrow won't be any better, meetings from
                          11-5.
  03     19:10 andrewsf   After 4 crashes in 5 minutes, I'm wondering if
                          paper would be a more productive
                          alterrnative to Microsoft Word.

Update your status:

  # twitmail -u is writing documentation for twitmail

Reply to an existing post (#3 ... @NAME is auto-added):

  # twitmail -r 3 Maybe you should write your paper in tweets

Check for new updates (note how replies to me (@hardaker) are singled out):

  # twitmail
  ---- replies ------------------------------------------------------
  01 Tue 20:13 jasonsalas @hardaker here's that @metajack post about
                          bot design...good stuff!    http://is.gd/bzDV
  ---- friends ------------------------------------------------------
  02 Tue 20:24 hardaker   @andrewsf Maybe you should write your paper
                          in tweets


=head1 DESCRIPTION

B<twitmail> implementas a "mail-like" interface to twitter.  In
particular, it presents it's data in a mail-reader like iterface where
incoming tweets are represented as "new" when you haven't read them.
It provides "reply" and "forward" actions so you can reply and retweet
to the things you're staring at.  Tweets are kept locally in your
local database so searching through past tweets is easy with it's
search facility.  And most importantly, it tries to collect "all the
tweets since the last time you read it", unlike the web interfaces
that only show you the last 20 or so.  It's designed so you never miss
those important tweets from friends!

To get started, simply run "twitmail" and it'll throw you into the
configuration section where you can fill out your twitter account
information and a few other settings.  Save the settings and you'll be
placed into the reading interface with a few "help" tweets in it.
There is a "help" line at the bottom of the page that shows you some
of the key bindings.  The ">" key will rotate through showing these
helpful keybinds.

B<twitmail> also provides a command-line client to keep track of
tweets that have occured.  Yes, I have fancy graphical clients to do
that too.  But, most of them don't easily show replies to my previous
mesasges without scrolling back a ways.  Especially when I've been
gone for 3 days.  So, twitmail was designed to accomodate that need
and just print a quick summary of tweets that have arrived.

=head1 OPTIONS (commnad line usage only)

=over

=item -a

Shows all messages, not just the most recent.

=item -m MODES

B<MODES> is a comma separated list of things to show:

=over

=item replies

Replies sent to you

=item friends

Updates from friends

=item public

Show updates from all the world.

=back

The default value is friends,replies

=item -u MESSAGE

Updates your twitter status to B<MESSAGE>

=item -r NUM MESSAGE

Replies to a particular message B<NUM>.  A I<@user> prefix will automatically
be added so all you need is the message number (the left most column
in the output).

=item -f USER

Follows a particular B<USER>.  Doesn't seem to work yet.

=item -S

Tells twitmail not to save the configuration again.  In particular,
this means that it will not remember you've just read the messages
you've read and you'll see them again next time.

=item -n COUNT

Only displays B<COUNT> messages.

(has an issue with some modes)

=back

=head1 TODO

Things on the todo list:

=over

=item -

Make replies indented and next to other messages

=item -

Detect that not enough messages could be retrieved and realize you skipped some.

=back

=head1 AUTHOR

Wes Hardaker <hardaker ATAT users.sourceforge.net>

AKA "hardaker" on twitter.com, IRC, and other places

=cut

