package com.jdbernard.twitter

import com.martiansoftware.nailgun.NGContext
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import twitter4j.Paging
import twitter4j.Status
import twitter4j.Twitter
import twitter4j.TwitterFactory
import twitter4j.conf.Configuration
import twitter4j.conf.PropertyConfiguration

public class TwitterCLI {

    private static String EOL = System.getProperty("line.separator")

    private static TwitterCLI nailgunInst
    private Twitter twitter

    private Scanner stdin

    private Map colors = [:]

    private int terminalWidth
    private boolean colored
    private boolean printStatusTimestamps
    private boolean strict
    private boolean warnings

    private static final VERSION = 1.0

    private Logger log = LoggerFactory.getLogger(getClass())


    /* ============================================= */
    /* ======== MAIN METHODS - Entry points ======== */
    /* ============================================= */

    public static void main(String[] args) {
        TwitterCLI inst = new TwitterCLI(new File(System.getProperty("user.home"),
            ".gritterrc"))

        // trim the last argumnet, not all cli's are well-behaved
        args[-1] = args[-1].trim()
        inst.run((args as List) as LinkedList)
    }

    public static void nailMain(NGContext context) {
        if (nailgunInst == null)
            nailgunInst = new TwitterCLI(new File(
                System.getProperty("user.home"), ".gritterrc"))
        else 
            nailgunInst.stdin = new Scanner(context.in)

        // trim the last argumnet, not all cli's are well-behaved
        context.args[-1] = context.args[-1].trim()

        nailgunInst.run((context.args as List) as LinkedList)
    }

    /* ===================================== */
    /* ======== CONFIGURATION/SETUP ======== */
    /* ===================================== */

    public static void reconfigure(LinkedList args) {
        if (nailgunInst == null) main(args as String[])
        else {
            nailgunInst = null
            nailgunInst = new TwitterCLI(new File(
                System.getProperty("user.home"), ".gritterrc"))

            nailgunInst.run(args)
        }
    }

    public TwitterCLI(File propFile) {

        // load the configuration
        Properties cfg = new Properties()
        propFile.withInputStream { is -> cfg.load(is) }

        // create a twitter instance
        twitter = (new TwitterFactory(new PropertyConfiguration(cfg))).getInstance()

        // configure the colors
        colors.author = new ConsoleColor(cfg.getProperty("colors.author", "CYAN:false"))
        colors.mentioned = new ConsoleColor(cfg.getProperty("colors.mentioned", "GREEN:false"))
        colors.error = new ConsoleColor(cfg.getProperty("colors.error", "RED:true"))
        colors.option = new ConsoleColor(cfg.getProperty("colors.option", "YELLOW:true"))
        colors.even = new ConsoleColor(cfg.getProperty("colors.even", "WHITE"))
        colors.odd = new ConsoleColor(cfg.getProperty("colors.odd", "YELLOW"))

        // configure the terminal width
        terminalWidth = (System.getenv().COLUMNS ?: cfg.terminalWidth ?: 79) as int

        colored = (cfg.colored ?: 'true') as boolean
        strict = (cfg.strict ?: 'false') as boolean
        warnings = (cfg.warnings ?: 'true') as boolean
        printStatusTimestamps = (cfg."timeline.printTimestamps" ?: 'true') as boolean

        stdin = new Scanner(System.in)
    }

    /* =================================== */
    /* ======== PARSING FUNCTIONS ======== */
    /* =================================== */

