#!/usr/bin/perl -w -CLASo

###############################################################################
# taskopen - file based notes with taskwarrior
#
# Copyright 2010-2020, Johannes Schlatow.
# All rights reserved.
#
# This program is free software; you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free Software
# Foundation; either version 2 of the License, or (at your option) any later
# version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# this program; if not, write to the
#
#     Free Software Foundation, Inc.,
#     51 Franklin Street, Fifth Floor,
#     Boston, MA
#     02110-1301
#     USA
#
###############################################################################

use JSON qw( decode_json );     # From CPAN
use File::Temp;
use strict;
use warnings;

# DON'T TOUCH THE FOLLOWING LINES
my $VERSION="v1.1.5";
my $REVNUM="297";
my $REVHASH="b9721f5";
# END-DON'T-TOUCH

my $HOME = $ENV{"HOME"};
my $XDG = "xdg-open";

if ($^O =~ m/.*darwin.*/) { #OSX
   $XDG = "open";
}

if ($^O =~ m/.*cygwin.*/) { #CYGWIN
   $XDG = "cygstart";
}

my $cfgfile;
if (exists $ENV{"TASKOPENRC"}) {
    $cfgfile = $ENV{"TASKOPENRC"};
}
else {
    $cfgfile = "$HOME/.taskopenrc";
}

# find alternative config file specification in argument list
for (my $i = 0; $i <= $#ARGV; ++$i) {
    my $arg = $ARGV[$i];
    if ($arg eq "-c") {
        $cfgfile = $ARGV[++$i];
        $cfgfile =~ s/^~/$HOME/;
        print "Using alternate config file $cfgfile\n";
        last;
    }
}

# create cfgfile if it doesn't exist (ask first)
unless (-e $cfgfile) {
    my $choice = "inval";
    while ($choice ne "y" &&
           $choice ne "Y" &&
           $choice ne "n" && 
           $choice ne "N" && 
           $choice ne "")
    {
        print STDOUT qq{Config file "$cfgfile" does not exist. Do you want to create it? (Y/n) };
        $choice = <STDIN>;
        chomp($choice);
    }

    if ($choice eq "n" || $choice eq "N") {
        print STDERR qq/Can not continue without a config file.\n/;
        exit 0;
    }
    else {
        open(my $fh, '>', $cfgfile) or die "can't open $cfgfile: $!";
        # write default config
        print $fh q<
#BROWSER='xdg-open $FILE &\>/dev/null'
#EDITOR='vim'
#FILE_CMD='xdg-open'
TASKBIN='task'

# If you sync tasks NOTES_FOLDER should be a location that syncs and is available to
# other computers, i.e. /users/dropbox/tasknotes
# NOTES_FOLDER to store notes in, must already exist!
NOTES_FOLDER="$HOME/tasknotes/"

# Preferred extension for tasknotes
NOTES_EXT=".txt"

# Path to notes file. UUID will be replaced with the actual uuid of
# the task. If NOTES_CMD 
# Default is: ${NOTES_FOLDER}UUID${NOTES_EXT}
#NOTES_FILE="$HOME/tasknotes/UUID.txt"

# Command that opens notes. UUID will be replaced with the actual uuid of 
# the task.
# Default is: $EDITOR $NOTES_FILE
#NOTES_CMD="vim "$HOME/tasknotes/$UUID.txt""

# Specify the default sorting.
# Default is taskwarrior's default sorting, i.e. sorting by task IDs.
#DEFAULT_SORT="urgency-,label,annot"

# Apply a default taskwarrior filter in order to exclude certain tasks.
# Default is: status.is:pending
#DEFAULT_FILTER=

# Default command for '-i'
# Default is: ls -la
#DEFAULT-i="ls -la"

# Add some paths to the taskopen's PATH variable
#PATH_EXT=/path/to/taskopen/scripts

# Regular expression that referes to the NOTES_FILE.
# Default is: Notes
#NOTES_REGEX="Notes" 

# Regular expression that identifies annotations openable by BROWSER.
# Default is: www|http
#BROWSER_REGEX="www|http"

# Regular expression that identifies file paths in annotations. Will be opened by xdg-open.
# Default is: \.|\/|~
#FILE_REGEX="\.|\/|~"

# Regular expression that identifies a text annotation. Automatically triggers raw edit mode like '-r'.
#TEXT_REGEX=".*"

# Custom regular expression that specifies annotations passed to CUSTOM1_CMD, e.g:
#CUSTOM1_REGEX="Message-[Ii][Dd]:|message:"
#CUSTOM1_CMD="muttjumpwrapper"

# Custom regular expression that specifies annotations passed to CUSTOM2_CMD.
#CUSTOM2_REGEX=""
#CUSTOM2_CMD=""

# Execute an arbitrary command if there is no annotation available. The corresponding taskwarrior IDs will
# be passed as arguments, e.g. "addnote 21 42"
#NO_ANNOTATION_HOOK=addnote

# Make additional taskwarrior attributes available as sort keys and environment variables.
# E.g. TASK_ATTRIBUTES="project,tags" allows to sort by "task_project" or "task_tags" and to use 
# "$TASK_PROJECT" or "$TASK_TAGS" within your (custom) commands.
#TASK_ATTRIBUTES=""
>;
        close $fh;
    }
}

