#!/usr/bin/perl
#
# TOFU - plain (stupid) text-based todo manager.
#

# Copyright (c) 2008, 2009, 2010, 2011 Sébastien Boillod <sbb at tuxfamily dot org>.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
# 
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
# 
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.

use strict;
use warnings;
use Encode qw(find_encoding);

# General constants
use constant VERSION => '3.2';
use constant DATE => '20110205';
use constant TOTEM => "Ciboulette"; # to ensure the DB is as expected.
use constant TOFURC => $ENV{"TOFURC"} ? $ENV{"TOFURC"} : "$ENV{HOME}/.tofurc";
use constant TOFUDIR => $ENV{"TOFUDIR"} ? $ENV{"TOFUDIR"} : "$ENV{HOME}/.tofu";
use constant TODOFILE => TOFUDIR . "/todo";
use constant TEXTFILE => TOFUDIR . "/text";
use constant LOCKFILE => TOFUDIR . "/lock";
use constant COLORS => ("",  "\e[0;1m", "\e[0;30m", "\e[0;31m", "\e[0;32m", "\e[0;33m",
                        "\e[0;34m", "\e[0;35m", "\e[0;36m");

# "Accessors/mutators" constants
use constant PREVIOUS => 0;     # previous queue/task.
use constant NEXT => 1;         # first/next task.
use constant PLACEHOLDER => 2;  # available place(s) after the task/root in main queue.
use constant TOTAL => 3;        # total of tasks in the queue.
use constant STATUS => 3;       # preselection of the task.
use constant TITLES => 4;       # the hash where task titles are stored.
use constant INDEX => 4;        # position of the task in the current queue.
use constant WRITEFLAG => 5;    # say if the todo must be saved.
use constant ID => 5;           # id of the task.
use constant TAGS => 6;         # tags associated to the task.
use constant QUEUE => 7;        # current queue name.
use constant COLOR => 7;        # the color to apply to the task 0 (none) -> 7
use constant PREQUEUE => 8;     # last queue name.
use constant NEXTID => 9;       # id to attribute to the next task.

# Global variables
my @CYCLES;         # structured records of each selection/action cycles.
my @TODO;           # store the todo file.
my %MACROS;         # store user-defined macros.
my %CONF;           # store user-defined configuration (some defaults bellow).
my $ENCODE = "";    # encoding directive to use when opening I/O descriptor.

# === Library ==================================================================

sub answer {
    # Return the value attached to the answer specified by the user.
    # $f ANSWERS, VALUES
    my ($alw, %val) = @_;
    while (<STDIN>) {
        chomp; print "Please give an allowed answer [$alw]: " and next unless (exists $val{$_});
        return $val{$_};
    }
}

sub color {
    # Color the task.
    # $f SELECTION, COLOR
    my ($sel, $c) = ($_[0], $_[1] =~ /^([0-8])$/ ? $1 : -1);
    warn "(W) \"$_[1]\" is not a correct spicing value.\n" and return 1 if ($c < 0);
    for (@$sel) {
        $_->[COLOR] = $c;
    }
    return $TODO[0]->[WRITEFLAG] = 1;
}

sub erase {
    # Remove the specified markers from the selected tasks.
    # $f TAGS, MODE/TAGS
    my ($sel, $mode) = (shift, shift);
    if ($mode eq "clean") {
        # Remove all (note: "scrub" tag will be noted "scrub\t").
        my $init = $TODO[0]->[QUEUE] ne "main" ? $TODO[0]->[QUEUE] : ""; # current queue is protected
        map {$_->[TAGS] = $init} @$sel;
    } else {
        my %rm = map {$_, 0} split(/[\t ,]+/, $mode);
        delete $rm{$TODO[0]->[QUEUE]} if $TODO[0]->[QUEUE] ne "main";
        for (@$sel) {
            $_->[TAGS] = join " ", grep {not exists $rm{$_}} split(/ /, $_->[TAGS]);
        }
    }
    return $TODO[0]->[WRITEFLAG] = 1;
}

