/**
 * RenameWand 2.2
 * Copyright 2007 Zach Scrivena
 * 2007-12-09
 * zachscrivena@gmail.com
 * http://renamewand.sourceforge.net/
 *
 * RenameWand is a simple command-line utility for renaming files or
 * directories using an intuitive but powerful syntax.
 *
 * TERMS AND CONDITIONS:
 * 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 3 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, see <http://www.gnu.org/licenses/>.
 */

package renamewand;

import java.io.Console;
import java.io.File;
import java.io.PrintWriter;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Deque;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.NavigableMap;
import java.util.Scanner;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.TreeMap;
import java.util.regex.PatternSyntaxException;


/**
 * RenameWand is a simple command-line utility for renaming files or
 * directories using an intuitive but powerful syntax.
 */
public class RenameWand
{
    /**************************************
    * CONSTANTS AND MISCELLANEOUS FIELDS *
    **************************************/

    /** constant: program title */
    private static final String PROGRAM_TITLE =
            "RenameWand 2.2   Copyright 2007 Zach Scrivena   2007-12-09";

    /** constant: special construct separator character */
    private static final char SPECIAL_CONSTRUCT_SEPARATOR_CHAR = '|';

    /** constant: substring range character */
    private static final char SUBSTRING_RANGE_CHAR = ':';

    /** constant: substring delimiter character */
    private static final char SUBSTRING_DELIMITER_CHAR = ',';

    /** constant: integer filter indicator character */
    private static final char INTEGER_FILTER_INDICATOR_CHAR = '@';

    /** constant: regex pattern for register names */
    private static final Pattern REGISTER_NAME_PATTERN = Pattern.compile(
            "[a-zA-Z_][a-zA-Z_0-9]*");

    /**
    * constant: regex pattern for special construct "<length|@expr>" in source pattern string.
    * Match groups: (1,"length"), (2,"@"), (3,"expr")
    */
    private static final Pattern SOURCE_SPECIAL_CONSTRUCT_PATTERN = Pattern.compile(
            "\\<(?:([\\sa-zA-Z_0-9\\." +
            Pattern.quote("+-*/^()[]!" + RenameWand.SUBSTRING_RANGE_CHAR  + RenameWand.SUBSTRING_DELIMITER_CHAR) +
            "]+)" + Pattern.quote(RenameWand.SPECIAL_CONSTRUCT_SEPARATOR_CHAR + "") + ")?(" +
            Pattern.quote(RenameWand.INTEGER_FILTER_INDICATOR_CHAR + "") +
            ")?([\\sa-zA-Z_0-9\\." +
            Pattern.quote("+-*/^()[]!" + RenameWand.SUBSTRING_RANGE_CHAR  + RenameWand.SUBSTRING_DELIMITER_CHAR) +
            "]+)\\>");

    /**
    * constant: regex pattern for special construct "<length|expr>" in target pattern string.
    * Match groups: (1,"length"), (2,"expr")
    */
    private static final Pattern TARGET_SPECIAL_CONSTRUCT_PATTERN = Pattern.compile(
            "\\<(?:([\\sa-zA-Z_0-9\\." +
            Pattern.quote("+-*/^()[]#!@" + RenameWand.SUBSTRING_RANGE_CHAR  + RenameWand.SUBSTRING_DELIMITER_CHAR) +
            "]+)" + Pattern.quote(RenameWand.SPECIAL_CONSTRUCT_SEPARATOR_CHAR + "") +
            ")?([\\sa-zA-Z_0-9\\." +
            Pattern.quote("+-*/^()[]#!@" + RenameWand.SUBSTRING_RANGE_CHAR  + RenameWand.SUBSTRING_DELIMITER_CHAR) +
            "]+)\\>");

    /**
    * constant: regex pattern for a numeric pattern, e.g. -123.45
    * Match groups: (1,"+" or "-"), (2,"123.45")
    */
    private static final Pattern NUMERIC_PATTERN = Pattern.compile(
            "([\\+\\-]?)([0-9]*(?:\\.[0-9]*)?)");

    /** constant: regex pattern for a positive integer pattern, e.g. 42 */
    private static final Pattern POSITIVE_INTEGER_PATTERN = Pattern.compile(
            "\\+?[0-9]+");

    /** operator precedence table for stack evaluation */
    private static final Map<String,Integer> OPERATOR_PRECEDENCE = new TreeMap<String,Integer>();

    /** singular noun for file/directory */
    private static String SINGULAR_NOUN;

    /** plural noun for files/directories */
    private static String PLURAL_NOUN;

    /** standard output */
    static PrintWriter stdout = null;

    /** standard error */
    static PrintWriter stderr = null;

    /** true if this is a Windows OS, false otherwise */
    private static boolean isWindowsOperatingSystem;

    /** current directory (absolute and canonical pathname) */
    static File currentDirectory;

    /** full pathname of the current directory (includes trailing separator) */
    static String currentDirectoryFullPathname;

    /** length of the full pathname of the current directory */
    static int currentDirectoryFullPathnameLength;

    /** register names mapping (register name ---> capture group index) */
    static final Map<String,Integer> registerNames = new TreeMap<String,Integer>();

    /** number of capture groups in source regex pattern */
    private static int numCaptureGroups;

    /** true if the source pattern string is reusable for different files/directories; false otherwise */
    private static boolean sourcePatternIsReusable = false;

    /** subdirectory counter */
    private static int numDirs = 0;

    /*********************
    * RENAME PARAMETERS *
    *********************/

    /** parameter: simulate only; do not actually rename files/directories (default = false) */
    private static boolean simulateOnly = false;

    /** parameter: ignore warnings; do not pause (default = false) */
    private static boolean ignoreWarnings = false;

    /** parameter: recurse into subdirectories (default = false) */
    private static boolean recurseIntoSubdirectories = false;

    /** parameter: automatically rename files/directories without prompting (default = false) */
    private static boolean automaticRename = false;

    /** parameter: match relative pathname, not just the name, of the files/directories (default = false) */
    private static boolean matchRelativePathname = false;

    /** parameter: match lower case name of the files/directories (default = false) */
    private static boolean matchLowerCase = false;

    /** parameter: true if renaming directories; false if renaming files (default = false) */
    private static boolean renameDirectories = false;

    /** parameter: default action on rename operation error (default = '\0') */
    private static char defaultActionOnRenameOperationError = '\0';

    /** parameter: source pattern string */
    private static String sourcePatternString;

    /** parameter: target pattern string */
    private static String targetPatternString;

    /*********************
    * REPORT STATISTICS *
    *********************/

    /** statistic: number of warnings encountered */
    private static int reportNumWarnings = 0;


    /**
    * Main entry point for the RenameWand program.
    *
    * @param args
    *     Command-line argument strings
    */
    public static void main(
            final String[] args)
    {
        /* initialize standard output and error streams */
        final Console console = System.console();

        if (console == null)
        {
            RenameWand.stdout = new PrintWriter(System.out);
            RenameWand.stderr = new PrintWriter(System.err);
        }
        else
        {
            RenameWand.stdout = console.writer();
            RenameWand.stderr = console.writer();
        }

        RenameWand.stdout.print("\n" + RenameWand.PROGRAM_TITLE);
        RenameWand.stdout.flush();

        /* exit status code to be reported to the OS when exiting (default = 0) */
        int exitCode = 0;

        try
        {
            /* determine if this is a Windows OS */
            RenameWand.isWindowsOperatingSystem = System.getProperty("os.name").toUpperCase(Locale.ENGLISH).contains("WINDOWS") &&
                    (File.separatorChar == '\\');

            /* initialize operator precedence table for stack evaluation */
            RenameWand.OPERATOR_PRECEDENCE.put("#",   7);
            RenameWand.OPERATOR_PRECEDENCE.put("#!",  7);
            RenameWand.OPERATOR_PRECEDENCE.put("##",  7);
            RenameWand.OPERATOR_PRECEDENCE.put("##!", 7);
            RenameWand.OPERATOR_PRECEDENCE.put("@",   7);
            RenameWand.OPERATOR_PRECEDENCE.put("@!",  7);
            RenameWand.OPERATOR_PRECEDENCE.put("@@",  7);
            RenameWand.OPERATOR_PRECEDENCE.put("@@!", 7);
            RenameWand.OPERATOR_PRECEDENCE.put("^",   6);
            RenameWand.OPERATOR_PRECEDENCE.put("~",   5); // unary minus (negative) sign
            RenameWand.OPERATOR_PRECEDENCE.put("*",   4);
            RenameWand.OPERATOR_PRECEDENCE.put("/",   4);
            RenameWand.OPERATOR_PRECEDENCE.put("+",   3);
            RenameWand.OPERATOR_PRECEDENCE.put("-",   3);
            RenameWand.OPERATOR_PRECEDENCE.put(RenameWand.SUBSTRING_RANGE_CHAR  + "", 2);
            RenameWand.OPERATOR_PRECEDENCE.put(RenameWand.SUBSTRING_DELIMITER_CHAR + "", 1);

            /* process command-line arguments and configure rename parameters */
            processArguments(args);

            /* nouns for file/directory */
            RenameWand.SINGULAR_NOUN = RenameWand.renameDirectories ? "directory" : "file";
            RenameWand.PLURAL_NOUN = RenameWand.renameDirectories ? "directories" : "files";

            RenameWand.stdout.print("\n\nSource pattern: \"" + RenameWand.sourcePatternString +
                    "\"\nTarget pattern: \"" + RenameWand.targetPatternString +
                    "\"\n\nGetting all candidate " + RenameWand.SINGULAR_NOUN +
                    " names in the current directory" +
                    (RenameWand.recurseIntoSubdirectories ? " recursively..." : "..."));
            RenameWand.stdout.flush();

            /* files/directories to be renamed */
            List<FileUnit> files = null;

            /* get match candidates */
            files = getMatchCandidates();
            final int numMatchCandidates = files.size();

            if (numMatchCandidates == 0)
            {
                RenameWand.stdout.print("\nNo candidate " + RenameWand.SINGULAR_NOUN + " names to match.");
            }
            else
            {
                /* perform source pattern matching on candidate file/directory names */
                RenameWand.stdout.print("\nPerforming source pattern matching on " + numMatchCandidates +
                        " candidate " + RenameWand.SINGULAR_NOUN + " " +
                        ((numMatchCandidates == 1) ? "name" : "names") + "...");
                RenameWand.stdout.flush();

                files = performSourcePatternMatching(files);
                final int numMatched = files.size();

                RenameWand.stdout.print("\n" + numMatched + " out of " + numMatchCandidates +
                        " " + RenameWand.SINGULAR_NOUN + " " +
                        ((numMatched == 1) ? "name" : "names") + " matched.");
                RenameWand.stdout.flush();

                if (numMatched == 0)
                {
                    RenameWand.stdout.print("\nNo " + RenameWand.PLURAL_NOUN + " to rename.");
                }
                else
                {
                    /* evaluate target file/directory names */
                    RenameWand.stdout.print("\nDetermining target " + RenameWand.SINGULAR_NOUN + " " +
                            ((numMatched == 1) ? "name" : "names") + " and renaming sequence...");
                    RenameWand.stdout.flush();

                    evaluateTargetPattern(files.toArray(new FileUnit[numMatched]));

                    /* determine renaming sequence and find clashes, bad names, etc. */
                    final List<RenameFilePair> renameOperations = getRenameOperations(files);
                    final int numRenameOperations = renameOperations.size();

                    /* prompt user before renaming */
                    final boolean proceedToRename = promptUserOnRename(files, numRenameOperations);

                    /* perform rename operations */
                    final int numRenameOperationsPerformed = proceedToRename ? performRenameOperations(renameOperations) : 0;

                    if (!RenameWand.simulateOnly && proceedToRename)
                    {
                        RenameWand.stdout.print("\n\n" + numRenameOperationsPerformed + " out of " +
                                numRenameOperations + " " + RenameWand.SINGULAR_NOUN + " rename " +
                                ((numRenameOperations == 1) ? "operation" : "operations") + " performed.");
                        RenameWand.stdout.flush();
                    }

                    /* report statistics */
                    final StringBuilder report = new StringBuilder();
                    report.append("\n\n" + (RenameWand.renameDirectories ? "DIRECTORY" : "FILE") + " RENAME REPORT");

                    if (RenameWand.reportNumWarnings > 0)
                    {
                        report.append("\n " + RenameWand.reportNumWarnings + " " +
                                ((RenameWand.reportNumWarnings == 1) ? "warning" : "warnings") + " encountered.");
                    }

                    report.append("\n No. of candidate " + RenameWand.SINGULAR_NOUN + " names to match  : " + numMatchCandidates +
                            "\n No. of " + RenameWand.SINGULAR_NOUN + " names matched             : " + numMatched +
                            "\n No. of " + RenameWand.SINGULAR_NOUN + " rename operations required: " + numRenameOperations);

                    if (!RenameWand.simulateOnly && (numRenameOperations > 0))
                    {
                        report.append("\n No. of successful " + RenameWand.SINGULAR_NOUN +
                                " rename operations performed: " + numRenameOperationsPerformed);
                    }

                    RenameWand.stdout.print(report.toString());
                }
            }

            RenameWand.stdout.print("\n\nRenameWand is done!\n\n");
        }
        catch (TerminatingException e)
        {
            /* terminating exception thrown; proceed to abort program */
            /* (this should be the only place where a TerminatingException is caught) */

            exitCode = e.getExitCode();

            if (exitCode != 0)
            {
                /* abnormal termination; print error message */
                RenameWand.stderr.print("\n\nERROR: " + e.getMessage() + "\n");
                RenameWand.stdout.print("\nRenameWand aborted.\n\n");
            }
        }
        catch (Exception e)
        {
            /* catch all other exceptions; proceed to abort program */
            RenameWand.stderr.print("\n\nERROR: An unexpected error has occurred:\n" +
                    getExceptionInformation(e) + "\n");

            exitCode = 1;
            RenameWand.stdout.print("\nRenameWand aborted.\n\n");
        }
        finally
        {
            /* perform clean-up before exiting */
            RenameWand.stderr.flush();
            RenameWand.stdout.flush();
        }

        System.exit(exitCode);
    }


