/*
 *
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 *
*/

// ------------------------------------------------------------------------------
// The logger module exports the following properties/functions:
//
// LOG                          - constant for the level LOG
// ERROR                        - constant for the level ERROR
// WARN                         - constant for the level WARN
// INFO                         - constant for the level INFO
// DEBUG                        - constant for the level DEBUG
// logLevel()                   - returns current log level
// logLevel(value)              - sets and returns a new log level
// useConsole()                 - returns whether logger is using console
// useConsole(value)            - sets and returns whether logger is using console
// log(message,...)             - logs a message at level LOG
// error(message,...)           - logs a message at level ERROR
// warn(message,...)            - logs a message at level WARN
// info(message,...)            - logs a message at level INFO
// debug(message,...)           - logs a message at level DEBUG
// logLevel(level,message,...)  - logs a message specified level
//
// ------------------------------------------------------------------------------

var logger = exports;

var exec = require('cordova/exec');

var UseConsole = false;
var UseLogger = true;
var Queued = [];
var DeviceReady = false;
var CurrentLevel;

var originalConsole = console;

/**
 * Logging levels
 */

var Levels = [
    'LOG',
    'ERROR',
    'WARN',
    'INFO',
    'DEBUG'
];

/*
 * add the logging levels to the logger object and
 * to a separate levelsMap object for testing
 */

var LevelsMap = {};
for (var i = 0; i < Levels.length; i++) {
    var level = Levels[i];
    LevelsMap[level] = i;
    logger[level] = level;
}

CurrentLevel = LevelsMap.WARN;

/**
 * Getter/Setter for the logging level
 *
 * Returns the current logging level.
 *
 * When a value is passed, sets the logging level to that value.
 * The values should be one of the following constants:
 *    logger.LOG
 *    logger.ERROR
 *    logger.WARN
 *    logger.INFO
 *    logger.DEBUG
 *
 * The value used determines which messages get printed.  The logging
 * values above are in order, and only messages logged at the logging
 * level or above will actually be displayed to the user.  E.g., the
 * default level is WARN, so only messages logged with LOG, ERROR, or
 * WARN will be displayed; INFO and DEBUG messages will be ignored.
 */
logger.level = function (value) {
    if (arguments.length) {
        if (LevelsMap[value] === null) {
            throw new Error('invalid logging level: ' + value);
        }
        CurrentLevel = LevelsMap[value];
    }

    return Levels[CurrentLevel];
};

/**
 * Getter/Setter for the useConsole functionality
 *
 * When useConsole is true, the logger will log via the
 * browser 'console' object.
 */
logger.useConsole = function (value) {
    if (arguments.length) UseConsole = !!value;

    if (UseConsole) {
        if (typeof console === 'undefined') {
            throw new Error('global console object is not defined');
        }

        if (typeof console.log !== 'function') {
            throw new Error('global console object does not have a log function');
        }

        if (typeof console.useLogger === 'function') {
            if (console.useLogger()) {
                throw new Error('console and logger are too intertwingly');
            }
        }
    }

    return UseConsole;
};

/**
 * Getter/Setter for the useLogger functionality
 *
 * When useLogger is true, the logger will log via the
 * native Logger plugin.
 */
logger.useLogger = function (value) {
    // Enforce boolean
    if (arguments.length) UseLogger = !!value;
    return UseLogger;
};

/**
 * Logs a message at the LOG level.
 *
 * Parameters passed after message are used applied to
 * the message with utils.format()
 */
logger.log = function (message) { logWithArgs('LOG', arguments); };

/**
 * Logs a message at the ERROR level.
 *
 * Parameters passed after message are used applied to
 * the message with utils.format()
 */
logger.error = function (message) { logWithArgs('ERROR', arguments); };

/**
 * Logs a message at the WARN level.
 *
 * Parameters passed after message are used applied to
 * the message with utils.format()
 */
logger.warn = function (message) { logWithArgs('WARN', arguments); };

/**
 * Logs a message at the INFO level.
 *
 * Parameters passed after message are used applied to
 * the message with utils.format()
 */
logger.info = function (message) { logWithArgs('INFO', arguments); };

/**
 * Logs a message at the DEBUG level.
 *
 * Parameters passed after message are used applied to
 * the message with utils.format()
 */
