Colorful console logging
@returns {Log}
@constructor
function Colog() {

    /**
     * Always create new object
     */
    if ((this instanceof Colog) === false) {
        return new Colog();
    }

    /**
     * Self
     * @type Colog
     */
    var self                        = this;

    /**
     * This is base for all effects. Replace %d [ or use util.format() ] with code number
     * @type {string}
     */
    var base                        = '\x1B[%dm';

    /**
     * This will reset all effects
     * @type {number}
     */
    var reset                       = 0;

    /**
     * Add 60 to color number to bright it out
     * @type {number}
     */
    var colorLightValueChange       = 60;

    /**
     * Last command executed
     */
    var lastCommand                 = 'log';

    /**
     * Last line printed in console
     * @type {number}
     */
    var lastLineLength              = 0;

    /**
     * Available colors
     * @type {{black: Array, red: Array, green: Array, yellow: Array, blue: Array, magenta: Array, cyan: Array, white: Array}}
     */
    var color = {
        'black':            [30, 39],
        'red':              [31, 39],
        'green':            [32, 39],
        'yellow':           [33, 39],
        'blue':             [34, 39],
        'magenta':          [35, 39],
        'cyan':             [36, 39],
        'white':            [37, 39]
    };

    /**
     * Available backgrounds
     * @type {{black: Array, red: Array, green: Array, yellow: Array, blue: Array, magenta: Array, cyan: Array, white: Array}}
     */
    var background = {
        'black':        [40, 49],
        'red':          [41, 49],
        'green':        [42, 49],
        'yellow':       [43, 49],
        'blue':         [44, 49],
        'magenta':      [45, 49],
        'cyan':         [46, 49],
        'white':        [47, 49]
    };

    /**
     * Available effects
     * @type {{bold: Array, underline: Array, strike: Array, inverse: Array}}
     */
    var effect = {
        'b':            [1, 22],
        'u':            [4, 24],
        'i':            [7, 27],
        's':            [9, 29],
        'bold':         [1, 22],
        'underline':    [4, 24],
        'inverse':      [7, 27],
        'strike':       [9, 29]
    };

    /**
     * Progress bar values
     * @type {Array}
     */
    var progress = [
        0,
        100
    ];

    /**
     * Default progress bar values
     * @type {{progress: {zero: string, one: string, left: string, right: string, showPostfix: boolean, prefixText: string}}}
     */
    var defaults = {
        progress: {
            zero: '░',
            one: '▓',
            length: 40,
            sufix: ' ~ %d%% (%s / %s)',
            effects: ['colorYellow'],
            description: ''
        }
    };

    var beSilent = false;

    var consoleWidth    = 100;
    if (process.stdout.isTTY) {
        consoleWidth = process.stdout.getWindowSize()[0];
        process.stdout.on('resize', function () {
            consoleWidth = process.stdout.getWindowSize()[0];
        });
    }


    /**
     * Create effect from code. For colors set lightColor true to bright them up
     * @param code
     * @param lightColor
     * @returns {*}
     */
    function use(code, lightColor) {
        return util.format(base, (lightColor === true) ? code + colorLightValueChange : code);
    }


    /**
     * Apply effect on both side of the string
     * @param message
     * @param effect
     * @param lightColor
     * @returns {string}
     */
    function text(message, effect, lightColor) {
        return [
            use(effect[0], lightColor),
            String(message),
            use(effect[1], lightColor)
        ].join('');
    }


    /**
     * Clear line until last new line white character
     */
    function clearLine() {
        if (process.stdout.clearLine) {
            process.stdout.clearLine();
            process.stdout.cursorTo(0);
        } else {
            var str = '\r',
                i = 0;
            for (i = 0; i < lastLineLength; i++) {
                str += ' ';
            }
            process.stdout.write(str);
        }
        lastLineLength = 0;

        return self;
    }


    this.silent = function (isSilent) {
        beSilent = (isSilent) ? true : false;
    }


    this.getWidth = function () {
        return consoleWidth;
    };


    /**
     * Configure progress bar
     * @param zero - character for 0 status
     * @param one - character for 1 status
     * @param left - character on the left of the bar
     * @param right - character on the right of the bar
     * @param showPostfix - show postfix after bar [like: (19 of 20)]
     * @param prefixText - show prefix text before bar
     * @returns {*}
     */
    this.configureProgress = function (zero, one, length, effects) {
        length = Math.abs(parseInt(length, 10));

        defaults.progress.zero = (zero !== undefined) ? zero : defaults.progress.zero;
        defaults.progress.one = (one !== undefined) ? one : defaults.progress.one;
        defaults.progress.length = (length > 0) ? length : defaults.progress.length;
        defaults.progress.effects = (effects instanceof Array) ? effects : defaults.progress.effects;

        return this;
    };


    this.setProgressDescription = function (text) {
        text = String(text);
        defaults.progress.description = text;

        return this;
    };


    /**
     * Generate progress bar. Don't pass arguments to increase progress bar value
     * @param int - each int will add one more progress bar
     */
    this.progress = function (minOrChange, max, effects, returnString) {
        if (beSilent) {
            return this;
        }
        if (lastCommand === 'progress') {
            clearLine();
        }

        var i = 0,
            prc = 0,
            fullBarsToDraw = 0,
            emptyBarsToDraw = 0,
            totalBars = defaults.progress.length;

        // Prepare values
        minOrChange = Math.abs(parseInt(minOrChange, 10));
        max = Math.abs(parseInt(max, 10));
        if (isNaN(minOrChange)) {
            minOrChange = 0;
        }
        if (isNaN(max)) {
            max = 0;
        }

        if (max > 0) {
            // create new progress bar
            progress[0] = minOrChange;
            progress[1] = max;
        } else {
            if (minOrChange === 0) {
                minOrChange = 1;
            }
            // add to current progress bar
            progress[0] += minOrChange;
        }

        minOrChange     = progress[0];
        max             = progress[1];
        if (minOrChange > max) {
            minOrChange = max;
        }

        if (max === 0) {
            prc = 100;
        } else {
            prc = Math.floor((minOrChange / max) * 100);
        }
        fullBarsToDraw = Math.floor((prc / 100) * totalBars);
        emptyBarsToDraw = totalBars - fullBarsToDraw;

        var str = '\r';

        if (returnString !== true) {
            clearLine();
        }

        for (i = 0; i < fullBarsToDraw; i++) {
            str = str + defaults.progress.one;
        }
        for (i = 0; i < emptyBarsToDraw; i++) {
            str = str + defaults.progress.zero;
        }
        str = str + this.getFormat(defaults.progress.sufix, prc, minOrChange, max);

        if (effects instanceof Array) {
            str = this.apply(str, effects);
        } else {
            str = this.apply(str, defaults.progress.effects);
        }

        if (defaults.progress.description.length) {
            str = str + ' ' + this.getFormat(defaults.progress.description);
        }

        //str = str.substr(0, consoleWidth);

        if (returnString === true) {
            lastCommand = 'progress';
            return str;
        }

        this.write(str);
        lastCommand = 'progress';

        return this;
    };


    /**
     * Return "reset all effects" code
     * @returns {*}
     */
    this.reset = function () {
        return use(reset);
    };


    /**
     * Short alias for bold
     * @param message
     * @returns {string}
     */
    this.b = function (message) {
        return text(message, effect.bold);
    };


    /**
     * Short alias for underline
     * @param message
     * @returns {string}
     */
    this.u = function (message) {
        return text(message, effect.underline);
    };


    /**
     * Short alias for strike
     * @param message
     * @returns {string}
     */
    this.s = function (message) {
        return text(message, effect.strike);
    };


    /**
     * Short alias for inverse
     * @param message
     * @returns {string}
     */
    this.i = function (message) {
        return text(message, effect.inverse);
    };


    /**
     * Make given text bold
     * @param message
     * @returns {string}
     */
    this.bold = function (message) {
        return this.b(message);
    };


    /**
     * Apply underline to text
     * @param message
     * @returns {string}
     */
    this.underline = function (message) {
        return this.u(message);
    };


    /**
     * Draw line on the text
     * @param message
     * @returns {string}
     */
    this.strike = function (message) {
        return this.s(message);
    };


    /**
     * Switch foreground and background colors
     * @param message
     * @returns {string}
     */
    this.inverse = function (message) {
        return this.i(message);
    };


    /**
     * Apply color on text by name
     * @param message
     * @param name
     * @param light
     * @returns {string}
     */
    this.color = function (message, name, light) {
        if (color[name] === undefined) {
            var colors = [],
                c;

            for (c in color) {
                if (color.hasOwnProperty(c)) {
                    colors.push(c);
                }
            }
            throw new Error('Undefined color. Use: ' + colors.join(', '));
        }
        return text(message, color[name], light);
    };


    /**
     * Apply black color
     * @param message
     * @param light
     * @returns {string}
     */
    this.black = function (message, light) {
        return text(message, color.black, light);
    };


    /**
     * Apply red color
     * @param message
     * @param light
     * @returns {string}
     */
    this.red = function (message, light) {
        return text(message, color.red, light);
    };


    /**
     * Apply green color
     * @param message
     * @param light
     * @returns {string}
     */
    this.green = function (message, light) {
        return text(message, color.green, light);
    };


    /**
     * Apply yellow color
     * @param message
     * @param light
     * @returns {string}
     */
    this.yellow = function (message, light) {
        return text(message, color.yellow, light);
    };


    /**
     * Apply blue color
     * @param message
     * @param light
     * @returns {string}
     */
    this.blue = function (message, light) {
        return text(message, color.blue, light);
    };


    /**
     * Apply magenta color
     * @param message
     * @param light
     * @returns {string}
     */
    this.magenta = function (message, light) {
        return text(message, color.magenta, light);
    };


    /**
     * Apply cyan color
     * @param message
     * @param light
     * @returns {string}
     */
    this.cyan = function (message, light) {
        return text(message, color.cyan, light);
    };


    /**
     * Apply white color
     * @param message
     * @param light
     * @returns {string}
     */
    this.white = function (message, light) {
        return text(message, color.white, light);
    };


    /**
     * Apply black color
     * @param message
     * @param light
     * @returns {string}
     */
    this.colorBlack = function (message, light) {
        return this.black(message, light);
    };


    /**
     * Apply red color
     * @param message
     * @param light
     * @returns {string}
     */
    this.colorRed = function (message, light) {
        return this.red(message, light);
    };


    /**
     * Apply green color
     * @param message
     * @param light
     * @returns {string}
     */
    this.colorGreen = function (message, light) {
        return this.green(message, light);
    };


    /**
     * Apply yellow color
     * @param message
     * @param light
     * @returns {string}
     */
    this.colorYellow = function (message, light) {
        return this.yellow(message, light);
    };


    /**
     * Apply blue color
     * @param message
     * @param light
     * @returns {string}
     */
    this.colorBlue = function (message, light) {
        return this.blue(message, light);
    };


    /**
     * Apply magenta color
     * @param message
     * @param light
     * @returns {string}
     */
    this.colorMagenta = function (message, light) {
        return this.magenta(message, light);
    };


    /**
     * Apply cyan color
     * @param message
     * @param light
     * @returns {string}
     */
    this.colorCyan = function (message, light) {
        return this.cyan(message, light);
    };


    /**
     * Apply white color
     * @param message
     * @param light
     * @returns {string}
     */
    this.colorWhite = function (message, light) {
        return this.white(message, light);
    };


    /**
     * Apply background color by name
     * @param message
     * @param name
     * @param light
     * @returns {string}
     */
    this.background = function (message, name, light) {
        if (background[name] === undefined) {
            var backgrounds = [],
                b;

            for (b in background) {
                if (background.hasOwnProperty(b)) {
                    backgrounds.push(b);
                }
            }
            throw new Error('Undefined color. Use: ' + backgrounds.join(', '));
        }
        return text(message, background[name], light);
    };


    /**
     * Apply black background
     * @param message
     * @param light
     * @returns {string}
     */
    this.bgBlack = function (message, light) {
        return text(message, background.black, light);
    };


    /**
     * Apply red background
     * @param message
     * @param light
     * @returns {string}
     */
    this.bgRed = function (message, light) {
        return text(message, background.red, light);
    };


    /**
     * Apply green background
     * @param message
     * @param light
     * @returns {string}
     */
    this.bgGreen = function (message, light) {
        return text(message, background.green, light);
    };


    /**
     * Apply yellow background
     * @param message
     * @param light
     * @returns {string}
     */
    this.bgYellow = function (message, light) {
        return text(message, background.yellow, light);
    };


    /**
     * Apply blue background
     * @param message
     * @param light
     * @returns {string}
     */
    this.bgBlue = function (message, light) {
        return text(message, background.blue, light);
    };


    /**
     * Apply magenta background
     * @param message
     * @param light
     * @returns {string}
     */
    this.bgMagenta = function (message, light) {
        return text(message, background.magenta, light);
    };


    /**
     * Apply cyan background
     * @param message
     * @param light
     * @returns {string}
     */
    this.bgCyan = function (message, light) {
        return text(message, background.cyan, light);
    };


    /**
     * Apply white background
     * @param message
     * @param light
     * @returns {string}
     */
    this.bgWhite = function (message, light) {
        return text(message, background.white, light);
    };


    /**
     * Apply black background
     * @param message
     * @param light
     * @returns {string}
     */
    this.backgroundBlack = function (message, light) {
        return this.bgBlack(message, light);
    };


    /**
     * Apply red background
     * @param message
     * @param light
     * @returns {string}
     */
    this.backgroundRed = function (message, light) {
        return this.bgRed(message, light);
    };


    /**
     * Apply green background
     * @param message
     * @param light
     * @returns {string}
     */
    this.backgroundGreen = function (message, light) {
        return this.bgGreen(message, light);
    };


    /**
     * Apply yellow background
     * @param message
     * @param light
     * @returns {string}
     */
    this.backgroundYellow = function (message, light) {
        return this.bgYellow(message, light);
    };


    /**
     * Apply blue background
     * @param message
     * @param light
     * @returns {string}
     */
    this.backgroundBlue = function (message, light) {
        return this.bgBlue(message, light);
    };


    /**
     * Apply magenta background
     * @param message
     * @param light
     * @returns {string}
     */
    this.backgroundMagenta = function (message, light) {
        return this.bgMagenta(message, light);
    };


    /**
     * Apply cyan background
     * @param message
     * @param light
     * @returns {string}
     */
    this.backgroundCyan = function (message, light) {
        return this.bgCyan(message, light);
    };


    /**
     * Apply white background
     * @param message
     * @param light
     * @returns {string}
     */
    this.backgroundWhite = function (message, light) {
        return this.bgWhite(message, light);
    };


    /**
     * Alias for console.log
     * @param message
     * @param light
     * @returns {string}
     */
    this.log = function (message) {
        if (beSilent) {
            return this;
        }
        if (lastCommand === 'progress' || lastCommand === 'write') {
            this.write('\n');
        }
        lastCommand = 'log';
        console.log(message);
        message += '';
        lastLineLength = message.length;

        return this;
    };


    /**
     * Write to stdout
     * @param message
     * @param light
     * @returns {string}
     */
    this.write = function (message) {
        if (beSilent) {
            return this;
        }
        lastCommand = 'write';
        process.stdout.write(message);

        message = String(message);
        if (lastCommand === 'write') {
            lastLineLength += message.length;
        } else {
            lastLineLength = message.length;
        }

        return this;
    };


    /**
     * Log with bold white text
     * @param message
     * @returns {string}
     */
    this.info = function (message, returnString) {
        var str = this.apply(message, ['bold', 'white']);
        return returnString === true ? str : this.log(str);
    };


    /**
     * Log with bold green text
     * @param message
     * @returns {string}
     */
    this.success = function (message, returnString) {
        var str = this.apply(message, ['bold', 'green']);
        return returnString === true ? str : this.log(str);
    };


    /**
     * Log with bold yellow text
     * @param message
     * @returns {string}
     */
    this.warning = function (message, returnString) {
        var str = this.apply(message, ['bold', 'yellow']);
        return returnString === true ? str : this.log(str);
    };


    /**
     * Log with bold red text
     * @param message
     * @returns {string}
     */
    this.error = function (message, returnString) {
        var str = this.apply(message, ['bold', 'red']);
        return returnString === true ? str : this.log(str);
    };


    /**
     * Log with bold cyan text
     * @param message
     * @returns {string}
     */
    this.question = function (message, returnString) {
        var str = this.apply(message, ['bold', 'cyan']);
        return returnString === true ? str : this.log(str);
    };


    /**
     * Log with bold magenta text
     * @param message
     * @returns {string}
     */
    this.answer = function (message, returnString) {
        var str = this.apply(message, ['bold', 'magenta']);
        return returnString === true ? str : this.log(str);
    };


    /**
     * Log with bold black text and white background
     * @param message
     * @returns {string}
     */
    this.headerInfo = function (message, returnString) {
        var str = this.apply(message, ['bold', 'black', 'bgWhite']);
        return returnString === true ? str : this.log(str);
    };


    /**
     * Log with bold white text and green background
     * @param message
     * @returns {string}
     */
    this.headerSuccess = function (message, returnString) {
        var str = this.apply(message, ['bold', 'white', 'bgGreen']);
        return returnString === true ? str : this.log(str);
    };


    /**
     * Log with bold black text and yellow background
     * @param message
     * @returns {string}
     */
    this.headerWarning = function (message, returnString) {
        var str = this.apply(message, ['bold', 'black', 'bgYellow']);
        return returnString === true ? str : this.log(str);
    };


    /**
     * Log with bold white text and red background
     * @param message
     * @returns {string}
     */
    this.headerError = function (message, returnString) {
        var str = this.apply(message, ['bold', 'white', 'bgRed']);
        return returnString === true ? str : this.log(str);
    };


    /**
     * Log with bold white text and cyan background
     * @param message
     * @returns {string}
     */
    this.headerQuestion = function (message, returnString) {
        var str = this.apply(message, ['bold', 'white', 'bgCyan']);
        return returnString === true ? str : this.log(str);
    };


    /**
     * Log with bold white text and magenta background
     * @param message
     * @returns {string}
     */
    this.headerAnswer = function (message, returnString) {
        var str = this.apply(message, ['bold', 'white', 'bgMagenta']);
        return returnString === true ? str : this.log(str);
    };


    /**
     * Apply effects to message
     * @param message
     * @param effects
     * @returns {*}
     */
    this.apply = function (message, effects) {
        if ((effects instanceof Array) === false) {
            throw new Error('Effects must be an array');
        }
        if (!effects.length) {
            return message;
        }
        var i = 0,
            limit = effects.length;

        for (i = 0; i < limit; i++) {
            if (this.hasOwnProperty(effects[i]) && typeof this[effects[i]] === 'function') {
                message = this[effects[i]](message);
            } else {
                var allEffects = [],
                    e = '';

                for (e in this) {
                    if (this.hasOwnProperty(e) && ((e.indexOf('color') === 0 && e !== 'color') || (e.indexOf('background') === 0 && e !== 'background') || effect[e] !== undefined)) {
                        allEffects.push(e);
                    }
                }
                throw new Error(util.format('Invalid effect "%s", you can use "%s"', effects[i], allEffects.join('", "')));
            }
        }

        return message;
    };


    /**
     * Alias for console.log that allow you to apply effects to dump
     * @param variable
     * @param effects
     * @returns {*}
     */
    this.dump = function (variable, effects) {
        if (beSilent) {
            return this;
        }
        if (!(effects instanceof Array)) {
            effects = [];
        }

        effects = this.apply('\n', effects);
        effects = effects.split('\n');
        this.write(effects[0]);
        this.write(util.inspect((typeof variable === 'function') ? variable.toString() : variable));
        this.write(effects[1] + '\n');

        return this;
    };


    /**
     * Get list of available effects
     * @returns {Array}
     */
    this.getAllEffects = function () {
        var allEffects = [],
            e = '';

        for (e in this) {
            if (this.hasOwnProperty(e) && ((e.indexOf('color') === 0 && e !== 'color') || ((e.indexOf('background') === 0 || e.indexOf('bg') === 0) && e !== 'background') || effect[e] !== undefined || color[e] !== undefined)) {
                allEffects.push(e);
            }
        }

        return allEffects;
    };


    /**
     * Log message with xml tags using effects name. For example <b>text</b>
     * @param message
     * @param log
     * @returns {*}
     */
    this.format = function () {
        if (beSilent) {
            return this;
        }
        this.log(this.getFormat.apply(this.getFormat, arguments));
        return this;
    };


    /**
     * Log message with xml tags using effects name. For example <b>text</b>
     * @param message
     * @param log
     * @returns {*}
     */
    this.getFormat = function () {
        var message = arguments.length > 0 ? arguments[0] : '';
        message = util.format.apply(util.format, arguments);
        var all = self.getAllEffects(),
            i = 0,
            limit = all.length,
            applyCallback = function (match, content) {
                return self.apply(content, [all[i]]);
            };
        for (i = 0; i < limit; i++) {
            message = message.replace(new RegExp(util.format('<%s>([\\s\\S]*?)<\/%s>', all[i], all[i]), 'g'), applyCallback);
        }

        return message;
    };


    this.stripFormat = function () {
        var message = arguments.length > 0 ? arguments[0] : '';
        message = util.format.apply(util.format, arguments);

        var all = this.getAllEffects(),
            i = 0,
            limit = all.length,
            applyCallback = function (match, content) {
                return content;
            };
        for (i = 0; i < limit; i++) {
            message = message.replace(new RegExp(util.format('<%s>([\\s\\S]*?)<\/%s>', all[i], all[i]), 'g'), applyCallback);
        }

        return message;
    };


    /**
     * Print new line
     * @returns {*}
     */
    this.nl = function () {
        this.write('\n');
        return this;
    };


    /**
     * Alias for nl
     * @type {*}
     */
    this.newLine = this.nl;


    this.status = function (text, status, textEffects, statusEffects, returnString) {
        if (beSilent) {
            return this;
        }
        if (text instanceof Object) {
            text = text.toString();
        }
        if (status instanceof Object) {
            status = status.toString();
        }

        var textLen = this.stripFormat(text).length,
            statusLen = this.stripFormat(status).length,
            emptyLength = consoleWidth - (textLen + statusLen),
            emptyString = '',
            i = 0,
            str = '';

        text = this.getFormat(text);
        status = this.getFormat(status);
        if (textEffects instanceof Array) {
            text = this.apply(text, textEffects);
        }
        if (statusEffects instanceof Array) {
            status = this.apply(status, statusEffects);
        }

        if (emptyLength < 2) {
            emptyLength = 2;
        }
        emptyLength -= 1;

        for (i = 0; i < emptyLength; i++) {
            emptyString = emptyString + ' ';
        }

        str = text + emptyString + status;

        if (returnString === true) {
            return str;
        }
        this.log(str);
        return this;
    };
}