my %config;
open(my $fh, '<', $cfgfile) or die "can't open $cfgfile: $!";
while (<$fh>) {
    chomp;
    s/#.*//; # Remove comments
    s/^\s+//; # Remove opening whitespace
    s/\s+$//;  # Remove closing whitespace
    next unless length;
    my ($key, $value) = split(/\s*=\s*/, $_, 2);
    $value =~ s/"(.*)"/$1/;
    $value =~ s/'(.*)'/$1/;
    $config{$key} = $value;
}
close $fh;

if (exists $config{"PATH_EXT"}) {
    $ENV{"PATH"} = qq/$config{"PATH_EXT"}:$ENV{"PATH"}/;
}

my $TASKBIN;
if (exists $config{"TASKBIN"}) {
    $TASKBIN = qq/$config{"TASKBIN"} rc.verbose=blank,label,edit rc.json.array=on/;
}
else {
    $TASKBIN = 'task rc.verbose=blank,label,edit rc.json.array=on';
}

my $FOLDER;
if (exists $config{"NOTES_FOLDER"}) {
    $FOLDER = $config{"NOTES_FOLDER"};
}
else {
    $FOLDER = "~/tasknotes/";
}

my $EXT;
if (exists $config{"NOTES_EXT"}) {
    $EXT = $config{"NOTES_EXT"};
}
else {
    $EXT = ".txt";
}

my $BROWSER;
if (exists $config{"BROWSER"}) {
    $BROWSER = $config{"BROWSER"};
}
else {
    $BROWSER = $XDG;
}

my $EDITOR;
if (exists $config{"EDITOR"}) {
    $EDITOR = $config{"EDITOR"};
}
elsif (exists $ENV{"VISUAL"}) {
    $EDITOR = $ENV{"VISUAL"};
}
elsif (exists $ENV{"EDITOR"}) {
    $EDITOR = $ENV{"EDITOR"};
}
elsif (!system("which vim &> /dev/null")) {
    $EDITOR = "vim";
}
elsif (!system("which nano &> /dev/null")) {
    $EDITOR = "nano";
}
else {
    die "Cannot find any editor. Please specify EDITOR in your ~/.taskopenrc\n";
}

my $NOTES_FILE;
if (exists $config{"NOTES_FILE"}) {
    $NOTES_FILE = $config{"NOTES_FILE"};
}
else {
    $NOTES_FILE = "${FOLDER}UUID$EXT";
}

my $NOTES_CMD;
if (exists $config{"NOTES_CMD"}) {
    $NOTES_CMD = $config{"NOTES_CMD"};
}
else {
    $NOTES_CMD = qq/$EDITOR \"$NOTES_FILE\"/;
}

my $EXCLUDE;
if (exists $config{"DEFAULT_FILTER"}) {
    $EXCLUDE = $config{"DEFAULT_FILTER"};
}
else {
    $EXCLUDE = "status.is:pending";
}

my $DEBUG;
if (exists $config{"DEBUG"} && $config{"DEBUG"} =~ m/\d+/) {
    $DEBUG = $config{"DEBUG"};
}
else {
    $DEBUG = 0;
}

my $SORT;
if (exists $config{"DEFAULT_SORT"}) {
    $SORT = $config{"DEFAULT_SORT"};
}
else {
    $SORT = "";
}
my $DEFAULT_SORT = $SORT;

my $SHOW_CMD;
if (exists $config{"DEFAULT-i"}) {
    $SHOW_CMD = $config{"DEFAULT-i"};
}
else {
    $SHOW_CMD = "ls -la";
}
my $DEFAULT_i = $SHOW_CMD;

my $DEFAULT_x;
if (exists $config{"DEFAULT-x"}) {
    $DEFAULT_x = $config{"DEFAULT-x"};
}
else {
#    $DEFAULT_x = qq/$ENV{"SHELL"} -c/;
    $DEFAULT_x = "\$FILE";
}

my $NOTES_REGEX;
if (exists $config{"NOTES_REGEX"}) {
    $NOTES_REGEX = $config{"NOTES_REGEX"};
}
else {
    $NOTES_REGEX = "Notes";
}

my $BROWSER_REGEX;
if (exists $config{"BROWSER_REGEX"}) {
    $BROWSER_REGEX = $config{"BROWSER_REGEX"};
}
else {
    $BROWSER_REGEX = "www|http";
}

my $FILE_REGEX;
if (exists $config{"FILE_REGEX"}) {
    $FILE_REGEX = $config{"FILE_REGEX"};
}
else {
    $FILE_REGEX = "\\\.|\\\/|~";
}