sub edit {
    # Edit the given tasks
    # $f SELECTION
    my ($do, $sel, $ttl) = (0, shift, defined($TODO[0]->[TITLES]) ? $TODO[0]->[TITLES] : loadtitles());
    explode($sel); # we generate the files.
    if ($CONF{"global-edition"}) {
        # The entire tasks list is given as argument.
        system($CONF{"editor"}, glob TOFUDIR."/*.txt");
    } else {
        # Tasks are edited each one after the other.
        $do = 2 unless @$sel > 1;
        for my $t (@$sel) {
            unless ($do > 1) {
                print "Edit next task ".$t->[INDEX].": \"".$ttl->{$t->[ID]}."\" [yYnN] ? ";
                $do = &answer(qw(YyNn n 0 N -1 y 1 Y 2));
            }
            if ($do > 0) {
                system ($CONF{"editor"}, TOFUDIR."/".$t->[ID].".txt");
            } elsif ($do < 0) {
                last;
            }
        }
    }
    mergetext();
    return 1;
}

sub explode {
    # Display the content of the selected tasks into files or on stdout.
    # $f SELECTION, MODE
    my ($sep, $sel, $mode) = (0, @_);
    open(TEXT, "<$ENCODE", TEXTFILE) or return 1;
    for my $tsk (@$sel) {
        my ($id, $i) = ($tsk->[ID], $tsk->[INDEX]);
        open(OUT, ">$ENCODE", TOFUDIR."/$id.txt") and select OUT unless defined($mode); # defined = "read"
        while (<TEXT>) {
            next unless s/^$id //;
            print $sep++ ? "*" x 80 . "\n($i) " : "($i) " if defined($mode);
            print $_;
            while (<TEXT>) {
                last unless s/^@//;
                print $_;
            }
        }
        seek(TEXT, 0, 0); 
        close OUT unless defined($mode);
    }
    close TEXT; select STDOUT; # Ensure default output is however stdout.
    return 1;
}

sub filter {
    # Build the selection upon given criteria.
    # $f EXPLICIT INCLUDE/EXCLUDE, POLICY, "WI/WIL/WOL/WO" TAGS, NEXT TASKS, MAXIMUM TASK
    preselect(shift);
    my ($accept, $i, $tsk, %wi, %wo, %wil, %wol, @sel, %vip) = (shift, 1, $TODO[0]);
    my ($tags, $lwi, $lwil, $lwol) = tagtables(\%wi, shift, \%wil, shift, \%wol, shift, \%wo, shift);
    while (defined($tsk->[NEXT])) {
        $tsk = $tsk->[NEXT];
        $tsk->[INDEX] = $i++;
        if ($accept or $tsk->[STATUS]) {
            push @sel, $tsk unless ($tsk->[STATUS] < 0);
            $vip{$tsk->[ID]} = 1 if ($tsk->[STATUS] > 0);
            $tsk->[STATUS] = 0;
        } elsif ($tags) {
            my ($one, $all, $none) = ($lwi, $lwil, $lwol);
            for (split / /, $tsk->[TAGS]) {
                ($all++ and last) if (exists($wo{$_}) or (exists($wol{$_}) and not --$none));
                $one = 0 if (exists $wi{$_});
                $all-- if (exists $wil{$_});
            }
            push @sel, $tsk unless ($one or $all);
        }
    }
    if (my $next = shift) {
        # last task, we add potential new ones.
        my $tag = ($TODO[0]->[QUEUE] ne "main") ? $TODO[0]->[QUEUE] : "";
        for (split(/\t+/, $next)) {
            $tsk = newtask($tsk, \$i, $tag, $_);
            $vip{$tsk->[ID]} = push @sel, $tsk;
         }
         mergetext();
         $TODO[0]->[WRITEFLAG] = 1;
    }
    $TODO[0]->[TOTAL] = $i - 1;
    return maxselect(\%vip, \@sel, shift);
}

sub jump {
    # Send the selected tasks at the absolute given position.
    # $f SELECTION, POSITION
    my ($sel, $pos) = (shift, shift =~ /^([0-9][0-9]*)$/ ? $1 : -1);
    return 1 unless ($pos > 0 and @$sel < $TODO[0]->[TOTAL]);
    my ($last, $tsk) = (packsel($sel, $pos), $TODO[0]);
    while (defined($tsk->[NEXT])) {
        # We reach the requested position.
        last unless (--$pos > 0);
        $tsk = $tsk->[NEXT];
    }
    ($tsk->[NEXT][PREVIOUS], $last->[NEXT]) = ($last, defined($tsk->[NEXT]) ? $tsk->[NEXT] : undef);
    ($tsk->[NEXT], $sel->[0][PREVIOUS]) = ($sel->[0], $tsk); # ^^^safe: undef $tsk->[NEXT][PREVIOUS]'ll be destroy.
    return $TODO[0]->[WRITEFLAG] = 1;
}

