/*
 AUTHOR James Padolsey (http://james.padolsey.com)
 VERSION 1.02
 UPDATED 07-06-2009
*/

var prettyPrint = (function(){
    
    /* These "util" functions are not part of the core
       functionality but are  all necessary - mostly DOM helpers */
    
    var util = {
        
        el: function(type, attrs) {
            
            /* Create new element */
            var el = document.createElement(type), attr;
            
            /*Copy to single object */
            attrs = util.merge({}, attrs);
            
            /* Add attributes to el */
            if (attrs && attrs.style) {
                var styles = attrs.style;
                util.applyCSS( el, attrs.style );
                delete attrs.style;
            }
            for (attr in attrs) {
                if (attrs.hasOwnProperty(attr)) {
                    el[attr] = attrs[attr];
                }
            }
            
            return el;
        
        },
        
        applyCSS: function(el, styles) {
            /* Applies CSS to a single element */
            for (var prop in styles) {
                if (styles.hasOwnProperty(prop)) {
                    try{
                        /* Yes, IE6 SUCKS! */
                        el.style[prop] = styles[prop];
                    }catch(e){}
                }
            }
        },
        
        txt: function(t) {
            /* Create text node */
            return document.createTextNode(t);
        },
        
        row: function(cells, type, cellType) {
            
            /* Creates new <tr> */
            cellType = cellType || 'td';
            
            /* colSpan is calculated by length of null items in array */
            var colSpan = util.count(cells, null) + 1,
                tr = util.el('tr'), td,
                attrs = {
                    style: util.getStyles(cellType, type),
                    colSpan: colSpan,
                    onmouseover: function() {
                        var tds = this.parentNode.childNodes;
                        util.forEach(tds, function(cell){
                            if (cell.nodeName.toLowerCase() !== 'td') { return; }
                            util.applyCSS(cell, util.getStyles('td_hover', type));
                        });
                    },
                    onmouseout: function() {
                        var tds = this.parentNode.childNodes;
                        util.forEach(tds, function(cell){
                            if (cell.nodeName.toLowerCase() !== 'td') { return; }
                            util.applyCSS(cell, util.getStyles('td', type));
                        });
                    }
                };
                
            util.forEach(cells, function(cell){
                
                if (cell === null) { return; }
                /* Default cell type is <td> */
                td = util.el(cellType, attrs);
                
                if (cell.nodeType) {
                    /* IsDomElement */
                    td.appendChild(cell);
                } else {
                    /* IsString */
                    td.innerHTML = util.shorten(cell.toString());
                }
                
                tr.appendChild(td);
            });
            
            return tr;
        },
        
        hRow: function(cells, type){
            /* Return new <th> */
            return util.row(cells, type, 'th');
        },
        
        table: function(headings, type){
            
            headings = headings || [];
            
            /* Creates new table: */
            var attrs = {
                    thead: {
                        style:util.getStyles('thead',type)
                    },
                    tbody: {
                        style:util.getStyles('tbody',type)
                    },
                    table: {
                        style:util.getStyles('table',type)
                    }
                },
                tbl = util.el('table', attrs.table),
                thead = util.el('thead', attrs.thead),
                tbody = util.el('tbody', attrs.tbody);
                
            if (headings.length) {
                tbl.appendChild(thead);
                thead.appendChild( util.hRow(headings, type) );
            }
            tbl.appendChild(tbody);
            
            return {
                /* Facade for dealing with table/tbody
                   Actual table node is this.node: */
                node: tbl,
                tbody: tbody,
                thead: thead,
                appendChild: function(node) {
                    this.tbody.appendChild(node);
                },
                addRow: function(cells, _type, cellType){
                    this.appendChild(util.row.call(util, cells, (_type || type), cellType));
                    return this;
                }
            };
        },
        
        shorten: function(str) {
            var max = 1024;
            str = str.replace(/^\s\s*|\s\s*$|\n/g,'');
            return str.length > max ? (str.substring(0, max-1) + '...') : str;
        },
        
        htmlentities: function(str) {
            return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
        },
        
        merge: function(target, source) {
            
            /* Merges two (or more) objects,
               giving the last one precedence */
            
            if ( typeof target !== 'object' ) {
                target = {};
            }
            
            for (var property in source) {
                
                if ( source.hasOwnProperty(property) ) {
                    
                    var sourceProperty = source[ property ];
                    
                    if ( typeof sourceProperty === 'object' ) {
                        target[ property ] = util.merge( target[ property ], sourceProperty );
                        continue;
                    }
                    
                    target[ property ] = sourceProperty;
                    
                }
                
            }
            
            for (var a = 2, l = arguments.length; a < l; a++) {
                util.merge(target, arguments[a]);
            }
            
            return target;
        },
        
        count: function(arr, item) {
            var count = 0;
            for (var i = 0, l = arr.length; i< l; i++) {
                if (arr[i] === item) {
                    count++;
                }
            }
            return count;
        },
        
        thead: function(tbl) {
            return tbl.getElementsByTagName('thead')[0];
        },
        
        forEach: function(arr, fn) {
            
            /* Helper: iteration */
            var len = arr.length, index = -1;
            
            while (len > ++index) {
                if(fn( arr[index], index, arr ) === false) {
                    break;
                }
            }
            
            return true;
        },
        
        type: function(v){
            try {
                /* Returns type, e.g. "string", "number", "array" etc.
                   Note, this is only used for precise typing. */
                if (v === null) { return 'null'; }
                if (v === undefined) { return 'undefined'; }
                var oType = Object.prototype.toString.call(v).match(/\s(.+?)\]/)[1].toLowerCase();
                if (v.nodeType) {
                    if (v.nodeType === 1) {
                        return 'domelement';
                    }
                    return 'domnode';
                }
                if (/^(string|number|array|regexp|function|date|boolean)$/.test(oType)) {
                    return oType;
                }
                if (typeof v === 'object') {
                    return 'object';
                }
                if (v === window || v === document) {
                    return 'object';
                }
                return 'default';
            } catch(e) {
                return 'default';
            }
        },
        
        within: function(ref) {
            /* Check existence of a val within an object
               RETURNS KEY */
            return {
                is: function(o) {
                    for (var i in ref) {
                        if (ref[i] === o) {
                            return i;
                        }
                    }
                    return '';
                }
            };
        },
        
        common: {
            circRef: function(obj, key, settings) {
                return util.expander(
                    '[POINTS BACK TO <strong>' + (key) + '</strong>]',
                    'Click to show this item anyway',
                    function() {
                        this.parentNode.appendChild( prettyPrintThis(obj,{maxDepth:1}) );
                    }
                );
            },
            depthReached: function(obj, settings) {
                return util.expander(
                    '[DEPTH REACHED]',
                    'Click to show this item anyway',
                    function() {
                        try {
                            this.parentNode.appendChild( prettyPrintThis(obj,{maxDepth:1}) );
                        } catch(e) {
                            this.parentNode.appendChild(
                                util.table(['ERROR OCCURED DURING OBJECT RETRIEVAL'],'error').addRow([e.message]).node   
                            );
                        }
                    }
                );
            }
        },
        
        getStyles: function(el, type) {
            type = prettyPrintThis.settings.styles[type] || {};
            return util.merge(
                {}, prettyPrintThis.settings.styles['default'][el], type[el]
            );
        },
        
        expander: function(text, title, clickFn) {
            return util.el('a', {
                innerHTML:  util.shorten(text) + ' <b style="visibility:hidden;">[+]</b>',
                title: title,
                onmouseover: function() {
                    this.getElementsByTagName('b')[0].style.visibility = 'visible';
                },
                onmouseout: function() {
                    this.getElementsByTagName('b')[0].style.visibility = 'hidden';
                },
                onclick: function() {
                    this.style.display = 'none';
                    clickFn.call(this);
                    return false;
                },
                style: {
                    cursor: 'pointer'
                }
            });
        },
        
        stringify: function(obj) {
            
            /* Bit of an ugly duckling!
               - This fn returns an ATTEMPT at converting an object/array/anyType
                 into a string, kinda like a JSON-deParser
               - This is used for when |settings.expanded === false| */
            
            var type = util.type(obj),
                str, first = true;
            if ( type === 'array' ) {
                str = '[';
                util.forEach(obj, function(item,i){
                    str += (i===0?'':', ') + util.stringify(item);
                });
                return str + ']';
            }
            if (typeof obj === 'object') {
                str = '{';
                for (var i in obj){
                    if (obj.hasOwnProperty(i)) {
                        str += (first?'':', ') + i + ':' + util.stringify(obj[i]);
                        first = false;
                    }
                }
                return str + '}';
            }
            if (type === 'regexp') {
                return '/' + obj.source + '/';
            }
            if (type === 'string') {
                return '"' + obj.replace(/"/g,'\\"') + '"';
            }
            return obj.toString();
        },
        
        headerGradient: (function(){
            
            var canvas = document.createElement('canvas');
            if (!canvas.getContext) { return ''; }
            var cx = canvas.getContext('2d');
            canvas.height = 30;
            canvas.width = 1;
            
            var linearGrad = cx.createLinearGradient(0,0,0,30);
            linearGrad.addColorStop(0,'rgba(0,0,0,0)');
            linearGrad.addColorStop(1,'rgba(0,0,0,0.25)');
            
            cx.fillStyle = linearGrad;
            cx.fillRect(0,0,1,30);
            
            var dataURL = canvas.toDataURL && canvas.toDataURL();
            return 'url(' + (dataURL || '') + ')';
        
        })()
        
    };
    
    // Main..
    var prettyPrintThis = function(obj, options) {
        
         /*
         *      obj :: Object to be printed                    
         *  options :: Options (merged with config)
         */
        
        options = options || {};
        
        var settings = util.merge( {}, prettyPrintThis.config, options ),
            container = util.el('div'),
            config = prettyPrintThis.config,
            currentDepth = 0,
            stack = {},
            hasRunOnce = false;
        
        /* Expose per-call settings.
           Note: "config" is overwritten (where necessary) by options/"settings"
           So, if you need to access/change *DEFAULT* settings then go via ".config" */
        prettyPrintThis.settings = settings;
        
        var typeDealer = {
            string : function(item){
                return util.txt('"' + util.shorten(item.replace(/"/g,'\\"')) + '"');
            },
            number : function(item) {
                return util.txt(item);
            },
            regexp : function(item) {
                
                var miniTable = util.table(['RegExp',null], 'regexp');
                var flags = util.table();
                var span = util.expander(
                    '/' + item.source + '/',
                    'Click to show more',
                    function() {
                        this.parentNode.appendChild(miniTable.node);
                    }
                );
                
                flags
                    .addRow(['g', item.global])
                    .addRow(['i', item.ignoreCase])
                    .addRow(['m', item.multiline]);
                
                miniTable
                    .addRow(['source', '/' + item.source + '/'])
                    .addRow(['flags', flags.node])
                    .addRow(['lastIndex', item.lastIndex]);
                    
                return settings.expanded ? miniTable.node : span;
            },
            domelement : function(element, depth) {
                
                var miniTable = util.table(['DOMElement',null], 'domelement'),
                    props = ['id', 'className', 'innerHTML'];
                
                miniTable.addRow(['tag', '&lt;' + element.nodeName.toLowerCase() + '&gt;']);
                    
                util.forEach(props, function(prop){
                    if ( element[prop] ) {
                        miniTable.addRow([ prop, util.htmlentities(element[prop]) ]);
                    }
                });
                
                return settings.expanded ? miniTable.node : util.expander(
                    'DOMElement (' + element.nodeName.toLowerCase() + ')',
                    'Click to show more',
                    function() {
                        this.parentNode.appendChild(miniTable.node);
                    }
                );
            },
            domnode : function(node){
                
                /* Deals with all DOMNodes that aren't elements (nodeType !== 1) */
                var miniTable = util.table(['DOMNode',null], 'domelement'),
                    data =  util.htmlentities( (node.data || 'UNDEFINED').replace(/\n/g,'\\n') );
                miniTable
                    .addRow(['nodeType', node.nodeType + ' (' + node.nodeName + ')'])
                    .addRow(['data', data]);
                
                return settings.expanded ? miniTable.node : util.expander(
                    'DOMNode',
                    'Click to show more',
                    function() {
                        this.parentNode.appendChild(miniTable.node);
                    }
                );
            },
            object : function(obj, depth, key) {
                
                /* Checking depth + circular refs */
                /* Note, check for circular refs before depth; just makes more sense */
                var stackKey = util.within(stack).is(obj);
                if ( stackKey ) {
                    return util.common.circRef(obj, stackKey, settings);
                }
                stack[key||'TOP'] = obj;
                if (depth === settings.maxDepth) {
                    return util.common.depthReached(obj, settings);
                }
                
                var table = util.table(['Object', null],'object'),
                    isEmpty = true;
                
                for (var i in obj) {
                    if (!obj.hasOwnProperty || obj.hasOwnProperty(i)) {
                        var item = obj[i],
                            type = util.type(item);
                        isEmpty = false;
                        try {
                            table.addRow([i, typeDealer[ type ](item, depth+1, i)], type);
                        } catch(e) {
                            /* Security errors are thrown on certain Window/DOM properties */
                            if (window.console && window.console.log) {
                                console.log(e.message);
                            }
                        }
                    }
                }
                
                if (isEmpty) {
                    table.addRow(['<small>[empty]</small>']);
                } else {
                    table.thead.appendChild(
                        util.hRow(['key','value'], 'colHeader')
                    );
                }
                
                var ret = (settings.expanded || hasRunOnce) ? table.node : util.expander(
                    util.stringify(obj),
                    'Click to show more',
                    function() {
                        this.parentNode.appendChild(table.node);
                    }
                );
                
                hasRunOnce = true;
                
                return ret;
                
            },
            array : function(arr, depth, key) {
                
                /* Checking depth + circular refs */
                /* Note, check for circular refs before depth; just makes more sense */
                var stackKey = util.within(stack).is(arr);
                if ( stackKey ) {
                    return util.common.circRef(arr, stackKey);
                }
                stack[key||'TOP'] = arr;
                if (depth === settings.maxDepth) {
                    return util.common.depthReached(arr);
                }
                
                /* Accepts a table and modifies it */
                var table = util.table(['Array(' + arr.length + ')', null], 'array'),
                    isEmpty = true;
                
                util.forEach(arr, function(item,i){
                    isEmpty = false;
                    table.addRow([i, typeDealer[ util.type(item) ](item, depth+1, i)]);
                });
                
                if (isEmpty) {
                    table.addRow(['<small>[empty]</small>']);
                } else {
                    table.thead.appendChild( util.hRow(['index','value'], 'colHeader') );
                }
                
                return settings.expanded ? table.node : util.expander(
                    util.stringify(arr),
                    'Click to show more',
                    function() {
                        this.parentNode.appendChild(table.node);
                    }
                );
                
            },
            'function' : function(fn, depth, key) {
                
                /* Checking JUST circular refs */
                var stackKey = util.within(stack).is(fn);
                if ( stackKey ) { return util.common.circRef(fn, stackKey); }
                stack[key||'TOP'] = fn;
                
                var miniTable = util.table(['Function',null], 'function'),
                    argsTable = util.table(['Arguments']),
                    args = fn.toString().match(/\((.+?)\)/),
                    body = fn.toString().match(/\(.*?\)\s+?\{?([\S\s]+)/)[1].replace(/\}?$/,'');
                    
                miniTable
                    .addRow(['arguments', args ? args[1].replace(/[^\w_,\s]/g,'') : '<small>[none/native]</small>'])
                    .addRow(['body', body]);
                    
                return settings.expanded ? miniTable.node : util.expander(
                    'function(){...}',
                    'Click to see more about this function.',
                    function(){
                        this.parentNode.appendChild(miniTable.node);
                    }
                );
            },
            'date' : function(date) {
                
                var miniTable = util.table(['Date',null], 'date'),
                    sDate = date.toString().split(/\s/);
                
                /* TODO: Make this work well in IE! */
                miniTable
                    .addRow(['Time', sDate[4]])
                    .addRow(['Date', sDate.slice(0,4).join('-')]);
                    
                return settings.expanded ? miniTable.node : util.expander(
                    'Date (timestamp): ' + (+date),
                    'Click to see a little more info about this date',
                    function() {
                        this.parentNode.appendChild(miniTable.node);
                    }
                );
                
            },
            'boolean' : function(bool) {
                return util.txt( bool.toString().toUpperCase() );
            },
            'undefined' : function() {
                return util.txt('UNDEFINED');
            },
            'null' : function() {
                return util.txt('NULL');
            },
            'default' : function() {
                /* When a type cannot be found */
                return util.txt('prettyPrint: TypeNotFound Error');
            }
        };
        
        container.appendChild( typeDealer[ (settings.forceObject) ? 'object' : util.type(obj) ](obj, currentDepth) );
        
        return container;
        
    };
    
    /* Configuration */
    
    /* All items can be overwridden by passing an
       "options" object when calling prettyPrint */
    prettyPrintThis.config = {
        
        /* Try setting this to false to save space */
        expanded: true,
        
        forceObject: false,
        maxDepth: 3,
        styles: {
            array: {
                th: {
                    backgroundColor: '#6DBD2A',
                    color: 'white'
                }
            },
            'function': {
                th: {
                    backgroundColor: '#D82525'
                }
            },
            regexp: {
                th: {
                    backgroundColor: '#E2F3FB',
                    color: '#000'
                }
            },
            object: {
                th: {
                    backgroundColor: '#1F96CF'
                }
            },
            error: {
                th: {
                    backgroundColor: 'red',
                    color: 'yellow'
                }
            },
            domelement: {
                th: {
                    backgroundColor: '#F3801E'
                }
            },
            date: {
                th: {
                    backgroundColor: '#A725D8'
                }
            },
            colHeader: {
                th: {
                    backgroundColor: '#EEE',
                    color: '#000',
                    textTransform: 'uppercase'
                }
            },
            'default': {
                table: {
                    borderCollapse: 'collapse',
                    width: '100%'
                },
                td: {
                    padding: '5px',
                    fontSize: '12px',
                    backgroundColor: '#FFF',
                    color: '#222',
                    border: '1px solid #000',
                    verticalAlign: 'top',
                    fontFamily: '"Consolas","Lucida Console",Courier,mono',
                    whiteSpace: 'nowrap'
                },
                td_hover: {
                    /* Styles defined here will apply to all tr:hover > td,
                        - Be aware that "inheritable" properties (e.g. fontWeight) WILL BE INHERITED */
                },
                th: {
                    padding: '5px',
                    fontSize: '12px',
                    backgroundColor: '#222',
                    color: '#EEE',
                    textAlign: 'left',
                    border: '1px solid #000',
                    verticalAlign: 'top',
                    fontFamily: '"Consolas","Lucida Console",Courier,mono',
                    backgroundImage: util.headerGradient,
                    backgroundRepeat: 'repeat-x'
                }
            }
        }
    };
    
    return prettyPrintThis;
    
})();