    /**
     | Main entry to the command line parsing system. In general, the parsing
     | system (this method and the others dedicated to parsing arguments)
     | follow some common conventions:
     | 
     | 1.  An argument is either considered a *command* or a *parameter*.
     | 2.  Arguments are parsed in order.
     | 3.  A command can have aliases to make invokation read naturally.
     | 4.  A command may expect further refining commands, one of which
     |     will be the default. So if a command has possible refining
     |     commands, but the next argument does not match any of them,
     |     the default will be assumed and the current argument will not
     |     be comsumed, but passed on to the refining command.
     |
     | For example: ::
     |
     |     gritter set colored off show mine
     |
     | is parsed: 
     |
     | *  the first argument is expected to be a command
     | *  the ``set`` command consumes two arguments, expecting both to
     |    be parameters
     | *  the set command is finished, but there are still arguments, so the
     |    parsing process starts again, expecting the next argument to be a
     |    command.
     | *  the ``show`` command consumes one argument and expects a command
     | *  ``mine`` does not match any of the possible refinements for ``show`` so
     |    the default command, `timeline`, is assumed.
     | *  the `show timeline` command consumes one argument, expecting a command
     | *  The `show timeline mine` command executes
     | *  No more arguments remain, so execution terminates.
     |
     | Recognized top-level commands are:
     |
     | +-----------------+-------------------+----------------------------+
     | | *Command*       | *Aliases*         | *Description*              |
     | +-----------------+-------------------+----------------------------+
     | | ``delete``      | ``destroy``,      | Delete a post, status, list|
     | |                 | ``remove``        | membership, etc.           |
     | +-----------------+-------------------+----------------------------+
     | | ``get``         | ``show``          | Display a list, timeline,  |
     | |                 |                   | list membership, etc.      |
     | +-----------------+-------------------+----------------------------+
     | | ``help``        |                   | Display help for commands  |
     | +-----------------+-------------------+----------------------------+
     | | ``post``        | `add`, ``create`` | Post a new status, add a   |
     | |                 |                   | new list subscription, etc.|
     | +-----------------+-------------------+----------------------------+
     | | ``reconfigure`` |                   | Cause the tool to reload   |
     | |                 |                   | its configuration file.    |
     | +-----------------+-------------------+----------------------------+
     | | ``set``         |                   | Set a configurable value   |
     | |                 |                   | at runtime.                |
     | +-----------------+-------------------+----------------------------+
     |
     | @param args A {@link java.util.LinkedList} of arguments to parse.
     */
    public void run(LinkedList args) {
        if (args.size() < 1) printUsage()

        log.debug("argument list: {}", args)

        while (args.peek()) {
            def command = args.poll()

            switch (command.toLowerCase()) {
                case ~/delete|destroy|remove/: delete(args); break
                case ~/get|show/: get(args); break            // get|show
                case ~/help/: help(args); break               // help
                case ~/post|add|create/: post(args); break    // post|add|create
                case ~/reconfigure/: reconfigure(args); break // reconfigure
                case ~/set/: set(args); break                 // set
                default:                                      // fallthrough
                    if (strict) {
                        log.error(color("Unrecognized command: '$command'", colors.error))
                    } else {
                        if (warnings) {
                            println "Command '$command' unrecognized: " +
                                "assuming this is a parameter to 'show'"
                        }

                        args.addFirst(command)
                        get(args)
                    }
            }
        }
    }

    /* -------- DELETE Subparsing -------- */

    /**
     | Parse a ``delete`` command. Valid options are:
     |
     | +-------------+--------------------------------------------------+
     | | *Argument*  | *Description*                                    |
     | +-------------+--------------------------------------------------+
     | | ``status``  | Destroy a status given a status id. *This is the |
     | |             | default command.*                                |
     | +-------------+--------------------------------------------------+
     | | ``list``    | Delete a list, remove list members, etc.         |
     | +-------------+--------------------------------------------------+
     |
     | @param args A {@link java.util.LinkedList} of arguments.
     */
    public void delete(LinkedList args) {
        def option = args.poll()

        log.debug("Processing a 'delete' command, option = {}.", option)

        switch(option) {
            case "status": deleteStatus(args); break
            case "list": deleteList(args); break
            default: args.addFirst(option)
                deleteStatus(args)
        }
    }