sub loadconfig {
    # Feed CONF/MACROS tables with the user config.
    # $f
    my ($enc, $charset) = (undef, ($ENV{"LANG"} or ""));
    $enc = find_encoding($charset) if ($charset =~ s/.*\.utf-?8$/UTF-8/i or $charset =~ s/.*\.([^.]+)$/$1/);
                                                            # ^^^ we use strict UTF-8.
    %CONF = (qw(use-color 1 recursive-macro-max 100), "editor", ($ENV{"EDITOR"} or "vi")); # some defaults...
    if (open(RC, "<", TOFURC)) {
        local $/;
        my ($mac, $rc) = ("", "\n".<RC>."\n");
        close(RC);
        $rc =~ s/[\t ]+/ /gs; # no tabs allowed.
        $rc =~ s/\n ?#[^\n]*|\n ?/\n/gs; # remove comments and crush empty lines.
        while ($rc =~ s/\n([^\n\( ]*?)\( *(.+?) *\) *\n/\n/s) {
            my ($txt, $name) = ($2.",", ":".$1); # "," to match ending "foo=<nothing>".
            $txt =~ s/\n/ /gs;
            $name .= "=" if ($txt =~ m/= *,/);
            $mac .= "$name\t$txt\t";
        }
        ($charset, $enc) = $1 eq "none" ? () : ($1, find_encoding($1)) if ($rc =~ s/\ncharset *= *([^ \n]+)\n//gs);
        if (ref($enc)) {
            $enc->decode($rc);
            $enc->decode($mac);
        }
        while ($rc =~ s/\n *([^ ]+) *= *([^ ].*?) *\n/\n/s) {
            $CONF{$1} = $2;
        } 
        %MACROS = split /\t/, $mac;
    }
    if (ref($enc)) {
        $ENCODE = ":encoding($charset)";
        binmode(STDOUT, $ENCODE);
        binmode(STDERR, $ENCODE);
        map {$_ = $enc->decode($_)} @ARGV;
    }
    return 1;
}

sub loadstate {
    # Load the last state of the todo
    # $f
    my ($fd, @tmp) = (undef, "main", 1);
    if (open($fd, "<$ENCODE", TODOFILE) or $fd = 0) {
        @tmp = split(/[\n\t]/, <$fd>);
        close $fd and die "(E) Your todo needs an upgrade (try to run tofuup)!\n" if (shift(@tmp) ne TOTEM);
    } elsif (! -d  TOFUDIR) {
        mkdir(TOFUDIR, 0755); # just try, will work in $HOME.
    } elsif (defined(glob TOFUDIR."/*/stack")) {
        die "(E) Your todo needs an upgrade!\n"; # old layout, ultimate test just in case...
    }
    push @TODO, [undef, undef, 0, 0, undef, 0, 0, "main", @tmp]; # first array in TODO describes whole todo state.
    return $fd;
}

sub loadtitles {
    # Extract and load the titles of the tasks.
    # $f
    my %titles = ();
    open(TEXT, "<$ENCODE", TEXTFILE) or return 0;
    while (<TEXT>) {
        chomp;
        $titles{$1} = $_ if s/^([0-9]+) //;
    }
    close TEXT;
    return $TODO[0]->[TITLES] = \%titles;
}

sub loadtodo {
    # Build the todo list in memory.
    # $f
    my $todo = loadstate() or return 0;
    my $tsk = $TODO[0];
    while (<$todo>) {
        chomp;
        $tsk = [$tsk, undef, 0, 0, 0, split(/\t/, $_)]; # MUST BE SYNCHRO WITH newtask().
        push @TODO, $tsk;
        $tsk->[PREVIOUS][NEXT] = $tsk;
    }
    close $todo;
}

sub macrophage {
    # Resolv the user-defined macros
    # $f MACRO NAME, MAX RECURSION, ANCESTOR MACRO NAME
    my ($arg, $mac, $lim, $first, @eat) = ("", @_);
    die "(E) Too many recursions in the \"$first\" macro (> ". $CONF{"recursive-macro-max"} . ").\n"
                                                                                     if ($lim-- < 0);
    ($mac, $arg) = ($1, $2) if ($mac =~ /^([^=]+=)(.+)/); # macros may have arguments.
    die "(E) \"$mac\" : undefined macro.\n" unless (exists $MACROS{$mac});
    $MACROS{$mac} = [split(/ *, */ ,$MACROS{$mac})] unless (ref($MACROS{$mac}));
    for my $i (@{$MACROS{$mac}}) {
        $i .= $arg if ($i =~ /= *$/);
        push @eat, ($i =~ /^:/) ? macrophage($i, $lim, $first) : $i unless ($i eq "");
    }
    return @eat;
}

sub maxselect {
    # Return the amount of first tasks in the selection.
    # $f PRESELECTED TASKS, SELECTION, MAXIMUM
    my ($vip, $sel, @cut) = (shift, shift);
    my $max = (shift =~  m/([0-9]+)\t$/) ? $1 : 0;
    my $free = $max - keys %$vip; # available places for non-preselected tasks.
    return @$sel unless $max; # no maximum specified, we don't touch anything.
    return splice(@$sel, 0, $max) if ($free == $max or @$sel == keys %$vip); # no/all preselected task, we just cut.
    # OK, here we need to parse selection in order to preserve preselected tasks.
    for (@$sel) {
        if (exists $$vip{$_->[ID]} or $free-- > 0) {
            push @cut, $_;
            --$max or last;
        }
    }
    return @cut;
}

sub mergequeue {
    # Merge current/main queues.
    # $f
    my ($main, $sub) = ($TODO[0], $TODO[0]->[NEXT]);
    ($TODO[0]->[NEXT], $TODO[0]->[PREVIOUS]) = ($TODO[0]->[PREVIOUS], undef); # back to the main queue.
    while (defined($sub) and defined($main)) {
        my $next = $main->[NEXT];
        if (! defined($next)) {
            # end of the main queue, we just append the sub-queue.
            ($main->[NEXT], $sub->[PREVIOUS]) = ($sub, $main);
        } elsif ($main->[PLACEHOLDER] > 0) {
            ($main->[NEXT], $sub->[PREVIOUS]) = ($sub, $main);
            while (defined($sub->[NEXT]) and --$main->[PLACEHOLDER]) {
                $sub = $sub->[NEXT];
            }
            ($sub, $next->[PREVIOUS], $sub->[NEXT]) = ($sub->[NEXT], $sub, $next);
        }
        $main = $next;
    }
}

sub mergetext {
    # Manage the content of the stack.
    # $f DELETED ID
    my $ttl = defined($TODO[0]->[TITLES]) ? $TODO[0]->[TITLES] : {};
    my %sel = @_ ? map { $_, 1 } @_ : ();
    open (NEW, ">$ENCODE", TEXTFILE.".new");
    unless (%sel) {
        # We first add the potential modified files (can optimize explode).
        for my $txt (glob TOFUDIR."/*.txt") {
            my $id = $txt =~ m/([0-9]+)\.txt$/ ? $1 : 0 || next;
            open(TXT, "<$ENCODE", $txt);
            while(<TXT>) {
                print NEW "@".$_ and next if exists $sel{$id};
                chomp;
                next unless /([^ \t].*)/; # We ensure there is at least a fed line (the title).
                $ttl->{$id} = $sel{$id} = $1;
                print NEW "$id $1\n";
            }
        }
    }
    if (open(OLD, "<$ENCODE", TEXTFILE)) {
        # We add the tasks that weren't touched.
        my $pass = 0;
        while (<OLD>) {
           $pass = exists $sel{$1} ? 1 : 0 if /^([0-9]+)/;
           next if $pass;
           print NEW $_;
        }
        close OLD;
    }
    close NEW;
    rename(TEXTFILE.".new", TEXTFILE);
    unlink(glob TOFUDIR."/*.txt ".TOFUDIR."/*~ ".TOFUDIR."/*.bak");
}

sub newtask {
    # Append a new task to the current queue.
    # $f LAST TASK, INDEX REF, QUEUE, TITLE.
    my ($id, $last, $i, $q, $ttl) = ($TODO[0]->[NEXTID]++, @_);
    $TODO[$$i] = [$last, undef, 0, 0, $$i, $id, $q, 0]; # MUST BE "SYNC" WITH loadtodo().
    if ($ttl) {
        open(TXT, ">$ENCODE", TOFUDIR."/$id.txt") or die "(E) Can't create new task.\n";
        print TXT "$ttl\n";
        close TXT;
    }
    return $last->[NEXT] = $TODO[$$i++];
}

sub packsel {
    # Pack the selected tasks.
    # $f SELECTION, POSISION
    my ($last, $sel, $i) = ([], shift, shift);
    $i = $TODO[0]->[TOTAL] - @$sel if ($i < 1 or $TODO[0]->[TOTAL] - @$sel < $i);
    for (@$sel) {
        # We first chain and remove selected tasks from the queue.
        $_->[NEXT][PREVIOUS] = $_->[PREVIOUS] if defined($_->[NEXT]); 
        ($_->[PREVIOUS][NEXT], $_->[INDEX]) = ($_->[NEXT], $i++);
        ($_->[PREVIOUS], $last->[NEXT], $last) = ($last, $_, $_);
    }
    return $last;
}

sub preselect {
    # Apply explicit exclusion/inclusion.
    # $f TASK POSITIONS
    for (split(/[ \t,]+/, shift)) {
        my ($i, $val) = ($_ < 0) ? ($_ * -1, -1) : ($_, 1); # $TODO[0] is not a task.
        next unless defined($TODO[$i]);
        $TODO[$i]->[STATUS] = $val unless ($TODO[$i]->[STATUS] < 0); # "most restrictive selection"
    }
}

sub printadigest {
    # Print a digest of the selected tasks.
    # $f SELECTION
    my ($sel, $ttl) = (shift, defined($TODO[0]->[TITLES]) ? $TODO[0]->[TITLES] : loadtitles());
    my @color = $CONF{"use-color"} ? (COLORS) : ("", "", "", "", "", "", "", "", "");
    for (@$sel) {
        my $tags = $_->[TAGS] ne "" ? join ", ", split(/ /, $_->[TAGS]) : "-";
        my ($beg, $end) = ($color[$_->[COLOR]], $_->[COLOR] ? "\e[0;0m" : "");
        printf("%s%-9i[%s]\n%9s%s%s\n\n", $beg, $_->[INDEX], $tags, "", $ttl->{$_->[ID]}, $end);
    }
    printf("%s\n\"%s\" queue, %i task(s) selected, %i task(s) total.\n", "*" x 80, 
           $TODO[0]->[QUEUE], scalar(@$sel), $TODO[0]->[TOTAL]);
    return 1;
}

sub printadump {
    # Print a machine-readable dump of the selected tasks.
    # $f SELECTION
    my ($sel, $ttl) = (shift, defined($TODO[0]->[TITLES]) ? $TODO[0]->[TITLES] : loadtitles());
    for (@$sel) {
        printf("%i\t%s\t%s\n", $_->[INDEX], $_->[TAGS], $ttl->{$_->[ID]});
    }
    return 1;
}

sub printalist {
    # Print a compact list of the selected tasks.
    # $f SELECTION
    my ($sel, $ttl) = (shift, defined($TODO[0]->[TITLES]) ? $TODO[0]->[TITLES] : loadtitles());
    my @color = $CONF{"use-color"} ? (COLORS) : ("", "", "", "", "", "", "", "", "");
    for (@$sel) {
        my ($beg, $end) = ($color[$_->[COLOR]], $_->[COLOR] ? "\e[0;0m" : "");
        printf("%s%-6s%s%s\n", $beg, $_->[INDEX], $ttl->{$_->[ID]}, $end);
    }
    return 1;
}

sub recycle {
    # Arrange and record a selection/action cycle, initializing the next.
    # $f SELECTION HASH, ACTIONS
    my ($dfp, $sel) = (1, shift @_);
    if (@_) {
        # default policy resolution.
        $dfp-- if ((length($$sel{"wil="}.$$sel{"wol="}.$$sel{"wo="}.$$sel{"wi="}) > 0)
               or (length($$sel{"next="}.$$sel{"N"}) > 0 
                   and index($$sel{"N"}, "-") + length($$sel{"all;"}) < 0));
        push @CYCLES, [$$sel{"queue="}, $$sel{"N"}, $dfp, $$sel{"wi="}, $$sel{"wil="}, 
                       $$sel{"wol="}, $$sel{"wo="}, $$sel{"next="}, $$sel{"max="}], [@_];
    }
    %$sel = ("queue=", "", "next=", "", "N", "", "all;", "", "wi=", "",
             "wil=", "", "wol=", "", "wo=", "", "max=", "");
    return ();
}

sub repairtodo {
    # Rebuilt things after a crash.
    # $f
    warn "(W) Last session didn't properly end, should your todo be repaired? y(es), n(o), a(bort)\n";
    my $do = answer("yna", "n", 0, "y", 1, "a", -1);
    exit 0 if $do < 0;
    if (my @txt = glob(TOFUDIR."/*.txt")) {
        $do > 0 ? mergetext() : unlink(@txt); # reintegrate or wipe out potential trailing edited *.txt
    }
    return 0 unless $do > 0;
    my ($tsk, $ttl, @del) = ($TODO[0]->[NEXT], loadtitles());
    while(defined($tsk)) {
        push @del, $tsk unless (delete $ttl->{$tsk->[ID]});
        last unless (defined($tsk->[NEXT]));
        $tsk = $tsk->[NEXT];
    }
    trash(\@del, "quiet"); # delete task that have no title.
    for (sort {$a <=> $b} keys %$ttl) {
        $tsk = newtask($tsk, \$_, "", undef); # init orphan tasks (note: we don't care of the index here).
    }
    savetodo();
    unlink LOCKFILE;
    exit 0; # safer.
}

sub ride {
    # Insert the selection before the given task.
    # $f SELECTION, TARGETED TASK
    my ($sel, $get) = (shift, shift =~ /^([0-9][0-9]*)$/ ? $1 : 0);
    return 1 unless ($get > 0 and defined($TODO[$get]));
    $get = $TODO[$get]->[ID];
    my ($i, $j, $id, $tsk, $hook) = (1, 0, $sel->[0]->[ID], $TODO[0], $TODO[0]);
    while (defined($tsk->[NEXT])) {
        # We get the task to tie on.
        $tsk = $tsk->[NEXT];
        if ($get == $tsk->[ID]) {
            $get = undef;
            last;
        } elsif ($id == $tsk->[ID]) {
            $id = $sel->[$j]->[ID] if (++$j < @$sel);
        } else {
            $i++;
            $hook = $tsk;
        }
    }
    return 1 if defined($get); # give up if the task is not in the queue.
    my $last = packsel($sel, $i);
    ($hook->[NEXT][PREVIOUS], $last->[NEXT]) = ($last, defined($hook->[NEXT]) ? $hook->[NEXT] : undef);
    ($hook->[NEXT], $sel->[0][PREVIOUS]) = ($sel->[0], $hook); # ^^^ undef $hook->[NEXT][PREVIOUS]'ll be destroy.
    return $TODO[0]->[WRITEFLAG] = 1;
}

sub savetodo {
    # Update the todo file.
    # $f
    mergequeue() if (defined $TODO[0]->[PREVIOUS]); # ensure we are back to the main queue.
    open (TODO, ">$ENCODE", TODOFILE.".new") or die "(E) Can't create".TODOFILE.".new\n";
    printf TODO ("%s\t%s\t%i\n", TOTEM, $TODO[0]->[QUEUE], $TODO[0]->[NEXTID]);
    my $tsk = $TODO[0]->[NEXT];
    while(defined($tsk)) {
        printf TODO ("%i\t%s\t%i\n", $tsk->[ID], $tsk->[TAGS], $tsk->[COLOR]);
        $tsk = $tsk->[NEXT];
    }
    close TODO;
    rename(TODOFILE.".new", TODOFILE); # more secure if the program breaks during the writing.
}

sub stamp {
    # Add the specified markers on the selected tasks.
    # $f SELECTION, NEW TAGS
    my ($sel, @tags) = (shift, map({$_, 0} split(/[\t, ]+/, shift)));
    for (@$sel) {
        # We add the markers to the existing ones.
        my %list =(@tags,  map({$_, 0} split(/ /, $_->[TAGS])));
        $_->[TAGS] = join " ", sort (keys %list);
    }
    return $TODO[0]->[WRITEFLAG] = 1;
}

sub swapqueue {
    # Make and swap a sub-queue with the main one.
    # $f
    my ($main, $sub, $i, $q) = ($TODO[0]->[NEXT], [], 0, shift @_);
    $TODO[0]->[PLACEHOLDER] = 0;
    while (defined($main)) {
        $main->[PLACEHOLDER] = 0; # init to be sure. ;)
        if (index(" $main->[TAGS] ", $q) > -1) {
            ($TODO[++$i], $main->[STATUS]) = ($main, 0); # here, the task can't be dereferenced, anyway. ;)
            $main->[PREVIOUS][PLACEHOLDER]++; # keep a place after the previous task, to come back.
            $main->[PREVIOUS][NEXT] = $main->[NEXT]; # squizz the task in the main queue
            $main->[NEXT][PREVIOUS] = $main->[PREVIOUS] if defined($main->[NEXT]); # avoid to create craps.
            ($sub->[NEXT], $main->[PREVIOUS], $sub)= ($main, $TODO[$i-1], $main); # move the task.
        }
        $main = $main->[NEXT];
    }
    ($TODO[0]->[PREVIOUS], $TODO[0]->[NEXT], $sub->[NEXT]) = ($TODO[0]->[NEXT], $i ? $TODO[1] : undef, undef);
}

sub tagtables {
    # Store searched tags in their respective hash.
    # $f HASH, TAG STRING, HASH, TAG STRING, HA...
    my ($full, $i, @c) = (0, 0, 0, 0, 0, 0);
    while (my $table = shift) {
        my %once = ();
        for my $tag (split /[ \t,]+/, shift) {
                next if ($tag eq "" or $once{$tag}++);
                $full = ++$c[$i];
                $$table{$tag} = undef; # don't need any significant value.
        }
        $i++;
    }
    return $full, @c; # return "tag flag" and amount of items in each hash.
}

sub trash {
    # Remove the given tasks from the stack.
    # $f SELECTION CONTEXT
    my ($ttl, $sel) = (defined($TODO[0]->[TITLES]) ? $TODO[0]->[TITLES] : loadtitles(), shift);
    my ($do, $fix, @rm, @keep) = (shift eq "quiet" ? 2 : 0, 0);
    while(my $del = shift(@$sel)) {
        $del->[INDEX] -= $fix; # fix index.
        if ($do != 2) {
            print "Delete task ".$del->[INDEX].": \"".$ttl->{$del->[ID]}."\" [yYnNr] ? ";
            $do = &answer(qw(YyNnr n 0 N -1 y 1 Y 2 r 3));
        }
        if ($do > 2) {
            &explode([$del], "read");
            $del->[INDEX] += $fix; redo; # undo fix (needed once) before redoing.
        } elsif ($do > 0) {
            $del->[PREVIOUS][NEXT] = $del->[NEXT]; # We remove the task from the chain.
            $del->[NEXT][PREVIOUS] = $del->[PREVIOUS] if(defined($del->[NEXT]));
            $fix = push @rm, $del->[ID];
            next;
        }
        push @keep, $del;
        last if ($do < 0);
    }
    ++$TODO[0]->[WRITEFLAG] and mergetext(@rm) if (@rm);
    map {$_->[INDEX] -= $fix} @$sel if ($fix and @$sel);
    @$sel = (@keep, @$sel) if (@keep);
    return @$sel ? 1 : 0; # will break the main loop if no tasks are left.
}

sub usage {
    # Print help and exit.
    # $f
    for ("Tofu ".VERSION." (".DATE.")", "Plain (stupid) text-based todo(s) manager.") {
        print "".(" " x ((80-length($_))/2))."$_\n";
    }
    print <<DIGEST;


    Usage: tofu [[selection] [actions]] [[selection] [actions]] ...


    Selectors: <n>, -<n>, all, max=<n>, next=<string>, queue=<tag>,
               wi=<taglist>, wil=<taglist>, wo=<taglist>, wol=<taglist>.

    Actions:   clean, color=<n>, delete, digest, dump, edit, erase=<taglist>,
               help, jump=<n>, list, read, ride=<n>, stamp=<taglist>.


    This is just a memento of the available commands, for a detailed
    description, please refer to "man 1 tofu".


    Bug reports, suggestions, feedbacks, questions, and so on, should be sent to
    Seb (author/maintainer, the license -- MIT/X11 -- is at the head of the
    script) here: <sbb at tuxfamily dot org>.

DIGEST
    exit 0;
}

sub whatsupdoc {
    # Parse the arguments of the script.
    # $f
    my (%a, %s) = ("clean;", \&erase, "delete;", \&trash, "digest;", \&printadigest, "dump;", \&printadump,
                   "edit;", \&edit, "erase=", \&erase, "jump=", \&jump, "list;", \&printalist,
                   "read;", \&explode, "ride=", \&ride, "color=", \&color, "stamp=", \&stamp);
    my @do = recycle(\%s); 
    $s{"queue="} = $TODO[0]->[PREQUEUE] . "\t" unless ($CONF{"reset-to-main"});
    for (@ARGV) {
        for my $i ((substr($_, 0, 1) eq ":") ? macrophage($_) : ($_)) {
            my ($check, $arg) = ($i =~ /^-?[1-9][0-9]*$/) ? ("N", $i) 
                                                          : ($i =~ /^(.+?)=.*?([^ ,\t].*)/) ? ($1 . "=", $2)
                                                                                            : ($i . ";", $i);
            $arg =~ s/\t+/ /g; # tabulation is a reserved character.
            if (exists $s{$check}) {
                @do = recycle(\%s, @do) if (@do and not $CONF{"mono-cycle"}); # we got action(s) before: new cycle.
                $s{$check} .= "$arg\t";
            } elsif (exists $a{$check}) {
                push @do, $a{$check}, $arg;
            } else {
                usage() if ($check eq "help;");
                die "(E) Invalid \"$i\" argument.\n";
            }
        }
    }
    recycle(\%s, @do ? @do : ($a{"digest;"}, undef)); # ensure there is an action in the last cycle.
    undef %MACROS; # useless from now on...
}

sub whichqueue {
    # Determine if the current queue should be rebuilt.
    # $f QUEUE NAME
    my $q = ($_[0] =~ m/([^ \t]+)\t$/) ? $1 : $TODO[0]->[QUEUE];
    return 0 unless ($q ne $TODO[0]->[QUEUE]); # do nothing if the queue didn't change.
    mergequeue() if (defined $TODO[0]->[PREVIOUS]); # We have stuff to restore in the main queue.
    if ($q ne "main") {
        swapqueue($q);
    } else {
        my ($tsk, $i) = ($TODO[0]->[NEXT], 1);
        while (defined($tsk)) {
            # Ok, just rebuilt index for pre-selection and re-init status...
            ($TODO[$i++], $tsk->[STATUS], $tsk) = ($tsk, 0, $tsk->[NEXT]);
        }
    }
    $TODO[0]->[QUEUE] = $q;
}

# === Main =====================================================================

loadconfig();
loadtodo();
repairtodo() if (-e LOCKFILE);
whatsupdoc();
open(LOCK, ">", LOCKFILE) and close LOCK || die "(E) \"".TOFUDIR."\" is not writable.\n";
my $cycle = @CYCLES / 2;
while (my $selection = shift @CYCLES) {
    whichqueue(shift @$selection);
    my ($actions, @hand) = (shift @CYCLES, filter(@$selection));
    (warn "(W) Empty selection in cycle ". ($cycle - @CYCLES / 2) .".\n" and next) unless (@hand);
    while(my $do = shift @$actions) {
        last unless &$do(\@hand, shift @$actions);
    }
}
$TODO[0]->[WRITEFLAG] = 1 unless ($CONF{"reset-to-main"} or ($TODO[0]->[QUEUE] eq $TODO[0]->[PREQUEUE]));
savetodo() if $TODO[0]->[WRITEFLAG];
unlink LOCKFILE;

# EoF
