'use strict';

var f = require('util').format,
    fs = require('fs'),
    printf = require('printf');

function parseMessage(line) {
    return line.substring('Error: '.length);
}

var filesCache = {};

function fetchSource(frame, options) {
    var sourceLines = filesCache[frame.file];
    if (!sourceLines) {
        try {
            var content = fs.readFileSync(frame.file, 'utf8');
            sourceLines = filesCache[frame.file] = content.split('\n');
        } catch (e) {
            if (!options.strict) { return undefined; }
            throw e;
        }
    }

    var source = {};

    var start = Math.max(frame.line - options.contextSize - 1, 0);
    var end = frame.line + options.contextSize;

    sourceLines
        .slice(start, end)
        .forEach(function (line, i) {
            source[i + start + 1] = { code: line };
        });

    return source;
}

function parseFrame(line, options) {

    function namedFrame(line) {
        var re = (/    at (.+) \((.+):(\d+):(\d+)\)/i);
        var match = re.exec(line);

        if (!match && options.strict) {
            throw new Error('Failed to parse frame line: ' + line);
        }

        return {
            function: match[1],
            file: match[2],
            line: parseInt(match[3], 10),
            column: parseInt(match[4], 10)
        };
    }

    function unnamedFrame(line) {
        var re = (/    at (.+):(\d+):(\d+)/i);
        var match = re.exec(line);

        if (!match && options.strict) {
            throw new Error('Failed to parse frame line: ' + line);
        }

        return {
            function: undefined,
            file: match[1],
            line: parseInt(match[2], 10),
            column: parseInt(match[3], 10)
        };
    }

    var frame = line.indexOf('(') !== -1 ? namedFrame(line) : unnamedFrame(line);

    if (!options.sources) { return frame; }

    frame.source = fetchSource(frame, options);

    return frame;
}

function parseSource(frame, line, options) {
    var match = line.match(/^[ ]*(\d+): (.+)$/i);
    frame.source[match[1]] = { code: match[2] };
}

function parseStack(stack, options) {
    var lines = stack.split('\n');
    var message = parseMessage(lines.shift());

    var frames = [];

    lines.forEach(function (line) {
        if (line.indexOf('    at') === 0) {
            frames.push(parseFrame(line, options));
        } else if (/^[ ]+\d+:/.test(line)) {
            var frame = frames[frames.length - 1];
            frame.source = frame.source || {};
            parseSource(frame, line, options);
        }
    });

    return {
        error: message,
        frames: frames
    };
}

function composeString(trace, options) {
    var result = [ 'Error: ' + trace.error ];
    trace.frames.forEach(function (frame) {
        if (frame.function) {
            result.push(f('    at %s (%s:%s:%s)', frame.function, frame.file, frame.line, frame.column));
        } else {
            result.push(f('    at %s:%s:%s', frame.file, frame.line, frame.column));
        }

        if (options.excludeSources || !frame.source) { return; }

        Object.keys(frame.source).forEach(function (line) {
            var code = frame.source[line].code;
            result.push(printf('% 12d: %s', line, code));
        });

    });
    return result.join('\n');
}

module.exports = function (error, options, callback) {

    if (typeof(options) === 'function') {
        callback = options;
        options = undefined;
    }

    options = options || {};
    options.contextSize = options.contextSize || 3;

    var trace = parseStack(error.stack, options);

    return {
        object: function () {
            return trace;
        },

        toString: function (options) {
            options = options || {};
            return composeString(trace, options);
        },

        json: function () {
            return JSON.stringify(trace);
        }
    };
};
@@ -157,24 +157,29 @@
 ing(
-'Error: '.length
+line.indexOf(':') + 2
 );%0A%7D

Fix parseMessage rule