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

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 sorted(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 = ''

    workers = []

    def worker(path, args):
        set_cached(path, subprocess.check_output(args) + '\n')

    for resource in resources:
        if isinstance(resource, basestring):
            resource = {'path': resource}

        path = os.path.join(plugin, resource['path'])
        if resource['path'].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)
                workers.append(gevent.spawn(worker, path, ['coffee', '-p', '-c', path]))
        if resource['path'].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)
                workers.append(gevent.spawn(worker, path, ['lessc', path]))

    gevent.joinall(workers)

    for resource in resources:
        if isinstance(resource, basestring):
            resource = {'path': resource}

        path = os.path.join(plugin, resource['path'])
        if resource['path'].endswith('.coffee'):
            all_js += get_cached(path)
        if resource['path'].endswith('.js'):
            logging.debug('Including %s' % path)
            all_js += open(path).read() + '\n'
        if resource['path'].endswith('.less'):
            all_css += get_cached(path)
        if resource['path'].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']
    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_bump(plugin):
    path = os.path.join(plugin, 'plugin.yml')
    output = ''
    bumped = False
    for l in open(path).read().splitlines():
        if l.startswith('version:'):
            prefix, counter = l.rsplit('.', 1)
            counter = counter.rstrip("'")
            counter = str(int(counter) + 1)
            l = prefix + '.' + counter
            if "'" in prefix:
                l += "'"
            bumped = True
        output += l + '\n'
    if bumped:
        with open(path, 'w') as f:
            f.write(output)
        logging.info('Bumped %s to %s.%s', plugin, prefix.split(':')[1].strip(" '"), counter)
    else:
        logging.warn('Could not find version info for %s', plugin)


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 run_xgettext(plugin):
    locale_path = os.path.join(plugin, 'locale')
    if not os.path.exists(locale_path):
        os.makedirs(locale_path + '/en/LC_MESSAGES')

    pot_path = os.path.join(locale_path, 'app.pot')
    if os.path.exists(pot_path):
        os.unlink(pot_path)

    logging.info('Extracting from %s', plugin)
    logging.info('           into %s', pot_path)

    if subprocess.call(['which', 'xgettext'], stdout=subprocess.PIPE) != 0:
        logging.error('xgettext not found!')
        sys.exit(1)

    if subprocess.call(['which', 'angular-gettext-cli'], stdout=subprocess.PIPE) != 0:
        logging.error('angular-gettext-cli not found (sudo npm -g install angular-gettext-cli)!')
        sys.exit(1)

    subprocess.check_call([
        'angular-gettext-cli',
        '--files', '%s/**/*.html' % plugin,
        '--dest', pot_path,
        '--marker-name', 'i18n',
    ])

    for (dirpath, dirnames, filenames) in os.walk(plugin, followlinks=True):
        if 'vendor' in dirpath or 'build' in dirpath:
            continue
        for f in filenames:
            path = os.path.join(dirpath, f)
            if f.endswith(('.coffee', '.js')):
                logging.info(' -> (js) %s' % path)
                subprocess.check_call([
                    'xgettext',
                    '--from-code', 'utf-8',
                    '-c', '-d', 'app',
                    '-L', 'javascript',
                    '--keyword=gettext',
                    '-o', pot_path,
                    '-j', path,
                ])
            if f.endswith('.py'):
                logging.info(' -> (py) %s' % path)
                subprocess.check_call([
                    'xgettext',
                    '--from-code', 'utf-8',
                    '-c', '-d', 'app',
                    '-o', pot_path,
                    '-j', path,
                ])

    for dir in os.listdir(locale_path):
        path = os.path.join(locale_path, dir, 'LC_MESSAGES')
        if os.path.isdir(path):
            logging.info(' :: processing %s' % dir)
            po_path = os.path.join(path, 'app.po')
            if os.path.exists(po_path):
                subprocess.check_call([
                    'msgmerge',
                    '-U',
                    po_path, pot_path,
                ])
            else:
                with open(po_path, 'w') as f:
                    f.write(open(pot_path).read())


def run_push_crowdin(plugins, add=False):
    dir = tempfile.mkdtemp()
    logging.info('Working in %s' % dir)
    for plugin in plugins:
        locale_path = os.path.join(plugin, 'locale')
        pot_path = os.path.join(locale_path, 'app.pot')
        if os.path.exists(pot_path):
            logging.info('Copying %s', pot_path)
            with open(os.path.join(dir, os.path.split(plugin)[1] + '.po'), 'w') as f:
                f.write(open(pot_path).read())

    try:
        key = open('.crowdin.key').read().strip()
    except Exception as e:
        logging.error('Could not read ".crowdin.key": %s', e)
        sys.exit(1)

    for file in os.listdir(dir):
        logging.info(' :: uploading %s' % file)
        subprocess.check_call([
            'curl', '-F', 'files[/2.0/%s]=@%s' % (
                file,
                os.path.join(dir, file),
            ),
            'http://api.crowdin.net/api/project/ajenti/%s?key=%s' % (
                'add-file' if add else 'update-file',
                key
            )
        ])

    shutil.rmtree(dir)


