/*******************************************************************************
TODO source code analyzer for Coedit projects/files

## Format: 

`` 
// TODO [fields] : text
``

- TODO/FIXME: used to detect that the comment is a "TODO" comment. The keyword is not
case sensitive.

- fields: an optional list of property with a format similar to the execution argument 
of a program: `-<char x><property for char x>-<char y><property for char y>`.
possible fields include:
    - c: TODO category, e.g: _-cserialization_, _-cpersistence_, _ -cerrorhandling_.
    - a: TODO assignee, e.g: _-aMisterFreeze_, _-aMadameMichou_, _-aJhonSmith_.
    - p: TODO priority, eg: _-p8_, _-p0_.
    - s: TODO status, e.g _-sPartiallyFixed_, _-sDone_.
  
- text: the literal message, e.g:  "set this property as const() to set it read only".

## Examples:

``// TODO: set this property as const() to make it read-only.``

``// TODO-cfeature: save this property in the inifile.``

``// TODO-cannnotations-p8: annotate the members of the module with @safe and if possible nothrow.``

``// FIXME-p8: This won't work if all the flags are OR-ed.``

## Widget-to-tool IPC:

The widget calls the tool with a file list as argument and reads the process
output on exit. The widget expects to find some _TODO items_ in _LFM_ format, 
according to the classes declarations of TTodoItems (the collection container) 
and TTodoItem(the collection item).

********************************************************************************/
module cetodo;

// std
import std.stdio, std.getopt, std.string, std.algorithm;
import std.array, std.conv, std.traits, std.ascii;
import std.file, std.path, std.range;

// libdparse
import std.d.lexer;

/// Encapsulates the fields of a _TODO comment_.
private struct TodoItem 
{
    /** 
     * Enumerates the possible fields of _a TODO comment_. 
     * They must match the published member of the widget-side class TTodoItem.
     */
    private static enum TodoField {filename, line, text, category, assignee, priority, status}
    private __gshared static string[TodoField] fFieldNames;
    private string[TodoField] fFields;
    
    static this()
    {
        foreach(member; EnumMembers!TodoField)
            fFieldNames[member] = to!string(member);
    }
     
    /**
     * Constructs a TODO item with its fields.
     * Params:
     * fname = the file where the _TODO comment_ is located. mandatory.
     * line = the line where the _TODO comment_ is located. mandatory.
     * text = the _TODO comment_ main text. mandatory.
     * cat = the _TODO comment_ category, optional.
     * ass = the _TODO comment_ assignee, optional.
     * prior = the _TODO comment_ priority, as an integer litteral, optional.
     * status = the _TODO comment_ status, optional.
     */
    @safe public this(string fname, string line, string text, string cat = "",  string ass = "", string prior = "", string status = "")
    {   
        // fname must really be valid
        if (!fname.exists) throw new Exception("TodoItem exception, the file name is invalid");
        
        // priority must be convertible to int
        if (prior.length) try auto i = to!long(prior);
        catch(Exception e) prior = "";
              
        fFields[TodoField.filename] = fname.idup;
        fFields[TodoField.line]     = line.idup;
        fFields[TodoField.text]     = text.idup;
        fFields[TodoField.category] = cat.idup;
        fFields[TodoField.assignee] = ass.idup;
        fFields[TodoField.priority] = prior.idup;
        fFields[TodoField.status]   = status.idup;
    }
    
    /**
     * The item writes itself as a TCollectionItem.
     * Params:
     * LfmString = the string containing the LFM script.
     */
    public void serialize(ref Appender!string lfmApp)
    {
        lfmApp.put("  \r    item\r");
        foreach(member; EnumMembers!TodoField)
            if (fFields[member].length)
                lfmApp.put(format("      %s = '%s'\r", fFieldNames[member], fFields[member]));   
        lfmApp.put("    end");
    }
}

private alias TodoItems = TodoItem * [];

/**
 * Application main procedure.
 * Params:
 * args = a list of files to analyze. 
 * Called each time a document is focused. Args is set using:
 * - the symbolic string `<CFF>` (current file is not in a project).
 * - the symbolic string `<CPFS>` (current file is in a project).
 */
