#!/usr/bin/env python
import coloredlogs
import getopt
import hashlib
import logging
import os
import shutil
import subprocess
import sys
import tempfile
import time
import yaml


yaml.add_multi_constructor('tag:yaml.org,2002:python/object:', lambda a, b, c: None)


def find_plugins():
    if os.path.exists('__init__.py'):
        yield '.'
    else:
        for dp, dn, fn in os.walk('.'):
            for d in dn:
                dir = os.path.join(dp, d)
                if os.path.exists(os.path.join(dir, 'plugin.yml')):
                    yield dir


def run_bower(path, cmdline):
    bower_json = os.path.join(path, 'bower.json')
    bower_rc = os.path.join(path, '.bowerrc')

    if not os.path.exists(bower_json):
        logging.warn('Plugin at %s has no bower.json' % path)
        return

    with open(bower_rc, 'w') as f:
        f.write('{"directory" : "resources/vendor"}')

    if not os.path.exists(os.path.join(path, 'resources/vendor')):
        os.makedirs(os.path.join(path, 'resources/vendor'))

    logging.info('Running bower %s in %s' % (cmdline, path))
    code = subprocess.call('bower -V --allow-root %s' % cmdline, shell=True, cwd=path)
    if code != 0:
        logging.error('Bower failed for %s' % path)
    os.unlink(bower_rc)


def run_build(plugin, cache_enabled):
    cache_path = '/tmp/.ajenti-resource-cache'
    if not os.path.exists(cache_path):
        os.makedirs(cache_path)

    def get_hash(name):
        return hashlib.sha512(name).hexdigest()

    def get_cached(name):
        if os.path.exists(os.path.join(cache_path, get_hash(name))):
            return open(os.path.join(cache_path, get_hash(name))).read()

    def get_cached_time(name):
        if os.path.exists(os.path.join(cache_path, get_hash(name))):
            return os.stat(os.path.join(cache_path, get_hash(name))).st_mtime

    def set_cached(name, content):
        open(os.path.join(cache_path, get_hash(name)), 'w').write(content)

    resources = yaml.load(open(os.path.join(plugin, 'plugin.yml')))['resources']

    if not resources:
        return
    logging.info('Building resources for %s' % plugin)

    if not os.path.exists(os.path.join(plugin, 'resources/build')):
        os.makedirs(os.path.join(plugin, 'resources/build'))

    all_js = ''
    all_css = ''
    for resource in resources:
        path = os.path.join(plugin, resource)
        if resource.endswith('.coffee'):
            if not cache_enabled or not get_cached(path) or get_cached_time(path) < os.stat(path).st_mtime:
                logging.info('Compiling %s' % path)
                set_cached(path, subprocess.check_output(['coffee', '-p', '-c', path]) + '\n')
            all_js += get_cached(path)
        if resource.endswith('.js'):
            logging.debug('Including %s' % path)
            all_js += open(path).read() + '\n'
        if resource.endswith('.less'):
            if not cache_enabled or not get_cached(path) or get_cached_time(path) < os.stat(path).st_mtime:
                logging.info('Compiling %s' % path)
                set_cached(path, subprocess.check_output(['lessc', path]) + '\n')
            all_css += get_cached(path)
        if resource.endswith('.css'):
            logging.debug('Including %s' % path)
            all_css += open(path).read() + '\n'

    with open(os.path.join(plugin, 'resources/build/all.js'), 'w') as f:
        f.write(all_js)
    with open(os.path.join(plugin, 'resources/build/all.css'), 'w') as f:
        f.write(all_css)