my $FILE_CMD;
if (exists $config{"FILE_CMD"}) {
    $FILE_CMD = $config{"FILE_CMD"};
}
else {
    $FILE_CMD = $XDG;
}

my %custom_regex = ();
for my $key (keys %config) {
	if ($key =~ m/CUSTOM(\d+)_REGEX/) {
		my $regexkey = "CUSTOM" . $1 . "_REGEX";
		my $cmdkey   = "CUSTOM" . $1 . "_CMD";
		if (exists $config{$cmdkey}) {
			$custom_regex{$config{$regexkey}} = $config{$cmdkey}
		}
		else {
			$custom_regex{$config{$regexkey}} = ""
		}
	}
}

my @TASK_ATTRIBUTES;
if (exists $config{"TASK_ATTRIBUTES"}) {
    @TASK_ATTRIBUTES = split(",", $config{"TASK_ATTRIBUTES"});
}

my $NO_ANNOTATION_HOOK = "";
if (exists $config{"NO_ANNOTATION_HOOK"}) {
    $NO_ANNOTATION_HOOK = $config{"NO_ANNOTATION_HOOK"};
}

my $REGEX_CODE = "fbn";
if (keys(%custom_regex)) {
	$REGEX_CODE = "${REGEX_CODE}c";
}

my $TEXT_REGEX = "";
if (exists $config{"TEXT_REGEX"}) {
    $TEXT_REGEX = $config{"TEXT_REGEX"};
    $REGEX_CODE = "${REGEX_CODE}t";
}
else {
    $REGEX_CODE = "${REGEX_CODE}T";
}

sub print_version {
    print "$VERSION $REVNUM $REVHASH\n";
}

sub print_version_long {
    print "\n";
    print "Taskopen, release $VERSION, revision $REVNUM ($REVHASH)\n";
    print "Copyright 2010-2020, Johannes Schlatow.\n";
    print "\n";
}

sub diag {
    print_version_long;

    my $task_version = qx{$TASKBIN _version};
    chomp($task_version);

    # FIXME no xdg --version on OSX
    my $xdg_version;
    if ($^O =~ m/.*darwin.*/) {    #OSX
        $xdg_version = "N/A on OSX";
    } elsif ($^O =~ m/.*cygwin.*/) { #CYGWIN
        my @output = qx{$XDG --version};
        if ($output[0] =~ m/.*\s+(.*)$/) {
            $xdg_version = $1;
        } else {
            $xdg_version = "Couldn't parse '$XDG --version'";
        }
    } else {
        $xdg_version  = qx{$XDG --version};
        chomp($xdg_version);
    }

    print "Environment\n";
    print "    Platform:      $^O\n";
    print "    Perl:          $^V\n";
    print "    Taskwarrior:   $task_version\n";
    print "    xdg-open:      $xdg_version\n";
    print "    Configuration: $cfgfile\n";
    print "\n";

    print "Current configuration\n";
    print "  Binaries and paths:\n";
    print "    BROWSER            = $BROWSER\n";
    print "    TASKBIN            = $TASKBIN\n";
    print "    EDITOR             = $EDITOR\n";
    print "    FILE_CMD           = $FILE_CMD\n";
    print "    PATH               = $ENV{'PATH'}\n";
    print "\n";
    print "  Others:\n";
    print "    NOTES_FOLDER       = $FOLDER\n";
    print "    NOTES_EXT          = $EXT\n";
    print "    NOTES_FILE         = $NOTES_FILE\n";
    print "    NOTES_CMD          = $NOTES_CMD\n";
    print "    DEFAULT_FILTER     = $EXCLUDE\n";
    print "    DEFAULT_SORT       = $DEFAULT_SORT\n";
    print "                current: $SORT\n";
    print "    DEFAULT-i          = $DEFAULT_i\n";
    print "                current: $SHOW_CMD\n";
    print "    DEFAULT-x          = $DEFAULT_x\n";
    print "    DEBUG              = $DEBUG\n";
    print "    NOTES_REGEX        = $NOTES_REGEX\n";
    print "    BROWSER_REGEX      = $BROWSER_REGEX\n";
    print "    FILE_REGEX         = $FILE_REGEX\n";
    print "    TEXT_REGEX         = $TEXT_REGEX\n";
    print "    NO_ANNOTATION_HOOK = $NO_ANNOTATION_HOOK\n";
    print "    TASK_ATTRIBUTES    = @TASK_ATTRIBUTES\n";
    print "  Custom regex commands:\n";
    for my $key (keys %custom_regex) {
	    my $cmd = $custom_regex{$key};
	    print ("    $key\n");
	    print ("        $cmd\n");
    }

    print "\n";
}

sub set_action
{
    my $force = $_[0];
    my $arg   = $_[1];
    my $action= $_[2];

    if ($force->{"action"}) {
        print STDERR qq/Cannot use $arg in conjunction with $force->{"arg"}\n/;
        exit 1;
    }
    else {
        $force->{"action"} = $action;
        $force->{"arg"}    = $arg;
    }
}