void main(string[] args)
{
    string[] files = args[1..$];
    Appender!string lfmApp;
    TodoItems todoItems;
   
    foreach(f; files)
    {
        if (!f.exists) continue;
        
        // load and parse the file
        auto src = cast(ubyte[]) read(f, size_t.max);
        auto config = LexerConfig(f, StringBehavior.source);
        StringCache cache = StringCache(StringCache.defaultBucketCount);
        auto lexer = DLexer(src, config, &cache);
        
        // analyze the tokens
        foreach(tok; lexer) token2TodoItem(tok, f, todoItems);                     
    }
    
    // efficient appending if the item text ~ fields is about 100 chars
    lfmApp.reserve(todoItems.length * 128 + 64);
    
    // serialize the items using the pascal component streaming text format
    lfmApp.put("object TTodoItems\r  items = <");
    foreach(todoItem; todoItems) todoItem.serialize(lfmApp);
    lfmApp.put(">\rend\r\n");
    
    // the widget has the LFM script in the output
    write(lfmApp.data);
    
    // TODO: NEVER call writeln() in this program otherwise the widget cant interpret the output
}

/// Try to transforms a Token into a a TODO item
@safe private void token2TodoItem(const(Token) atok, string fname, ref TodoItems todoItems)
{
    if (atok.type != (tok!"comment")) return;
    auto text = atok.text.strip;
    string identifier;
  
    
    // detects single line comments, incl. ddoc
    bool isSingleLineComment; 
    while (!text.empty)
    {
        auto front = text.front;
        if (front == '/') identifier ~= front;
        else
        {
            if (!isSingleLineComment)
                isSingleLineComment = (identifier.length > 1);
            if (!front.isWhite) break; 
        }
        text.popFront;
    }   
    if (!isSingleLineComment) return;
    identifier = ""; 
    
    
    // looks for the TODO/FIXME token
    bool isTodoComment;
    while (!text.empty)
    {
        identifier ~= std.ascii.toUpper(text.front);
        text.popFront;   
        if (canFind(["TODO","FIXME"], identifier)) 
        {
            isTodoComment = true;
            break;
        }            
    }
    if (!isTodoComment) return;
    identifier = "";
    
    
    // separates the description fields from the text body
    bool isWellFormed;
    string comment, fields;
    while (!text.empty)
    {
        auto front = text.front;
        identifier ~= front;
        text.popFront;
        if (front == ':')
        {
            if (identifier.length) fields = identifier;
            isWellFormed = (text.length > 0);
            break;
        }
    }
    if (!isWellFormed) return;
    identifier = ""; 
    
    
    // parses the item description fields
    string a, c, p, s;
    if (fields.length) while (!fields.empty)
    {
        auto front = fields.front;
        fields.popFront;
        if ((front == '-' || fields.empty) && identifier.length > 2)
        {
            auto field0 = identifier[0..2].toUpper;
            auto field1 = identifier[2..$].strip;  
                 if (field0 == "-A") a = field1;
            else if (field0 == "-C") c = field1;
            else if (field0 == "-P") p = field1;
            else if (field0 == "-S") s = field1;
            identifier = "";                
        }
        identifier ~= front;  
    }
    
    
    string line;
    try line = to!string(atok.line);
    catch(ConvException e) line = "0";
    todoItems ~= new TodoItem(fname, line, text, c, a, p, s);
}

// samples for testing the program as a runnable ('Compile and run file ...') with '<CFF>'

// fixme-p8: èuèuuè``u`èuùè 
// fixme-p8: fixme also handled
// TODO-cINVALID_because_no_content:
////TODO:set this property as const() to set it read only.
// TODO-cfeature-sDone:save this property in the inifile.
// TODO-cannnotations-p8: annotates the member of the module as @safe and if possible nothrow.
// TODO-cfeature-sDone: save this property in the inifile.
// TODO-aMe-cCat-p1-sjkjkj:todo body
