#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# Poof: List and uninstall/remove macOS packages
# Copyright (c) 2011-2017 Rudá Moura <ruda.moura@gmail.com>
#

"""Poof is a command line utility to list and uninstall/remove macOS packages.

*NO WARRANTY* DON'T BLAME ME if you destroy your installation!
NEVER REMOVE com.apple.* packages unless you know what you are doing.

How it works:

It first removes all files and directories declared by the package and
then forget the metadata (the receipt data).

Usage:

List packages (but ignore all from Apple).

    $ ./poof.py | grep -v com.apple.pkg
    com.accessagility.wifiscanner
    com.adobe.pkg.FlashPlayer
    com.amazon.Kindle
    com.christiankienle.CoreDataEditor
    com.ea.realracing2.mac.bv
    com.google.pkg.GoogleVoiceAndVideo
    com.google.pkg.Keystone
    com.Growl.GrowlHelperApp
    com.lightheadsw.caffeine
    com.Logitech.Control Center.pkg
    ...

Remove FlashPlayer (com.adobe.pkg.FlashPlayer).

    $ sudo ./poof.py com.adobe.pkg.FlashPlayer
    ...
    Forgot package 'com.adobe.pkg.FlashPlayer' on '/'.
"""

from subprocess import Popen, PIPE
import sys
import os


class Shell(object):
    def __getattribute__(self, attr):
        return Command(attr)


class Command(object):
    def __init__(self, command):
        self.command = command

    def __call__(self, params=None):
        args = [self.command]
        if params:
            args += params.split()
        return self.run(args)

    def run(self, args):
        p = Popen(args, stdout=PIPE, stderr=PIPE)
        out, err = p.communicate()
        out, err = out.strip(), err.strip()
        if sys.version_info >= (3, 0):
            out, err = str(out, 'utf-8'), str(err, 'utf-8')
        if p.returncode == 0:
            return True, out.split('\n')
        else:
            return False, err.split('\n')


def package_list():
    sh = Shell()
    sts, out = sh.pkgutil('--pkgs')
    return out


def package_info(package_id):
    sh = Shell()
    ok, info = sh.pkgutil('--pkg-info ' + package_id)
    if ok == False:
        raise IOError('Unknown package or name mispelled')
    info = [x.split(': ') for x in info]
    return dict(info)


def package_files(package_id):
    sh = Shell()
    ok, files = sh.pkgutil('--only-files --files ' + package_id)
    ok, dirs = sh.pkgutil('--only-dirs --files ' + package_id)
    # Guess AppStore receipt
    for dir in dirs:
        if dir.endswith('.app'):
            dirs.append(dir + '/Contents/_MASReceipt')
            files.append(dir + '/Contents/_MASReceipt/receipt')
            break
    return files, dirs


def package_forget(package_id):
    sh = Shell()
    ok, msg = sh.pkgutil('--verbose --forget ' + package_id)
    return msg


def package_remove(package_id, force=True, verbose=False):
    try:
        info = package_info(package_id)
    except IOError as e:
        print("%s: '%s'" % (e, package_id))
        return False
    prefix = info['volume']
    if info['location']:
        prefix += info['location'] + os.sep
    files, dirs = package_files(package_id)
    files = [prefix + x for x in files]
    clean = True
    for path in files:
        try:
            os.remove(path)
        except OSError as e:
            clean = False
            print(e)
    dirs = [prefix + x for x in dirs]
    kcmp = lambda p1, p2: p1.count('/') - p2.count('/')
    if sys.version_info >= (3, 0):
        dirs.sort(key=cmp_to_key(kcmp), reverse=True)
    else:
        dirs.sort(kcmp, reverse=True)
    for dir in dirs:
        try:
            os.rmdir(dir)
            if verbose:
                print('Removing', dir)
        except OSError as e:
            clean = False
            print(e)
    if force or clean:
        msg = package_forget(package_id)
        print(msg[0])
    return clean


# From https://docs.python.org/3/howto/sorting.html
def cmp_to_key(mycmp):
    'Convert a cmp= function into a key= function'
    class K:
        def __init__(self, obj, *args):
            self.obj = obj
        def __lt__(self, other):
            return mycmp(self.obj, other.obj) < 0
        def __gt__(self, other):
            return mycmp(self.obj, other.obj) > 0
        def __eq__(self, other):
            return mycmp(self.obj, other.obj) == 0
        def __le__(self, other):
            return mycmp(self.obj, other.obj) <= 0
        def __ge__(self, other):
            return mycmp(self.obj, other.obj) >= 0
        def __ne__(self, other):
            return mycmp(self.obj, other.obj) != 0
    return K


def main(argv=None):
    if argv == None:
        argv = sys.argv
    if len(argv) == 1:
        for pkg in package_list():
            print(pkg)
    for arg in argv[1:]:
        package_remove(arg)
    return 0


if __name__ == '__main__':
    sys.exit(main())