    /**
     | Parse a ``delete list`` command. Valid options are:
     |
     | +------------------+---------------------------------------------+
     | | *Argument*       | *Description*                               |
     | +------------------+---------------------------------------------+
     | | ``member``       | Remove a member from a list.                |
     | +------------------+---------------------------------------------+
     | | ``subscription`` | Unsubcribe from a given list.               |
     | +------------------+---------------------------------------------+
     | | *list-reference* | Delete the list specified by the reference. |
     | +------------------+---------------------------------------------+
     |
     | @param args A {@link java.util.LinkedList} of arguments.
     */
    public void deleteList(LinkedList args) {
        def option = args.poll()

        log.debug("Processing a 'delete list' command, option = {}.", option)

        switch(option) {
            case "member": deleteListMember(args); break
            case "subscription": deleteListSubscription(args); break
            default: args.addFirst(option)
                doDeleteList(args)
        }
    }

    /* -------- GET/SHOW Subparsing -------- */


    /**
     | Parse a ``get`` command. Valid options are:
     |
     | +-------------------+-------------------------------------------------+
     | | *Argument*        | *Description*                                   |
     | +-------------------+-------------------------------------------------+
     | | ``list``          | Show a list timeline, members, subs, etc.       |
     | +-------------------+-------------------------------------------------+
     | | ``lists``         | Show lists all lists owned by a given user      |
     | +-------------------+-------------------------------------------------+
     | | ``subscriptions`` | Show all of the lists a given user is subcribed |
     | |                   | to.                                             |
     | +-------------------+-------------------------------------------------+
     | | ``timeline``      | Show a timeline of tweets. *This is the default |
     | |                   | command.*                                       |
     | +-------------------+-------------------------------------------------+
     | | ``user``          | Show information about a given users.           |
     | +-------------------+-------------------------------------------------+
     |
     | @param args A {@link java.util.LinkedList} of arguments.
     */
     public void get(LinkedList args) {
        def option = args.poll()

        log.debug("Processing a 'get' command, option = {}.", option)

        switch(option) {
            case "list":  showList(args); break
            case "lists": showLists(args); break
            case ~/subs.*/:   showSubscriptions(args); break
            case "timeline":   showTimeline(args); break
            case "user":   showUser(args); break
            default: args.addFirst(option)
                showTimeline(args)
        }
    }

    /**
     | Parse a ``show list`` command. Valid options are:
     |
     | +--------------------+-----------------------------------------------+
     | | *Argument*         | *Description*                                 |
     | +--------------------+-----------------------------------------------+
     | | ``members``        | Show the members of a given list. This is the |
     | |                    | list of users who's tweets comprise the list. |
     | +--------------------+-----------------------------------------------+
     | | ``subscribers``    | Show the subscribers of a given list. This is |
     | |                    | the list of users who are see the list.       |
     | +--------------------+-----------------------------------------------+
     | | ``subscriptions``  | Show all of the lists a given user is         |
     | |                    | subscribed to.                                |
     | +--------------------+-----------------------------------------------+
     | | *list-reference*   | Show the timeline for a given list.           |
     | +--------------------+-----------------------------------------------+
     |
     | @param args a {@link java.util.LinkedList} of arguments.
     */
    public void showList(LinkedList args) {
        def option = args.poll()

        log.debug("Processing a 'show list' command, option = '{}'", option)

        switch(option) {
            case "members": showListMembers(args); break
            case "subscribers": showListSubscribers(args); break
            case "subscriptions": showListSubscriptions(args); break
            default: args.addFirst(option)
                showListTimeline(args)
        }
    }

    /**
     | Parse a ``show lists`` command. ``show lists`` consumes at most one
     | argument, representing a user reference. It shows all of the lists
     | owned by the given user. If no user reference is given ``show lists``
     | shows the lists owned by the currently logged in user. 
     |
     | @param args a {@link java.util.LinkedList} of arguments.
     */
    public void showLists(LinkedList args) {
        def user = args.poll()

        log.debug("Processing a 'show lists' command, user = '{}'", user)

        if (!user) user = twitter.screenName

        printLists(twitter.getUserLists(user, -1)) // TODO paging
    }

    /**
     | Parse a ``show list members`` command. ``show list members`` consumes
     | one argument, a reference to the list in question.
     |
     | @param args a {@util java.util.LinkedList} of arguments.
     */
    public void showListMembers(LinkedList args) {
        def listRef = parseListReference(args)

        log.debug("Processing a 'show list members' command, list = '{}'",
            listRef)

        if (!listRef) {
            println color("show list members", colors.option) +
                color(" command requires a list reference in the form of " +
                "user/list: ", colors.error) +
                "gritter show list members <user/list>"
            return
        }

        def userList = twitter.getUserListMembers(listRef.username,
            listRef.listId, -1)

        printUserList(userList) // TODO paging
    }