logger.debug = function (message) { logWithArgs('DEBUG', arguments); };

// log at the specified level with args
function logWithArgs (level, args) {
    args = [level].concat([].slice.call(args));
    logger.logLevel.apply(logger, args);
}

// return the correct formatString for an object
function formatStringForMessage (message) {
    return (typeof message === 'string') ? '' : '%o';
}

/**
 * Logs a message at the specified level.
 *
 * Parameters passed after message are used applied to
 * the message with utils.format()
 */
logger.logLevel = function (level /* , ... */) {
    // format the message with the parameters
    var formatArgs = [].slice.call(arguments, 1);
    var fmtString = formatStringForMessage(formatArgs[0]);
    if (fmtString.length > 0) {
        formatArgs.unshift(fmtString); // add formatString
    }

    var message = logger.format.apply(logger.format, formatArgs);

    if (LevelsMap[level] === null) {
        throw new Error('invalid logging level: ' + level);
    }

    if (LevelsMap[level] > CurrentLevel) return;

    // queue the message if not yet at deviceready
    if (!DeviceReady && !UseConsole) {
        Queued.push([level, message]);
        return;
    }

    // Log using the native logger if that is enabled
    if (UseLogger) {
        exec(null, null, 'Console', 'logLevel', [level, message]);
    }

    // Log using the console if that is enabled
    if (UseConsole) {
        // make sure console is not using logger
        if (console.useLogger()) {
            throw new Error('console and logger are too intertwingly');
        }

        // log to the console
        switch (level) {
        case logger.LOG: originalConsole.log(message); break;
        case logger.ERROR: originalConsole.log('ERROR: ' + message); break;
        case logger.WARN: originalConsole.log('WARN: ' + message); break;
        case logger.INFO: originalConsole.log('INFO: ' + message); break;
        case logger.DEBUG: originalConsole.log('DEBUG: ' + message); break;
        }
    }
};

/**
 * Formats a string and arguments following it ala console.log()
 *
 * Any remaining arguments will be appended to the formatted string.
 *
 * for rationale, see FireBug's Console API:
 *    http://getfirebug.com/wiki/index.php/Console_API
 */
logger.format = function (formatString, args) {
    return __format(arguments[0], [].slice.call(arguments, 1)).join(' ');
};

// ------------------------------------------------------------------------------
/**
 * Formats a string and arguments following it ala vsprintf()
 *
 * format chars:
 *   %j - format arg as JSON
 *   %o - format arg as JSON
 *   %c - format arg as ''
 *   %% - replace with '%'
 * any other char following % will format it's
 * arg via toString().
 *
 * Returns an array containing the formatted string and any remaining
 * arguments.
 */
function __format (formatString, args) {
    if (formatString === null || formatString === undefined) return [''];
    if (arguments.length === 1) return [formatString.toString()];

    if (typeof formatString !== 'string') { formatString = formatString.toString(); }

    var pattern = /(.*?)%(.)(.*)/;
    var rest = formatString;
    var result = [];

    while (args.length) {
        var match = pattern.exec(rest);
        if (!match) break;

        var arg = args.shift();
        rest = match[3];
        result.push(match[1]);

        if (match[2] === '%') {
            result.push('%');
            args.unshift(arg);
            continue;
        }

        result.push(__formatted(arg, match[2]));
    }

    result.push(rest);

    var remainingArgs = [].slice.call(args);
    remainingArgs.unshift(result.join(''));
    return remainingArgs;
}

function __formatted (object, formatChar) {
    try {
        switch (formatChar) {
        case 'j':
        case 'o': return JSON.stringify(object);
        case 'c': return '';
        }
    } catch (e) {
        return 'error JSON.stringify()ing argument: ' + e;
    }

    if ((object === null) || (object === undefined)) {
        return Object.prototype.toString.call(object);
    }

    return object.toString();
}

// ------------------------------------------------------------------------------
// when deviceready fires, log queued messages
logger.__onDeviceReady = function () {
    if (DeviceReady) return;

    DeviceReady = true;

    for (var i = 0; i < Queued.length; i++) {
        var messageArgs = Queued[i];
        logger.logLevel(messageArgs[0], messageArgs[1]);
    }

    Queued = null;
};

// add a deviceready event to log queued messages
document.addEventListener('deviceready', logger.__onDeviceReady, false);