sub set_mode
{
    my $mode = $_[0];
    my $val  = $_[1];

    if ($$mode && $$mode ne $val) {
        print STDERR qq/Cannot use combine arguments -b with -l or -L\n/;
        exit 1;
    }
    else {
        $$mode = $val;
    }
}

# argument parsing
my $FILTER = "";
my $ID_CMD = "ids";
my $LABEL;
my $HELP;
my $LIST_ANN;
my $LIST_EXEC;
my %FORCE;
my $MATCH;
my $TYPE;
my $BROKEN = 0;
my $MODE;
my $SHOW;
for (my $i = 0; $i <= $#ARGV; ++$i) {
    my $arg = $ARGV[$i];
    if ($arg eq "-h") {
        $HELP = 1;
    }
    elsif ($arg eq "-v") {
        print_version;
        exit 0;
    }
    elsif ($arg eq "-V") {
        diag;
        exit 0;
    }
    elsif ($arg eq "-l") {
        set_mode(\$MODE, "list");
        $LIST_ANN = 1;
    }
    elsif ($arg eq "-L") {
        set_mode(\$MODE, "list");
        $LIST_EXEC = 1;
    }
    elsif ($arg eq "-n") {
        if ($REGEX_CODE =~ m/N/) {
            print STDERR "Cannot combine argument -n with -N\n";
            exit 1;
        }
        if (length($REGEX_CODE) == 1) {
            print STDERR "Cannot combine argument -n with -f/-t\n";
            exit 1;
        }
        $REGEX_CODE = "n";
    }
    elsif ($arg eq "-N") {
        if ($REGEX_CODE eq "n") {
            print STDERR "Cannot combine argument -N with -n\n";
            exit 1;
        }
        $REGEX_CODE =~ s/n/N/;
    }
    elsif ($arg eq "-f") {
        if ($REGEX_CODE =~ m/F/) {
            print STDERR "Cannot combine argument -f with -F\n";
            exit 1;
        }
        if (length($REGEX_CODE) == 1) {
            print STDERR "Cannot combine argument -f with -n/-t\n";
            exit 1;
        }
        $REGEX_CODE = "f";
    }
    elsif ($arg eq "-F") {
        if ($REGEX_CODE eq "f") {
            print STDERR "Cannot combine argument -F with -f\n";
            exit 1;
        }
        $REGEX_CODE =~ s/f/F/;
    }
    elsif ($arg eq "-t") {
	     if ($REGEX_CODE =~ m/T/) {
            print STDERR "Cannot combine argument -t with -T\n";
            exit 1;
	     }
        if (length($REGEX_CODE) == 1) {
            print STDERR "Cannot combine argument -t with -n/-f\n";
            exit 1;
        }
        $REGEX_CODE = "t";
    }
    elsif ($arg eq "-T") {
        if ($REGEX_CODE eq "t") {
            print STDERR "Cannot combine argument -T with -t\n";
            exit 1;
        }
        $REGEX_CODE =~ s/t/T/;
    }
    elsif ($arg eq "-a") {
        $EXCLUDE = "";
    }
    elsif ($arg eq "-A") {
        $EXCLUDE = "";
        $ID_CMD  = "uuids";
    }
    elsif ($arg eq "-b") {
        set_mode(\$MODE, "batch");
    }
    elsif ($arg eq "-B") {
        $BROKEN = 1;
    }
    elsif ($arg eq "-D") {
        set_action(\%FORCE, "-D", "\\del");
    }
    elsif ($arg eq "-r") {
        set_action(\%FORCE, "-r", "\\raw");
    }
    elsif ($arg eq "-s") {
        $SORT = $ARGV[++$i];
        if (!$SORT || $SORT =~ m/^-/) {
            print STDERR "Missing argument after $arg\n";
            exit 1;
        }
    }
    elsif ($arg eq "-m") {
        $MATCH = $ARGV[++$i];
        if (!$MATCH || $MATCH =~ m/^-/) {
            print STDERR "Missing expression after -m\n";
            exit 1;
        }
    }
    elsif ($arg eq "--type") {
        $TYPE = $ARGV[++$i];
        if (!$TYPE || $TYPE =~ m/^-/) {
            printf STDERR "Missing expression after -t\n";
            exit 1;
        }
    }
    elsif ($arg eq "-e") {
        set_action(\%FORCE, "-e", $EDITOR);
    }
    elsif ($arg eq "-x") {
        my $action;
        if ($i >= $#ARGV || $ARGV[$i+1] =~ m/^-/) {
            $action = $DEFAULT_x;
        }
        else {
            $action = $ARGV[++$i];
        }
        set_action(\%FORCE, "-x", $action);
    }
    elsif ($arg eq "-i") {
        if ($i < $#ARGV && $ARGV[$i+1] !~ m/^-/) {
            $SHOW_CMD = $ARGV[++$i];
        }
        $SHOW = 1;
    }
    elsif ($arg =~ m/\\+(.+)/) {
        $LABEL = $1;
    }
    elsif ($arg eq "-c") {
        # just skip (handled above)
        $i++;
    }
    elsif ($arg eq "-p") {
        if ($i < $#ARGV && $ARGV[$i+1] !~ m/^-/) {
            my $cmd = $ARGV[++$i];
            open(PIPE, "|-", $cmd) or die "Cannot execute '$cmd'!\n";
            select(PIPE);
            $|++;
        }
        else {
            print STDERR "Missing argument after $arg\n";
            exit 1;
        }
    }
    else {
        $FILTER = "$FILTER $arg";
    }
}

