/*
       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.
*/

// requires
var path = require('path');
var et = require('elementtree');
var ConfigParser = require('./ConfigParser.js');
var nopt = require('nopt');

var spawn = require('cordova-common').superspawn.spawn;
var execSync = require('child_process').execSync;

// paths
var platformRoot = path.join(__dirname, '..', '..');
var projectRoot = path.join(platformRoot, '..', '..');
var configPath = path.join(projectRoot, 'config.xml');

// constants
var APP_TRACING_LOG = 'Microsoft-Windows-AppHost/ApplicationTracing';
var ADMIN_LOG = 'Microsoft-Windows-AppHost/Admin';
var ONE_MINUTE = 60 * 1000;

// variables
var appTracingInitialState = null;
var appTracingCurrentState = null;
var adminInitialState = null;
var adminCurrentState = null;
var timers = [];
var timeDiff = 10 * ONE_MINUTE; // show last 10 minutes by default
var appName;

/*
 * Gets windows AppHost/ApplicationTracing and AppHost/Admin logs
 * and prints them to console
 */
module.exports.run = function (args) {
    var knownOpts = { 'minutes': Number, 'dump': Boolean, 'help': Boolean };
    var shortHands = { 'mins': ['--minutes'], 'h': ['--help'] };
    var parsedOpts = nopt(knownOpts, shortHands, args, 0);

    if (parsedOpts.help) {
        module.exports.help();
        return;
    }
    if (parsedOpts.dump) {
        if (parsedOpts.minutes) {
            timeDiff = parsedOpts.minutes * ONE_MINUTE;
        }
        dumpLogs(timeDiff);
        return;
    }

    getLogState(ADMIN_LOG).then(function (state) {
        adminInitialState = adminCurrentState = state;
        return getLogState(APP_TRACING_LOG);
    }).then(function (state) {
        appTracingInitialState = appTracingCurrentState = state;
        if (!adminCurrentState) {
            return enableChannel(ADMIN_LOG).then(function () {
                return getLogState(ADMIN_LOG);
            }).then(function (state) {
                adminCurrentState = state;
            });
        }
    }).then(function () {
        if (!appTracingCurrentState) {
            return enableChannel(APP_TRACING_LOG).then(function () {
                return getLogState(APP_TRACING_LOG);
            }).then(function (state) {
                appTracingCurrentState = state;
            });
        }
    }).then(function () {
        if (!adminCurrentState && !appTracingCurrentState) {
            throw 'No log channels enabled. Exiting...';
        }
        try {
            var config = new ConfigParser(configPath);
            appName = config.name();
        } catch (err) {
            console.warn('Unable to read app name from config, showing logs for all applications.');
        }
    }).then(function () {
        console.log('Now printing logs. To stop, press Ctrl+C once.');
        startLogging(ADMIN_LOG);
        startLogging(APP_TRACING_LOG);
    }).catch(function (error) {
        console.error(error);
    });

    // Catch Ctrl+C message and exit gracefully
    process.once('SIGINT', function () {
        exitGracefully(0);
    });

    // Catch SIGTERM message and exit gracefully
    process.once('SIGTERM', function () {
        exitGracefully(0);
    });

    // Catch uncaught exceptions, print trace, then exit gracefully
    process.once('uncaughtException', function (e) {
        console.log(e.stack);
        exitGracefully(1);
    });
};

module.exports.help = function () {
    console.log();
    console.log('Usage: ' + path.relative(process.cwd(), path.join(platformRoot, 'cordova', 'log [options]')));
    console.log('Continuously prints your app logs to the command line.');
    console.log('Please run with Administrator privileges or manually enable Microsoft-Windows-AppHost/ApplicationTracing channel in Event Viewer.');
    console.log();
    console.log('Options:');
    console.log('  --dump: Dumps logs to console instead of continuous output.');
    console.log('  --mins <minutes>: Used only with --dump. Dumps logs starting from this much minutes back.');
    console.log();
    console.log('  Example: ' + path.relative(process.cwd(), path.join(platformRoot, 'cordova', 'log --dump --mins 5')));
    process.exit(0);
};

