#!/usr/bin/env python

"""
Themer is a colorscheme generator and manager for your desktop.

Themer supports colorscheme generation from numerous sources and can be extended via python modules.
See README.md for more information.
"""

from __future__ import print_function, unicode_literals

import logging
import optparse
import os
import random
import re
import shutil
import sys
import yaml

from jinja2 import Environment, FileSystemLoader
try:
    import Image
except ImportError:
    from PIL import Image

try:
       input = raw_input # raw_input fix in python2
except NameError:
       pass

logger = logging.getLogger(__name__)
plugins = { 'activators': [], 'parsers': [] }

DEFAULT_CONTEXT_CONFIG = {
    'primary': 'magenta',
    'secondary': 'green',
    'tertiary': 'blue',
}

CONFIG_DIR = os.getenv('XDG_CONFIG_HOME', os.path.join(os.getenv('HOME'), '.config'))
THEMER_ROOT = os.path.join(CONFIG_DIR, 'themer')
TEMPLATE_ROOT = os.path.join(THEMER_ROOT, 'templates')

if not os.path.isdir(THEMER_ROOT):
    shutil.copytree('/usr/share/themer/default', THEMER_ROOT)

def dict_update(parent, child):
    """Recursively update parent dict with child dict."""
    for key in child:
        value = child[key]
        if key in parent and isinstance(parent[key], dict):
            parent[key] = dict_update(parent[key], value)
        else:
            parent[key] = value
    return parent

def read_config(config_file):
    """Read a YAML config file."""
    logger.debug('Reading config file: {}'.format(config_file))
    config_dir = os.path.dirname(config_file)
    base_config = {}
    with open(config_file) as fh:
        data = yaml.load(fh)

    if data.get('extends'):
        parent_config = os.path.join(config_dir, data['extends'])
        base_config = read_config(parent_config)

    return dict_update(base_config, data)

def render_templates(template_dir, files, context):
    """Render templates from `template_dir`."""
    env = Environment(loader=FileSystemLoader(template_dir))
    logger.debug('Jinja environment configured for: {}'.format(template_dir))
    for src, dest in files.items():
        dir_name = os.path.dirname(dest)
        if not os.path.exists(dir_name):
            logger.debug('Creating directory {}'.format(dir_name))
            os.makedirs(dir_name)
        if src.endswith(('tpl', 'conf')):
            logger.info('Writing {} -> {}'.format(src, dest))
            template = env.get_template(src)
            with open(dest, 'wb') as fh:
                fh.write(template.render(**context).encode('utf-8'))
            shutil.copymode(os.path.join(template_dir, src), dest) # copy permissions
        else:
            logger.info('Copying {} -> {}'.format(src, dest))
            if src.lower().endswith(('.jpg', '.png', '.jpeg')):
                src = os.path.abspath(src)
            else:
                src = os.path.join(template_dir, src)
            shutil.copy(src, dest)
            shutil.copymode(src, dest) # copy permissions

def munge_context(variables, colors):
    context = {}
    context.update(variables)

    # Handle case when a variable may reference a color, e.g.
    # `primary` = `alt_red`, then `primary` = `#fd1a2b`
    for key, value in context.items():
        if value in colors:
            context[key] = colors[value]
    context.update(colors)

    for key, value in DEFAULT_CONTEXT_CONFIG.items():
        if key not in context:
            context[key] = context[value]

    return context

def hex_to_rgb(h):
    h = h.lstrip('#')
    return tuple(map(lambda n: int(n, 16), [h[i:i+2] for i in range(0, 6, 2)]))

def symlink(theme_dir):
    """Set up a symlink for the new theme."""
    current = os.path.join(THEMER_ROOT, 'current')
    if os.path.islink(current):
        os.unlink(current)
    os.symlink(theme_dir, current)

def activate(theme_name):
    """Activate the given theme."""
    logger.info('Setting {} as current theme'.format(theme_name))
    theme_dir = os.path.join(THEMER_ROOT, theme_name)
    symlink(theme_dir)
    for activator in plugins['activators']:
        logger.debug('activating using {} (from {})'.format(activator.__name__, activator.__module__))
        act = activator(theme_name, theme_dir, logger)
        act.activate()

def generate(color_source, config, template_dir, theme_name):
    """Generate a new theme."""
    destination = os.path.join(THEMER_ROOT, theme_name)
    wallpaper = None

    for parser in plugins['parsers']:
        if hasattr(parser.check, '__call__') and not parser.check(color_source):
            continue
        if (isinstance(parser.check, str) or isinstance(parser.check, re._pattern_type)) and not re.search(parser.check, color_source, re.IGNORECASE):
            continue
        parse = parser(color_source, config, logger)
        colors = parse.read()
        wallpaper = parse.wallpaper
        if not wallpaper:
            continue
        logger.debug('Using {} (from {}) to parse {}'.format(parser.__name__, parser.__module__, color_source))
        break
    else:
        panic('No Parser found to parse {} with'.format(color_source))

    context = munge_context(config['variables'], colors)
    files = {
        key: os.path.join(destination, value)
        for key, value in config['files'].items()}
    if wallpaper:
        # Add wallpaper to the list of files to copy.
        files[wallpaper] = os.path.join(
            destination,
            'wallpaper{}'.format(os.path.splitext(wallpaper)[1]))

    render_templates(template_dir, files, context)

    # Save a copy of the colors in the generated theme folder.
    with open(os.path.join(destination, 'colors.yaml'), 'w') as fh:
        yaml.dump(context, fh, default_flow_style=False)