    /**
    * Process command-line arguments.
    *
    * @param args
    *     Command-line argument strings
    */
    private static void processArguments(
            final String[] args)
    {
        final String howHelp = "\nTo display help, run RenameWand without any command-line arguments.";

        /* print usage documentation, if no arguments */
        if (args.length == 0)
        {
            printUsage();
            throw new TerminatingException(null, 0);
        }

        if (args.length < 2)
            throw new TerminatingException("Insufficient arguments:\nThe source and target pattern strings must be specified." + howHelp);

        /* source and target pattern strings */
        RenameWand.sourcePatternString = args[args.length - 2];
        RenameWand.targetPatternString = args[args.length - 1];

        /* check for illegal characters in pattern strings */
        if (RenameWand.sourcePatternString.contains("\0"))
            throw new TerminatingException("Illegal null-character found in source pattern string.");

        if (RenameWand.targetPatternString.contains("\0"))
            throw new TerminatingException("Illegal null-character found in target pattern string.");

        /* default action on rename operation error */
        int skipOnRenameOperationError = 0;
        int undoAllOnRenameOperationError = 0;
        int abortOnRenameOperationError = 0;

        /* process command-line switches */
        for (int i = 0; i < args.length - 2; i++)
        {
            final String sw = args[i];

            if ("--recurse".equals(sw) || "-r".equals(sw))
            {
                /* recurse into subdirectories */
                RenameWand.recurseIntoSubdirectories = true;
            }
            else if ("--dirs".equals(sw) || "-d".equals(sw))
            {
                /* rename directories instead of files */
                RenameWand.renameDirectories = true;
            }
            else if ("--path".equals(sw) || "-p".equals(sw))
            {
                /* match relative pathname, not just the name, of the files/directories */
                RenameWand.matchRelativePathname = true;
            }
            else if ("--lower".equals(sw) || "-l".equals(sw))
            {
                /* match lower case name of the files/directories */
                RenameWand.matchLowerCase = true;
            }
            else if ("--yes".equals(sw) || "-y".equals(sw))
            {
                /* automatically rename files/directories without prompting */
                RenameWand.automaticRename = true;
            }
            else if ("--simulate".equals(sw) || "-s".equals(sw))
            {
                /* simulate only; do not actually rename files/directories */
                RenameWand.simulateOnly = true;
                RenameWand.ignoreWarnings = true;
            }
            else if ("--ignorewarnings".equals(sw) || "-i".equals(sw))
            {
                /* ignore warnings; do not pause */
                RenameWand.ignoreWarnings = true;
            }
            else if ("--skip".equals(sw))
            {
                /* skip on rename operation error */
                skipOnRenameOperationError = 1;
            }
            else if ("--undoall".equals(sw))
            {
                /* undo all on rename operation error */
                undoAllOnRenameOperationError = 1;
            }
            else if ("--abort".equals(sw))
            {
                /* abort on rename operation error */
                abortOnRenameOperationError = 1;
            }
            else
            {
                /* invalid switch */
                throw new TerminatingException("\"" + sw + "\" is not a valid switch." + howHelp);
            }
        }

        if (RenameWand.simulateOnly && RenameWand.automaticRename)
            throw new TerminatingException("Switches --simulate and --yes cannot be used together." + howHelp);

        if (skipOnRenameOperationError + undoAllOnRenameOperationError + abortOnRenameOperationError > 1)
            throw new TerminatingException("Only one of the three switches --skip, --undoall, and --abort may be specified." + howHelp);

        if (RenameWand.simulateOnly && (skipOnRenameOperationError + undoAllOnRenameOperationError + abortOnRenameOperationError > 0))
            throw new TerminatingException("Switches --skip, --undoall, and --abort cannot be used together with --simulate." + howHelp);

        /* default action on rename operation error */
        if (skipOnRenameOperationError > 0)
            RenameWand.defaultActionOnRenameOperationError = 'S';

        if (undoAllOnRenameOperationError > 0)
            RenameWand.defaultActionOnRenameOperationError = 'U';

        if (abortOnRenameOperationError > 0)
            RenameWand.defaultActionOnRenameOperationError = 'A';
    }


    /**
    * Scan current directory to get candidate files/directories for matching.
    *
    * @return
    *     Candidate files/directories for matching
    */
    private static List<FileUnit> getMatchCandidates()
    {
        /* get absolute canonical path of the current directory */
        RenameWand.currentDirectory = new File("");

        try
        {
            RenameWand.currentDirectory = RenameWand.currentDirectory.getCanonicalFile();
        }
        catch (Exception e)
        {
            throw new TerminatingException("Failed to get full pathname of the current directory \"" +
                    RenameWand.currentDirectory.getPath() + "\":\n" + getExceptionInformation(e));
        }

        RenameWand.currentDirectoryFullPathname = RenameWand.currentDirectory.getPath();

        /* include trailing separator */
        if (!RenameWand.currentDirectoryFullPathname.endsWith(File.separator))
            RenameWand.currentDirectoryFullPathname += File.separator;

        RenameWand.currentDirectoryFullPathnameLength = RenameWand.currentDirectoryFullPathname.length();

        /* return value: match candidate files/directories */
        final List<FileUnit> matchCandidates = new ArrayList<FileUnit>();

        /* stack containing the subdirectories to be scanned */
        final Deque<File> subdirectories = new ArrayDeque<File>();
        subdirectories.push(RenameWand.currentDirectory);

        /* reset number of subdirectories scanned */
        RenameWand.numDirs = 0;

        /* perform a DFS scanning of the subdirectories */
        while (!subdirectories.isEmpty())
        {
            RenameWand.numDirs++;

            /* get a directory to be scanned */
            final File dir = subdirectories.pop();
            final File[] listFiles = dir.listFiles();

            if (listFiles == null)
            {
                final String path = dir.getPath();

                reportWarning("Failed to get contents of directory \"" + path +
                        (path.endsWith(File.separator) ? "" : File.separator) +
                        "\".\nThis directory will be ignored.");
            }
            else
            {
                /* subdirectories under this directory */
                final List<File> subdirs = new ArrayList<File>();

                for (File f : listFiles)
                {
                    final boolean isDirectory = f.isDirectory();

                    if (RenameWand.renameDirectories == isDirectory)
                    {
                        final FileUnit u = new FileUnit();
                        u.source = f;
                        u.parentDirId = RenameWand.numDirs;
                        matchCandidates.add(u);
                    }

                    if (isDirectory)
                        subdirs.add(f);
                }

                if (RenameWand.recurseIntoSubdirectories)
                {
                    for (int i = subdirs.size() - 1; i >= 0; i--)
                        subdirectories.push(subdirs.get(i));
                }
            }
        }

        return matchCandidates;
    }


    /**
    * Perform source pattern matching against the names of the candidate
    * files/directories, and return files/directories that match.
    *
    * @param matchCandidates
    *     Candidate files/directories
    * @return
    *     Files/directories with names that match the source pattern
    */
    private static List<FileUnit> performSourcePatternMatching(
            final List<FileUnit> matchCandidates)
    {
        /* return value: files/directories with names that match the source pattern */
        final List<FileUnit> matched = new ArrayList<FileUnit>();

        /* regex pattern used for matching file/directory names */
        Pattern sourcePattern = null;

        /* is the regex pattern matcher reusable for different files/directories? */
        RenameWand.sourcePatternIsReusable = false;

        /* match each candidate file or directory */
        for (FileUnit u : matchCandidates)
        {
            if (!RenameWand.sourcePatternIsReusable)
                sourcePattern = getFileSourcePattern(u);

            /* check if source pattern is successfully generated */
            if (sourcePattern != null)
            {
                /* name string to be matched */
                String name = null;

                if (RenameWand.matchRelativePathname)
                {
                    name = u.source.getPath();

                    if (name.startsWith(RenameWand.currentDirectoryFullPathname))
                        name = name.substring(RenameWand.currentDirectoryFullPathnameLength);
                }
                else
                {
                    name = u.source.getName();
                }

                /* trim off trailing separator */
                while (name.endsWith(File.separator))
                    name = name.substring(0, name.length() - File.separator.length());

                if (RenameWand.matchLowerCase)
                    name = name.toLowerCase(Locale.ENGLISH);

                /* regex pattern matcher */
                final Matcher sourceMatcher = sourcePattern.matcher(name);

                if (sourceMatcher.matches())
                {
                    /* add capture group values to FileUnit's registerValues, and */
                    /* add this file/directory to our list of successful matches  */

                    u.registerValues = new String[RenameWand.numCaptureGroups + 1]; // add index offset 1

                    for (int i = 1; i <= RenameWand.numCaptureGroups; i++)
                        u.registerValues[i] = sourceMatcher.group(i);

                    matched.add(u);
                }
            }
        }

        return matched;
    }