    /**
     | Parse a ``show list subscribers`` command. ``show list subscribers
     | consumes one argument, a reference to the list in question.
     |
     | @param args a {@util java.util.LinkedList} of arguments.
     */
    public void showListSubscribers(LinkedList args) {
        def listRef = parseListReference(args)

        log.debug("Processing a 'show list subscribers' command, list = '{}'",
            listRef)

        if (!listRef) {
            println color("show list subscribers", colors.option) +
                color(" command requires a list reference in the form of " +
                "user/list: ", colors.error) +
                "gritter show list subscribers <user/list>"
            return
        }

        printUserList(twitter.getUserListSubscribers(
            listRef.username, listRef.listId, -1))  // TODO: paging
    }

    /**
     | Parse a ``show list members`` command. ``show list members consumes one
     | argument, a reference to the list in question.
     |
     | @param args a {@util java.util.LinkedList} of arguments.
     */
    public void showListSubscriptions(LinkedList args) {
        def user = args.poll()

        log.debug("Processing a 'show list subscriptions' command, list = '{}'",
            listRef)

        if (!user) user = twitter.screenName

        printLists(twitter.getUserListSubscriptions(user, -1)) // TODO: paging
    }

    public void showListTimeline(LinkedList args) {
        if (args.size() < 1) {
            println color("show list", colors.option) +
                color(" command requires a list reference in the form of " +
                "user/list: ", colors.error) +
                "gritter show list <user/list>"
            return
        }

        def listRef = parseListReference(args)

        log.debug("Showing a list timeline, list = '{}'", listRef)

        if (listRef.listId == -1) {
            println color("show list", colors.option) +
                color(" command requires a list reference in the form of " +
                "user/list: ", colors.error) +
                "gritter show list <user/list>"
            return
        }

        printTimeline(twitter.getUserListStatuses(
            listRef.username, listRef.listId, new Paging()))  // TODO: paging
    }

    public void showTimeline(LinkedList args) {

        String timeline = args.poll() ?: "home"
        
        log.debug("Processing a 'show timeline' command, timeline = '{}'",
            timeline)

        switch (timeline) {
            // friends
            case "friends": printTimeline(twitter.friendsTimeline); break
            // home
            case "home": printTimeline(twitter.homeTimeline); break
            // mine
            case "mine": printTimeline(twitter.userTimeline); break
            // public
            case "public": printTimeline(twitter.publicTimeline); break
            // user
            case "user":
                String user = args.poll()
                if (user) {
                    if (user.isNumber())
                        printTimeline(twitter.getUserTimeline(user as int))
                    else printTimeline(twitter.getUserTimeline(user))
                } else println color("No user specified.", colors.error)
                break;
            default:
                println color("Unknown timeline: ", colors.error) +
                    color(timeline, colors.option)
                break;
        }
    }

    public void showUser(LinkedList args) {
        def user = args.poll()

        log.debug("Processing a 'show user' command, user = '{}'", user)

        println color("show user", colors.option) +
            color(" is not yet implemented.", colors.error)
    }

    public void createList(LinkedList args) {
        def option = args.poll()

        switch(option) {
            case "member": addListMember(args); break
            case "subscription": addListSubscription(args); break
            default: args.addFirst(option)
                createNewList(args); break
        }
    }

    public void post(LinkedList args) {
        def option = args.poll()

        log.debug("Processing a 'post' command: option = '{}'", option)

        if (!option) {
            println color("post", colors.option) +
                color(" command requires at least two parameters: ",
                colors.error) + "gritter post <status|retweet|list> " +
                "<options>..."
            return
        }

        switch (option) {
            case "status": postStatus(args.poll()); break
            case "retweet": retweetStatus(args.poll()); break
            case "list": createList(args); break
            default: postStatus(option)
        }
    }

