#!/usr/bin/python
# encoding: utf-8
#
# Copyright 2017 Greg Neagle.
#
# Licensed 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
#
#      https://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.
"""
app_usage_monitor

Created by Greg Neagle 14 Feb 2017

A tool to monitor application usage and record it to a database.
Borrowing lots of code and ideas from the crankd project, part of pyamcadmin:
    https://github.com/MacSysadmin/pymacadmin
and the application_usage scripts created by Google MacOps:
    https://github.com/google/macops/tree/master/crankd
"""

# standard Python libs
import logging
import os
import sys

try:
    # Apple frameworks
    from AppKit import NSWorkspace
    from Foundation import NSDate
    from Foundation import NSDictionary
    from Foundation import NSDistributedNotificationCenter
    from Foundation import NSObject
    from Foundation import NSRunLoop
except ImportError:
    logging.critical("PyObjC wrappers for Apple frameworks are missing.")
    sys.exit(-1)

# our libs
from munkilib import app_usage
from munkilib import prefs


class NotificationHandler(NSObject):
    """A subclass of NSObject to handle workspace notifications"""

    # Disable PyLint complaining about 'invalid' camelCase names
    # pylint: disable=C0103

    def init(self):
        """NSObject-compatible initializer"""
        self = super(NotificationHandler, self).init()
        if self is None:
            return None

        self.usage = app_usage.ApplicationUsageRecorder()
        self.ws_nc = NSWorkspace.sharedWorkspace().notificationCenter()

        self.ws_nc.addObserver_selector_name_object_(
            self, 'didLaunchApplicationNotification:',
            'NSWorkspaceDidLaunchApplicationNotification', None)

        self.ws_nc.addObserver_selector_name_object_(
            self, 'didActivateApplicationNotification:',
            'NSWorkspaceDidActivateApplicationNotification', None)

        self.ws_nc.addObserver_selector_name_object_(
            self, 'didTerminateApplicationNotification:',
            'NSWorkspaceDidTerminateApplicationNotification', None)

        self.dnc = NSDistributedNotificationCenter.defaultCenter()
        self.dnc.addObserver_selector_name_object_(
            self, 'requestedItemForInstall:',
            'com.googlecode.munki.managedsoftwareupdate.installrequest', None)

        return self # NOTE: Unlike Python, NSObject's init() must return self!

    def __del__(self):
        """Unregister for all the notifications we registered for"""
        self.ws_nc.removeObserver_(self)
        self.dnc.removeObserver_(self)

    def get_app_dict(self, app_object):
        """Returns a dict with info about an application.
        Args:
            app_object: NSRunningApplication object
        Returns:
            app_dict: {bundle_id: str,
                       path: str,
                       version: str}"""
        # pylint: disable=no-self-use
        if app_object:
            try:
                url = app_object.bundleURL()
                app_path = url.path()
            except AttributeError:
                app_path = None
            try:
                bundle_id = app_object.bundleIdentifier()
            except AttributeError:
                # use the base filename
                if app_path:
                    bundle_id = os.path.basename(app_path)
            if app_path:
                app_info_plist = NSDictionary.dictionaryWithContentsOfFile_(
                    '%s/Contents/Info.plist' % app_path)
                if app_info_plist:
                    app_version = app_info_plist.get(
                        'CFBundleShortVersionString',
                        app_info_plist.get('CFBundleVersion', '0'))
                else:
                    app_version = '0'

        return {'bundle_id': bundle_id,
                'path': app_path,
                'version': app_version}

    def didLaunchApplicationNotification_(self, notification):
        """Handle NSWorkspaceDidLaunchApplicationNotification"""
        app_object = notification.userInfo().get('NSWorkspaceApplicationKey')
        app_dict = self.get_app_dict(app_object)
        self.usage.log_application_usage('launch', app_dict)

    def didActivateApplicationNotification_(self, notification):
        """Handle NSWorkspaceDidActivateApplicationNotification"""
        app_object = notification.userInfo().get('NSWorkspaceApplicationKey')
        app_dict = self.get_app_dict(app_object)
        self.usage.log_application_usage('activate', app_dict)

    def didTerminateApplicationNotification_(self, notification):
        """Handle NSWorkspaceDidTerminateApplicationNotification"""
        app_object = notification.userInfo().get('NSWorkspaceApplicationKey')
        app_dict = self.get_app_dict(app_object)
        self.usage.log_application_usage('quit', app_dict)

    def requestedItemForInstall_(self, notification):
        """Handle com.googlecode.munki.managedsoftwareupdate.installrequest"""
        logging.info('got install request notification')
        user_info = notification.userInfo()
        self.usage.log_install_request(user_info)


def main():
    """Initialize our handler object and let NSWorkspace's notification center
    know we are interested in notifications"""

    # configure logging
    logpath = os.path.join(
        os.path.dirname(prefs.pref('LogFile')), 'app_usage_monitor.log')
    logging.basicConfig(
        filename=logpath,
        format='%(asctime)s %(levelname)s:%(message)s',
        level=logging.INFO)

    logging.info('app_usage_monitor started')
    # PyLint can't tell that NotificationHandler' NSObject superclass
    # has an alloc() method
    # pylint: disable=no-member
    notification_handler = NotificationHandler.alloc().init()
    # pylint: enable=no-member

    while True:
        # listen for notifications forever
        NSRunLoop.currentRunLoop().runUntilDate_(
            NSDate.dateWithTimeIntervalSinceNow_(0.1))


if __name__ == '__main__':
    main()
