// vim: et:ts=4:sw=4:sts=4:ft=javascript

'use strict';

/*@
Ox.SyntaxHighlighter <function> Syntax Highlighter
    (options[, self]) -> <o> Syntax Highlighter
    options <o> Options
        lineLength <n|0> If larger than zero, show edge of page
        offset <n|1> First line number
        replace <[[]]|[]> Array of replacements
            Each array element is an array of two arguments to be passed to the
            replace function, like [str, str], [regexp, str] or [regexp, fn]
        showLinebreaks <b|false> If true, show linebreaks
        showLineNumbers <b|false> If true, show line numbers
        showWhitespace <b|false> If true, show whitespace
        showTabs <b|false> If true, show tabs
        source <s|''> JavaScript source
        stripComments <b|false> If true, strip comments
        tabSize <n|4> Number of spaces per tab
    self <o> Shared private variable
@*/

Ox.SyntaxHighlighter = function(options, self) {

    self = self || {};
    var that = Ox.Element({}, self)
            .defaults({
                lineLength: 0,
                offset: 1,
                replace: [],
                showLinebreaks: false,
                showLineNumbers: false,
                showTabs: false,
                showWhitespace: false,
                source: '',
                stripComments: false,
                tabSize: 4,
            })
            .options(options || {})
            .addClass('OxSyntaxHighlighter');

    renderSource();

    function renderSource() {
        var $lineNumbers, $line, $source, width,
            lines, source = '', tokens,
            linebreak = (
                self.options.showLinebreaks
                ? '<span class="OxLinebreak">\u21A9</span>' : ''
            ) + '<br/>',
            tab = (
                self.options.showTabs ?
                '<span class="OxTab">\u2192</span>' : ''
            ) + Ox.repeat('&nbsp;', self.options.tabSize - self.options.showTabs),
            whitespace = self.options.showWhitespace ? '\u00B7' : '&nbsp;';
        self.options.source = self.options.source
            .replace(/\r\n/g, '\n')
            .replace(/\r/g, '\n');
        tokens = Ox.tokenize(self.options.source);
        tokens.forEach(function(token, i) {
            var classNames,
                substr = self.options.source.substr(token.offset, token.length);
            if (
                !(self.options.stripComments && token.type == 'comment')
            ) {
                classNames = 'Ox' + Ox.toTitleCase(token.type);
                if (self.options.showWhitespace && token.type == 'whitespace') {
                    if (isAfterLinebreak() && hasIrregularSpaces()) {
                        classNames += ' OxLeading';
                    } else if (isBeforeLinebreak()) {
                        classNames += ' OxTrailing';
                    }
                }
                source += '<span class="' + classNames + '">' +
                    Ox.encodeHTML(substr)
                    .replace(/ /g, whitespace)
                    .replace(/\t/g, tab)
                    .replace(/\n/g, linebreak) + '</span>';
            }
            function isAfterLinebreak() {
                return i == 0 ||
                    tokens[i - 1].type == 'linebreak';
            }
            function isBeforeLinebreak() {
                return i == tokens.length - 1 ||
                    tokens[i + 1].type == 'linebreak';
            }
            function hasIrregularSpaces() {
                return substr.split('').reduce(function(prev, curr) {
                    return prev + (curr == ' ' ? 1 : 0);
                }, 0) % self.options.tabSize;
            }
        });
        lines = source.split('<br/>');
        that.empty();
        if (self.options.showLineNumbers) {
            $lineNumbers = Ox.Element()
                .addClass('OxLineNumbers')
                .html(
                    Ox.range(lines.length).map(function(line) {
                        return (line + self.options.offset);
                    }).join('<br/>')
                )
                .appendTo(that);
        }

        self.options.replace.forEach(function(replace) {
            source = source.replace(replace[0], replace[1])
        });
        
        $source = Ox.Element()
            .addClass('OxSourceCode')
            .html(source)
            .appendTo(that);
        if (self.options.lineLength) {
            $line = Ox.Element()
                .css({
                    position: 'absolute',
                    top: '-1000px'
                })
                .html(Ox.repeat('&nbsp;', self.options.lineLength))
                .appendTo(that);
            width = $line.width() + 4; // add padding
            $line.remove();
            ['moz', 'webkit'].forEach(function(browser) {
                $source.css({
                    background: '-' + browser +
                        '-linear-gradient(left, rgb(255, 255, 255) ' +
                        width + 'px, rgb(192, 192, 192) ' + width +
                        'px, rgb(255, 255, 255) ' + (width + 1) + 'px)'
                });
            });
        }
    }

    self.setOption = function(key, value) {
        renderSource();
    };

    return that;

};