    public void set(LinkedList args) {
        def option = args.poll()
        def value = args.poll()

        log.debug("Processing a 'set' command: option = '{}', value = '{}'",
            option, value)

        if (!value) {   // note: if option is null, value is null
            println color("set", colors.option) +
                color(" command requires two options: ", colors.error) +
                "gritter set <param> <value>"
            return
        }

        switch (option) {
            case "terminalWidth": terminalWidth = value as int; break
            case "colored": colored = value.toLowerCase() ==~ /true|t|on|yes|y/
                break

            default:
                println color("No property named ", colors.error) +
                    color(option, colors.option) +
                    color(" exists.", colors.error)
        }
    }

    /* ======== WORKER FUNCTIONS ========*/

    public void deleteListMember(LinkedList args) {
        def listRef = parseListReference(args)
        def user = args.poll()

        log.debug("Deleting a member from a list: list='{}', user='{}'",
            listRef, user)

        if (!user) {
            println color("delete list member", colors.option) +
                color(" requires two parameters: ", colors.error) +
                "gritter delete list member <list-ref> <user>"
            return
        }

        // look up the user id if neccessary
        if (user.isLong()) user = user as long
        else user = twitter.showUser(user).id

        twitter.deleteUserListMember(listRef.listId, user)

        // TODO: error checking? print list name?
        println "Deleted " + color("@$user", colors.mentioned) +
            " from your list."
    }

    public void deleteListSubscribtion(LinkedList args) {
        def listRef = parseListReference(args)

        log.debug("Unsubscribing from a list: listRef='{}', user='{}'",
            listRef, user)

        if (!listRef) {
            println color("delete list subscription", colors.option) +
                color(" requires a list reference: ", colors.error) +
                "gritter delete list subscription <list-ref>"
            return
        }

        twitter.unsubscribeUserList(listRef.username, listRef.listId)

        // TODO: error checking?
        println "Unsubscribed from list: " + color(listRef.toString(),
            colors.option)
    }

    public void doDeleteList(LinkedList args) {
        def listRef = parseListReference(args)

        log.debug("Destroying a list: listRef='{}'", listRef)

        if (!listRef) {
            println color("destroy list", colors.option) +
                color(" requries a list reference: ", colors.error) +
                "gritter destroy list <list-ref>"
            return
        }

        println "Really destroy list '" + color(listRef.toString(),
           colors.option) + "' ?"
        if (stdin.nextLine() ==~ /yes|y|true|t/) {
            try {
                twitter.destroyUserList(listRef.listId)
                println "List destroyed."
            } catch (Exception e) {
                println "An error occurred trying to delete the list: '" +
                    e.localizedMessage
                log.error("Error destroying list:", e)
            }
        } else println "Destroy list canceled."
 
    }

    public void deleteStatus(LinkedList args) {
        def statusId = args.poll()

        log.debug("Destroying a status: id='{}'", statusId)

        if (!statusId || !statusId.isLong()) {
            println color("destroy status", colors.option) +
                color(" requires a status id: ", colors.error) +
                "gritter delete status <status-id>"
            return
        }

        statusId = statusId as long

        println "Really destroy status '" + color(statusId.toString(),
           colors.option) + "' ?"
        if (stdin.nextLine() ==~ /yes|y|true|t/) {
            try {
                twitter.destroyStatus(statusId)
                println "Status destroyed."
            } catch (Exception e) {
                println "An error occurred trying to destroy the status: '" +
                    e.localizedMessage
                log.error("Error destroying status:", e)
            }
        } else println "Destroy status canceled."
    }

    public void printLists(def lists) {
        int colSize = 0

        // fins largest indentation needed
        lists.each { list ->
            def curColSize = list.user.screenName.length() +
                list.slug.length() + list.id.toString().length() + 3
                
            colSize = Math.max(colSize, curColSize)
            //println colSize //TODO, fix column alignment
        }

        lists.each { list ->
            //println colSize
            def col1 = color("@${list.user.screenName}", colors.author) + "/" +
                color("${list.slug} (${list.id})", colors.option)

            println col1.padLeft(colSize) + ": ${list.memberCount} members " +
                "and ${list.subscriberCount} subscribers"

            println wrapToWidth(list.description, terminalWidth,
                "".padLeft(8), "")
            //println col1.length()
        }
    }

