/*
 * Copyright (C) 2006  Cybozu Labs, Inc.
 * 
 * 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; version 2 of the License.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 */

// create RegExp.(left|rightContext) for safari, et. al.
(function () {
    "abc".match(/b/);
    if (RegExp.leftContext != "a") {
	var match = String.prototype.match;
	String.prototype.match = function (re) {
	    var r = match.apply(this, arguments);
	    if (r) {
		RegExp.leftContext = this.substring(0, r.index);
		RegExp.rightContext = this.substring(r.index + r[0].length);
	    }
	    return r;
	};
    }
})();

if (typeof Wiki != 'object' || typeof Wiki != 'function') {
    var Wiki = {};
}

// core code starts here

Wiki.Formatter = function (args) {
    if (typeof args != 'object') {
        args = {};
    }
    // clear fields
    this.out = [];
    this.modename = undefined;
    this.modelevel = 0;
    // set attributes and callbacks
    var tas = function (n, d) {
        this[n] = typeof args[n] != 'undefined' ? args[n] : d;
    };
    tas.call(this, 'linkifyWikiNames', true);
    tas.call(this, 'linkifyEmails', true);
    tas.call(this, 'linkifyURLs', true);
    this.nameToLink = args.nameToLink || undefined;
    this.foldLines = ! ! args.foldLines;
    this.addNote = args.addNote || function (note) {
        if (! this.notes) {
            this.notes = [];
        }
        this.notes.push('*' + (this.notes.length + 1) + ': ' + note);
        return '<sup>*' + this.notes.length + '</sup>';
    };
    this.notesToHTML = args.noteToHTML || function () {
        return this.notes ?
            '<div style="notes">\n'
                + this.notes.join('<br />\n')
                + '</div>\n' :
            '';
    };
    // setup handlers
    this.filters = [];
    this.statements = [];
    this.expressions = {};
    (args.initHandlers || this.initHandlers).call(this);
};

Wiki.Formatter.VERSION = 0.02;

Wiki.Formatter.prototype.match = function (s, r) {
    var m = s.match(r);
    if (! m) {
        return undefined;
    }
    var o = {
        lastMatch:    m[0],
        leftContext:  RegExp.leftContext,
        rightContext: RegExp.rightContext
    };
    for (var i = 1; i < m.length; i++) {
        o['$' + i] = m[i];
    }
    return o;
}