function exitGracefully (exitCode) {
    if (appTracingInitialState === false && appTracingCurrentState === true) {
        disableChannel(APP_TRACING_LOG);
    }
    if (adminInitialState === false && adminCurrentState === true) {
        disableChannel(ADMIN_LOG);
    }
    timers.forEach(function (timer) {
        clearInterval(timer);
    });
    // give async call some time to execute
    console.log('Exiting in 2 seconds. Do not interrupt the process!');
    setTimeout(function () {
        process.exit(exitCode);
    }, 2000);
}

function startLogging (channel) {
    var lastPollDate = new Date();
    timers.push(setInterval(function () {
        timeDiff = (new Date()).getTime() - lastPollDate.getTime();
        var events = getEvents(channel, timeDiff);
        events.forEach(function (evt) {
            console.log(stringifyEvent(evt));
        });
        lastPollDate = new Date();
    }, 1000));
}

function dumpLogs (timeDiff) {
    console.log('Dumping logs dating back ' + msToHumanReadable(timeDiff));
    var appTracingEvents = getEvents(APP_TRACING_LOG, timeDiff);
    var adminEvents = getEvents(ADMIN_LOG, timeDiff);
    appTracingEvents.concat(adminEvents)
        .sort(function (evt1, evt2) {
            if (evt1.timeCreated < evt2.timeCreated) {
                return -1;
            } else if (evt1.timeCreated > evt2.timeCreated) {
                return 1;
            }
            return 0;
        })
        .forEach(function (evt) {
            console.log(stringifyEvent(evt));
        });
}

function getEvents (channel, timeDiff) {
    var command = 'wevtutil';
    var args = ['qe', channel, '/q:"*[System [TimeCreated[timediff(@SystemTime)<=' + timeDiff + ']]]"', '/e:root'];
    command = command + ' ' + args.join(' ');
    var events = execSync(command);
    return parseEvents(events.toString());
}

function getElementValue (et, element, attribute) {
    var result;

    var found = et.findall(element);
    if (found.length > 0) {
        if (attribute) {
            result = found[0].get(attribute);
        } else {
            result = found[0].text;
        }
    }

    return result;
}

function parseEvents (output) {
    var etree = et.parse(output);
    var events = etree.getroot().findall('./Event');
    var results = [];

    events.forEach(function (event) {
        // Get only informative logs
        if ((getElementValue(event, './System/Channel') === ADMIN_LOG) &&
            (typeof getElementValue(event, './UserData/WWAUnhandledApplicationException') === 'undefined') &&
            (typeof getElementValue(event, './UserData/WWATerminateApplication') === 'undefined')) {
            return;
        }
        if ((getElementValue(event, './System/Channel') === APP_TRACING_LOG) &&
            (typeof getElementValue(event, './UserData/WWADevToolBarLog') === 'undefined')) {
            return;
        }

        var result = {
            channel: getElementValue(event, './System/Channel'),
            timeCreated: getElementValue(event, './System/TimeCreated', 'SystemTime'),
            pid: getElementValue(event, './System/Execution', 'ProcessID'),
            source: getElementValue(event, './UserData/WWADevToolBarLog/Source'),
            documentFile: getElementValue(event, './UserData/WWADevToolBarLog/DocumentFile') ||
                              getElementValue(event, './UserData/WWAUnhandledApplicationException/DocumentFile') ||
                              getElementValue(event, './UserData/WWATerminateApplication/DocumentFile'),
            displayName: getElementValue(event, './UserData/WWADevToolBarLog/DisplayName') ||
                              getElementValue(event, './UserData/WWAUnhandledApplicationException/DisplayName') ||
                              getElementValue(event, './UserData/WWATerminateApplication/DisplayName'),
            line: getElementValue(event, './UserData/WWADevToolBarLog/Line'),
            column: getElementValue(event, './UserData/WWADevToolBarLog/Column'),
            sourceFile: getElementValue(event, './UserData/WWAUnhandledApplicationException/SourceFile'),
            sourceLine: getElementValue(event, './UserData/WWAUnhandledApplicationException/SourceLine'),
            sourceColumn: getElementValue(event, './UserData/WWAUnhandledApplicationException/SourceColumn'),
            message: getElementValue(event, './UserData/WWADevToolBarLog/Message'),
            appName: getElementValue(event, './UserData/WWAUnhandledApplicationException/ApplicationName'),
            errorType: getElementValue(event, './UserData/WWAUnhandledApplicationException/ErrorType'),
            errorDescription: getElementValue(event, './UserData/WWAUnhandledApplicationException/ErrorDescription') ||
                              getElementValue(event, './UserData/WWATerminateApplication/ErrorDescription'),
            stackTrace: getElementValue(event, './UserData/WWAUnhandledApplicationException/StackTrace') ||
                              getElementValue(event, './UserData/WWATerminateApplication/StackTrace')
        };

        // filter out events from other applications
        if (typeof result.displayName !== 'undefined' && typeof appName !== 'undefined' && result.displayName !== appName) {
            return;
        }

        // do not show Process ID, App Name and Display Name for filtered events
        if (typeof appName !== 'undefined') {
            result.pid = undefined;
            result.appName = undefined;
            result.displayName = undefined;
        }

        // cut out uninformative fields
        if ((result.line === '0') && (result.column === '0')) {
            result.line = undefined;
            result.column = undefined;
        }

        // trim whitespace
        if (typeof result.errorDescription !== 'undefined') {
            result.errorDescription = result.errorDescription.trim();
        }
        if (typeof result.message !== 'undefined') {
            result.message = result.message.trim();
        }

        results.push(result);
    });

    return results;
}