    public void printUserList(def users) {
        int colSize = 0
        colSize = users.inject(0) {
            curMax, user -> Math.max(curMax, user.id.toString().length()) }

        users.each { user ->
            println "${user.id.toString().padLeft(colSize)} - " +
                color(user.screenName, colors.author) + ": ${user.name}"
        }
    }

    public void printTimeline(def timeline) {

        log.debug("Printing a timeline: {}", timeline)

        Map formatOptions = [:]

        formatOptions.authorLength = 0

        timeline.each { status ->
            if (status.user.screenName.length() > formatOptions.authorLength)
                formatOptions.authorLength = status.user.screenName.length()
        }
            
        formatOptions.indent = "".padLeft(formatOptions.authorLength + 2)

        timeline.eachWithIndex { status, rowNum ->
            formatOptions.rowNum = rowNum
            println formatStatus(status, formatOptions)
        }
    }

    public void postStatus(String status) {

        log.debug("Posting a status: '{}'", status)

        if (!status) {
            println color("post status ", colors.option) +
                color("command requires one option: ", colors.error) +
                "gritter post status <status>"
            return
        }

        if (status.length() > 140)  {
            println color("Status exceeds Twitter's 140 character limit.", colors.error)
            return
        }

        println "Update status: '$status'? "
        if (stdin.nextLine() ==~ /yes|y|true|t/) {
            try {
                twitter.updateStatus(status)
                println "Status posted."
            } catch (Exception e) {
                println "An error occurred trying to post the status: '" +
                    e.localizedMessage
                log.error("Error posting status:", e)
            }
        } else println "Status post canceled."
    }

    public void retweetStatus(def statusId) {
        
        log.debug("Retweeting a status: '{}'", statusId)

        if (!statusId.isLong() || !statusId) {
            println color("retweet ", colors.option) +
                color("command requires a status id: ", colors.error) +
                "gritter post retweet <statusId>"
        }

        statusId = statusId as long
        def status = twitter.showStatus(statusId)

        println "Retweet '" + color(status.text, colors.odd) + "'? "
        if (stdin.nextLine() ==~ /yes|y|true|t/) {
            twitter.retweetStatus(statusId)
            println "Status retweeted."
        } else { println "Retweet cancelled." }
    }

    public void addListMember(LinkedList args) {
        def listRef = parseListReference(args)
        def user = args.poll()

        log.debug("Adding a member to a list: list='{}', user='{}'",
            listRef, user)

        if (!user) {
            println color("add list member", colors.option) +
                color(" requires two parameters: ", colors.error) +
                "gritter add list member <list-ref> <user>"
            return
        }

        if (!listRef) {
            println color("No list found that matches the given description: ",
                colors.error) + color(listRef, colors.option)
            return 
        }

        // look up the user id if neccessary
        if (user.isLong()) user = user as long
        else user = twitter.showUser(user).id

        twitter.addUserListMember(listRef.listId, user)

        // TODO: error checking?
        println "Added list member."
    }

    public void addListSubscription(LinkedList args) {
        def listRef = parseListReference(args)

        log.debug("Subscribing to a list: list='{}'", listRef)

        if (!listRef) {
            println color("add list subscription ", colors.option) +
                color("expects a list name, user/list: ", colors.error) +
                "gritter add list subscription <user/list>"
            return 
        }

        twitter.subscribeUserList(listRef.username, listRef.listId)

        // TODO: error checking?
        println "Subscribed to list."
    }
    
    public void createNewList(LinkedList args) {
        def name = args.poll()
        def isPublic = args.poll()
        def desc = args.poll()

        log.debug("Creating a new list: name='{}', isPublic='{}', desc='{}'",
            name, isPublic, desc)

        if (desc == null) {
            println color("create list ", colors.option) +
                color("command requires three arguments: ", colors.error) +
                "gritter create list <listName> <isPublic> <listDescription>"
            return 
        }

        println "Create list '${color(name, colors.option)}'?"
        if (stdin.nextLine() ==~ /yes|y|true|t/) {
            twitter.createUserList(name, isPublic ==~ /yes|y|true|t/, desc)
            println "List created."
        } else { println "List creation cancelled." }
    }