sub execute {
    _execute(0, $_[0])
}

sub execute_fork {
    _execute(1, $_[0])
}

sub _execute {
    my $fork = $_[0];
    my $cmd  = $_[1];

    if ($DEBUG > 0) {
        if ($fork > 0) {
            print "forking: ";
        }
        else {
            print "executing: ";
        }
        print "$cmd\n";
    }

    if ($fork > 0) {
        system($cmd);
    }
    else {
        exec($cmd);
    }
}

sub parse_number {
    my $input = $_[0];
    my $max   = $_[1];

    my @items = split(m/[^\d\.-]+/, $input);
    if ($#items >= 0) {
        my @result;
        foreach my $item (@items) {
            if ($item =~ m/(\d+)(?:\.\.|-)(\d+)/) {
                my $start = $1;
                my $end   = $2;
                if ($start < 1 || $end > $max) {
                    print STDERR qq/"$item" is an invalid range\n/;
                    exit 1;
                }
                while ($start <= $end) {
                    push(@result, $start);
                    $start++;
                }
            }
            elsif ($item =~ m/\d+/) {
                if ($item < 1 || $item > $max) {
                    print STDERR qq/"$item" is an invalid number\n/;
                    exit 1;
                }
                push(@result, $item);
            }
            else {
                print STDERR qq/"$item" is not a number\n/;
                exit 1;
            }
        }
        return @result;
    }
    else {
        print STDERR qq/Invalid input "$input"\n/;
        exit 1;
    }
}

sub get_filepath {
    my $ann = $_[0];
    my $file = $ann->{"annot"};
    
    if ($file =~ m/^$NOTES_REGEX/ || $ann->{"raw"} =~ m/^$NOTES_REGEX/) {
        $file = $NOTES_FILE;
        $file =~ s/([^\$])UUID/$1$ann->{"uuid"}/g;
    }

   $file =~ s/^~/$HOME/;

   return $file;
}

sub raw_edit {
    my $old = $_[0];

    my $filename = File::Temp::tmpnam();
    open(my $fh, '>', $filename) or die "can't open $filename: $!";
    print($fh $old);
    close($fh); 

    system(qq/$EDITOR "$filename"/);

    open($fh, '<', $filename) or die "can't open $filename: $!'";
    my @lines = <$fh>;
    close($fh);
    unlink($filename);

    # taskwarrior does not support multi-line annotations
    # TODO fix if #1172 has been solved
    
    my $result = $lines[0];
    chomp($result);
    return $result;
}

sub reset_env {
    delete $ENV{"UUID"};
    delete $ENV{"FILE"};
    delete $ENV{"ID"};
    delete $ENV{"LABEL"};
    delete $ENV{"ANNOTATION"};
    delete $ENV{"LAST_MATCH"};

    foreach my $att (@TASK_ATTRIBUTES) {
        my $uc_att = uc($att);
        delete $ENV{"TASK_$uc_att"};
    }
}

sub prepare_cmd {
    my $cmd  = $_[0];
    my $ann  = $_[1];
    my $file = $_[2];
    my $match = $_[3];

    if ($cmd !~ m/\$/) {
        $cmd = qq/$cmd '$file'/;
    }
    else {
        $ENV{"FILE"}       = $file;
        $ENV{"UUID"}       = $ann->{"uuid"};
        $ENV{"ID"}         = $ann->{"id"};
        $ENV{"LABEL"}      = $ann->{"label"};
        $ENV{"ANNOTATION"} = $ann->{"raw"};
        $ENV{"LAST_MATCH"} = $match;

        foreach my $att (@TASK_ATTRIBUTES) {
            if (exists $ann->{"task_$att"}) {
                my $uc_att = uc($att);
                $ENV{"TASK_$uc_att"} = $ann->{"task_$att"};
            }
        }
    }

    return $cmd;
}

sub modify_annotation {
	my $ann = $_[0];
	my $raw = $_[1];

   if ($raw ne $ann->{"raw"}) {
       # TODO remove as soon as tw bug TW-1821 (Github #1837) has been fixed ('/'s must still be escaped)
       if ($ann->{'raw'} =~ m/\// || $raw =~ m/\//) {
           return qq{echo "Cannot replace annotations which contain '/'s (see #1837)."}
       }
       # END REMOVE
       return qq%$TASKBIN $ann->{"uuid"} mod '/$ann->{"raw"}/$raw/'%;
   }
   else {
       return qq/echo "No changes detected"/;
   }
}

