#!/usr/bin/env python
import coloredlogs
import getopt
import hashlib
import json
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 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 = ''
    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)
                set_cached(path, subprocess.check_output(['coffee', '-p', '-c', path]) + '\n')
            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'):
            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['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'].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_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)