    /* ======== HELP DISPLAY FUNCTIONS ======== */

    public void help(LinkedList args) {

        log.debug("Processing a 'help' command.")

        def command = args.poll()
        if (command != null) {
            switch (command.toLowerCase()) {
                case ~/delete|destroy|remove/: helpDelete(); break
                case ~/get|show/: helpGet(); break            // get|show
                case ~/help/: helpHelp(); break                   // help
                case ~/post|add|create/: helpPost(); break    // post|add|create
                case ~/reconfigure/: helpReconfigure(); break // reconfigure
                case ~/set/: helpSet(); break                 // set
                case~/list-reference/: helpListReference(); break // list-reference
                default:                                          // fallthrough
                    print color("Not a valid command: ", colors.error)
                    println color(command, colors.option)
                    break;
            }
        } else { 
            println "Gritter v${VERSION} -- A command-line twitter client."
            println "Author: Jonathan Bernard <jdbernard@gmail.com>"
            println "Website: https://www.github.com/jdbernard/gritter"
            println ""
            println "usage: " + color("gritter <command> ...", colors.option)
            println ""
            println "Valid commands (with interchangable aliases):"
            println "   delete (destroy, remove)"
            println "   get (show)"
            println "   help"
            println "   post (add, create)"
            println "   reconfigure"
            println "   set"
            println ""
            println "use " + color("gritter help <command>", colors.option) +
                "for more information about a \nspecific command."
            println ""
        }
    }

    public void helpDelete() {

        println color("gritter delete ...", colors.option)
        println ""
        println "Valid uses:"
        println ""
        println "  Destroy a status (tweet) by id:"
        println "    gritter destroy <status-id>"
        println "    gritter destroy status <status-id>"
        println ""
        println "  Destroy a list:"
        println "    gritter destroy list <list-reference>"
        println ""
        println "  Remove a user from a list (stop following them in that list):"
        println "    gritter remove list member <list-reference> <username>"
        println ""
        println "  Delete list subscription (stop following the list):"
        println "    gritter delete list subscription <list-reference>"
        println ""
        println "For more information about list references, use "
        println color("gritter help list-reference", colors.option)
    }

    public void helpGet() {

        println color("gritter get ...", colors.option)
        println ""
        println "Valid uses:"
        println ""
        println "  View a timeline:"
        println "    gritter show [timeline] (friends|home|mine|public|user <user-id>)"
        println ""
        println "  View available lists for a user (defaults to current user):"
        println "    gritter show lists"
        println "    gritter show lists <user-id>"
        println ""
        println "  View a specific list (see the recent tweets):"
        println "    gritter show list <list-reference>"
        println ""
        // TODO
        //println "  View your current list subscriptions:"
        //println "    gritter show list subscriptions"
        //println ""
        println "  View user information:"
        println "    gritter show user <user-id>"
        println ""
    }

    public void helpHelp() {

        println color("gritter help <command>", colors.option)
        println ""
        println "Show usage information about a specific gritter command."
        println ""

    }

    public void helpPost() {

        println color("gritter post ...", colors.option)
        println ""
        println "Valid uses:"
        println ""
        println "  Post a status update (tweet):"
        println "    gritter post <message>"
        println "    gritter post status <message>"
        println ""
        println "  Retweet a status:"
        println "    gritter post retweet <status-id>"
        println ""
        println "  Create a new list:"
        println "    gritter create list <list-name> <is-public?> <description>"
        println ""
        println "  Add a user to a list:"
        println "    gritter add list member <list-reference> <user-id>"
        println ""
        println "  Subscribe to a list:"
        println "    gritter add list subscription <list-reference>"
        println ""
    }

    public void helpReconfigure() {
        
        println color("gritter reconfigure", colors.option)
        println ""
        println "Causes gritter to release resources, forget variables set"
        println "manually and reload it's configuration file."
        println ""
    }