sub create_cmd {
    my $ann = $_[0];
    my $FORCE = $_[1];
    my $file = $ann->{"annot"};

    reset_env();

    my $cmd;
    my $match = "";
    if ($FORCE->{"action"}) {
        if ($FORCE->{"action"} eq "\\del") {
            return qq%$TASKBIN $ann->{'uuid'} denotate -- "$ann->{'raw'}"%;
        }
        elsif ($FORCE->{"action"} eq "\\raw") {
            my $raw = raw_edit($ann->{"raw"});
            return modify_annotation($ann, $raw);
        }
        else {
            $file = get_filepath($ann);
            $cmd = qq/$FORCE->{"action"}/;
        }
    }
    else {
        if ($REGEX_CODE !~ m/N/ and ($file =~ m/^($NOTES_REGEX)/ || $ann->{"raw"} =~ m/^($NOTES_REGEX)/)) {
            # $file = get_filepath($ann);
            $match = $+;
            $cmd = $NOTES_CMD;
            $cmd =~ s/([^\$])UUID/$1\$UUID/g;
        }
        elsif ($file =~ m/^($BROWSER_REGEX)/ ) {
            $match = $+;
            if ($file =~ m/^www/) {
                # prepend http://
                $file = "http://$file";
            }
            $cmd = qq{$BROWSER};
        }
        elsif ($REGEX_CODE !~ m/F/ and $file =~ m/^($FILE_REGEX)/) {
            $match = $+;
            # use XDG for all file types
            $cmd = qq{$FILE_CMD};
        }
        else {
	         my $found = 0;
	         for my $regex (keys %custom_regex) {
		         if ($file =~ m/^($regex)/) {
			         $match = $+;
			         $cmd = qq{$custom_regex{$regex}};
			         $found = 1;
			         last;
			      }
	         }
	         if (not $found) {
		         if ($REGEX_CODE !~ m/T/ and length $TEXT_REGEX and $file =~ m/^($TEXT_REGEX)/ ) {
            		my $raw = raw_edit($ann->{"raw"});
            		return modify_annotation($ann, $raw);
		         }
		         else {
			         die("no command to execute");
		         }
	         }
        }
    }

    return prepare_cmd($cmd, $ann, $file, $match);
}

sub sort_hasharr
{
    my $arr      = $_[0];
    my $sortkeys = $_[1];

    return sort {
        foreach my $sortkey (@{$sortkeys}) {
            $sortkey =~ m/(.*?)(\+|-)?$/;
            if (!$a->{$1} && !$b->{$1}) {
                next;
            }
            elsif (!$a->{$1}) {
                return 1;
            }
            elsif (!$b->{$1}) {
                return -1;
            }
            if ($a->{$1} eq $b->{$1}) {
                next;
            }
            elsif ($2 && $2 eq "-") {
                return $b->{$1} cmp $a->{$1};
            }
            else {
                return $a->{$1} cmp $b->{$1};
            }
        }
        return 0;
    } @{$arr};
}

my $tmpregex="";
my $tmplabelregex="";
foreach my $code (split("", $REGEX_CODE)) {
    if ($code eq "n") {
        $tmpregex = "${tmpregex}${NOTES_REGEX}|";
        $tmplabelregex = "${tmplabelregex}${NOTES_REGEX}|";
    }
    elsif ($code eq "f") {
        $tmpregex = "${tmpregex}${FILE_REGEX}|";
    }
    elsif ($code eq "b") {
        $tmpregex = "${tmpregex}${BROWSER_REGEX}|";
    }
    elsif ($code eq "c") {
	     for my $regex (keys %custom_regex) {
        	  $tmpregex = "${tmpregex}${regex}|";
	     }
    }
    elsif ($code eq "t") {
	    $tmpregex = "${tmpregex}${TEXT_REGEX}|"
    }
}
chop($tmpregex);
my $FILEREGEX = qr{^((?:$tmpregex).*)};
my $LABELREGEX = qr{^($tmplabelregex)$};