    /**
    * Generate source regex pattern corresponding to the given file/directory.
    *
    * @param u
    *     File/directory for which to generate source regex pattern
    * @return
    *     Source regex pattern corresponding to the given file/directory;
    *     null if regex pattern cannot be generated.
    */
    private static Pattern getFileSourcePattern(
            final FileUnit u)
    {
        /* reset register names and capture group counter */
        RenameWand.registerNames.clear();
        int captureGroupIndex = 0;

        /* assume that source pattern is reusable */
        RenameWand.sourcePatternIsReusable = true;

        /* Stack to keep track of the parser mode: */
        /* "--" : Base mode (first on the stack)   */
        /* "[]" : Square brackets mode "[...]"     */
        /* "{}" : Curly braces mode "{...}"        */
        final Deque<String> parserMode = new ArrayDeque<String>();
        parserMode.push("--"); // base mode

        final int sourcePatternStringLength = RenameWand.sourcePatternString.length();
        int index = 0; // index in sourcePatternString

        /* regex pattern equivalent to sourcePatternString */
        final StringBuilder sourceRegex = new StringBuilder();

        /* parse each character of the source pattern string */
        while (index < sourcePatternStringLength)
        {
            char c = RenameWand.sourcePatternString.charAt(index++);

            if (c == '\\')
            {
                /***********************
                * (1) ESCAPE SEQUENCE *
                ***********************/

                if (index == sourcePatternStringLength)
                {
                    /* no characters left, so treat '\' as literal char */
                    sourceRegex.append(Pattern.quote("\\"));
                }
                else
                {
                    /* read next character */
                    c = RenameWand.sourcePatternString.charAt(index);
                    final String s = c + "";

                    if (("--".equals(parserMode.peek()) && "\\<>[]{}?*".contains(s)) ||
                            ("[]".equals(parserMode.peek()) && "\\<>[]{}?*!-".contains(s)) ||
                            ("{}".equals(parserMode.peek()) && "\\<>[]{}?*,".contains(s)))
                    {
                        /* escape the construct char */
                        index++;
                        sourceRegex.append(Pattern.quote(s));
                    }
                    else
                    {
                        /* treat '\' as literal char */
                        sourceRegex.append(Pattern.quote("\\"));
                    }
                }
            }
            else if (c == '*')
            {
                /************************
                * (2) GLOB PATTERN '*' *
                ************************/

                /* create non-capturing group to match zero or more characters */
                sourceRegex.append(".*");
            }
            else if (c == '?')
            {
                /************************
                * (3) GLOB PATTERN '?' *
                ************************/

                /* create non-capturing group to match exactly one character */
                sourceRegex.append('.');
            }
            else if (c == '[')
            {
                /****************************
                * (4) GLOB PATTERN "[...]" *
                ****************************/

                /* opening square bracket '[' */
                /* create non-capturing group to match exactly one character */
                /* inside the sequence */
                sourceRegex.append('[');
                parserMode.push("[]");

                /* check for negation character '!' immediately after */
                /* the opening bracket '[' */
                if ((index < sourcePatternStringLength) &&
                        (RenameWand.sourcePatternString.charAt(index) == '!'))
                {
                    index++;
                    sourceRegex.append('^');
                }
            }
            else if ((c == ']') && "[]".equals(parserMode.peek()))
            {
                /* closing square bracket ']' */
                sourceRegex.append(']');
                parserMode.pop();
            }
            else if ((c == '-') && "[]".equals(parserMode.peek()))
            {
                /* character range '-' in "[...]" */
                sourceRegex.append('-');
            }
            else if (c == '{')
            {
                /****************************
                * (5) GLOB PATTERN "{...}" *
                ****************************/

                /* opening curly brace '{' */
                /* create non-capturing group to match one of the */
                /* strings inside the sequence */
                sourceRegex.append("(?:(?:");
                parserMode.push("{}");
            }
            else if ((c == '}') && "{}".equals(parserMode.peek()))
            {
                /* closing curly brace '}' */
                sourceRegex.append("))");
                parserMode.pop();
            }
            else if ((c == ',') && "{}".equals(parserMode.peek()))
            {
                /* comma between strings in "{...}" */
                sourceRegex.append(")|(?:");
            }
            else if (c == '<')
            {
                /*********************************
                * (6) SPECIAL CONSTRUCT "<...>" *
                *********************************/

                final StringBuilder specialConstruct = new StringBuilder("<");
                boolean closingAngleBracketFound = false;

                /* read until the first (unescaped) closing '>' */
                while (!closingAngleBracketFound && (index < sourcePatternStringLength))
                {
                    c = RenameWand.sourcePatternString.charAt(index++);
                    specialConstruct.append(c);

                    if ((c == '>') && (specialConstruct.charAt(specialConstruct.length() - 2) != '\\'))
                        closingAngleBracketFound = true;
                }

                if (!closingAngleBracketFound)
                    throw new TerminatingException("Failed to find matching closing angle bracket > for special construct \"" +
                            specialConstruct + "\" in source pattern string. Please ensure that each literal < is escaped as \\<.");

                /* check if special construct is in the form "<length|@expr>" */
                final Matcher specialConstructMatcher =
                        RenameWand.SOURCE_SPECIAL_CONSTRUCT_PATTERN.matcher(specialConstruct);

                if (!specialConstructMatcher.matches())
                    throw new TerminatingException("Invalid special construct \"" + specialConstruct + "\" in source pattern string: " +
                            "Not in the form <length" + RenameWand.SPECIAL_CONSTRUCT_SEPARATOR_CHAR + RenameWand.INTEGER_FILTER_INDICATOR_CHAR + "expr>.");

                /* match groups */
                String length = specialConstructMatcher.group(1);
                if (length != null) length = length.trim();
                final boolean integerFilter = (specialConstructMatcher.group(2) != null);
                String expr = specialConstructMatcher.group(3).trim();

                /* evaluate the length string if it is not already a positive integer */
                if ((length != null) &&
                        !RenameWand.POSITIVE_INTEGER_PATTERN.matcher(length).matches())
                {
                    RenameWand.sourcePatternIsReusable = false; // this construct is file-specific
                    final EvaluationResult<String> result = evaluateSpecialConstruct(new FileUnit[] {u}, length);

                    if (result.success)
                    {
                        length = result.output[0];

                        /* check that length string is a positive integer now */
                        if (!RenameWand.POSITIVE_INTEGER_PATTERN.matcher(length).matches())
                            throw new TerminatingException("Invalid length string for special construct \"" +
                                    specialConstruct + "\" in source pattern string for " +
                                    RenameWand.SINGULAR_NOUN + " \"" + u.source.getPath() + "\": " +
                                    length + " is not a positive integer.");
                    }
                    else
                    {
                        reportWarning("Failed to evaluate length string of special construct \"" +
                                specialConstruct + "\" in source pattern string for " +
                                RenameWand.SINGULAR_NOUN + " \"" + u.source.getPath() + "\": " +
                                result.error + "\nThis " + RenameWand.SINGULAR_NOUN + " will be ignored.");

                        return null; // failed to generate regex pattern
                    }
                }

                /* check if this construct is a register assignment or back reference */
                if (((length == null) || RenameWand.POSITIVE_INTEGER_PATTERN.matcher(length).matches()) &&
                        (u.evaluateMacro(expr) == null) &&
                        RenameWand.REGISTER_NAME_PATTERN.matcher(expr).matches())
                {
                    if (RenameWand.registerNames.containsKey(expr))
                    {
                        /* register is already captured, so we create a back reference to it */
                        if ((length != null) || integerFilter)
                            throw new TerminatingException("Invalid back reference \"" + specialConstruct +
                                    "\" to register \"" + expr + "\" near position " + index +
                                    " of source pattern string: Length string and integer filter indicator @ not allowed.");

                        sourceRegex.append("\\" + RenameWand.registerNames.get(expr));
                    }
                    else
                    {
                        /* register has not been captured, so we create a regex capturing group for it */
                        RenameWand.registerNames.put(expr, ++captureGroupIndex);

                        sourceRegex.append("(" + (integerFilter ? "[0-9]" : ".") +
                                ((length == null) ? "*" : ("{" + Integer.parseInt(length.trim()) + "}")) + ")");
                    }
                }
                else
                {
                    /* proceed to parse the expression string */

                    RenameWand.sourcePatternIsReusable = false; // this construct is file-specific

                    if (integerFilter)
                        throw new TerminatingException("Invalid special construct expression \"" + specialConstruct +
                                "\" in source pattern string: Integer filter indicator @ not allowed here because \"" +
                                expr + "\" is not a register name.");

                    /* evaluate expr string */
                    final EvaluationResult<String> result = evaluateSpecialConstruct(new FileUnit[] {u}, expr);

                    if (result.success)
                    {
                        expr = result.output[0];

                        /* perform length formatting if specified */
                        if (length != null)
                            expr = padString(expr, Integer.parseInt(length), isNumeric(expr));

                        /* convert literal string to a regex string */
                        sourceRegex.append(Pattern.quote(expr));
                    }
                    else
                    {
                        reportWarning("Failed to evaluate expression string of special construct \"" +
                                specialConstruct + "\" in source pattern string for " +
                                RenameWand.SINGULAR_NOUN + " \"" + u.source.getPath() + "\": " +
                                result.error + "\nThis " + RenameWand.SINGULAR_NOUN + " will be ignored.");

                        return null; // failed to generate regex pattern
                    }
                }
            }
            else if ((c == '/') && RenameWand.isWindowsOperatingSystem)
            {
                /****************************************
                * (7) ALTERNATE WINDOWS FILE SEPARATOR *
                ****************************************/

                sourceRegex.append(Pattern.quote("\\"));
            }
            else
            {
                /*************************
                * (8) LITERAL CHARACTER *
                *************************/

                /* convert literal character to a regex string */
                sourceRegex.append(Pattern.quote(c + ""));
            }
        }

        /* check for mismatched [...] or {...} */
        if ("[]".equals(parserMode.peek()))
            throw new TerminatingException("Failed to find matching closing square bracket ] in source pattern string.");

        if ("{}".equals(parserMode.peek()))
            throw new TerminatingException("Failed to find matching closing curly brace } in source pattern string.");

        /* set total number of capture groups in the source pattern */
        RenameWand.numCaptureGroups = captureGroupIndex;

        /* compile regex string */
        Pattern sourceRegexCompiledPattern = null;

        try
        {
            sourceRegexCompiledPattern = Pattern.compile(sourceRegex.toString());
        }
        catch (PatternSyntaxException e)
        {
            throw new TerminatingException("Failed to compile source pattern string for " +
                    RenameWand.SINGULAR_NOUN + " \"" + u.source.getPath() + "\":\n" +
                    getExceptionInformation(e));
        }

        return sourceRegexCompiledPattern;
    }