    public void helpSet() {

        println color("gritter set <key> <value>", colors.option)
        println ""
        println "Sets a configuration value manually. Configurable values are:"
        println ""
        println "  terminalWidth <integer> Gritter wraps its output to fit "
        println "                          within this width."
        println ""
        println "  colored       <boolean> Should gritter's output be in color?"
        println "                          yes,no,true,false are all valid."
        println ""
    }

    public void helpListReference() {
        println "  a ${color('<list-reference>', colors.option)} is defined as:"
        println "    ${color('[<username>/]<list-id>', colors.option)}" +
                                          "where username is optional (defaults"
        println "    to the current user) and list-id may be the list name or "
        println "    internal id number."
        println ""
    }
    /* ======== UTILITY FUNCTIONS ======== */

    public def parseListReference(LinkedList args) {
        def username = args.poll()
        def listId

        log.debug("Looking up a list: ref = '{}'", username)
        log.debug("remaining args='{}'", args)

        if (!username) return false

        username = username.split("/")

        if (username.length != 2) {
            listId = username[0]
            username = twitter.screenName
        } else {
            listId = username[1]
            username = username[0]
        }

        if (listId.isInteger()) listId = listId as int
        else listId = findListByName(username, listId)

        // TODO: err if list does not exist

        return [username: username, listId: listId]
    }

    public int findListByName(String userName, String listName) {

        def userLists
        long cursor = -1
        int listId = -1

        while(listId == -1) {
            userLists = twitter.getUserLists(userName, cursor)
            userLists.each { list ->
                if (listName == list.name || listName == list.slug)
                    listId = list.id
            }

            if (!userLists.hasNext()) break

            cursor = userLists.nextCursor
        }

        return listId
    }

    public String formatStatus(Status status, Map formatOptions) {

        def indent = formatOptions.indent ?: ""
        def authorLength = formatOptions.authorLength ?: 0
        def rowNum = formatOptions.rowNum ?: 0

        String result
        def textColor = (rowNum % 2 == 0 ? colors.even : colors.odd)

        // add author's username
        result = color(status.user.screenName.padLeft(
            authorLength) + ": ", colors.author, textColor)

        // format the status text
        String text = status.text

        // if this status takes up more room than we have left on the line
        if (text.length() > terminalWidth - indent.length()) {
            // wrap text to terminal width
            text = wrapToWidth(text, terminalWidth, indent, "").
                substring(indent.length())

            // if we are 
        } 
            
        // color @mentions in the tweet
        text = text.replaceAll(/(@\w+)/, color("\$1", colors.mentioned, textColor))

        result += text

        return result
    }

    public static void printUsage() {
        // TODO
    }

    public String resetColor() { colored ? "\u001b[m" : "" }

    public String color(def message, ConsoleColor color,
    ConsoleColor existing = null) {
        if (!colored) return message
        return color.toString() + message + (existing ?: resetColor())
    }

    static String wrapToWidth(String text, int width, String prefix, String suffix) {
        int lastSpaceIdx = 0;
        int curLineLength = 0;
        int lineStartIdx = 0;
        int i = 0;
        int actualWidth = width - prefix.length() - suffix.length()
        String wrapped = ""

        text = text.replaceAll("[\n\r]", " ")

        for (i = 0; i < text.length(); i++) {

            curLineLength++
            if (curLineLength > actualWidth) {
                
                if (lastSpaceIdx == -1) // we haven't seen a space on this line
                    lastSpaceIdx = lineStartIdx + actualWidth - 1

                wrapped += prefix + text[lineStartIdx..<lastSpaceIdx] + suffix + EOL
                curLineLength = 0
                lineStartIdx = lastSpaceIdx + 1
                i = lastSpaceIdx + 1
                lastSpaceIdx = -1
            }

            if (text.charAt(i).isWhitespace())
                lastSpaceIdx = i
        }

        if (i - lineStartIdx > 1)
            wrapped += prefix + text[lineStartIdx..<text.length()]

        return wrapped
    }
}