Wiki.Formatter.prototype.escapeHTML = function (text) {
    return text.replace(
        /[<>\"&]/g,
        function (m) {
            return {
                '<': '&lt;',
                '>': '&gt;',
                '"': '&quot;',
                '&': '&amp;'
            }[m];
        });
};

Wiki.Formatter.prototype.stripTags = function (html) {
    return html.replace(/<.*?>/g, '');
};

Wiki.Formatter.prototype.buildCloseTag = function (tag) {
    var closeTag = tag.replace(/^\s*<(\S+).*>\s*$/, '</$1>');
    if (tag == closeTag) {
        alert('Failed to convert ' + tag + ' to a close tag');
        throw 'failed to convert tag';
    }
    return closeTag;
};

Wiki.Formatter.prototype.buildLink = function (text, url, noEscape) {
    if (! noEscape) {
        text = this.escapeHTML(text);
    }
    return '<a href="' + this.escapeHTML(url) + '">' + text + '</a>';
};

Wiki.Formatter.prototype.compileLeaf = function (text, isInLink) {
    var out = '';
    var r;
    while (r = this.match(text, /((?:https?|ftp):\/\/\S+)|([\w\-\.]+\@[A-Za-z0-9\-\.]+\.[A-Za-z]{2,})|([A-Z]+[a-z_]+[A-Z]+[a-z_]+[A-Za-z_]*)/)) {
        out += this.escapeHTML(r.leftContext);
        text = r.rightContext;
        if (out.match(/\\\\$/)) {
            out = RegExp.leftContext + this.escapeHTML(r.lastMatch);
        } else if (r.$1 && ! isInLink && this.linkifyURLs) {
            out += this.buildLink(r.$1, r.$1);
        } else if (r.$2 && ! isInLink && this.linkifyEmails) {
            out += this.buildLink(r.$2, 'mailto:' + r.$2);
        } else if (r.$3 && ! isInLink && this.linkifyWikiNames
            && this.nameToLink && ! out.match(/[a-z_]$/)) {
            out += this.buildLink(r.$3, this.nameToLink.call(this, r.$3));
        } else {
            out += this.escapeHTML(r.lastMatch);
        }
    }
    out += this.escapeHTML(text);
    return out;
};

Wiki.Formatter.prototype.initLineCompiler = function () {
    var reMap = {};
    for (var tag in this.expressions) {
        var e = this.expressions[tag];
        if (e && e.openRE) {
            reMap[this.expressions[tag].openRE] = 1;
        } else {
            reMap[tag.replace(/(.)/g, '\\$1')] = 1;
        }
    }
    var reList = [];
    for (var i in reMap) {
        reList.push('\\\\\\\\' + i);
        reList.push(i);
    }
    reList.sort(
        function (a, b) {
            return b.length - a.length;
        });
    this.compileLineRE = new RegExp(reList.join('|'));
    if (! this.compileLineRE) {
        alert("failed to compile expression table");
    }
}

Wiki.Formatter.prototype.compileLine = function (line) {
    var array_find = function (v) {
        for (var i = 0; i < this.length; i++) {
            if (this[i] == v) {
                return true;
            }
        }
        return false;
    };
    var stack = [
        {
            expression: {},
            isInLink: false,
            out: ''
        }
    ];
    
    this.line = line;
    var leaf = '';
    var r;
    while (r = this.match(this.line, this.compileLineRE)) {
        this.line = r.rightContext;
        leaf += r.leftContext;
        if (r.lastMatch.match(/^\\\\/)) {
            leaf += RegExp.rightContext;
        } else if (stack[0].expression.closeTags
            && array_find.call(stack[0].expression.closeTags, r.lastMatch)) {
            stack[0].out += this.compileLeaf(leaf, stack[0].isInLink);
            leaf = '';
            stack[0].expression.call(this, stack, r.lastMatch);
            stack.shift();
        } else {
            var name = r.lastMatch;
            var params;
            if (r.lastMatch.match(/^(.+)\s*\((.*?)\)/)) {
                params = RegExp.$2;
                name = RegExp.$1;
                if (r.lastMatch.match(/\{$/)) {
                    name += '\{';
                }
            }
            var expression = this.expressions[name];
            if (typeof expression == 'function'
                && (expression.isLink != 1 || ! stack[0].isInLink)) {
                stack[0].out += this.compileLeaf(leaf, stack[0].isInLink);
                leaf = '';
                stack.unshift({
                    expression: expression,
                    out: '',
                    params: params
                });
                stack[0].isInLink =
                    stack[1].isInLink + expression.isLink > 0;
                if (! expression.closeTags) {
                    expression.call(this, stack);
                    stack.shift();
                }
            } else {
                leaf += r.lastMatch;
            }
        }
    }
    stack[0].out += this.compileLeaf(leaf + this.line, stack[0].isInLink);
    while (stack.length != 1) {
        stack[0].expression.call(this, stack);
        stack.shift();
    }
    
    return stack[0].out;
};

Wiki.Formatter.prototype.taggedLine = function (tag, text) {
    this.out.push(tag + this.compileLine(text) + this.buildCloseTag(tag));
};

Wiki.Formatter.prototype.mode = function (name, level) {
    if (this.modename == name && this.modelevel == level) {
        return;
    }
    if (this.foldLines &&
        this.out.length != 0 && this.out[this.out.length - 1] == '<AUTOBR>') {
        this.out.pop();
    }
    if (this.modename != name) {
        for (; this.modelevel != 0; --this.modelevel) {
            this.out.push('</' + this.modename + '>');
        }
        this.modename = name;
    }
    for (; this.modelevel < level; ++this.modelevel) {
        this.out.push('<' + name + '>');
    }
    for (; level < this.modelevel; --this.modelevel) {
        this.out.push('</' + name + '>');
    }
};

Wiki.Formatter.prototype.sortStatements = function () {
    this.statements.sort(
        function (a, b) {
            var d = b.pattern.length - a.pattern.length;
            if (d != 0) {
                return d;
            }
            if (b.pattern < a.pattern) {
                return -1;
            } else if (b.pattern > a.pattern) {
                return 1;
            }
            return b.index - a.index;
        });
};

Wiki.Formatter.prototype.format = function (text) {
    var lines = text.split('\n');
    this.initLineCompiler();
    this.sortStatements();
    for (var i = 0; i < lines.length; i++) {
        var l = lines[i];
        for (var j = 0; j < this.filters.length; j++) {
            l = this.filters[j].call(this, l);
            if (typeof l == 'undefined') {
                break;
            }
        }
        if (typeof l == 'undefined') {
            continue;
        }
        for (var j = 0; j < this.statements.length; j++) {
            var pat = this.statements[j].pattern;
            if (l.substring(0, pat.length) == pat) {
                this.statements[j].call(this, l.substring(pat.length));
                break;
            }
        }
        if (j == this.statements.length) {
            if (l == '') {
                this.mode();
            } else {
                this.mode('p', 1);
                this.out.push(this.compileLine(l));
                if (this.foldLines) {
                    this.out.push('<AUTOBR>');
                }
            }
        }
    }
    this.mode();
    for (var i = 0; i < this.out.length; i++) {
        if (this.out[i] == '<AUTOBR>') {
             this.out[i] = '<br />';
        }
    }
    
    return this.out.join('\n') + '\n' + this.notesToHTML();
};

Wiki.Formatter.prototype.addExpression = function (func, openTag, closeTags, openRE) {
    var expression = function () {
        func.apply(this, arguments);
    };
    expression.openTag = openTag;
    expression.openRE = openRE;
    this.expressions[openTag] = expression;
    if (typeof closeTags != 'undefined') {
        if (typeof closeTags == 'string') {
            closeTags = [ closeTags ];
        }
        expression.closeTags = closeTags;
        for (var i = 0; i < closeTags.length; i++) {
            if (typeof this.expressions[closeTags[i]] == 'undefined') {
                this.expressions[closeTags[i]] = false;
            }
        }
    }
    expression.isLink = 0;
    return expression;
};

Wiki.Formatter.prototype.addTagExpression = function (htmlTag, openTag, closeTags, openRE) {
    var handler;
    if (closeTags) {
        handler = function (stack) {
            stack[1].out +=
                htmlTag + stack[0].out + this.buildCloseTag(htmlTag);
        };
    } else {
        handler = function (stack) {
            stack[1].out += htmlTag;
        };
    }
    this.addExpression(handler, openTag, closeTags, openRE);
};

Wiki.Formatter.prototype.addStatement = function (pat, func) {
    var f = function () { func.apply(this, arguments); }
    f.pattern = pat;
    f.index = this.statements.length;
    this.statements.push(f);
};

Wiki.Formatter.prototype.addTaggedStatement = function (pat, tag, mode, level) {
    this.addStatement(
        pat,
        function (text) {
            this.mode(mode, level);
            this.taggedLine(tag, text);
        });
};

Wiki.Formatter.prototype.addFilter = function (func) {
    this.filters.push(func);
};

// core code ends here

Wiki.Formatter.prototype.initHandlers = function () {
    // add filters
    this.addCommentFilter(/^\/\//);
    this.addQuoteFilter();
    // add statement handlers
    this.addTaggedStatement('!', '<h1>');
    this.addTaggedStatement('!!', '<h2>');
    this.addTaggedStatement('!!!', '<h3>');
    this.addTaggedStatement('*', '<h1>');
    this.addTaggedStatement('**', '<h2>');
    this.addTaggedStatement('***', '<h3>');
    this.addTaggedStatement('-', '<li>', 'ul', 1);
    this.addTaggedStatement('--', '<li>', 'ul', 2);
    this.addTaggedStatement('---', '<li>', 'ul', 3);
    this.addTaggedStatement('+', '<li>', 'ol', 1);
    this.addTaggedStatement('++', '<li>', 'ol', 2);
    this.addTaggedStatement('+++', '<li>', 'ol', 3);
    this.addDefListStatement(':', '|');
    this.addTableStatement('|');
    this.addTableStatement(',');
    this.addPreStatement(' ');
    this.addPreStatement('\t');
    this.addHRStatement('----');
    // add expression handlers
    this.addTagExpression('<br>', '~~');
    this.addTagExpression('<em>', "''", "''");
    this.addTagExpression('<i>', "'''", "'''");
    this.addTagExpression('<del>', '%%', '%%');
    this.addTagExpression('<ins>', '%%%', '%%%');
    this.addTagExpression('<sub>', '__', '__');
    this.addSizeExpression();
    this.addColorExpression();
    this.addImageExpression();
    if (this.nameToLink) {
        this.addWikiLinkExpression();
    }
    if (this.addNote) {
        this.addNoteExpression('((', '))');
    }
    this.addLinkExpression();
};

Wiki.Formatter.prototype.addCommentFilter = function (pat) {
    this.addFilter(
        function (line) {
            return line.match(pat) ? undefined : line;
        });
};

Wiki.Formatter.prototype.addQuoteFilter = function () {
    this.quoteFilter = {
        level: 0
    };
    this.addFilter(
        function (line) {
            var newLevel = 0;
            if (line.match(/^(>{1,3})/)) {
                line = RegExp.rightContext;
                newLevel = RegExp.$1.length;
            }
            if (this.quoteFilter.level != newLevel) {
                this.mode();
                for (;
                    newLevel < this.quoteFilter.level;
                    --this.quoteFilter.level) {
                    this.out.push('</blockquote>');
                }
                for (;
                    this.quoteFilter.level < newLevel;
                    ++this.quoteFilter.level) {
                    this.out.push('<blockquote>');
                }
            }
            return line;
        });
};

Wiki.Formatter.prototype.addWikiLinkExpression = function (start, end) {
    var expression = this.addExpression(
        function (stack, closeTag) {
            var link;
            if (closeTag == '>') {
                if (this.line.match(/^\s*(.*?)\s*\]\]/)) {
                    this.line = RegExp.rightContext;
                    stack[1].out += this.buildLink(
                        stack[0].out,
                        this.nameToLink(RegExp.$1),
                        true);
                } else {
                    stack[1].out += '\[\[' + stack[0].out + '>';
                }
            } else {
                stack[1].out += this.buildLink(
                    stack[0].out,
                    this.nameToLink(this.stripTags(stack[0].out)),
                    true);
            }
        },
        '\[\[',
        [ '\]\]', '>' ]);
    expression.isLink = 1;
};

Wiki.Formatter.prototype.addNoteExpression = function (start, end) {
    var expression = this.addExpression(
        function (stack) {
            stack[1].out += this.addNote(stack[0].out);
        },
        start,
        end);
    expression.isLink = -1;
};

Wiki.Formatter.prototype.addLinkExpression = function () {
    var expression = this.addExpression(
        function (stack, closeTag) {
            if (! closeTag) {
                stack[1].out += '\[' + stack[0].out;
            } else if (this.line.match(/^\s*(.*?)\s*\]/)) {
                this.line = RegExp.rightContext;
                stack[1].out += this.buildLink(stack[0].out, RegExp.$1, true);
            } else {
                stack[1].out += '\[' + stack[0].out + '>';
            }
        },
        '\[',
        '>');
    expression.isLink = 1;
};