    /**
    * Evaluate the target pattern for the given matched files/directories,
    * and store the results in the respective FileUnit's.
    *
    * @param matchedFiles
    *     Matched files/directories for which to evaluate the target pattern
    */
    private static void evaluateTargetPattern(
            final FileUnit[] matchedFiles)
    {
        /* number of matched files */
        final int n = matchedFiles.length;

        /* count number of files in each local directory */
        int[] localCounts = new int[RenameWand.numDirs + 1];

        Arrays.fill(localCounts, 0);

        for (FileUnit u : matchedFiles)
            localCounts[u.parentDirId]++;

        /* reset file/directory attributes */
        for (FileUnit u : matchedFiles)
        {
            u.globalCount = n;
            u.localCount = localCounts[u.parentDirId];
            u.targetFilename = new StringBuilder();
        }

        final int targetPatternStringLength = RenameWand.targetPatternString.length();
        int index = 0; // index in targetPatternString

        /* parse each character of the target pattern string */
        while (index < targetPatternStringLength)
        {
            char c = RenameWand.targetPatternString.charAt(index++);

            if (c == '\\')
            {
                /***********************
                * (1) ESCAPE SEQUENCE *
                ***********************/

                if (index == targetPatternStringLength)
                {
                    /* no characters left, so treat '\' as literal char */
                    for (FileUnit u : matchedFiles)
                        u.targetFilename.append('\\');
                }
                else
                {
                    /* read next char */
                    c = RenameWand.targetPatternString.charAt(index);

                    if ((c == '<') || (c == '\\'))
                    {
                        /* escape the construct char */
                        index++;

                        for (FileUnit u : matchedFiles)
                            u.targetFilename.append(c);
                    }
                    else
                    {
                        /* treat '\' as literal char */
                        for (FileUnit u : matchedFiles)
                            u.targetFilename.append('\\');
                    }
                }
            }
            else if (c == '<')
            {
                /*********************************
                * (2) SPECIAL CONSTRUCT "<...>" *
                *********************************/

                final StringBuilder specialConstruct = new StringBuilder("<");
                boolean closingAngleBracketFound = false;

                /* read until the first (unescaped) closing '>' */
                while ((!closingAngleBracketFound) && (index < targetPatternStringLength))
                {
                    c = RenameWand.targetPatternString.charAt(index++);
                    specialConstruct.append(c);

                    if ((c == '>') && (specialConstruct.charAt(specialConstruct.length() - 2) != '\\'))
                        closingAngleBracketFound = true;
                }

                if (!closingAngleBracketFound)
                    throw new TerminatingException("Failed to find matching closing angle bracket > for special construct \"" +
                            specialConstruct + "\" in target pattern string. Please ensure that each literal < is escaped as \\<.");

                /* check if special construct is in the form "<length|expr>" */
                final Matcher specialConstructMatcher =
                        RenameWand.TARGET_SPECIAL_CONSTRUCT_PATTERN.matcher(specialConstruct);

                if (!specialConstructMatcher.matches())
                    throw new TerminatingException("Invalid special construct \"" + specialConstruct +
                            "\" in target pattern string: Not in the form <length" + RenameWand.SPECIAL_CONSTRUCT_SEPARATOR_CHAR + "expr>.");

                /* match groups */
                final String length = specialConstructMatcher.group(1);
                final String expr = specialConstructMatcher.group(2).trim();

                String[] lengths = null;

                /* evaluate the length string if it is not already a positive integer */
                if ((length != null) &&
                        (!RenameWand.POSITIVE_INTEGER_PATTERN.matcher(length).matches()))
                {
                    final EvaluationResult<String> result = evaluateSpecialConstruct(matchedFiles, length);

                    if (result.success)
                    {
                        lengths = result.output;

                        /* check that length string is a positive integer now */
                        for (int i = 0; i < n; i++)
                            if (!RenameWand.POSITIVE_INTEGER_PATTERN.matcher(lengths[i]).matches())
                                throw new TerminatingException("Invalid length string for special construct \"" +
                                        specialConstruct + "\" in target pattern string for " + SINGULAR_NOUN + " \"" +
                                        matchedFiles[i].source.getPath() + "\": " + lengths[i] + " is not a positive integer.");
                    }
                    else
                    {
                        throw new TerminatingException("Failed to evaluate length string of special construct \"" +
                                specialConstruct + "\" in target pattern string: " + result.error);
                    }
                }

                /* proceed to parse the expression string */
                final EvaluationResult<String> result = evaluateSpecialConstruct(matchedFiles, expr);

                if (!result.success)
                    throw new TerminatingException("Failed to evaluate expression string of special construct \"" +
                            specialConstruct + "\" in target pattern string: " + result.error);

                /* perform length formatting */
                if (lengths == null)
                {
                    if (length == null)
                    {
                        /* no length string specified */
                        for (int i = 0; i < n; i++)
                            matchedFiles[i].targetFilename.append(result.output[i]);
                    }
                    else
                    {
                        /* constant length string specified */
                        final int len = Integer.parseInt(length);

                        /* check if all expr are numeric */
                        final boolean isNumeric = (getNonNumericIndex(result.output) < 0);

                        for (int i = 0; i < n; i++)
                            matchedFiles[i].targetFilename.append(
                                    padString(result.output[i], len, isNumeric));
                    }
                }
                else
                {
                    /* file-specific length strings used */

                    /* check if all expr are numeric */
                    final boolean isNumeric = (getNonNumericIndex(result.output) < 0);

                    for (int i = 0; i < n; i++)
                        matchedFiles[i].targetFilename.append(
                                padString(result.output[i], Integer.parseInt(lengths[i]), isNumeric));
                }
            }
            else if ((c == '/') && RenameWand.isWindowsOperatingSystem)
            {
                /****************************************
                * (3) ALTERNATE WINDOWS FILE SEPARATOR *
                ****************************************/

                for (FileUnit u : matchedFiles)
                    u.targetFilename.append('\\');
            }
            else
            {
                /*************************
                * (4) LITERAL CHARACTER *
                *************************/

                /* handle all other characters as literals */
                for (FileUnit u : matchedFiles)
                    u.targetFilename.append(c);
            }
        }
    }


    /**
    * Evaluate the given special construct for the given files/directories, and
    * return the results.
    *
    * @param file
    *     Files/directories for which to evaluate the given special construct
    * @param specialConstruct
    *     Special construct string
    * @return
    *     Results of evaluation
    */
    private static EvaluationResult<String> evaluateSpecialConstruct(
            final FileUnit[] files,
            final String specialConstruct)
    {
        /* use a stack to evaluate the special construct */

        /* return value */
        final EvaluationResult<String> result = new EvaluationResult<String>();

        /* tokenize expr */
        final StringManipulator.Token[] tempTokens = StringManipulator.tokenize(
                "(" + specialConstruct + ")",  /* surround special construct with (...) */
                "(((\\#++)\\!?)|((\\@++)\\!?)|[" +
                Pattern.quote("[]()*/^+-" + RenameWand.SUBSTRING_RANGE_CHAR  + RenameWand.SUBSTRING_DELIMITER_CHAR) + "])",
                true);

        /* preprocess tokens */
        final List<StringManipulator.Token> tokens = new ArrayList<StringManipulator.Token>();

        for (StringManipulator.Token token : tempTokens)
        {
            token.val = token.val.trim();

            if (token.val.isEmpty())
                continue;

            if ("-".equals(token.val) &&
                    tokens.get(tokens.size() - 1).isDelimiter &&
                    !")]".contains(tokens.get(tokens.size() - 1).val))
            {
                /* unary minus (negative) sign */
                token.val = "~";
            }
            else if  ("(".equals(token.val) &&
                    (tokens.size() > 0) &&
                    (!tokens.get(tokens.size() - 1).isDelimiter ||
                    ")".equals(tokens.get(tokens.size() - 1).val)))
            {
                /* implicit multiplication sign */
                final StringManipulator.Token multiplicationSign =
                        new StringManipulator.Token("*", true);

                tokens.add(multiplicationSign);
            }

            tokens.add(token);
        }

        /* number of file units */
        final int n = files.length;

        /* operator and operand stacks */
        final Deque<String> operators = new ArrayDeque<String>();
        final Deque<String[]> operands = new ArrayDeque<String[]>();

        /* process each token */
        ProcessNextToken:
        for (StringManipulator.Token token : tokens)
        {
            final String tokenVal = token.val;

            if (token.isDelimiter)
            {
                /* token is a delimiter */

                final String op = token.val;

                if ("(".equals(op))
                {
                    operators.push(op);
                }
                else if ("[".equals(op))
                {
                    operators.push(op);
                }
                else if (")".equals(op))
                {
                    /* eval (...) */
                    while (!operators.isEmpty() &&
                            !"(".equals(operators.peek()))
                    {
                        final EvaluationResult<Void> stepResult = evaluateStep(files, operators, operands);

                        if (!stepResult.success)
                        {
                            result.error = stepResult.error;
                            return result;
                        }
                    }

                    if (operators.isEmpty() || !"(".equals(operators.pop()))
                    {
                        result.error = "Mismatched brackets ( ) in special construct expression.";
                        return result;
                    }
                }
                else if ("]".equals(op))
                {
                    /* eval substring a[...] */
                    while ((!operators.isEmpty()) &&
                            (!"[".equals(operators.peek())))
                    {
                        final EvaluationResult<Void> stepResult = evaluateStep(files, operators, operands);

                        if (!stepResult.success)
                        {
                            result.error = stepResult.error;
                            return result;
                        }
                    }

                    if (operators.isEmpty() || !"[".equals(operators.pop()))
                    {
                        result.error = "Mismatched brackets [ ] in special construct expression.";
                        return result;
                    }

                    /* proceed to evaluate substring */
                    if (operands.size() < 2)
                    {
                        result.error = "Insufficient operands for substring [ ] operation.";
                        return result;
                    }

                    final String[] formats = operands.pop();
                    final String[] exprs = operands.pop();
                    String[] ans = new String[n];

                    for (int i = 0; i < n; i++)
                    {
                        final String format = formats[i];
                        final String expr = exprs[i];

                        ans[i] = StringManipulator.substring(
                                expr,
                                format,
                                RenameWand.SUBSTRING_RANGE_CHAR ,
                                RenameWand.SUBSTRING_DELIMITER_CHAR);

                        if (ans[i] == null)
                        {
                            result.error = "Invalid substring operation \"" +
                                    expr + "[" + format + "]\".";

                            return result;
                        }
                    }

                    /* push answer on operand stack */
                    operands.push(ans);
                }
                else
                {
                    /* all other operators */

                    /* eval stack if possible */
                    while (!operators.isEmpty() &&
                            !"[]()".contains(operators.peek()) &&
                            (RenameWand.OPERATOR_PRECEDENCE.get(op).intValue() <=
                            RenameWand.OPERATOR_PRECEDENCE.get(operators.peek()).intValue()))
                    {
                        final EvaluationResult<Void> stepResult = evaluateStep(files, operators, operands);

                        if (!stepResult.success)
                        {
                            result.error = stepResult.error;
                            return result;
                        }
                    }

                    operators.push(op);
                }
            }
            else
            {
                /* token is an operand */

                String[] tokenVals = new String[n];

                if (RenameWand.NUMERIC_PATTERN.matcher(tokenVal).matches())
                {
                    /* token is a numeric value */
                    for (int i = 0; i < n; i++)
                        tokenVals[i] = tokenVal;
                }
                else if (files[0].evaluateMacro(tokenVal) != null)
                {
                    /* token is a register or macro */
                    for (int i = 0; i < n; i++)
                        tokenVals[i] = files[i].evaluateMacro(tokenVal);
                }
                else
                {
                    /* treat as literal text */
                    for (int i = 0; i < n; i++)
                        tokenVals[i] = tokenVal;
                }

                /* push evaluated token onto operands stack */
                operands.push(tokenVals);
            }
        }

        /* extract final result */
        if (operators.isEmpty() && (operands.size() == 1))
        {
            /* valid return value */
            result.success = true;
            result.output = operands.pop();
        }
        else
        {
            /* error in evaluation */
            result.error = "Mismatched operators/operands in special construct expression.";
        }

        return result;
    }


