#!/usr/bin/python
# encoding: utf-8
#
# Copyright (c) 2013 deanishe@deanishe.net.
#
# MIT Licence. See http://opensource.org/licenses/MIT
#
# Created on 2013-11-01
#

"""workflow-install [options] [<workflow-directory>...]

Install Alfred workflow(s).

You can specify where to install with -w or in ~/.workflow-install.json

If <workflow-directory> is not specified, the script will search the
current working directory recursively for a workflow (a directory
containing an `info.plist` file).

Usage:
    workflow-install [-v|-q|-d] [-s] [-w <directory>] [<workflow-directory>...]
    workflow-install (-h|--help)

Options:
    -s, --symlink                   Symlink workflow directory
                                    instead of copying it.
    -w, --workflows=<directory>     Where to install workflows.
    -V, --version                   Show version number and exit.
    -h, --help                      Show this message and exit.
    -q, --quiet                     Show error messages and above.
    -v, --verbose                   Show info messages and above.
    -d, --debug                     Show debug messages.

"""

from __future__ import print_function, unicode_literals

import sys
import os
import logging
import logging.handlers
import json
import plistlib
import shutil
import subprocess

__version__ = "0.2"
__author__ = "deanishe@deanishe.net"


log = None

DEFAULT_LOG_LEVEL = logging.WARNING
# LOGPATH = os.path.expanduser('~/Library/Logs/MyScripts.log')
# LOGSIZE = 1024 * 1024 * 5  # 5 megabytes

CONFIG_PATH = os.path.expanduser('~/.workflow-install.json')
DEFAULT_CONFIG = dict(workflows_directory='')

ALFRED_PREFS = os.path.expanduser(
    '~/Library/Preferences/com.runningwithcrayons.Alfred-Preferences-3.plist')


class TechnicolorFormatter(logging.Formatter):
    """
    Prepend level name to any message not level logging.INFO.

    Also, colour!

    """

    BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = range(8)

    RESET = "\033[0m"
    COLOUR_BASE = "\033[1;{:d}m"
    BOLD = "\033[1m"

    LEVEL_COLOURS = {
        logging.DEBUG: BLUE,
        logging.INFO: WHITE,
        logging.WARNING: YELLOW,
        logging.ERROR: MAGENTA,
        logging.CRITICAL: RED,
    }

    def __init__(self, fmt=None, datefmt=None, technicolor=True):
        logging.Formatter.__init__(self, fmt, datefmt)
        self.technicolor = technicolor
        self._isatty = sys.stderr.isatty()

    def format(self, record):
        if record.levelno == logging.INFO:
            msg = logging.Formatter.format(self, record)
            return msg
        if self.technicolor and self._isatty:
            colour = self.LEVEL_COLOURS[record.levelno]
            bold = (False, True)[record.levelno > logging.INFO]
            levelname = self.colourise('{:9s}'.format(record.levelname),
                                       colour, bold)
        else:
            levelname = '{:9s}'.format(record.levelname)
        return (levelname + logging.Formatter.format(self, record))

    def colourise(self, text, colour, bold=False):
        colour = self.COLOUR_BASE.format(colour + 30)
        output = []
        if bold:
            output.append(self.BOLD)
        output.append(colour)
        output.append(text)
        output.append(self.RESET)
        return ''.join(output)


# logfile
# logfile = logging.handlers.RotatingFileHandler(LOGPATH, maxBytes=LOGSIZE,
#                                                backupCount=5)
# formatter = logging.Formatter(
#     '%(asctime)s %(levelname)-8s [%(name)-12s] %(message)s',
#     datefmt="%d/%m %H:%M:%S")
# logfile.setFormatter(formatter)
# logfile.setLevel(logging.DEBUG)

# console output
console = logging.StreamHandler()
formatter = TechnicolorFormatter('%(message)s')
console.setFormatter(formatter)
console.setLevel(logging.DEBUG)

log = logging.getLogger('')
# log.addHandler(logfile)
log.addHandler(console)


def read_plist(path):
    """Convert plist to XML and read its contents."""
    cmd = [b'plutil', b'-convert', b'xml1', b'-o', b'-', path]
    xml = subprocess.check_output(cmd)
    return plistlib.readPlistFromString(xml)


def get_workflow_directory():
    """Return path to Alfred's workflow directory."""
    prefs = read_plist(ALFRED_PREFS)
    syncdir = prefs.get('syncfolder')

    if not syncdir:
        log.debug('Alfred sync folder not found')
        return None

    syncdir = os.path.expanduser(syncdir)
    wf_dir = os.path.join(syncdir, 'Alfred.alfredpreferences/workflows')
    log.debug('Workflow sync dir : %r', wf_dir)

    if os.path.exists(wf_dir):
        log.debug('Workflow directory retrieved from Alfred preferences')
        return wf_dir

    log.debug('Alfred.alfredpreferences/workflows not found')
    return None