Wiki.Formatter.prototype.addSizeExpression = function () {
    this.addExpression(
        function (stack) {
            stack[1].out +=
                '<span style="font-size: ' + stack[0].params + ';">'
                + stack[0].out
                + '</span>';
        },
        '&size\{',
        '\}',
        '&size\\\(\\\s*\\\d+\\\s*\\\)\\\s*\\\{');
};

Wiki.Formatter.prototype.addColorExpression = function () {
    this.addExpression(
        function (stack) {
            var params = stack[0].params.split(',', 2);
            for (var i = 0; i < params.length; i++) {
                if (params[i].match(/([A-Za-z#0-9]+)/)) {
                    params[i] =
                        (['color:', 'background-color:'])[i] + RegExp.$1 + ';';
                } else {
                    params[i] = '';
                }
            }
            stack[1].out +=
                '<span style="' + params.join(';') + '">'
                + stack[0].out
                + '</span>';
        },
        '&color\{',
        '\}',
        '&color\\\(.*?\\\)\\\s*\\\{');
};

Wiki.Formatter.prototype.addImageExpression = function () {
    var buildHTML = function (isInLink, ref, width, height) {
        if (! ref.match('/')) {
            if (! this.filenameToLink) {
                return;
            }
            ref = this.filenameToLink(ref);
        }
        var html = '';
        if (! isInLink) {
            html += '<a href="' + this.escapeHTML(ref) + '">';
        }
        html += '<img src="' + this.escapeHTML(ref) + '"';
        if (width) {
            html += ' width="' + (width - 0) + '"';
        }
        if (height) {
            html += ' height="' + (height - 0) + '"';
        }
        html += ' />';
        if (! isInLink) {
            html += '</a>';
        }
        return html;
    };
    this.addExpression(
        function (stack) {
            var args =
                stack[0].params.replace(/^\s*(.*)\s*$/, "$1").split(/\s*,\s*/);
            args.unshift(stack[0].isInLink);
            var html = buildHTML.apply(this, args);
            stack[1].out += html ? html : '#img(' + stack[0].params + ')';
        },
        '#img',
        undefined,
        '#img\\\(.*?\\\)');
};
                        
Wiki.Formatter.prototype.addDefListStatement = function (start, sep) {
    var sepRE = new RegExp(sep.replace(/(.)g/, '\\$1'));
    this.addStatement(
        start,
        function (text) {
            this.mode('dl', 1);
            if (text.match(sepRE)) {
                var title = RegExp.leftContext, detail = RegExp.rightContext;
                this.taggedLine('<dt>', title);
                this.taggedLine('<dd>', detail);
            } else {
                this.taggedLine('<dt>', text);
            }
        });
};

Wiki.Formatter.prototype.addTableStatement = function (sep) {
    var sepRE = new RegExp(
        '^\\s*(?:(\\\')(.*?)\\\'|(\\")(.*?)\\"|(.*?))\\s*\\' + sep);
    var endRE = new RegExp('\\' + sep + '\\s*$');
    this.addStatement(
        sep,
        function (text) {
            this.mode('table', 1);
            this.out.push('<tr>');
            if (! text.match(endRE)) {
                text += sep;
            }
            while (text.match(sepRE)) {
                text = RegExp.rightContext;
                this.taggedLine(
                    '<td>',
                    RegExp.$1 ?
                        RegExp.$2 :
                        RegExp.$3 ? RegExp.$4 : RegExp.$5);
            }
            this.out.push('</tr>');
        });
};

Wiki.Formatter.prototype.addPreStatement = function (pat) {
    this.addStatement(
        pat,
        function (text) {
            this.mode('pre', 1);
            this.out.push(this.compileLeaf(text));
        });
};

Wiki.Formatter.prototype.addHRStatement = function (pat) {
    this.addStatement(
        pat,
        function (text) {
            this.mode();
            this.out.push('<hr />');
        });
};