    /**
    * Evaluate a single step, given the operators and operands stacks, for the
    * given files/directories.
    *
    * @param files
    *     Files/directories for which to evaluate the single step
    * @param operators
    *     Operators stack
    * @param operands
    *     Operands stack
    * @return
    *     Results of the evaluation
    */
    private static EvaluationResult<Void> evaluateStep(
            final FileUnit[] files,
            final Deque<String> operators,
            final Deque<String[]> operands)
    {
        final int n = files.length;

        /* return value */
        final EvaluationResult<Void> result = new EvaluationResult<Void>();

        if (operators.isEmpty())
        {
            result.error = "Operators stack is empty.";
            return result;
        }

        final String op = operators.pop();

        if ("^".equals(op))
        {
            /**************************
            * (1) EXPONENTIATION '^' *
            **************************/

            if (operands.size() < 2)
            {
                result.error = "Insufficient operands for exponentiation '^' operation.";
                return result;
            }

            final String[] args2 = operands.pop();
            final String[] args1 = operands.pop();

            /* check that arguments are all numeric */
            final int args1NonNumericIndex = getNonNumericIndex(args1);
            final int args2NonNumericIndex = getNonNumericIndex(args2);

            if ((args1NonNumericIndex >= 0) || (args2NonNumericIndex >= 0))
            {
                int nonNumericIndex;
                String nonNumericArg;

                if (args1NonNumericIndex >= 0)
                {
                    nonNumericIndex = args1NonNumericIndex;
                    nonNumericArg = args1[args1NonNumericIndex];
                }
                else
                {
                    nonNumericIndex = args2NonNumericIndex;
                    nonNumericArg = args2[args2NonNumericIndex];
                }

                result.error = "Invalid operand encountered for exponentiation '^' operation: " +
                        "The operand \"" + nonNumericArg + "\" corresponding to " +
                        RenameWand.SINGULAR_NOUN + " \"" + files[nonNumericIndex].source.getPath() +
                        "\" is non-numeric.";

                operators.push(op);
                operands.push(args1);
                operands.push(args2);
                return result;
            }

            String[] ans = new String[n];

            for (int i = 0; i < n; i++)
                ans[i] = (int) Math.pow((int) Double.parseDouble(args1[i]), (int) Double.parseDouble(args2[i])) + "";

            /* push answer on operand stack */
            operands.push(ans);
            result.success = true;
        }
        else if ("*".equals(op))
        {
            /**************************
            * (2) MULTIPLICATION '*' *
            **************************/

            if (operands.size() < 2)
            {
                result.error = "Insufficient operands for multiplication '*' operation.";
                return result;
            }

            final String[] args2 = operands.pop();
            final String[] args1 = operands.pop();

            /* check that arguments are all numeric */
            final int args1NonNumericIndex = getNonNumericIndex(args1);
            final int args2NonNumericIndex = getNonNumericIndex(args2);

            if ((args1NonNumericIndex >= 0) || (args2NonNumericIndex >= 0))
            {
                int nonNumericIndex;
                String nonNumericArg;

                if (args1NonNumericIndex >= 0)
                {
                    nonNumericIndex = args1NonNumericIndex;
                    nonNumericArg = args1[args1NonNumericIndex];
                }
                else
                {
                    nonNumericIndex = args2NonNumericIndex;
                    nonNumericArg = args2[args2NonNumericIndex];
                }

                result.error = "Invalid operand encountered for multiplication '*' operation: " +
                        "The operand \"" + nonNumericArg + "\" corresponding to " +
                        RenameWand.SINGULAR_NOUN + " \"" + files[nonNumericIndex].source.getPath() +
                        "\" is non-numeric.";

                operators.push(op);
                operands.push(args1);
                operands.push(args2);
                return result;
            }

            String[] ans = new String[n];

            for (int i = 0; i < n; i++)
                ans[i] = (((int) Double.parseDouble(args1[i])) * ((int) Double.parseDouble(args2[i]))) + "";

            /* push answer on operand stack */
            operands.push(ans);
            result.success = true;
        }
        else if ("/".equals(op))
        {
            /********************
            * (3) DIVISION '/' *
            ********************/

            if (operands.size() < 2)
            {
                result.error = "Insufficient operands for division '/' operation.";
                return result;
            }

            final String[] args2 = operands.pop();
            final String[] args1 = operands.pop();

            /* check that arguments are numeric */
            final int args1NonNumericIndex = getNonNumericIndex(args1);
            final int args2NonNumericIndex = getNonNumericIndex(args2);

            if ((args1NonNumericIndex >= 0) || (args2NonNumericIndex >= 0))
            {
                int nonNumericIndex;
                String nonNumericArg;

                if (args1NonNumericIndex >= 0)
                {
                    nonNumericIndex = args1NonNumericIndex;
                    nonNumericArg = args1[args1NonNumericIndex];
                }
                else
                {
                    nonNumericIndex = args2NonNumericIndex;
                    nonNumericArg = args2[args2NonNumericIndex];
                }

                result.error = "Invalid operand encountered for division '/' operation: " +
                        "The operand \"" + nonNumericArg + "\" corresponding to " +
                        RenameWand.SINGULAR_NOUN + " \"" + files[nonNumericIndex].source.getPath() +
                        "\" is non-numeric.";

                operators.push(op);
                operands.push(args1);
                operands.push(args2);
                return result;
            }

            String[] ans = new String[n];

            for (int i = 0; i < n; i++)
            {
                try
                {
                    ans[i] = (((int) Double.parseDouble(args1[i])) / ((int) Double.parseDouble(args2[i]))) + "";
                }
                catch (ArithmeticException e)
                {
                    result.error = "Division by zero.";
                    operators.push(op);
                    operands.push(args1);
                    operands.push(args2);
                    return result;
                }
            }

            /* push answer on operand stack */
            operands.push(ans);
            result.success = true;
        }
        else if ("-".equals(op))
        {
            /***********************
            * (4) SUBTRACTION '-' *
            ***********************/

            if (operands.size() < 2)
            {
                result.error = "Insufficient operands for subtraction '-' operation.";
                return result;
            }

            final String[] args2 = operands.pop();
            final String[] args1 = operands.pop();

            /* check that arguments are numeric */
            final int args1NonNumericIndex = getNonNumericIndex(args1);
            final int args2NonNumericIndex = getNonNumericIndex(args2);

            if ((args1NonNumericIndex >= 0) || (args2NonNumericIndex >= 0))
            {
                int nonNumericIndex;
                String nonNumericArg;

                if (args1NonNumericIndex >= 0)
                {
                    nonNumericIndex = args1NonNumericIndex;
                    nonNumericArg = args1[args1NonNumericIndex];
                }
                else
                {
                    nonNumericIndex = args2NonNumericIndex;
                    nonNumericArg = args2[args2NonNumericIndex];
                }

                result.error = "Invalid operand encountered for subtraction '-' operation: " +
                        "The operand \"" + nonNumericArg + "\" corresponding to " +
                        RenameWand.SINGULAR_NOUN + " \"" + files[nonNumericIndex].source.getPath() +
                        "\" is non-numeric.";

                operators.push(op);
                operands.push(args1);
                operands.push(args2);
                return result;
            }

            String[] ans = new String[n];

            for (int i = 0; i < n; i++)
                ans[i] = (((int) Double.parseDouble(args1[i])) - ((int) Double.parseDouble(args2[i]))) + "";

            /* push answer on operand stack */
            operands.push(ans);
            result.success = true;
        }
        else if ("+".equals(op))
        {
            /********************
            * (5) ADDITION '+' *
            ********************/

            if (operands.size() < 2)
            {
                result.error = "Insufficient operands for addition '+' operation.";
                return result;
            }

            final String[] args2 = operands.pop();
            final String[] args1 = operands.pop();
            String[] ans = new String[n];

            /* check if arguments are numeric */
            final int args1NonNumericIndex = getNonNumericIndex(args1);
            final int args2NonNumericIndex = getNonNumericIndex(args2);

            if ((args1NonNumericIndex < 0) && (args2NonNumericIndex < 0))
            {
                /* add the two arguments */
                for (int i = 0; i < n; i++)
                    ans[i] = (((int) Double.parseDouble(args1[i])) + ((int) Double.parseDouble(args2[i]))) + "";
            }
            else
            {
                /* concatenate the two arguments */
                for (int i = 0; i < n; i++)
                    ans[i] = args1[i] + args2[i];
            }

            /* push answer on operand stack */
            operands.push(ans);
            result.success = true;
        }
        else if (":".equals(op))
        {
            /********************************************
            * (6) RANGE OPERATOR FOR SUBSTRING "[ : ]" *
            ********************************************/

            if (operands.size() < 2)
            {
                result.error = "Insufficient operands for substring range operator ':'.";
                return result;
            }

            final String[] args2 = operands.pop();
            final String[] args1 = operands.pop();
            String[] ans = new String[n];

            for (int i = 0; i < n; i++)
                ans[i] = args1[i] + ":" + args2[i];

            /* push answer on operand stack */
            operands.push(ans);
            result.success = true;
        }
        else if (",".equals(op))
        {
            /************************************************
            * (7) DELIMITER OPERATOR FOR SUBSTRING "[ , ]" *
            ************************************************/

            if (operands.size() < 2)
            {
                result.error = "Insufficient operands for substring delimiter operator ','.";
                return result;
            }

            final String[] args2 = operands.pop();
            final String[] args1 = operands.pop();
            String[] ans = new String[n];

            for (int i = 0; i < n; i++)
                ans[i] = args1[i] + "," + args2[i];

            /* push answer on operand stack */
            operands.push(ans);
            result.success = true;
        }
        else if ("~".equals(op))
        {
            /***************************************
            * (8) UNARY MINUS (NEGATIVE) SIGN '~' *
            ***************************************/

            if (operands.size() < 1)
            {
                result.error = "Insufficient operands for negative sign '-' operator.";
                return result;
            }

            final String[] args1 = operands.pop();

            /* check that argument is numeric */
            final int nonNumericIndex = getNonNumericIndex(args1);

            if (nonNumericIndex >= 0)
            {
                final String nonNumericArg = args1[nonNumericIndex];

                result.error = "Invalid operand encountered for negative sign '-' operator: " +
                        "The operand \"" + nonNumericArg + "\" corresponding to " +
                        RenameWand.SINGULAR_NOUN + " \"" + files[nonNumericIndex].source.getPath() +
                        "\" is non-numeric.";

                operators.push(op);
                operands.push(args1);
                return result;
            }

            String[] ans = new String[n];

            for (int i = 0; i < n; i++)
                ans[i] = (-((int) Double.parseDouble(args1[i]))) + "";

            /* push answer on operand stack */
            operands.push(ans);
            result.success = true;

        }
        else if ("#".equals(op) ||
                "#!".equals(op) ||
                "##".equals(op) ||
                "##!".equals(op) ||
                "@".equals(op) ||
                "@!".equals(op) ||
                "@@".equals(op) ||
                "@@!".equals(op))
        {
            /*****************************
            * (9) ENUMERATION OPERATORS *
            *****************************/

            if (operands.size() < 1)
            {
                result.error = "Insufficient operands for enumeration operator " + op + ".";
                return result;
            }

            /* sort files, and enumerate them accordingly */

            final String[] args1 = operands.pop();

            /* global and local order */
            int[] globalOrder = new int[n];
            int[] localOrder = new int[n];

            /* directory counts */
            int[] dirCount = new int[RenameWand.numDirs + 1];
            Arrays.fill(dirCount, 0);

            if (getNonNumericIndex(args1) < 0)
            {
                /* treat arguments as doubles */
                DoubleEnumerationUnit[] eu = new DoubleEnumerationUnit[n];

                for (int i = 0; i < n; i++)
                    eu[i] = new DoubleEnumerationUnit(i, Double.parseDouble(args1[i]));

                Arrays.sort(eu);

                for (int i = 0; i < n; i++)
                {
                    final int index = eu[i].index;
                    final int parentDirId = files[index].parentDirId;
                    dirCount[parentDirId]++;
                    globalOrder[index] = i + 1;
                    localOrder[index] = dirCount[parentDirId];
                }
            }
            else
            {
                /* treat arguments as strings */
                StringEnumerationUnit[] eu = new StringEnumerationUnit[n];

                for (int i = 0; i < n; i++)
                    eu[i] = new StringEnumerationUnit(i, args1[i]);

                Arrays.sort(eu);

                for (int i = 0; i < n; i++)
                {
                    final int index = eu[i].index;
                    final int parentDirId = files[index].parentDirId;
                    dirCount[parentDirId]++;
                    globalOrder[index] = i + 1;
                    localOrder[index] = dirCount[parentDirId];
                }
            }

            String[] ans = new String[n];

            if ("#".equals(op))
            {
                /* directory ordering */
                for (int i = 0; i < n; i++)
                    ans[i] = localOrder[i] + "";
            }
            else if ("#!".equals(op))
            {
                /* reverse directory ordering */
                for (int i = 0; i < n; i++)
                    ans[i] = (files[i].localCount + 1 - localOrder[i]) + "";
            }
            else if ("##".equals(op))
            {
                /* global ordering */
                for (int i = 0; i < n; i++)
                    ans[i] = globalOrder[i] + "";
            }
            else if ("##!".equals(op))
            {
                /* reverse global ordering */
                for (int i = 0; i < n; i++)
                    ans[i] = (n + 1 - globalOrder[i]) + "";
            }
            else if ("@".equals(op))
            {
                /* first elements in directory ordering */
                int[] firsts = new int[RenameWand.numDirs + 1];

                for (int i = 0; i < n; i++)
                    if (localOrder[i] == 1) firsts[files[i].parentDirId] = i;

                for (int i = 0; i < n; i++)
                    ans[i] = args1[firsts[files[i].parentDirId]];
            }
            else if ("@!".equals(op))
            {
                /* last elements in directory ordering */
                int[] lasts = new int[RenameWand.numDirs + 1];

                for (int i = 0; i < n; i++)
                    if (localOrder[i] == files[i].localCount) lasts[files[i].parentDirId] = i;

                for (int i = 0; i < n; i++)
                    ans[i] = args1[lasts[files[i].parentDirId]];
            }
            else if ("@@".equals(op))
            {
                /* look for first element */
                int first = 0;

                for (int i = 0; i < n; i++)
                    if (globalOrder[i] == 1) first = i;

                Arrays.fill(ans, args1[first]);
            }
            else if ("@@!".equals(op))
            {
                /* look for last element */
                int last = 0;

                for (int i = 0; i < n; i++)
                    if (globalOrder[i] == n) last = i;

                Arrays.fill(ans, args1[last]);
            }

            /* push answer on operands stack */
            operands.push(ans);
            result.success = true;
        }
        else
        {
            /* error */
            result.error = "Unexpected operator '" + op + "' encountered.";
        }

        return result;
    }