def find_workflow_dir(dirpath):
    """Recursively search `dirpath` for a workflow.

    A workflow is a directory containing an `info.plist` file.

    """
    for root, dirnames, filenames in os.walk(dirpath):
        if 'info.plist' in filenames:
            log.debug('Workflow found at %r', root)
            return root

    return None


def printable_path(dirpath):
    """Replace $HOME with ~."""
    return dirpath.replace(os.getenv('HOME'), '~')


def load_config():
    """Load configuration from file."""
    if not os.path.exists(CONFIG_PATH):
        with open(CONFIG_PATH, 'wb') as file:
            json.dump(DEFAULT_CONFIG, file)
            return DEFAULT_CONFIG

    with open(CONFIG_PATH) as file:
        return json.load(file)


def install_workflow(workflow_dir, install_base, symlink=False):
    """Install workflow at `workflow_dir` under directory `install_base`."""
    if symlink:
        log.debug("Linking workflow at %r to %r", workflow_dir, install_base)
    else:
        log.debug("Installing workflow at %r to %r", workflow_dir, install_base)

    infopath = os.path.join(workflow_dir, 'info.plist')
    if not os.path.exists(infopath):
        log.error('info.plist not found : %s', infopath)
        return False

    info = plistlib.readPlist(infopath)
    name = info['name']
    bundleid = info['bundleid']

    if not bundleid:
        log.error('Bundle ID is not set : %s', infopath)
        return False

    install_path = os.path.join(install_base, bundleid)

    action = ('Installing', 'Linking')[symlink]
    log.info('%s workflow `%s` to `%s` ...',
             action, name, printable_path(install_path))

    # delete existing workflow
    if os.path.exists(install_path) or os.path.lexists(install_path):

        log.info('Deleting existing workflow ...')

        if os.path.islink(install_path) or os.path.isfile(install_path):
            os.unlink(install_path)
        elif os.path.isdir(install_path):
            log.info('Directory : %s', install_path)
            shutil.rmtree(install_path)
        else:
            log.info('Something else : %s', install_path)
            os.unlink(install_path)

    # Symlink or copy workflow to destination
    if symlink:
        relpath = os.path.relpath(workflow_dir, os.path.dirname(install_path))
        log.debug('relative path : %r', relpath)
        os.symlink(relpath, install_path)
    else:
        shutil.copytree(workflow_dir, install_path)

    return True


def main(args=None):
    """Run program."""
    from docopt import docopt
    args = docopt(__doc__, version=__version__)

    if args.get('--verbose'):
        log.setLevel(logging.INFO)
    elif args.get('--quiet'):
        log.setLevel(logging.ERROR)
    elif args.get('--debug'):
        log.setLevel(logging.DEBUG)
    else:
        log.setLevel(DEFAULT_LOG_LEVEL)

    log.debug("Set log level to %s" %
              logging.getLevelName(log.level))

    log.debug('args : \n%s', args)

    workflows_directory = (args.get('--workflows') or
                           get_workflow_directory() or
                           load_config().get('workflows_directory'))
    if not workflows_directory:
        log.error("You didn't specify where to install the workflow(s).\n"
                  "Try -w workflow/install/path or -h for more info.")
        return 1
    workflows_directory = os.path.expanduser(workflows_directory)

    # Ensure workflows_directory is Unicode
    if not isinstance(workflows_directory, unicode):
        workflows_directory = unicode(workflows_directory, 'utf-8')

    workflow_paths = args.get('<workflow-directory>')

    if not workflow_paths:
        cwd = os.getcwd()
        wfdir = find_workflow_dir(cwd)
        if not wfdir:
            log.critical('No workflow found under %r', cwd)
            return 1
        workflow_paths = [wfdir]
    errors = False

    for path in workflow_paths:
        if not isinstance(path, unicode):
            path = unicode(path, 'utf-8')
        path = os.path.abspath(path)
        if not os.path.exists(path):
            log.error('Directory does not exist : %s', path)
            continue
        if not os.path.isdir(path):
            log.error('Not a directory : %s', path)
            continue
        if not install_workflow(path, workflows_directory,
                                args.get('--symlink')):
            errors = True

    if errors:
        return 1
    return 0

if __name__ == '__main__':
    sys.exit(main(sys.argv[1:]))