function formatField (event, fieldName, fieldShownName, offset) {
    var whitespace = ''; // to align all field values
    var multiLineWhitespace = ' '; // to align multiline fields (i.e. Stack Trace) correctly
    for (var i = 0; i < offset; i++) {
        if (i >= fieldShownName.length) {
            whitespace += ' ';
        }
        multiLineWhitespace += ' ';
    }

    if (event.hasOwnProperty(fieldName) && (typeof event[fieldName] !== 'undefined')) {
        event[fieldName] = event[fieldName].replace(/\n\s*/g, '\n' + multiLineWhitespace);
        return ('\n' + fieldShownName + ':' + whitespace + event[fieldName]).replace(/\n$/m, '');
    }
    return '';
}

function stringifyEvent (event) {
    if (typeof event === 'undefined') {
        return;
    }

    var result = '';
    var offset = 18;

    result += formatField(event, 'channel', 'Channel', offset);
    result += formatField(event, 'timeCreated', 'Time Created', offset);
    result += formatField(event, 'pid', 'Process ID', offset);
    result += formatField(event, 'source', 'Source', offset);
    result += formatField(event, 'documentFile', 'Document File', offset);
    result += formatField(event, 'displayName', 'Display Name', offset);
    result += formatField(event, 'line', 'Line', offset);
    result += formatField(event, 'column', 'Column', offset);
    result += formatField(event, 'message', 'Message', offset);
    result += formatField(event, 'appName', 'App Name', offset);
    result += formatField(event, 'errorType', 'Error Type', offset);
    result += formatField(event, 'errorDescription', 'Error Description', offset);
    result += formatField(event, 'sourceFile', 'Source File', offset);
    result += formatField(event, 'sourceLine', 'Source Line', offset);
    result += formatField(event, 'sourceColumn', 'Source Column', offset);
    result += formatField(event, 'stackTrace', 'Stack Trace', offset);

    return result;
}

function getLogState (channel) {
    return spawn('wevtutil', ['get-log', channel])
        .then(function (output) {
            return output.indexOf('enabled: true') !== -1;
        });
}

function enableChannel (channel) {
    console.log('Enabling channel ' + channel);
    return spawn('wevtutil', ['set-log', channel, '/e:false', '/q:true'])
        .then(function () {
            return spawn('wevtutil', ['set-log', channel, '/e:true', '/rt:true', '/ms:4194304', '/q:true']);
        }, function () {
            console.warn('Cannot enable log channel: ' + channel);
            console.warn('Try running the script with administrator privileges.');
        });
}

function disableChannel (channel) {
    console.log('Disabling channel ' + channel);
    spawn('wevtutil', ['set-log', channel, '/e:false', '/q:true']);
}

function msToHumanReadable (ms) {
    var m = Math.floor(ms / 60000);
    ms -= m * 60000;
    var s = ms / 1000;
    return m + 'm ' + s + 's';
}