    /**
    * Determine sequence of rename operations to be performed, in order to rename
    * the given matched files/directories.
    *
    * @param matchedFiles
    *     Matched files/directories to be renamed
    * @return
    *     Rename file/directory pairs indicating sequence of rename operations
    */
    private static List<RenameFilePair> getRenameOperations(
            final List<FileUnit> matchedFiles)
    {
        /* determine target files, check validity, and detect clashes */
        final Map<File,FileUnit> targetFilesMap = new TreeMap<File,FileUnit>();

        for (FileUnit u : matchedFiles)
        {
            final String targetFilename = u.targetFilename.toString();

            /* check for empty file/directory name */
            if (targetFilename.isEmpty())
                throw new TerminatingException("Invalid target " + RenameWand.SINGULAR_NOUN + " name encountered:\n" +
                        "\"" + u.source.getPath() + "\" ---> \"" + targetFilename + "\"");

            if (targetFilename.contains(File.separator))
            {
                /* contains a filename separator, so we rename          */
                /* file/directory relative to the present work directory */
                u.target = new File(targetFilename);
            }
            else
            {
                /* does not contain filename separator, so we        */
                /* rename file/directory in its original subdirectory */
                u.target = new File(u.source.getParentFile(), targetFilename);
            }

            try
            {
                /* get canonical pathname */
                u.target = new File(u.target.getCanonicalFile().getParentFile(),
                        u.target.getName());
            }
            catch (Exception e)
            {
                throw new TerminatingException("Invalid target " + RenameWand.SINGULAR_NOUN + " name encountered:\n" +
                        "\"" + u.source.getPath() + "\" ---> \"" + targetFilename + "\"\n" +
                        getExceptionInformation(e));
            }

            /* check for clash (i.e. nonunique target filenames) */
            final FileUnit w = targetFilesMap.get(u.target);

            if (w == null)
            {
                targetFilesMap.put(u.target, u);
            }
            else
            {
                throw new TerminatingException("Target " + RenameWand.SINGULAR_NOUN + " name clash:\n" +
                    "[A] \"" + w.source.getPath() + "\"\n  ---> \"" + w.target.getPath() + "\"\n" +
                    "[B] \"" + u.source.getPath() + "\"\n  ---> \"" + u.target.getPath() + "\"");
            }
        }

        /* create (source,target) rename pairs, and determine renaming sequence */
        final NavigableMap<File,LinkedList<RenameFilePair>> sequenceHeads = new TreeMap<File,LinkedList<RenameFilePair>>();
        final NavigableMap<File,LinkedList<RenameFilePair>> sequenceTails = new TreeMap<File,LinkedList<RenameFilePair>>();

        for (FileUnit u : matchedFiles)
        {
            /* check for unnecessary rename operations */
            if (u.source.getPath().equals(u.target.getPath()))
                continue;

            /* look for a sequence head with source = this target */
            final LinkedList<RenameFilePair> headSequence = sequenceHeads.get(u.target);

            /* look for a sequence tail with target = this source */
            final LinkedList<RenameFilePair> tailSequence = sequenceTails.get(u.source);

            if ((headSequence == null) && (tailSequence == null))
            {
                /* add this pair as a new sequence */
                final LinkedList<RenameFilePair> ns = new LinkedList<RenameFilePair>();
                ns.add(new RenameFilePair(u.source, u.target));
                sequenceHeads.put(u.source, ns);
                sequenceTails.put(u.target, ns);

            }
            else if ((headSequence != null) && (tailSequence == null))
            {
                /* add this pair to the head of an existing sequence */
                headSequence.addFirst(new RenameFilePair(u.source, u.target));
                sequenceHeads.remove(u.target);
                sequenceHeads.put(u.source, headSequence);
            }
            else if ((headSequence == null) && (tailSequence != null))
            {
                /* add this pair to the tail of an existing sequence */
                tailSequence.addLast(new RenameFilePair(u.source, u.target));
                sequenceTails.remove(u.source);
                sequenceTails.put(u.target, tailSequence);
            }
            else if ((headSequence != null) && (tailSequence != null))
            {
                if (headSequence == tailSequence)
                {
                    /* loop detected, so we use a temporary target file/directory name */

                    /* create a temporary file/directory name */
                    final File parentDir = u.target.getParentFile();
                    final String filename = u.target.getName();

                    File temp =  new File(parentDir, filename + ".rw");

                    if (temp.exists() || targetFilesMap.containsKey(temp))
                    {
                        /* temp filename is already used; find another temp filename */
                        for (long i = 0; i < Long.MAX_VALUE; i++)
                        {
                            temp = new File(parentDir, filename + ".rw." + i);

                            if (temp.exists() || targetFilesMap.containsKey(temp))
                            {
                                /* temp filename is already used; find another temp filename */
                                temp = null;
                            }
                            else
                            {
                                /* found an unused name */
                                targetFilesMap.put(temp, null);
                                break;
                            }
                        }
                    }

                    if (temp == null)
                        throw new TerminatingException("Ran out of suffixes for temporary name of " +
                                RenameWand.SINGULAR_NOUN + " \"" + u.target.getPath() + "\".");

                    /* add a leading and trailing rename file pair to the existing sequence */
                    headSequence.addFirst(new RenameFilePair(temp, u.target));
                    tailSequence.addLast(new RenameFilePair(u.source, temp));
                    sequenceHeads.remove(u.target);
                    sequenceHeads.put(temp, headSequence);
                    sequenceTails.remove(u.source);
                    sequenceTails.put(temp, tailSequence);
                }
                else
                {
                    /* link two distinct sequences together */
                    tailSequence.addLast(new RenameFilePair(u.source, u.target));
                    tailSequence.addAll(headSequence);
                    sequenceHeads.remove(u.target);
                    sequenceTails.remove(u.source);
                    sequenceTails.put(tailSequence.peekLast().target, tailSequence);
                }
            }
        }

        /* return value */
        final List<RenameFilePair> renameOperations = new ArrayList<RenameFilePair>();

        /* sequence deeper subdirectories for renaming first (approx), if renaming directories */
        final NavigableMap<File,LinkedList<RenameFilePair>> sequences =
                RenameWand.renameDirectories ? sequenceHeads.descendingMap() : sequenceHeads;

        for (LinkedList<RenameFilePair> s : sequences.values())
        {
            /* get reversed order of rename file pairs within the sequence */
            final Iterator<RenameFilePair> iter = s.descendingIterator();

            while (iter.hasNext())
            {
                final RenameFilePair r = iter.next();

                if (!r.source.getPath().equals(r.target.getPath()))
                    renameOperations.add(r);
            }
        }

        return renameOperations;
    }


    /**
    * Print the matched files/directories and their target names, and
    * prompt the user on whether to proceed with the renaming operations.
    *
    * @param matchedFiles
    *     Matched files to be renamed
    * @param numRenameOperations
    *     Number of rename operations to be performed
    * @return
    *     True if proceeding with rename; false otherwise
    */
    private static boolean promptUserOnRename(
            final List<FileUnit> matchedFiles,
            final int numRenameOperations)
    {
        /* categorize files/directories by their subdirectory */
        final List<List<FileUnit>> subdirs = new ArrayList<List<FileUnit>>();

        subdirs.add(0, null);

        for (int parentDirId = 1; parentDirId <= RenameWand.numDirs; parentDirId++)
            subdirs.add(parentDirId, new ArrayList<FileUnit>());

        for (FileUnit u : matchedFiles)
            subdirs.get(u.parentDirId).add(u);

        /* subdirectory counter */
        int subdirCount = 0;

        /* file/directory counter */
        int fileCount = 0;

        for (List<FileUnit> subdir : subdirs)
        {
            if ((subdir == null) || subdir.isEmpty())
                continue;

            subdirCount++;

            final String subdirPath = subdir.get(0).source.getParent();

            RenameWand.stdout.print("\n\nSUBDIRECTORY: \"" +
                    (subdirPath.endsWith(File.separator) ? subdirPath : (subdirPath + File.separator)) + "\"");

            for (FileUnit u : subdir)
            {
                RenameWand.stdout.print("\n["+ (++fileCount) +"] " +
                        "\"" + u.source.getName() + "\" ---> \"" + u.targetFilename + "\"");
            }
        }

        /* number of files/directories to rename */
        final int n = matchedFiles.size();

        RenameWand.stdout.print("\n\n" + numRenameOperations + " " +
                ((numRenameOperations == 1) ? "operation" : "operations") +
                " required to rename the above " +
                ((n == 1) ? RenameWand.SINGULAR_NOUN : (n + " " + RenameWand.PLURAL_NOUN)) +
                " in " + subdirCount + " " +
                ((subdirCount == 1) ? "subdirectory" : "subdirectories") + ".\n");
        RenameWand.stdout.flush();

        /* no rename operations to perform */
        if (numRenameOperations == 0)
            return false;

        /* prompt user on whether to continue with renaming operations */
        char choice = '\0';

        if (RenameWand.simulateOnly)
        {
            choice =  'Y';
        }
        else if (RenameWand.automaticRename)
        {
            RenameWand.stdout.print("Proceed to rename " +
                    ((n == 1) ? RenameWand.SINGULAR_NOUN : RenameWand.PLURAL_NOUN) +
                    "? (Y)es/(N)o: Y");
            RenameWand.stdout.flush();

            choice =  'Y';
        }
        else
        {
            choice = UserIO.userCharPrompt("Proceed to rename " +
                    ((n == 1) ? RenameWand.SINGULAR_NOUN : RenameWand.PLURAL_NOUN) +
                    "? (Y)es/(N)o: ",
                    "YN");
        }

        return (choice == 'Y');
    }


