#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright (C) 2016 Shea G Craig
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

"""
Help user set default mail client despite OS X bug.

Sets the default mail reader to Apple Mail (because we configure our
clients to use Outlook) using the LaunchServices framework, and then
immediately log out.

Displays a dialog instructing user to close open applications that may
block logout from occuring, rather than execute a Volcanic Instant Death
Logout.

This uses code from my auto_logout project for the applescripting and
alert dialog presentation.

This was written to be part of an OnDemand item in Munki's Managed
Software Center, using outset's on-demand feature to run as the current
console user (as it would otherwise run as root and not work).
"""


import os
import subprocess
import sys

# pylint: disable=no-name-in-module
from AppKit import (NSImage, NSAlert, NSTimer, NSRunLoop, NSApplication,
                    NSSound, NSModalPanelRunLoopMode, NSApp,
                    NSRunAbortedResponse, NSAlertFirstButtonReturn)
# pylint: enable=no-name-in-module
from LaunchServices import LSSetDefaultHandlerForURLScheme
from SystemConfiguration import SCDynamicStoreCopyConsoleUser


# Sound played when alert is presented. See README.
ALERT_SOUND = "Submarine"
# Icon used in the alerts. If not present, the Python rocket is used
# instead.
ICON = "/usr/local/sas/sas.png"


# Methods are named according to PyObjC/Cocoa style.
# pylint: disable=invalid-name
class Alert(NSAlert):
    """Subclasses NSAlert to include a timeout."""

    def init(self):  # pylint: disable=super-on-old-class
        """Add an instance variable for our timer."""
        self = super(Alert, self).init()
        self.timer = None
        self.alert_sound = None
        return self

    def setIconWithContentsOfFile_(self, path):
        """Convenience method for adding an icon.
        Args:
            path: String path to a valid NSImage filetype (png)
        """
        icon = NSImage.alloc().initWithContentsOfFile_(path)
        self.setIcon_(icon)  # pylint: disable=no-member

    def setAlertSound_(self, name):
        """Set the sound to play when alert is presented.
        Args:
            name: String name of a system sound. See the README.
        """
        self.alert_sound = name

    def setTimeToGiveUp_(self, time):
        """Configure alert to give up after time seconds."""
        # Cocoa objects must use class func alloc().init(), so pylint
        # doesn't see our init().
        # pylint: disable=attribute-defined-outside-init
        self.timer = \
            NSTimer.timerWithTimeInterval_target_selector_userInfo_repeats_(
                time, self, "_killWindow", None, False)
        # pylint: enable=attribute-defined-outside-init

    def present(self):
        """Present the Alert, giving up after configured time..
        Returns: Int result code, based on PyObjC enums. See NSAlert
            Class reference, but result should be one of:
                User clicked the cancel button:
                    NSAlertFirstButtonReturn = 1000
                Alert timed out:
                    NSRunAbortedResponse = -1001
        """
        if self.timer:
            NSRunLoop.currentRunLoop().addTimer_forMode_(
                self.timer, NSModalPanelRunLoopMode)
        # Start a Cocoa application by getting the shared app object.
        # Make the python app the active app so alert is noticed.
        app = NSApplication.sharedApplication()
        app.activateIgnoringOtherApps_(True)
        if self.alert_sound:
            sound = NSSound.soundNamed_(self.alert_sound).play()
        result = self.runModal()  # pylint: disable=no-member
        print result
        return result

    # pylint: disable=no-self-use
    def _killWindow(self):
        """Abort the modal window as managed by NSApp."""
        NSApp.abortModal()
    # pylint: enable=no-self-use

# pylint: enable=no-init
# pylint: enable=invalid-name


def build_alert():
    """Build an alert for auto-logout notifications."""
    alert = Alert.alloc().init()  # pylint: disable=no-member
    alert.setMessageText_(
        "Setting the default mail reader requires an immediate logout "
        "due to a bug in OS X.")
    alert.setInformativeText_("Please quit all applications and hit 'Okay'. "
                              "Your computer will then logout.")
    alert.addButtonWithTitle_("Okay")
    alert.addButtonWithTitle_("Cancel")
    alert.setIconWithContentsOfFile_(ICON)
    alert.setAlertSound_(ALERT_SOUND)
    return alert


def set_mail_reader(bundle_id):
    """Use LaunchServices to set mailto handler.

    There is a bug in OS X that allows you to set this only once.
    Afterwards, if you set it again, it will revert to the previous
    setting within about 10 seconds. Until this is fixed, logging out
    really quickly seems to work around it.

    Args:
        bundle_id (String): Bundle Identifier for the app to handle
            mail. Caps do not seem to matter.

    Returns:
        Integer return code (0 is a success) as per
        https://developer.apple.com/library/mac/documentation/Carbon/Reference/LaunchServicesReference/
    """
    return LSSetDefaultHandlerForURLScheme("mailto", bundle_id)


def really_log_out():
    """Log out without the prompt. Will still ask about open apps."""
    run_applescript('tell application "loginwindow" to «event aevtrlgo»')


def run_applescript(script):
    """Run an applescript"""
    process = subprocess.Popen(['osascript', '-'], stdout=subprocess.PIPE,
                               stdin=subprocess.PIPE, stderr=subprocess.PIPE)
    result, err = process.communicate(script)

    if err:
        raise Exception(err)
    return process.returncode


def build_abort_alert():
    """Build an alert for letting user know it failed."""
    alert = Alert.alloc().init()  # pylint: disable=no-member
    alert.setMessageText_(
        "Failed to set default mail handler")
    alert.setInformativeText_("Please contact the Helpdesk.")
    alert.addButtonWithTitle_("Okay")
    alert.addButtonWithTitle_("Cancel")
    alert.setIconWithContentsOfFile_(ICON)
    alert.setAlertSound_(ALERT_SOUND)
    return alert


def main():
    alert = build_alert()
    if alert.present() != NSAlertFirstButtonReturn:
        print "User Cancelled"
        sys.exit()
    else:
        if set_mail_reader("com.apple.mail") == 0:
            really_log_out()
        else:
            abort_alert = build_abort_alert()
            alert.present()
            sys.exit(1)


if __name__ == "__main__":
    main()