if ($HELP) {
	print "Usage: $0 [options] [id|filter1 filter2 ... filterN] [\\\\label]\n\n";

    print "Available options:\n";
    print "  -h                Show this text\n";
    print "  -v                Print version information\n";
    print "  -V                Print diagnostic information\n";
    print "  -l                List-only mode, does not open any file; shows annotations\n";
    print "  -L                List-only mode, does not open any file; shows command line\n";
    print "  -b                Batch mode, processes every file in the list\n";
    print "  -n                Only show/open notes file, i.e. annotations matching '$NOTES_REGEX'\n";
    print "  -N                Show all but notes files; inverse of -n\n";
    print "  -f                Only show/open real files, i.e. annotations matching '$FILE_REGEX'\n";
    print "  -F                Show all but real files; inverse of -f\n";
    print "  -B                Show only files that don't exist (combine with -f)\n";
    print "  -t                Only show/open text annotations, i.e. annotations matching '$TEXT_REGEX'\n";
    print "  -T                Show all but text annotations; inverse of -t\n";
    print "  -a                Query all active tasks; clears the DEFAULT_FILTER\n";
    print "  -A                Query all tasks, i.e. completed and deleted tasks as well (very slow)\n";
    print "  -D                Delete the annotation rather than opening it\n";
    print "  -r                Raw mode, opens the annotation text with your EDITOR\n";
    print "  -m 'regex'        Only include annotations that match 'regex'\n";
    print "  --type 'regex'    Only open files whose type (as returned by 'file') matches 'regex'\n";
    print "  -s 'key1+,key2-'  Sort annotations by the given key which can be a taskwarrior field or\n";
    print "                    'annot', 'label', 'entry', 'size', 'type', 'time', 'mtime' or 'atime'\n";
    print "  -e                Force to open file with EDITOR\n";
    print "  -x ['cmd']        Execute file, optionally prepend cmd to the command line\n";
    print "  -i ['cmd']        Execute 'cmd <filename>' for each file and shows its output interleaved with the other output.\n";
    print "  -c filepath       Use alternate taskopenrc file as specified by 'filepath'\n";
    print "  -p 'cmd'          Pipe output into 'cmd' (as in 'taskopen | cmd')\n";
    print "\n";

	exit 1;
}

my @SORT_KEYS;
if ($SORT) {
    @SORT_KEYS = split(',', $SORT);
}

if ($DEBUG > 0) {
    print "[DEBUG] Applying filter: $EXCLUDE$FILTER\n";
}
my $ID = qx{$TASKBIN $ID_CMD $EXCLUDE$FILTER};
chomp($ID);

if ($ID eq "") {
    print STDERR "There is no task that matches: $EXCLUDE$FILTER\n";
    exit 1;
}

sub parse_annotations {
    my $ID = $_[0];

    # query IDs and parse json
    my $json = qx{$TASKBIN $ID export};
    my @decoded_json = @{decode_json("$json")};

    # Reorganize data
    my @annotations;
    foreach my $task (@decoded_json) {
        if (exists $task->{"annotations"}) {
            foreach my $ann (@{$task->{"annotations"}}) {
                $ann->{"description"} =~ m/(?:(\S+):\s+)?(.*)/;
                my $file = $2;
                my $label = $1;
                if ( ($file  && $file  =~ m/$FILEREGEX/ ) ||
                     ($label && $label =~ m/$LABELREGEX/) )
                {
                    if (!$MATCH || ($file =~ m/$MATCH/)) {
                        if (!$label) {
                            $label = "";
                        }
                        if (!$LABEL || ($LABEL eq $label) ) {
                            my %entry = ( "annot"       => $file,
                                          "uuid"        => $task->{"uuid"},
                                          "id"          => $task->{"id"},
                                          "raw"         => $ann->{"description"},
                                          "label"       => $label,
                                          "description" => $task->{"description"});

                            if ($BROKEN) {
                                my $filepath = get_filepath(\%entry);
                                if (-e $filepath) {
                                    if ($DEBUG > 0) {
                                        print qq/[DEBUG] Skipping existing file "$filepath".\n/;
                                    }
                                    next;
                                } 
                            }

                            foreach my $att (@TASK_ATTRIBUTES) {
                                if (exists $task->{$att}) {
                                    if (ref($task->{$att}) eq "ARRAY") {
                                        $entry{"task_$att"} = join(",", @{$task->{$att}});
                                    }
                                    else {
                                        $entry{"task_$att"} = $task->{$att};
                                    }
                                }
                                else {
                                    $entry{"task_$att"} = "";
                                }
                            }

                            # Copy sort keys
                            foreach my $key (@SORT_KEYS) {
                                $key =~ m/(.*?)(\+|-)?$/;
                                if (!exists $entry{$1}) {
                                    if (exists $ann->{$1})
                                    {
                                        $entry{$1} = $ann->{$1};
                                    }
                                    elsif (exists $ann->{"task_$1"})
                                    {
                                        $entry{$1} = $ann->{"task_$1"};
                                    }
                                    elsif (exists $task->{$1})
                                    {
                                        # FIXME this is only present due to backwards compatibility
                                        printf STDERR qq/You are using the taskwarrior attribute "$1" for sorting. Please use "task_$1" instead and add "$1" to TASK_ATTRIBUTES in your taskopenrc.\n/;
                                        exit 1;
                                    }
                                    elsif ($1 eq "size") {
                                        my $filepath = get_filepath(\%entry);
                                        $entry{$1} = qx{stat -c "%s" "$filepath"};
                                    }
                                    elsif ($1 eq "mtime") {
                                        my $filepath = get_filepath(\%entry);
                                        $entry{$1} = qx{stat -c "%Y" "$filepath"};
                                    }
                                    elsif ($1 eq "time") {
                                        my $filepath = get_filepath(\%entry);
                                        $entry{$1} = qx{stat -c "%W" "$filepath"};
                                    }
                                    elsif ($1 eq "atime") {
                                        my $filepath = get_filepath(\%entry);
                                        $entry{$1} = qx{stat -c "%X" "$filepath"};
                                    }
                                    elsif ($1 eq "type") {
                                        my $filepath = get_filepath(\%entry);
                                        $entry{$1} = qx{file "$filepath"};
                                    }
                                    else {
                                        print STDERR qq/Unknown sort key "$1"\n/;
                                        exit 1;
                                    }
                                }
                            }

                            if ($TYPE) {
                                my $filetype;
                                if (!$entry{"type"}) {
                                    my $filepath = get_filepath(\%entry);
                                    $filetype = qx{file "$filepath"};
                                }
                                else {
                                    $filetype = $entry{"type"};
                                }
                                if ($filetype =~ m/$TYPE/) {
                                    push(@annotations, \%entry);
                                }
                                elsif ($DEBUG > 0) {
                                    print qq/[DEBUG] Skipping file "$entry{'annot'}" whose type doesn't match '$TYPE'\n/;
                                }
                            }
                            else {
                                push(@annotations, \%entry);
                            }
                        }
                        elsif ($DEBUG > 0) {
                            if (!$label) {
                                print qq/[DEBUG] Skipping unlabeled annotation "$ann->{"description"}"\n/;
                            }
                            else {
                                print qq/[DEBUG] Skipping label "$label" (!= "$LABEL")\n/;
                            }
                        }
                    }
                    elsif ($MATCH && ($DEBUG > 0)) {
                        print qq/[DEBUG] Skipping annotation which doesn't match '$MATCH': $ann->{"description"}\n/;
                    }
                }
                elsif ($DEBUG > 0) {
                    print qq/[DEBUG] Skipping annotation "$ann->{"description"}"\n/;
                }
            }
        }
    }

    return @annotations;
}