    /**
    * Perform rename operations on files/directories.
    *
    * @param renameOperations
    *     Sequence of rename operations to be performed
    * @return
    *     Number of successful rename operations performed
    */
    private static int performRenameOperations(
            final List<RenameFilePair> renameOperations)
    {
        if (RenameWand.simulateOnly)
        {
            RenameWand.stdout.print("\n\nSimulating renaming of " +
                    ((renameOperations.size() == 1) ? RenameWand.SINGULAR_NOUN : RenameWand.PLURAL_NOUN) +
                    "...");
            RenameWand.stdout.flush();
        }
        else
        {
            RenameWand.stdout.print("\n\nRenaming " +
                    ((renameOperations.size() == 1) ? RenameWand.SINGULAR_NOUN : RenameWand.PLURAL_NOUN) +
                    "...");
            RenameWand.stdout.flush();
        }

        for (int i = 0; i < renameOperations.size(); i++)
        {
            final RenameFilePair r = renameOperations.get(i);

            RenameWand.stdout.print("\n[R" + (i + 1) + "] "+
                    "\"" + r.source.getPath() + "\"\n  ---> \"" + r.target.getPath() + "\"");
            RenameWand.stdout.flush();

            /* if simulating, just continue to the next rename operation */
            if (RenameWand.simulateOnly)
                continue;

            /* check for existing distinct target file/directory */
            if (r.target.exists() && !r.target.equals(r.source))
            {
                r.success = false;

                RenameWand.stdout.print("\nRename operation failed: A " +
                        (r.target.isDirectory() ? "directory" : "file") +
                        " of the same target name already exists.\n");
                RenameWand.stdout.flush();
            }
            else
            {
                r.target.getParentFile().mkdirs();
                r.success = r.source.renameTo(r.target);

                if (!r.success)
                {
                    RenameWand.stdout.print("\nRename operation failed. ");
                    RenameWand.stdout.flush();
                }
            }

            /* check if renaming operation was successful */
            if (!r.success)
            {
                /* prompt user on action */
                char choice = '\0';

                if (RenameWand.defaultActionOnRenameOperationError == '\0')
                {
                    if (RenameWand.automaticRename)
                    {
                        RenameWand.stdout.print("(R)etry/(S)kip/(U)ndo all/(A)bort: U\n");
                        RenameWand.stdout.flush();

                        choice = 'U';
                    }
                    else
                    {
                        choice = UserIO.userCharPrompt("(R)etry/(S)kip/(U)ndo all/(A)bort: ", "RSUA");
                    }
                }
                else
                {
                    /* use default action */
                    choice = RenameWand.defaultActionOnRenameOperationError;
                }

                /* take action */
                if (choice == 'R')
                {
                    /* retry rename operation */
                    i--;
                }
                else if (choice == 'S')
                {
                    /* skip to next file/directory */
                    continue;
                }
                else if (choice == 'U')
                {
                    /* undo all previous rename operations */
                    RenameWand.stdout.print("\nUndoing previous " + RenameWand.SINGULAR_NOUN + " rename operations...");
                    RenameWand.stdout.flush();

                    for (int j = i - 1; j >= 0; j--)
                    {
                        final RenameFilePair t = renameOperations.get(j);

                        if (t.success)
                        {
                            RenameWand.stdout.print("\n[R" + (j + 1) + "] "+
                                    "\"" + t.source.getPath() + "\"\n  <--- \"" + t.target.getPath() + "\"");
                            RenameWand.stdout.flush();

                            t.source.getParentFile().mkdirs();
                            t.success = !t.target.renameTo(t.source);

                            if (t.success)
                                reportWarning("Rename operation failed.");
                        }
                    }

                    break;
                }
                else if (choice == 'A')
                {
                    /* abort */
                    break;
                }
            }
        }

        /* return value */
        int numRenameOperationsPerformed = 0;

        for (RenameFilePair r : renameOperations)
            if (r.success) numRenameOperationsPerformed++;

        return numRenameOperationsPerformed;
    }


    /**
    * Pad the given string so that it is at least the specified number of
    * character long. If the string is numeric, pad it with leading zeros;
    * otherwise, pad it with trailing spaces.
    *
    * @param in
    *     String to be padded
    * @param len
    *     Desired length of padded string
    * @param isNumeric
    *     Indicates if given string is to be treated as numeric
    * @return
    *     The padded string
    */
    private static String padString(
            final String in,
            final int len,
            final boolean isNumeric)
    {
        /* return value */
        final StringBuilder out = new StringBuilder();

        /* number of additional characters to insert */
        final int padLen = len - in.length();

        if (isNumeric)
        {
            /* pad with leading zeros */
            final Matcher numericMatcher = RenameWand.NUMERIC_PATTERN.matcher(in);

            if (numericMatcher.matches())
            {
                /* match groups */
                final String sign = numericMatcher.group(1);
                final String val = numericMatcher.group(2);

                if (sign != null)
                    out.append(sign);

                for (int i = 0; i < padLen; i++)
                    out.append('0');

                out.append(val);
                return out.toString();
            }
        }

        /* pad with trailing spaces */
        out.append(in);

        for (int i = 0; i < padLen; i++)
            out.append(' ');

        return out.toString();
    }


    /**
    * Return true if the given string is numeric; false otherwise.
    *
    * @param arg
    *     String to be tested
    * @return
    *     True if the given string is numeric; false otherwise
    */
    private static boolean isNumeric(
            final String arg)
    {
        /* check if argument is numeric */

        /* check if empty string */
        if (arg.isEmpty())
            return false;

        /* check if string matches numeric pattern */
        if (!RenameWand.NUMERIC_PATTERN.matcher(arg).matches())
            return false;

        /* argument is numeric */
        return true;
    }


    /**
    * Return -1 if all the strings in the given array are numeric;
    * otherwise, return the index of a non-numeric string.
    *
    * @param arg
    *     Strings to be tested
    * @return
    *     -1 if all the strings in the given array are numeric;
    *     otherwise, return the index of a non-numeric string
    */
    private static int getNonNumericIndex(
            final String[] args)
    {
        /* check if all arguments are numeric */
        for (int i = 0; i < args.length; i++)
        {
            /* check if empty string */
            if (args[i].isEmpty())
                return i;

            /* check if string matches numeric pattern */
            if (!RenameWand.NUMERIC_PATTERN.matcher(args[i]).matches())
                return i;
        }

        /* all strings are numeric */
        return -1;
    }


    /**
    * Print a warning message and pause.
    *
    * @param message
    *     Warning message to be printed on issuing the warning
    */
    private static void reportWarning(
            final Object message)
    {
        RenameWand.reportNumWarnings++;

        if (RenameWand.ignoreWarnings)
        {
            RenameWand.stderr.print("\n\nWARNING: " + message + "\n");
            RenameWand.stderr.flush();
        }
        else
        {
            RenameWand.stderr.print("\n\nWARNING: " + message + "\nPress ENTER to continue...");
            RenameWand.stderr.flush();

            (new Scanner(System.in)).nextLine(); // blocks until user responds
        }
    }


    /**
    * Get custom exception information string for the given exception.
    * String contains the exception class name, error description string,
    * and stack trace.
    *
    * @param e
    *     Exception for which to generate the custom exception information string
    * @return
    *     Custom exception information string
    */
    private static String getExceptionInformation(
            final Exception e)
    {
        final StringBuilder s = new StringBuilder();

        s.append("\nJava exception information (" + e.getClass() +
                "):\n\"" + e.getMessage() + "\"");

        for (StackTraceElement t : e.getStackTrace())
        {
            s.append("\n  at ");
            s.append(t.toString());
        }

        s.append('\n');
        return s.toString();
    }