def render(config, template_dir, theme_name):
    """Render an existing theme."""
    src = os.path.join(THEMER_ROOT, theme_name)
    files = {
        key: os.path.join(src, value)
        for key, value in config['files'].items()}

    colors = os.path.join(src, 'colors.yaml')
    if not os.path.isfile(colors):
        panic('Unkown theme "{}"'.format(theme_name))
    with open(colors, 'r') as fh:
        context = yaml.load(fh)
        render_templates(template_dir, files, context)

def load_plugins(config):
    def imp(name):
        mod = __import__('.'.join(name.split('.')[:-1]))
        for n in name.split('.')[1:]:
            mod = getattr(mod, n)
        return mod

    for kind in plugins:
        for name in config['plugins'][kind]:
            plugins[kind].append(imp(name))
            logger.info('Loaded plugin "{}"'.format(name))

def get_parser():
    parser = optparse.OptionParser(usage='usage: %prog [options] [list|activate|render|generate|current|delete] theme_name [color file]')
    parser.add_option('-t', '--template', dest='template_dir', default='i3')
    parser.add_option('-c', '--config', dest='config_file', default='config.yaml')
    parser.add_option('-a', '--activate', dest='activate', action='store_true')
    parser.add_option('-v', '--verbose', dest='verbose', action='store_true')
    parser.add_option('-d', '--debug', dest='debug', action='store_true')
    return parser

def panic(msg):
    print(msg, file=sys.stderr)
    sys.exit(1)

if __name__ == '__main__':
    parser = get_parser()
    options, args = parser.parse_args()

    if not args:
        panic(parser.get_usage())

    action = args[0]
    if action not in ('list', 'activate', 'generate', 'render', 'current', 'delete', 'plugins'):
        panic('Unknown action "{}"'.format(action))

    if action == 'plugins':
        template_dir = os.path.join(TEMPLATE_ROOT, options.template_dir)
        config = os.path.join(template_dir, options.config_file)
        if not os.path.exists(config):
            panic('Unable to find config file: "{}"'.format(config))
        config = read_config(config)

        for kind in plugins:
            print('Enabled {}:'.format(kind))
            for name in config['plugins'][kind]:
                print('\t{}'.format(name))
        sys.exit(0)
    if action not in ('list', 'current') and len(args) == 1:
        panic('Missing required argument "theme_name"')
    elif action == 'list':
        themes = [
            t for t in os.listdir(THEMER_ROOT)
            if t not in ('templates', 'current', 'plugins', 'plugins')]
        print('\n'.join(sorted(themes)))
        sys.exit(0)
    elif action == 'current':
        current = os.path.join(THEMER_ROOT, 'current')
        if not os.path.exists(current):
            print('No theme')
        else:
            print(os.path.basename(os.path.realpath(
                os.path.join(THEMER_ROOT, 'current'))))
            os.system('colortheme')
        sys.exit(0)

    theme_name = args[1]
    if theme_name in ('current', 'templates', 'plugins') or (theme_name == 'all' and action != 'render'):
        panic('"{}" is not a valid theme'.format(theme_name))

    # Add logging handlers.
    if options.verbose or options.debug:
        handler = logging.StreamHandler()
        logger.addHandler(handler)
    if options.debug:
        logger.setLevel(logging.DEBUG)

    # Find the appropriate yaml config file and load it.
    template_dir = os.path.join(TEMPLATE_ROOT, options.template_dir)
    config = os.path.join(template_dir, options.config_file)
    if not os.path.exists(config):
        panic('Unable to find config file: "{}"'.format(config))
    sys.path.append(os.path.join(template_dir, "plugins"))
    config = read_config(config)
    load_plugins(config)

    if action == 'activate':
        path = os.path.join(THEMER_ROOT, theme_name)
        if not os.path.isdir(path):
            panic('"{}" is not an existing theme'.format(theme_name))
        activate(theme_name)
    elif action == 'delete':
        path = os.path.join(THEMER_ROOT, theme_name)
        if not os.path.isdir(path):
            panic('"{}" is not an existing theme'.format(theme_name))

        current = os.path.basename(os.path.realpath( os.path.join(THEMER_ROOT, 'current')))
        if current == theme_name:
            current = input('"{}" is the current theme. Enter a new theme to activate (or empty to exit): '.format(theme_name))
            while not os.path.isdir(os.path.join(THEMER_ROOT, current)):
                print('"{}" is not an existing theme'.format(current))
                current = input('new theme (or empty to exit): ')
            if current.strip() != "":
                activate(current)
        shutil.rmtree(path)
        logger.info('Removed {}'.format(theme_name))
    else:
        if action == 'generate':
            if not len(args) == 3:
                panic('Missing required color source')
            else:
                color_file = args[2]
            generate(color_file, config, template_dir, theme_name)
        elif action == 'render':
            if not len(args) == 2:
                panic('Missing required argument "theme_name"')
            else:
                if theme_name == 'all':
                    for theme in [ t for t in os.listdir(THEMER_ROOT)
                            if t not in ('templates', 'current', 'plugins', 'plugins')]:
                        render(config, template_dir, theme)
                else:
                    render(config, template_dir, theme_name)

        if theme_name != 'all' and (options.activate or input('Activate now? yN ').lower() == 'y'):
            activate(theme_name)