my @annotations = parse_annotations($ID);

if ($#annotations < 0) {
    print STDERR "No compatible annotation found.";
    if ($NO_ANNOTATION_HOOK) {
        print STDERR " Executing NO_ANNOTATION_HOOK:\n";
        execute_fork("$NO_ANNOTATION_HOOK $ID");
        @annotations = parse_annotations($ID);
        if ($#annotations < 0) {
            print STDERR "Still no annotations found.\n";
            exit 1;
        }
    }
    else {
        print STDERR "\n";
        exit 1;
    }
}

if ($#SORT_KEYS >= 0) {
    @annotations = sort_hasharr(\@annotations, \@SORT_KEYS);
}

# choose an annotation/file to open
my @choices = (0);
if ($#annotations > 0 || ($MODE && $MODE eq "list")) {
    print "\n";
    if (!$MODE) {
        print "Please select an annotation:\n";
    }

    my $i = 1;
    foreach my $ann (@annotations) {
        print "    $i)";
        if (!$MODE || $MODE ne "list" || $LIST_ANN) {
            my $id = $ann->{'id'};
            if ($id == 0) {
                $id = $ann->{'uuid'};
            }
            my $text = qq/$ann->{'raw'} ("$ann->{'description'}") -- $id/;
            print " $text\n";
        }

        if ($LIST_EXEC) {
            if ($LIST_ANN) {
                print "       executes:";
            }
            my $cmd = create_cmd($ann, \%FORCE);
            print " $cmd\n";
        }

        if ($SHOW) {
            reset_env();
            my $filepath = get_filepath($ann);
            my $cmd = prepare_cmd($SHOW_CMD, $ann, $filepath);
            my $output = qx/$cmd/;
            chomp($output);
            print "       $output\n";
        }
        $i++;
    }

    if ($MODE && $MODE eq "list") {
        exit 0;
    }
    elsif ($MODE && $MODE eq "batch") {
        @choices = (1..$#annotations+1);
    }
    else {
        # read input
        select(STDOUT);
        close(PIPE);
        print STDOUT "Type number(s): ";
        my $choice = <STDIN>;
        chomp ($choice);

        @choices = parse_number($choice, $#annotations+1);
    }
}

##############################################
#open annotations[$choice] with an appropriate program

if ($#choices > 0) {
    my $tmp = join(",", @choices);
    print "\n";
    print STDOUT qq/Do you really want to process files $tmp? (y\/N)\n/;
    my $choice = <STDIN>;
    chomp ($choice);
    if ($choice !~ m/^y/i) {
        exit 0;
    }

    foreach my $choice (@choices) {
        my $ann = $annotations[$choice-1];
        execute_fork(create_cmd($ann, \%FORCE));
    }
}
else {
    my $ann = $annotations[$choices[0]-1];
    execute(create_cmd($ann, \%FORCE));
}