def run_pull_crowdin(plugins):
    try:
        key = open('.crowdin.key').read().strip()
    except Exception as e:
        logging.error('Could not read ".crowdin.key": %s', e)
        sys.exit(1)

    map = dict((os.path.split(p)[1], p) for p in plugins)
    dir = tempfile.mkdtemp()
    logging.info('Working in %s' % dir)

    logging.info('Requesting build')
    subprocess.check_call([
        'curl', 'http://api.crowdin.net/api/project/ajenti/export?key=%s' % key
    ])

    logging.info('Downloading')
    zip_path = os.path.join(dir, 'all.zip')
    subprocess.check_call([
        'wget', 'http://api.crowdin.net/api/project/ajenti/download/all.zip?key=%s' % key,
        '-O', zip_path
    ])

    subprocess.check_call([
        'unzip', 'all.zip'
    ], cwd=dir)

    os.unlink(zip_path)

    for lang in os.listdir(dir):
        if lang == 'ajenti':
            continue
        logging.info(' -> processing %s', lang)
        for name, plugin in map.iteritems():
            zip_po_path = os.path.join(dir, lang, '2.0', name + '.po')
            if os.path.exists(zip_po_path):
                locale_path = os.path.join(plugin, 'locale', lang, 'LC_MESSAGES')
                if not os.path.exists(locale_path):
                    os.makedirs(locale_path)
                po_path = os.path.join(locale_path, 'app.po')
                with open(po_path, 'w') as f:
                    f.write(open(zip_po_path).read())

    shutil.rmtree(dir)


def run_msgfmt(plugin):
    locale_path = os.path.join(plugin, 'locale')
    if not os.path.exists(locale_path):
        return

    logging.info('Compiling in %s', locale_path)

    if subprocess.call(['which', 'msgfmt'], stdout=subprocess.PIPE) != 0:
        logging.error('msgfmt not found!')
        sys.exit(1)

    if subprocess.call(['which', 'angular-gettext-cli'], stdout=subprocess.PIPE) != 0:
        logging.error('angular-gettext-cli not found (sudo npm -g install angular-gettext-cli)!')
        sys.exit(1)

    for lang in os.listdir(locale_path):
        if lang in ['app.pot', 'en']:
            continue

        po_path = os.path.join(locale_path, lang, 'LC_MESSAGES', 'app.po')
        js_path = os.path.join(locale_path, lang, 'LC_MESSAGES', 'app.js')

        '''
        subprocess.check_call([
            'msgfmt',
            po_path,
            '-o',
            os.path.join(locale_path, lang, 'LC_MESSAGES', 'app.mo'),
        ])
        '''

        js_locale = {}
        msgid = None
        for line in open(po_path):
            if line.startswith('msgid'):
                msgid = line.split(None, 1)[1].strip().strip('"')
            if line.startswith('msgstr'):
                msgstr = line.split(None, 1)[1].strip().strip('"')
                js_locale[msgid] = msgstr

        with open(js_path, 'w') as f:
            f.write(json.dumps(js_locale))


def usage():
    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
    --bump                     - Bump plugin's version
    --find-outdated            - Find plugins that have unpublished changes
    --xgettext                 - Extracts localizable strings
    --msgfmt                   - Compiles translated localizable strings
    """ % 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=',
                'bump',
                'find-outdated',
                'xgettext',
                'msgfmt',
                'add-crowdin',
                'push-crowdin',
                'pull-crowdin',
            ]
        )
    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', '--autologin', '--stock-plugins', '--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)
        elif o == '--build':
            for plugin in find_plugins():
                run_build(plugin, True)
            logging.info('Resource build complete')
            sys.exit(0)
        elif o == '--rebuild':
            for plugin in find_plugins():
                run_build(plugin, False)
            logging.info('Resource rebuild complete')
            sys.exit(0)
        elif o == '--setuppy':
            for plugin in find_plugins():
                run_setuptools(plugin, a)
            sys.exit(0)
        elif o == '--bump':
            for plugin in find_plugins():
                run_bump(plugin)
            sys.exit(0)
        elif 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 == '--xgettext':
            for plugin in find_plugins():
                run_xgettext(plugin)
            sys.exit(0)
        elif o == '--msgfmt':
            for plugin in find_plugins():
                run_msgfmt(plugin)
            sys.exit(0)
        elif o == '--add-crowdin':
            run_push_crowdin(list(find_plugins()), add=True)
            sys.exit(0)
        elif o == '--push-crowdin':
            run_push_crowdin(list(find_plugins()))
            sys.exit(0)
        elif o == '--pull-crowdin':
            run_pull_crowdin(list(find_plugins()))
            sys.exit(0)

    usage()
    sys.exit(2)