    /**
    * Print out usage syntax, notes, and comments.
    */
    private static void printUsage()
    {
        /* RULER   00000000011111111112222222222333333333344444444445555555555666666666677777777778 */
        /* RULER   12345678901234567890123456789012345678901234567890123456789012345678901234567890 */
        RenameWand.stdout.print("\n" +
                "\nRenameWand is a simple command-line utility for renaming files or directories" +
                "\nusing an intuitive but powerful syntax." +
                "\n" +
                "\nUSAGE:" +
                "\n" +
                "\njava -jar RenameWand.jar  <switches>  [\"SourcePattern\"]  [\"TargetPattern\"]" +
                "\n" +
                "\nFor each file in the current directory with a name that matches" +
                "\n[\"SourcePattern\"], rename it to [\"TargetPattern\"]. Patterns should be in" +
                "\nquotes so that the shell passes them to RenameWand correctly." +
                "\nThe user is prompted before files are renamed, and whenever errors occur." +
                "\nFile rename operations are sequenced so that they are conflict-free," +
                "\nand temporary filenames are automatically used when necessary." +
                "\n" +
                "\n<Switches>:" +
                "\n" +
                "\n -r, --recurse         Recurse into subdirectories" +
                "\n -d, --dirs            Rename directories instead of files" +
                "\n -p, --path            Match [\"SourcePattern\"] against relative pathnames" +
                "\n                        (e.g. \"2007\\Jan\\Report.txt\" instead of \"Report.txt\")" +
                "\n -l, --lower           Match [\"SourcePattern\"] against lower case names" +
                "\n                        (e.g. \"HelloWorld2007.JPG\" ---> \"helloworld2007.jpg\")" +
                "\n -y, --yes             Automatically rename files without prompting" +
                "\n -s, --simulate        Simulate only; do not actually rename files" +
                "\n -i, --ignorewarnings  Ignore warnings; do not pause" +
                "\n     --skip            Skip files that cannot be renamed successfully" +
                "\n     --undoall         Undo all previous renames when a file cannot be renamed" +
                "\n                        successfully" +
                "\n     --abort           Abort subsequent renames when a file cannot be renamed" +
                "\n                        successfully" +
                "\n" +
                "\n[\"SourcePattern\"]:" +
                "\n" +
                "\n Files with names matching [\"SourcePattern\"] are renamed. This pattern string" +
                "\n may contain literal (i.e. ordinary) characters and the following constructs" +
                "\n (more details below):" +
                "\n" +
                "\n  1. Glob patterns and wildcards, e.g. *, ?, [ ], { }" +
                "\n  2. Register capture groups, e.g. <a>, <5|song>, <2|@track>" +
                "\n  3. Special construct <...> that may involve macros," +
                "\n      e.g. <FN.parent>, <CT.date>, <FS+FT.yyyy>" +
                "\n" +
                "\n To use a construct symbol (e.g. [, {, ?) as a literal character, insert a" +
                "\n backslash before it, e.g. use \\[ for the literal character [." +
                "\n Use \\\\ for the literal backslash character \\." +
                "\n" +
                "\n The file separator in Windows can be specified by \\\\ or /." +
                "\n" +
                "\n[\"TargetPattern\"]:" +
                "\n" +
                "\n The target names of the matched files are specified by [\"TargetPattern\"]." +
                "\n This pattern string may contain literal (i.e. ordinary) characters and the" +
                "\n special construct <...> that may involve registers and macros (more details" +
                "\n below)." +
                "\n" +
                "\n To use a construct symbol (e.g. <, >) as a literal character, insert a" +
                "\n backslash before it, e.g. use \\< for the literal character <." +
                "\n Use \\\\ for the literal backslash character \\." +
                "\n" +
                "\n The file separator in Windows can be specified by \\\\ or /." +
                "\n" +
                "\n If the evaluated pattern string contains a file separator (e.g. / or \\)," +
                "\n then the target name is resolved with respect to the current directory;" +
                "\n otherwise, the target name shares the same parent directory as the source" +
                "\n file (i.e. the source is renamed \"in place\")." +
                "\n" +
                "\nGLOB PATTERNS AND WILDCARDS" +
                "\n" +
                "\n The four common glob patterns are supported in [\"SourcePattern\"]:" +
                "\n" +
                "\n  *    Match a string of 0 or more characters" +
                "\n  ?    Match exactly 1 character" +
                "\n [ ]   Match exactly 1 character inside the brackets:" +
                "\n         [abc]       Match a, b, or c" +
                "\n         [!abc]      Match any character except a, b, or c (negation)" +
                "\n         [a-z0-9]    Match any character a through z, or 0 through 9," +
                "\n                      inclusive (range)" +
                "\n { }   Match exactly 1 comma-delimited string inside the braces:" +
                "\n         {a,bc,def}  Match either a, bc, or def" +
                "\n" +
                "\nREGISTER CAPTURE GROUPS" +
                "\n" +
                "\n Register capture groups are used in [\"SourcePattern\"] to capture a string of" +
                "\n zero or more characters. A register name can contain only letters, digits, or" +
                "\n underscores, but cannot begin with a digit." +
                "\n" +
                "\n  <myreg>     Capture a string of 0 or more characters" +
                "\n  <@myreg>    Capture a string of 0 or more digits" +
                "\n  <n|myreg>   Capture a string of exactly n characters" +
                "\n  <n|@myreg>  Capture a string of exactly n digits" +
                "\n" +
                "\n Backreferences can be applied by reusing the register name, e.g." +
                "\n \"<3|myreg><myreg>.txt\" matches a filename that is a repetition of a" +
                "\n three-character string, followed by the txt extension." +
                "\n" +
                "\nSPECIAL CONSTRUCTS" +
                "\n" +
                "\n Special constructs are supported in both [\"SourcePattern\"] and" +
                "\n [\"TargetPattern\"]. In general, they take the form <length|expr>, where the" +
                "\n expressions \"length\" and \"expr\" can involve registers, macros, and use" +
                "\n operations involving arithmetic, substrings, and enumerations (more details" +
                "\n below). Parentheses ( ) can be used to group values." +
                "\n" +
                "\n  <expr>         Insert the evaluated expression \"expr\" using as many" +
                "\n                  characters as necessary" +
                "\n  <length|expr>  Insert the evaluated expression \"expr\" with padding:" +
                "\n                  If the evaluated expression is numeric, pad the number with" +
                "\n                  leading zeros so that it occupies at least \"length\"" +
                "\n                  characters;" +
                "\n                  if the evaluated expression is non-numeric, pad the string" +
                "\n                  with trailing spaces so that it occupies at least \"length\"" +
                "\n                  characters." +
                "\n" +
                "\nARITHMETIC OPERATIONS" +
                "\n" +
                "\n The standard arithmetic operations + - * / ^ are supported for numeric values;" +
                "\n for non-numeric strings, + is interpreted as concatenation. The standard rules" +
                "\n of operator precedence are observed. Values are automatically cast as integers" +
                "\n before and after every operation." +
                "\n" +
                "\n For example, <a+b/(c-d)^(e*f)> evaluates an arithmetic expression involving" +
                "\n registers a, b, c, d, e, and f." +
                "\n" +
                "\nSUBSTRING OPERATIONS" +
                "\n" +
                "\n Substrings can be extracted from any value by inserting a comma-delimited list" +
                "\n of indices and index ranges in brackets [ ] after the value. Index 1 denotes" +
                "\n the 1st character, index 2 the 2nd character, and so on." +
                "\n" +
                "\n Negative indices denote character positions counting from the end of the" +
                "\n string, i.e. index -1 denotes the last character, index -2 the second-last" +
                "\n character, and so on." +
                "\n" +
                "\n Index ranges are denoted using three parameters, e.g. 1:2:11 is equivalent to" +
                "\n indices 1, 3, 5, ..., 11. The middle parameter is optional; it is assumed to" +
                "\n be 1 if missing, e.g. 5:8 is equivalent to indices 5, 6, 7, 8." +
                "\n" +
                "\n  myreg[1,5,3]   Extract the 1st, 5th, and 3rd characters, in that order" +
                "\n  myreg[2:6,1]   Extract the 2nd through 6th characters, followed by the 1st" +
                "\n                  character, in that order" +
                "\n  myreg[1:2:-1]  Extract the 1st, 3rd, 5th, ... characters" +
                "\n  myreg[-1:1]    Extract the last through first characters" +
                "\n                  (effectively reverses the string)" +
                "\n" +
                "\n Indices are automatically clipped if they are too big or small, e.g. if" +
                "\n myreg is a 3-character string, then myreg[10] is equivalent to myreg[3]." +
                "\n" +
                "\nENUMERATION OPERATIONS" +
                "\n" +
                "\n Matched files can be sorted by any value, and then numbered in sequence." +
                "\n Numerical sorting is applied if all the values are numeric; otherwise," +
                "\n lexicographic sorting is applied." +
                "\n" +
                "\n The 1st file in the sorted sequence is assigned the number 1, the 2nd is" +
                "\n assigned number 2, and so on; ties are broken arbitrarily." +
                "\n" +
                "\n  #myreg   Return the sequence number of the respective file when sorted in" +
                "\n            ascending order of the value in register myreg" +
                "\n  #!myreg  Return the sequence number of the respective file when sorted in" +
                "\n            descending order of the value in register myreg" +
                "\n  @myreg   Return the value in register myreg of the first file in the" +
                "\n            sequence sorted in ascending order" +
                "\n  @!myreg  Return the value in register myreg of the first file in the" +
                "\n            sequence sorted in descending order" +
                "\n" +
                "\n The above operations apply to matched files enumerated locally within their" +
                "\n respective subdirectories; to enumerate all matched files globally (i.e. when" +
                "\n using the --recurse switch), use ## instead of #, and @@ instead of @." +
                "\n" +
                "\n For example, to enumerate matched files locally and globally by their" +
                "\n last-modified time, we can use the target pattern string" +
                "\n \"Local <2|#FT> of <2|RW.N> -vs- Global <2|##FT> of <2|RW.NN>.txt\"," +
                "\n where macros FT, RW.N, and RW.NN represent the last-modified time of the file," +
                "\n the number of local matched files, and the number of global matched files," +
                "\n respectively (more details below)." +
                "\n" +
                "\n Arbitrary values such as arithmetic expressions and substrings can also be" +
                "\n used for enumeration, e.g. <#(a*(b+c))>, <#(FN.name[1:3])>." +
                "\n" +
                "\nMACROS" +
                "\n" +
                "\n Macros are defined for a variety of file and system attributes, such as" +
                "\n file name, file size, file last-modified time, current time," +
                "\n system environment variables, and system properties." +
                "\n" +
                "\n FILE NAME" +
                "\n" +
                "\n  Example: \"C:\\Work\\2007\\Jan\\Report.txt\", with \"C:\\Work\" as current directory" +
                "\n" +
                "\n  FN              Filename (\"Report.txt\")" +
                "\n  FN.ext          File extension (\"txt\")" +
                "\n  FN.name         Base filename without extension (\"Report\")" +
                "\n  FN.path         Relative pathname (\"2007\\Jan\\Report.txt\")" +
                "\n  FN.parent       Parent directory (\"Jan\")" +
                "\n  FN.parentpath   Relative pathname of parent directory (\"2007\\Jan\")" +
                "\n" +
                "\n FILE SIZE (all values are cast as integers)" +
                "\n" +
                "\n  FS              File size in bytes" +
                "\n  FS.kB           File size in kilobytes (2^10 bytes)" +
                "\n  FS.MB           File size in megabytes (2^20 bytes)" +
                "\n  FS.GB           File size in gigabytes (2^30 bytes)" +
                "\n  FS.TB           File size in terabytes (2^40 bytes)" +
                "\n" +
                "\n FILE LAST-MODIFIED TIME" +
                "\n" +
                "\n  FT              Number of milliseconds since the epoch" +
                "\n                   (January 1, 1970 00:00:00.000 GMT, Gregorian)" +
                "\n  FT.date         Date in the form yyyyMMdd" +
                "\n  FT.time         Time in the form HHmmss" +
                "\n  FT.ap           am/pm in lower case" +
                "\n  FT.AP           AM/PM in upper case" +
                "\n" +
                "\n  Date and time pattern letters from Java are also supported." +
                "\n  Repeat letters to change the representation, e.g. FT.MMMM, FT.MMM," +
                "\n  FT.MM could represent \"April\", \"Apr\", \"4\", respectively." +
                "\n" +
                "\n    G  Era designator            H  Hour in day (0-23)" +
                "\n    y  Year                      k  Hour in day (1-24)" +
                "\n    M  Month in year             K  Hour in AM/PM (0-11)" +
                "\n    w  Week in year              h  Hour in AM/PM (1-12)" +
                "\n    W  Week in month             m  Minute in hour" +
                "\n    D  Day in year               s  Second in minute" +
                "\n    d  Day in month              S  Millisecond" +
                "\n    F  Day of week in month      z  Time zone (general time zone)" +
                "\n    E  Day in week               Z  Time zone (RFC 822 time zone)" +
                "\n    a  AM/PM marker" +
                "\n" +
                "\n CURRENT TIME" +
                "\n" +
                "\n  Macros for the current time are obtained by using CT instead of FT in the" +
                "\n  above macros for file last-modified time." +
                "\n" +
                "\n SYSTEM ENVIRONMENT VARIABLES" +
                "\n" +
                "\n  ENV.var   System environment variable named \"var\"" +
                "\n" +
                "\n SYSTEM PROPERTIES (see Java API for the full list)" +
                "\n" +
                "\n  SYS.os.name         Operating system name" +
                "\n  SYS.os.arch         Operating system architecture" +
                "\n  SYS.os.version      Operating system version" +
                "\n  SYS.file.separator  File separator (e.g. \"/\" or \"\\\")" +
                "\n  SYS.path.separator  Path separator (e.g. \":\" or \";\")" +
                "\n  SYS.line.separator  Line separator" +
                "\n  SYS.user.name       User's account name" +
                "\n  SYS.user.home       User's home directory" +
                "\n  SYS.user.dir        User's current working directory" +
                "\n" +
                "\n MISCELLANEOUS" +
                "\n" +
                "\n  RW.cd      Full pathname of the current directory (e.g. \"C:\\Work\")" +
                "\n  RW.N       Number of local matched files (i.e. in the file's subdirectory)" +
                "\n  RW.NN      Number of global matched files (i.e. in all subdirectories)" +
                "\n  RW.random  Generate a string of 10 random digits" +
                "\n" +
                "\nMACRO & REGISTER MODIFIERS" +
                "\n" +
                "\n To use the following modifiers, append a period followed by the modifier name" +
                "\n to a macro or register name. Modifiers can also be chained, e.g." +
                "\n myreg.title.trim, FN.name.pascal.trim." +
                "\n" +
                "\n  Example: \"hello WORLD\"" +
                "\n" +
                "\n  len             Length of the string (11)" +
                "\n  upper           Convert to upper case (\"HELLO WORLD\")" +
                "\n  lower           Convert to lower case (\"hello world\")" +
                "\n  capitalize      Capitalize only the first character (\"Hello world\")" +
                "\n  title           Convert to title case (\"Hello World\")" +
                "\n  camel           Convert to camelCase (\"helloWorld\")" +
                "\n  pascal          Convert to PascalCase (\"HelloWorld\")" +
                "\n  swapcase        Swap the case (\"HELLO world\")" +
                "\n  abbrev          Abbreviate to initials (\"h W\")" +
                "\n  reverse         Reverse the string (\"DLROW olleh\")" +
                "\n  trim            Trim away whitespace on the left and right" +
                "\n  ltrim           Trim away whitespace on the left" +
                "\n  rtrim           Trim away whitespace on the right" +
                "\n  delspace        Delete whitespace in the string (\"helloWORLD\")" +
                "\n  delextraspace   Delete extra whitespace by replacing contiguous whitespace" +
                "\n                   with a single space (\"How   are  YOU\" ---> \"How are YOU\")" +
                "\n  delpunctuation  Delete punctuation marks in the string" +
                "\n  spaceout        Space out words by inserting a space between connected words" +
                "\n                   (\"HowAreYou\" ---> \"How Are You\")" +
                "\n" +
                "\n Shortcut macros are defined for single-letter register names for convenience." +
                "\n Suppose we have the register \"a\"; then the following macros are automatically" +
                "\n defined if there are no name clashes:" +
                "\n" +
                "\n  AA  Convert to upper case (.upper)" +
                "\n  aa  Convert to lower case (.lower)" +
                "\n  Aa  Convert to title case (.title)" +
                "\n  aA  Swap the case (.swapcase)" +
                "\n" +
                "\nEXAMPLES:" +
                "\n" +
                "\n1. Convert the filename, less extension, to title case," +
                "\n    e.g. \"foo bar.txt\" ---> \"Foo Bar.txt\":" +
                "\n   java -jar RenameWand.jar \"<a>.<b>\" \"<Aa>.<b>\"" +
                "\n" +
                "\n2. Convert disc and track numbers to a single number, and swap artist name" +
                "\n    with song name," +
                "\n    e.g. \"Disc 2 Track 5_SONG_ARTIST.mp3\" --->" +
                "\n    \"015-Artist-Song.mp3\":" +
                "\n   java -jar RenameWand.jar \"Disc <@disc> Track <@track>_<song>_<artist>.mp3\"" +
                "\n    \"<3|10*(disc-1)+track>-<artist.title>-<song.title>.mp3\"" +
                "\n" +
                "\n3. Insert the file date, and enumerate files by their last-modified time," +
                "\n    e.g. \"SCAN004001.jpg\" ---> \"doc20050512 (Page 01 of 42).jpg\":" +
                "\n   java -jar RenameWand.jar \"SCAN*.jpg\"" +
                "\n    \"doc<FT.date> (Page <2|#FT> of <2|RW.N>).jpg\"" +
                "\n" +
                "\n4. Rename files into directories based on their names," +
                "\n    e.g. \"Daily Report May-28-2007.doc\" ---> \"2007/May/Daily Report 28.doc\":" +
                "\n   java -jar RenameWand.jar \"Daily Report <month>-<day>-<year>.doc\"" +
                "\n    \"<year>/<month>/Daily Report <day>.doc\"" +
                "\n\n");
    }
}