def run_setuptools(plugin, cmd):
    info = yaml.load(open(os.path.join(plugin, 'plugin.yml')))
    info['pypi_name'] = info['name'].replace('_', '-')
    if 'demo_' in plugin:
        return
    workspace = tempfile.mkdtemp()
    logging.info('Running setup.py for %s', plugin)
    logging.debug('Working under %s' % workspace)
    workspace_plugin = os.path.join(workspace, 'ajenti_plugin_%s' % info['name'])

    dist = os.path.join(plugin, 'dist')
    if os.path.exists(dist):
        shutil.rmtree(dist)

    shutil.copytree(plugin, workspace_plugin)
    shutil.copy(os.path.join(plugin, 'requirements.txt'), workspace)

    setuppy = '''
#!/usr/bin/env python
from setuptools import setup, find_packages

import os

__requires = filter(None, open('requirements.txt').read().splitlines())

setup(
    name='ajenti.plugin.%(pypi_name)s',
    version='%(version)s',
    install_requires=__requires,
    description='%(title)s',
    long_description='A %(title)s plugin for Ajenti panel',
    author='%(author)s',
    author_email='%(email)s',
    url='%(url)s',
    packages=find_packages(),
    include_package_data=True,
)
    '''.strip() % info
    with open(os.path.join(workspace, 'setup.py'), 'w') as f:
        f.write(setuppy)

    open(os.path.join(workspace, 'README'), 'w').close()

    manifest = '''
recursive-include ajenti_plugin_%(name)s * *.*
recursive-exclude ajenti_plugin_%(name)s *~ *.pyc
include ajenti_plugin_%(name)s/plugin.yml
include MANIFEST.in
include requirements.txt
    ''' % info
    with open(os.path.join(workspace, 'MANIFEST.in'), 'w') as f:
        f.write(manifest)

    if 'pre_build' in info:
        logging.info('  -> running pre-build script')
        f = tempfile.NamedTemporaryFile(delete=False)
        try:
            f.write(info['pre_build'])
            f.close()
            subprocess.check_call(['sh', f.name], cwd=workspace_plugin)
        finally:
            os.unlink(f.name)

    logging.info('  -> setup.py %s', cmd)
    try:
        subprocess.check_output('python setup.py %s' % cmd, cwd=workspace, shell=True)
    except subprocess.CalledProcessError as e:
        logging.error('Output: %s', e.output)
        logging.error('setup.py failed for %s, code %s', plugin, e.returncode)
        return

    dist = os.path.join(workspace, 'dist')
    sdist = os.path.join(plugin, 'dist')
    if os.path.exists(sdist):
        shutil.rmtree(sdist)
    if os.path.exists(dist):
        shutil.copytree(dist, sdist)

    shutil.rmtree(workspace)

    if 'upload' in cmd.split():
        open(os.path.join(plugin, '.last-upload'), 'w').write(str(time.time()))

    logging.info('setup.py has finished')


def run_find_outdated(plugin):
    if 'demo_' in plugin:
        return
    last_upload = 0
    last_file = os.path.join(plugin, '.last-upload')
    if os.path.exists(last_file):
        last_upload = float(open(last_file).read())

    last_changed = 0
    for d, dn, fn in os.walk(plugin):
        if d.endswith('/dist'):
            continue
        if d.endswith('/resources/build'):
            continue
        for f in fn:
            if os.path.splitext(f)[-1] in ['.pyc']:
                continue
            if os.stat(os.path.join(d, f)).st_mtime > last_upload + 10:
                logging.info('*** %s/%s', d, f)
            last_changed = max(last_changed, os.stat(os.path.join(d, f)).st_mtime)

    if last_changed > last_upload + 10:
        logging.warn('Plugin %s has unpublished changes', plugin)
        return True


def usage():
    #General commands
    #    --create-plugin <name>     - creates a new plugin in a subdirectory
    print("""
Usage: %s [options]

Plugin commands (these operate on all plugins found within current directory)
    --run                      - Run Ajenti with plugins from the current directory
    --run-dev                  - Run Ajenti in dev mode with plugins from the current directory
    --bower '<cmdline>'        - Run Bower, e.g. --bower install
    --build                    - Compile resources
    --rebuild                  - Force recompile resources
    --setuppy '<args>          - Run a setuptools build
    --find-outdated            - Find plugins that have unpublished changes
    """ % sys.argv[0])


if __name__ == '__main__':
    coloredlogs.install(logging.DEBUG)
    sys.path.insert(0, '.')

    try:
        opts, args = getopt.getopt(
            sys.argv[1:],
            '',
            [
                'run',
                'run-dev',
                'bower=',
                'build',
                'rebuild',
                'setuppy=',
                'find-outdated',
            ]
        )
    except getopt.GetoptError as e:
        print(str(e))
        usage()
        sys.exit(2)

    for o, a in opts:
        if o.startswith('--run'):
            cmd = [
                'ajenti-panel',
                '-v', '--plugins', '.'
            ]
            if o == '--run-dev':
                cmd += ['--dev']
            try:
                subprocess.call(cmd)
            except KeyboardInterrupt:
                pass
            sys.exit(0)
        if o == '--bower':
            for plugin in find_plugins():
                run_bower(plugin, a)
            sys.exit(0)
        if o == '--build':
            for plugin in find_plugins():
                run_build(plugin, True)
            logging.info('Resource build complete')
            sys.exit(0)
        if o == '--rebuild':
            for plugin in find_plugins():
                run_build(plugin, False)
            logging.info('Resource rebuild complete')
            sys.exit(0)
        if o == '--setuppy':
            for plugin in find_plugins():
                run_setuptools(plugin, a)
            sys.exit(0)
        if o == '--find-outdated':
            found = 0
            for plugin in find_plugins():
                if run_find_outdated(plugin):
                    found += 1
            logging.info('Scan complete, %s updated plugin(s) found', found)
            sys.exit(0)
        elif o == '--set-platform':
            ajenti.platform = a

    usage()
    sys.exit(2)
