#!python
import sh
import os
import re
import io
import sys
import json
import fnmatch
import logging
import getpass
import textwrap
import pkg_resources
from glob import glob
from os.path import join, exists, abspath, expanduser, realpath, dirname
from argparse import ArgumentParser, Namespace
from atom.api import Atom, Bool, Callable, Dict, List, Unicode, Float, Int, Instance, set_default
from contextlib import contextmanager
from collections import OrderedDict

try:
    from ConfigParser import ConfigParser
except:
    from configparser import ConfigParser

@contextmanager
def cd(newdir):
    prevdir = os.getcwd()
    print("[DEBUG]:   -> running cd {}".format(newdir))
    os.chdir(os.path.expanduser(newdir))
    try:
        yield
    finally:
        print("[DEBUG]:   -> running  cd {}".format(prevdir))
        os.chdir(prevdir)


class Command(Atom):
    _instance = None
    #: Subcommand name ex enaml-native <name>
    title = Unicode()

    #: Subcommand short description
    desc = Unicode()

    #: Subcommand help text
    help = Unicode()

    #: Package context used to retrieve app config and env
    ctx = Dict()

    #: Reference to other CLI commands
    cmds = Dict()

    #: Arguments this command accepts
    args = List(tuple)

    #: Parser this command uses. Generated automatically.
    parser = Instance(ArgumentParser)

    #: If the command requires running in an app dir
    app_dir_required = Bool(True)

    @classmethod
    def instance(cls):
        return cls._instance

    def run(self, args):
        pass


class Init(Command):
    title = set_default('init')
    help = set_default("Start a new enaml-native project")
    args = set_default([
        ('name', dict(help="Project name")),
        ('bundle_id', dict(help="App bundleId")),
        ('destination', dict(help="Project destination folder (optional) "
                                  "Note: If not given will attempt to build in the cwd",
                             nargs="?")),
        ('--dev', dict(help="Install dev version of enaml-native from the given pip resource")),
        ('--dev-cli', dict(help="Install dev version of the enaml-native-cli from the given pip resource")),
    ])

    #: Can be run from anywhere
    app_dir_required = set_default(False)

    def run(self, args):
        #: Append project name
        if args.destination:
            dest = join(abspath(args.destination), args.name.replace(" ", ""))
            assert not exists(dest), "Error: Destination folder already exists! ({})".format(dest)
        else:
            dest = '.'
        print("Init new project {} ({}) in {}".format(args.name, args.bundle_id, abspath(dest)))

        root = os.environ['ENAML_NATIVE_ROOT']

        #: Get package to install
        enaml_native = abspath(args.dev) if args.dev else 'enaml-native'
        enaml_native_cli = abspath(args.dev_cli) if args.dev_cli else 'enaml-native-cli'

        #: Clean root android project
        #with cd(root):
        #    self.cmds['clean-android'].run()

        #: Make project dir
        if not exists(dest):
            os.makedirs(dest)
        with cd(dest):
            #: Copy everything
            for f in ['android', 'ios']:
                shprint(sh.cp, '-R', join(root, f), '.')

            #: Create package.json
            with open('package.json', 'w') as f:
                f.write(json.dumps(self.ctx, indent=4))

            #: TODO: Make a readme
            with open('README.md', 'w') as f:
                f.write(textwrap.dedent("""
                # {name}
                
                Activate the `enaml-native` virtual environment (`source venv/bin/activate`) 
                to add and remove p4a and enaml-native packages.
                
                The `android/` folder can be opened in Android Studio.
                The `ios/` workspace can be opened in XCode.
                The `src/` is where your python/enaml code goes.
                
                Cheers!
                """))

            #: Make simlink to enaml-native cli
            #shprint(sh.ln, '-s', join(root, 'enaml-native'), 'enaml-native')

            #: Patch package
            with open('package.json') as f:
                pkg = json.load(f, object_pairs_hook=OrderedDict)

            pkg['name'] = args.name
            pkg['version'] = "1.0"
            pkg['bundle_id'] = args.bundle_id
            with open('package.json', 'w') as f:
                json.dump(pkg, f, indent=4)

            #: Patch android
            #: This is a hack haha
            with cd('android/app/'):
                build_gradle = []
                with open("build.gradle") as f:
                    lines = f.read().split("\n")

                for line in lines:
                    if re.match(r'\s+applicationId\s+".+"', line):
                        line = '        applicationId "{}"'.format(args.bundle_id)
                    elif re.match(r'\s+versionCode\s+\d+', line):
                        line = '        versionCode 1'
                    elif re.match(r'\s+versionName\s+".+"', line):
                        line = '        versionName "1.0"'
                    build_gradle.append(line)

                with open("build.gradle", 'w') as f:
                    f.write('\n'.join(build_gradle))

                with cd("src/main/"):

                    #: Update manifest id
                    for src in ['AndroidManifest.xml',
                                'java/com/codelv/enamlnative/demo/MainActivity.java',
                                'java/com/codelv/enamlnative/demo/MainApplication.java',
                                ]:
                        with open(src) as f:
                            data = f.read()
                        with open(src, 'w') as f:
                            f.write(data.replace("com.codelv.enamlnative.demo", args.bundle_id))

                    #: Move the app and activity files to the correct folders
                    with cd("java"):
                        new_app_dir = args.bundle_id.replace(".", "/")
                        if not exists(new_app_dir):
                            os.makedirs(new_app_dir)
                        for f in ['MainActivity.java', 'MainApplication.java']:
                            sh.mv('com/codelv/enamlnative/demo/{}'.format(f), new_app_dir)

                        #: Remove empty stuff after the move
                        sh.find('.', '-empty', '-type', 'd', '-delete')

                    with cd("res/values/"):
                        with open("strings.xml") as f:
                            data = f.read()
                        with open("strings.xml", "w") as f:
                            f.write(data.replace(
                                '<string name="app_name">Enaml-Native Demo</string>',
                                '<string name="app_name">{}</string>'.format(args.name)
                            ))

            #: Update view.enaml
            if not exists('src'):
                os.makedirs('src')
            with cd('src'):
                with open('main.py', 'w') as f:
                    f.write(textwrap.dedent("""
                    import sys
                    import os
                    
                    def main():
                        #: Called to start your app 
                        from enamlnative.android.app import AndroidApplication
                        app = AndroidApplication()
                        #app.debug = True #: Makes a lot of lag!
                        #app.dev = 'server' # "10.0.2.2" # or 'server'
                        app.reload_view = reload_view
                        app.deferred_call(load_view, app)
                        app.start()
                    
                    def load_view(app):
                        #: Create and show the enaml view
                        import enaml
                        with enaml.imports():
                            from view import ContentView
                            app.view = ContentView()
                        #: Time how long it takes
                        app.show_view()
                    
                    
                    def reload_view(app):
                        #: This is called when an app reload is requested in dev mode
                        import enaml
                        with enaml.imports():
                            import view
                            reload(view)
                            app.view = view.ContentView()
                    """))

                with open('view.enaml', 'w') as f:
                    f.write(textwrap.dedent("""
                    from enamlnative.core.api import *
                    from enamlnative.widgets.api import *
                    
                    enamldef ContentView(Flexbox):
                        flex_direction = "column"
                        TextView:
                            text = "{}"
                    
                    """.format(args.name)))



            #: Init VCS
            shprint(sh.git, 'init')

            #: Create a venv if one doesn't already exist
            if not exists('venv'): #: TODO: it's not possible for this to exist at the moment!
                shprint(sh.virtualenv, 'venv')

            if args.dev_cli:
                #: Try to install from enaml-native-cli folder
                print("Installing enaml-native-cli...")
                cli = join(dirname(enaml_native), 'enaml-native-cli')
                shprint(sh.bash, '-c', 'source venv/bin/activate && '
                                   'pip install {enaml_native_cli}'
                                   .format(enaml_native_cli=enaml_native_cli))

            print("Installing enaml-native...")
            shprint(sh.bash, '-c', 'source venv/bin/activate && '
                                   'pip install {enaml_native}'.format(enaml_native=enaml_native))

            print("==============================================================")
            print(sh.bash('-c', 'source venv/bin/activate && '
                                    'enaml-native --help'))

        print("==============================================================")
        print("Project created successfully!")
        print("Now go to the project root `cd {}`".format(dest))
        print("Then activate the venv using `source venv/bin/activate`")
        print("Use the `enaml-native` command to build and run your project ")
        print("==============================================================")


class CleanPython(Command):
    title = set_default("clean-python")
    help = set_default("Remove python-for-android build and .so libs")
    args = set_default([
        ('-a', dict(action='store_true', help="Clean all")),
        ('--ios', dict(action='store_true', help="Clean iOS only")),
        ('--android', dict(action='store_true', help="Clean android only")),
    ])

    def run(self, args=None):
        ctx = self.ctx

        if args is None or not args.ios:
            with cd(ctx['android']['p4a']):
                if args and args.a:
                    shprint(sh.python, 'p4a.py', 'clean_all')
                    #for arch in ctx['arches']:
                    #    #: Clean so
                    #    shprint(sh.rm, '-R', 'android/app/src/main/libs/{}/'.format(arch))
                else:
                    shprint(sh.python, 'p4a.py', 'clean_dists')
                    shprint(sh.python, 'p4a.py', 'clean_builds')
        if (args is None or not args.android) and sys.platform=='darwin':
            if args and args.a: #: Only for clean all, building takes FOREVER
                with cd(ctx['ios']['p4i']):
                    recipes = []
                    for line in sh.python('toolchain.py','status').stdout.split("\n"):
                        print(line)
                        if line.split("-")[-1].strip().lower()=="not built":
                            continue
                        recipes.append(line.split(" ")[0])
                    print("Cleaning {}".format(recipes))
                    for r in recipes:
                        shprint(sh.python, 'toolchain.py', 'clean', r)


class NdkBuild(Command):
    title = set_default("ndk-build")
    help = set_default("Run ndk-build on the android project")

    def run(self, args=None):
        ctx = self.ctx
        ndk_build = sh.Command(os.path.expanduser(join(ctx['android']['ndk'], 'ndk-build')))
        arches = ctx['android']['arches']

        jni_dir = ctx['android'].get(
            'jni_dir',
            "{}/{}/enaml-native/android/src/main/jni".format(os.getcwd(), Link.package_dir)
        )

        with cd(jni_dir):

            #: Patch Applicaiton.mk to have the correct ABI's
            with open('Application.mk') as f:
                app_mk = f.read()

            #: APP_ABI := armeabi-v7a
            new_mk = []
            for line in app_mk.split("\n"):
                if re.match(r'APP_ABI\s*:=\s*.+', line):
                    line = 'APP_ABI := {}'.format(" ".join(arches))
                new_mk.append(line)

            with open('Application.mk','w') as f:
                f.write("\n".join(new_mk))

            #: Now run nkd-build
            shprint(ndk_build)


class CrossCompile(Command):
    title = set_default("cross-compile")
    help = set_default("Build the python requirements for ios")

    def run(self, args=None):
        import crosscompile
        import logging
        logger = logging.getLogger('p4a')
        logger.setLevel(logging.DEBUG)
        from crosscompile.recipe import Platform,Arch

        b = crosscompile.Builder(
            requirements=[
                crosscompile.recipes.python.Recipe,
            ],
            platforms=[
                # Platform(name='macOS',arches=[
                #     Arch(name='macosx.x86_64'),
                # ]),
                Platform(name='iOS',arches=[
                    Arch(name='iphonesimulator.x86_64'),
                    Arch(name='iphonesimulator.i386'),
                    Arch(name='iphoneos.armv7'),
                    Arch(name='iphoneos.armv7s'),
                    Arch(name='iphoneos.armv64'),
                ]),
                # Platform(name='tvOS',arches=[
                #     Arch(name='appletvsimulator.x86_64'),
                #     Arch(name='appletvos.arm64'),
                # ]),
                # Platform(name='watchOS',arches=[
                #     Arch(name='watchsimulator.i386'),
                #     Arch(name='watchos.armv7k'),
                # ]),
            ],

        )
        b.build()


class BuildPython(Command):
    """ Builds an enaml-native python app using the following steps:

        For Android:
        1. Runs ndk-build to build JNI modules
        2. Run p4a apk to build python requirements for each arch
            and copies them to the lib a local folder
        3. Pull all so files from site-packages and rename to lib.pkg.to.so
            and place them in the lib/<arch> folder

        For iOS:
        1. Run p4i toolchain build <req>
    """
    title = set_default("build-python")
    help = set_default("Build the python requirements")
    args = set_default([
        ('-d', dict(action='store_true', help="Print full debug log")),
        ('--ios', dict(action='store_true', help="iOS only")),
        ('--android', dict(action='store_true', help="Android only")),
        ('--minify', dict(action='store_true', help="Minify")),
        ('--release', dict(action='store_true', help="Build for release")),
        ('--skip-ndk-build', dict(action='store_true', help="Skip ndk-build (for travis)")),
    ])

    def run(self, args=None):
        if args is None or not args.ios:
            self.run_android(args)
        if (args is None or not args.android) and sys.platform == 'darwin':
            self.run_ios(args)

        #: Extra cleanup
        self.cmds['trim-assets'].run(args)

    def run_android(self,args):
        ctx = self.ctx
        env = ctx['android']
        reqs = ",".join(env['dependencies'].keys())

        #: Run ndk build
        if not args.skip_ndk_build:
            #: Um, we're passing args from another command?
            self.cmds['ndk-build'].run(args)

        #: Build for each arch
        for arch in env['arches']:
            cfg = dict(
                arch=arch,
                reqs=reqs,
                bundle_id=ctx['bundle_id']
            )
            cfg.update(env)

            #: Get current directory where command was run
            cfg['ndk_build_dir'] = env.get(
                'ndk_build_dir',
                "{}/{}/enaml-native/android/src/main/libs".format(os.getcwd(), Link.package_dir)
            )

            #: Add debug arg if needed
            cfg['debug'] = '--debug ' if (args and args.d) else ''

            #: Add minification
            cfg['minify'] = '--minify ' if (args and args.minify) else ''

            #: Where should we put our built python modules and packages
            #cfg['python_build_dir'] = os.path.abspath('build/python')
            if not os.path.exists(cfg['python_build_dir']):
                os.makedirs(cfg['python_build_dir'])

            if args and args.d:
                logger = logging.getLogger('p4a')
                logger.level = logging.DEBUG

            with cd(ctx['android']['p4a']):
                #: Um, we're passing args from another command?
                #: Clean whatever build from previous arch
                self.cmds['clean-python'].run()

                shprint(sh.python, *'p4a.py create --arch={arch} '
                                    '--private=../src '
                                    '--package={bundle_id} '
                                    '--name=EnamlNativeApplication '
                                    '--dist-name=enaml-native '
                                    '--version=0.1 '
                                    '--requirements={reqs} '
                                    '--android-api=25 '
                                    '--bootstrap=enaml '
                                    '--sdk-dir={sdk} '
                                    '--ndk-dir={ndk} '
                                    '--ndk-platform=21 '
                                    '--ndk-build-dir={ndk_build_dir} '
                                    '{debug}'
                                    '{minify}'
                                    '--copy-libs'.format(**cfg).split(' '), _debug=True)

            #: Copy lib folder
            shprint(sh.cp,'-R',
                    expanduser('~/.local/share/python-for-android/dists/enaml-native/libs/{arch}'.format(**cfg)),
                    cfg['ndk_build_dir'])

            #: Copy modules
            for f in ['modules', 'site-packages']:
                shprint(sh.cp,'-R',
                        expanduser('~/.local/share/python-for-android/dists/enaml-native/python/{}'.format(f)),
                        '{python_build_dir}/{arch}'.format(**cfg))

            #: Collect all entry_points.txt and merge them into entry_points.json
            #: This allows use to use wnetry points in the app by reading from the file
            with cd('{python_build_dir}/{arch}/site-packages/'.format(**cfg)):
                entry_points = {}
                try:
                    for ep in glob("*/entry_points.txt"):
                        config = ConfigParser()
                        config.read(ep)
                        for section in config.sections():
                            if section not in entry_points:
                                entry_points[section] = {}
                            for opt in config.options(section):
                                entry_points[section][opt] = config.get(section, opt)

                    if entry_points:
                        with open('entry_points.json', 'w') as f:
                            f.write(json.dumps(entry_points, indent=2))
                except:
                    print("Failed to convert entry points")

            #: Copy ca-cert (tornado only)
            cert_path = '{python_build_dir}/{arch}/site-packages/tornado/'.format(**cfg)
            if os.path.exists(cert_path):
                with cd(cert_path):
                    shprint(sh.cp, '/etc/ssl/certs/ca-certificates.crt', '.')

            #: Where .so files go
            dst = abspath('{ndk_build_dir}/{arch}'.format(**cfg))

            #: Collect all .so files and rename them
            with cd('{python_build_dir}/{arch}'.format(**cfg)):
                for mod in sh.find('.', '-name', '*.so').stdout.strip().split("\n"):
                    #: Strip ./modules or ./site-packages
                    pgk = ".".join(['lib']+mod.split("/")[2:])
                    #: Rename mod.so to pkg.mod.so and move to libs
                    shprint(sh.mv, mod, join(dst, pgk))

    def run_ios(self, args):
        ctx = self.ctx
        env = ctx['ios']
        #: Replace crystax with just python on ios
        reqs = env['dependencies'].keys()

        with cd('python-for-ios'):
            shprint(sh.python, 'toolchain.py', 'build', *reqs)


class TrimAssets(Command):
    title = set_default("trim-assets")
    help = set_default("Trim away unused files from the python install for each arch")
    args = set_default([
        ('--minify', dict(action='store_true', help="Minify using pyminifier")),
    ])

    def run(self, args=None):
        ctx = self.ctx

        #: Clean each arch
        env = ctx['android']

        for arch in env['arches']:
            with cd('{python_build_dir}/{arch}/site-packages/'.format(arch=arch, **env)):
                shprint(sh.find,'.','-type','f','-name','*.py','-delete')
                shprint(sh.find,'.','-type','f','-name','*.pyc','-delete')
                #shprint(sh.find,'.','-type','f','-name','*.pyo','-delete') #: Use pyo
                for p in [
                    'enaml/qt',
                    'tornado/test',
                    '*.egg-info',
                    '*.dist-info',
                    'tests',
                    'usr',
                ]:
                    try:
                        sh.rm('-R', *glob(p))
                    except:
                        pass


class BundleAssets(Command):
    """ This is used by the gradle build to pack python into a zip.
    """
    title = set_default("bundle-assets")
    help = set_default("Creates a python bundle of all .py and .enaml files")
    args = set_default([
        ('-p', dict(action='store_true', help="Create bundle by pulling from device")),
        ('--release', dict(action='store_true', help="Create a release bundle")),
    ])

    def run(self, args=None):
        ctx = self.ctx
        env = ctx['android']

        #: Now copy to android assets folder
        #: Extracted file type
        bundle = 'python.tar.gz'

        #: Clean each arch
        for arch in env['arches']:
            #: Remove old
            cfg = dict(arch=arch, bundle_id=ctx['bundle_id'])
            cfg.update(env)
            root = abspath(os.getcwd())

            #: Create
            if not os.path.exists(env['python_build_dir']):
                raise RuntimeError("Error: Python build doesn't exist. "
                                   "You should run './enaml-native build-python' first!")

            with cd(env['python_build_dir']):
                #: Remove old build
                if os.path.exists('build'):
                    shprint(sh.rm, '-R', 'build')

                if args and args.p:
                    #: Restart as root
                    shprint(sh.adb, 'root')

                    #: Pull assets and cache from device
                    shprint(sh.adb, 'pull',
                            '/data/user/0/{bundle_id}/assets/python/'.format(**cfg),
                            'build')
                else:
                    #: Extract stdlib.zip to build/
                    shprint(sh.mkdir, 'build')

                    #with cd('build'):
                    #    shprint(sh.unzip,
                    #            '{ndk}/sources/python/2.7/libs/{arch}/stdlib.zip'.format(**cfg),
                    #            '-d', 'stdlib')

                    #: Copy site-packages to build/
                    with cd('{arch}/site-packages/'.format(**cfg)):
                        shprint(sh.cp, '-R', '.', '../../build/')

                    #: Copy sources from app source
                    for src in ctx['sources']:
                        shprint(sh.cp, '-R', join(root, src, '.'), 'build')

                    #: Clean any excluded sources
                    with cd('build'):
                        for pattern in env.get('excluded', []):
                            matches = glob(pattern)
                            if matches:
                                shprint(sh.rm, '-R', *matches)

                #: Remove old
                if os.path.exists('python.zip'):
                    shprint(sh.rm, 'python.zip')

                #: Build tar.lz4
                if os.path.exists('python.tar.lz4'):
                    shprint(sh.rm, 'python.tar.lz4')

                #: Zip everything and copy to assets arch to build
                with cd('build'):
                    #shprint(sh.zip, '-r', 'android/app/src/main/assets/python/python.zip', '.')
                    #shprint(sh.zip, '-r', '../python.zip', '.')

                    #shprint(sh.bash, '-c', 'tar czf - build | lz4 -9 - python.tar.lz4')
                    shprint(sh.tar, '-zcvf', '../python.tar.gz', '.')


            break  #: They should all be the same so stop after the first

        #: Now copy the tar.lz4 and rename as a special ".so" file
        #: to trick android into extracting from the apk on install
        #for a in env['arches']:
        #    shprint(sh.cp,
        #            '{python_build_dir}/python.tar.lz4'.format(**env),
        #            'android/app/src/main/libs/{arch}/libpymodules.so'.format(arch=a))

        #: Tar is about 25% smaller and significantly 4x faster at unpacking
        if not exists('android/app/src/main/assets/python/'):
            os.makedirs('android/app/src/main/assets/python/')

        shprint(sh.cp,
                '{python_build_dir}/{bundle}'.format(bundle=bundle,**env),
                'android/app/src/main/assets/python/{bundle}'.format(bundle=bundle))

        #: And wth, just copy it to the ios folder too :)
        shprint(sh.cp,
                '{python_build_dir}/{bundle}'.format(bundle=bundle,**env),
                'ios/App/Python/{bundle}'.format(bundle=bundle))
        #: Can iOS unpack this??


class InitPackage(Command):
    title = set_default("init-package")
    help = set_default("Create a new enaml-native package")
    args = set_default([
        ('name', dict(help="Package name")),
        ('destination', dict(help="Project destination folder")),
    ])

    def run(self, args):
        dest = join(args.destination,args.name)
        if exists(dest):
            raise ValueError("{} already exists".format(dest))

        #: Create the basic structure
        os.makedirs(dest)
        with cd(dest):
            with open('readme.md', 'w') as f:
                f.write(textwrap.dedent("""

                # {name}

                A package for enaml-native.

                ### Installation

                To install:

                `enaml-native install {name}`

                To remove:

                `enaml-native uninstall {name}`

                """.format(name=args.name)))

            #: enaml-native package setup
            with open('setup.py', 'w') as f:
                f.write(textwrap.dedent("""
                #: ====================================================================
                #: Created with 'enaml-native init-package'
                #: Modify as needed
                #: ====================================================================
                import os
                import fnmatch
                from setuptools import setup

                def find_data_files(dest, *folders):
                    matches = dict()
                    excluded_types = ['.pyc', '.enamlc']
                    excluded_dirs = ['build']
                    dest = os.path.join('packages',dest) #: goes into venv/packages/<name>
                    for folder in folders:
                        for dirpath, dirnames, files in os.walk(folder):
                            #: Skip build folders and exclude hidden dirs
                            if [d for d in dirpath.split("/") if d.startswith(".") or d in excluded_dirs]:
                                continue
                            k = os.path.join(dest,dirpath)
                            if k not in matches:
                                matches[k] = []
                            for f in fnmatch.filter(files, '*'):
                                if [p for p in excluded_types if f.endswith(p)]:
                                    continue
                                m = os.path.join(dirpath, f)
                                matches[k].append(m)
                    return matches.items()

                setup(
                    name="{name}",
                    version="1.0",
                    author="{user}",
                    author_email="",
                    license='MIT',
                    url="",
                    description="{name} package for enaml-native-cli",
                    long_description=open("README.md").read(),
                    data_files=find_data_files("{name}",['android','ios','src']),
                    install_requires=['enaml-native-cli'],
                    classifiers=["Framework :: enaml-native"],
                    entry_points={{
                        'enaml_native_package': [
                            '{pkg} = {pkg}.package:get_package'
                        ]
                    }},
                )
                """.format(name=args.name, pkg=args.name.replace("-", "_"),
                           user=getpass.getuser()).lstrip()))

            #: Create folders
            os.makedirs('src')  #: Python source here
            with cd('src'):
                #: App  package setup
                with open('setup.py', 'w') as f:
                    f.write(textwrap.dedent("""
                    #: ====================================================================
                    #: Created with 'enaml-native init-package'
                    #: Modify as needed
                    #: ====================================================================
                    from setuptools import setup, find_packages

                    #: Put your library dependencies here
                    setup(
                        name="{name}",
                        version="1.0",
                        author="{user}",
                        author_email="",
                        license='MIT',
                        url="",
                        description="{name} package for enaml-native",
                        packages=find_packages('.'),
                        #package_dir={{'': '*'}},
                        install_requires=['enaml-native'],
                    )
                    """.format(name=args.name, user=getpass.getuser())).lstrip())

            os.makedirs('android')  #: Android here
            os.makedirs('ios')  #: IOS here
            #: Init git
            sh.git('init')


class ListPackages(Command):
    title = set_default("list")
    help = set_default("List installed packages")

    def run(self, args):
        try:
            print(sh.pipdeptree())
        except:
            print("Warning: pipdeptree is not installed, falling pack on pip!")
            print(sh.pip('list', '-l'))


class Install(Command):
    """ The "Install" command does a pip install of the package names given and then runs the 
     linker command.
      
    A custom post_install_hook can be used by adding a "enaml_native_post_install" entry_point 
    which shall be a function that receives the app package.json (context) an argument. This is
    called before linking is done. The return value is ignored.
    
    Example
    ----------
    
    def post_install(ctx):
        #: Do any post_install steps here (ex maybe collect install stats?)
        #: print links to docs, ask setup questions, etc.. 
    
    """
    title = set_default("install")
    help = set_default("Install and link an enaml-native package")
    args = set_default([
        ('names', dict(help="Package name", nargs="+")),
        ('--save', dict(action='store_true', help="Add to app dependencies")),
        ('--nolink', dict(action='store_true', help="Skip linking")),
    ])

    def run(self, args):
        print("Installing {}...".format(", ".join(args.names)))
        #: Install enaml-native package with pip
        #: TODO: Check that we're in a virtualenv!
        if not hasattr(sys, 'real_prefix'):
            print("Warning: It's highly recommended to use enaml-native in a virtual env!")

        shprint(sh.pip, 'install', " ".join(args.names))

        for name in args.names:
            #: Check if a custom post_install_hook exists to handle this package
            for ep in pkg_resources.iter_entry_points(group="enaml_native_post_install"):
                if ep.name.replace("-", '_') == name.replace("-", '_'):
                    post_install_hook = ep.load()
                    print("Custom post_install_hook {} found for '{}'. ".format(post_install_hook,
                                                                                 name))
                    post_install_hook(self.ctx)
                    break

        if not args.nolink:
            #: TODO: Detect actual name from name given (ex filter out http:// git+ etc.. pip reqs)
            #: Link everything for now
            self.cmds['link'].run()
            #self.cmds['link'].run(args)

        if args.save:
            for name in args.names:
                ctx = self.ctx
                #: TODO: Get version?
                ctx['android']['dependencies'][name] = ""
                #: Dump before openting
                pkg = json.dumps(ctx, indent=4)
                with open('package.json', 'w') as f:
                    f.write(pkg)


class Uninstall(Command):
    """ The "Uninstall" command unlinks the package (if needed) and does a pip uninstall
    of the package names given. 
      
    A custom pre_uninstall_hook can be used by adding a "enaml_native_pre_uninstall" entry_point 
    which shall be a function that receives the app package.json (context) an argument. This is
    called after unlinking is done. The return value is ignored.
    
    Example
    ----------
    
    def pre_uninstall(ctx):
        # Do any pre_uninstall steps here (ex maybe collect uninstall stats?)

    """
    title = set_default("uninstall")
    help = set_default("Uninstall and unlink enaml-native package")
    args = set_default([
        ('names', dict(help="Package name", nargs="+")),
        ('--save', dict(action='store_true', help="Remove from requirements")),
        ('--nolink', dict(action='store_true', help="Skip unlinking")),
    ])

    def run(self, args):
        print("Unistalling {}...".format(", ".join(args.names)))

        if not hasattr(sys, 'real_prefix'):
            print("Warning: It's highly recommended to use enaml-native in a virtual env!")

        #: Unlink first
        if not args.nolink:
            self.cmds['unlink'].run(args)

        #: Install enaml-native package with pip
        graph = json.loads(sh.pipdeptree('-j').strip())
        for name in args.names:
            #: Find all packages that only depend on this

            #: Check if a custom pre_uninstall_hook exists to handle this package
            for ep in pkg_resources.iter_entry_points(group="enaml_native_pre_uninstall"):
                if ep.name.replace("-", '_') == name.replace("-", '_'):
                    pre_uninstall_hook = ep.load()
                    print("Custom pre_uninstall_hook {} found for '{}'. ".format(pre_uninstall_hook,
                                                                                 name))
                    pre_uninstall_hook(self.ctx)
                    break

            depends = [name]

            #: TODO: Needs to also remove all dependencies' dependencies
            # dependencies = [d['key'] for d in
            #                 [g['dependencies'] for g in graph if g['package']['key'] == name][0]]
            # for d in dependencies:
            #     #: If this dependency is only there due to the given package, also remove it
            #     if len([line for line in sh.pipdeptree('-r', '-p', d)
            #             if line.startswith("  - ")]) == 1:
            #         depends.append(d)

            shprint(sh.pip, 'uninstall', '--yes', " ".join(depends))

        if args.save:
            for name in args.names:
                ctx = self.ctx
                #: TODO: Get version?
                if name in ctx['android']['dependencies']:
                    del ctx['android']['dependencies'][name]
                #: Dump before openting
                pkg = json.dumps(ctx, indent=4)
                with open('package.json', 'w') as f:
                    f.write(pkg)


class Link(Command):
    """ The "Link" command tries to modify the android and ios projects
        to include all of the necessary changes for this package to work.
          
        A custom linkiner can be used by adding a "enaml_native_linker" entry_point which
        shall be a function that receives the app package.json (context) an argument. 
        
        Example
        ----------
        
        def linker(ctx):
            # Link android and ios projects here
            return True #: To tell the cli the linking was handled and should return
    
    """
    title = set_default("link")
    help = set_default("Link an enaml-native package (updates android and ios projects)")
    args = set_default([
        ('names', dict(help="Package name (optional) If not set links all projects.", nargs='*')),
    ])

    #: Where "enaml native packages" are installed
    package_dir = 'venv/packages'

    def run(self, args=None):
        print("Linking {}".format(args.names if args else "all packages..."))

        if args and args.names:
            for name in args.names:
                self.link(self.package_dir, name)
        else:
            #: Link everything
            for pkg in os.listdir(self.package_dir):
                if pkg == 'enaml-native-cli':
                    continue
                elif not os.path.isfile(pkg):
                    self.link(self.package_dir, pkg)

    def link(self, path, pkg):
        """ Link the package in the current directory.
        """
        #: Check if a custom linker exists to handle linking this package
        for ep in pkg_resources.iter_entry_points(group="enaml_native_linker"):
            if ep.name.replace("-", '_') == pkg.replace("-", '_'):
                linker = ep.load()
                print("Custom linker {} found for '{}'. Linking...".format(linker, pkg))
                if linker(self.ctx, path):
                    return

        #: Use the default builtin linker script
        if exists(join(path, pkg, 'android', 'build.gradle')):
            self.link_android(path, pkg)
        else:
            print("Android project does not need linked for {}".format(pkg))
        if exists(join(path, pkg, 'ios', 'Podfile')):
            self.link_ios(path, pkg)
        else:
            print("iOS project does not need linked for {}".format(pkg))

    @staticmethod
    def is_settings_linked(source, pkg):
        """ Returns true if the "include ':<project>'" line exists in the file """
        for line in source.split("\n"):
            if re.search(r"include\s*['\"]:{}['\"]".format(pkg), line):
                return True
        return False

    @staticmethod
    def is_build_linked(source, pkg):
        """ Returns true if the "compile project(':<project>')"
            line exists exists in the file """
        for line in source.split("\n"):
            if re.search(r"compile\s+project\(['\"]:{}['\"]\)".format(pkg), line):
                return True
        return False

    @staticmethod
    def find_packages(path):
        """ Find all java files matching the "*Package.java" pattern within
            the given enaml package directory relative to the java source path."""
        matches = []
        root = join(path, 'android', 'src', 'main', 'java')
        for folder, dirnames, filenames in os.walk(root):
            for filename in fnmatch.filter(filenames, '*Package.java'):
                #: Open and make sure it's an EnamlPackage somewhere
                with open(join(folder, filename)) as f:
                    if "implements EnamlPackage" in f.read():
                        package = os.path.relpath(folder, root)
                        matches.append(os.path.join(package, filename))
        return matches

    @staticmethod
    def is_app_linked(source, pkg, java_package):
        """ Returns true if the compile project line exists exists in the file """
        for line in source.split("\n"):
            if java_package in line:
                return True
        return False

    def link_android(self, path, pkg):
        """ Link's the android project to this library.

            1. Includes this project's directory in the app's android/settings.gradle
                It adds:
                    include ':<project-name>'
                    project(':<project-name>').projectDir = new File(rootProject.projectDir, '../packages/<project-name>/android')

            2. Add's this project as a dependency to the android/app/build.gradle
                It adds:
                    compile project(':<project-name>')
                to the dependencies.

            3. If preset, adds the import and package statement
               to the android/app/src/main/java/<bundle/id>/MainApplication.java

        """

        bundle_id = self.ctx['bundle_id']

        #: Check if it's already linked
        with open('android/settings.gradle') as f:
            settings_gradle = f.read()
        with open('android/app/build.gradle') as f:
            build_gradle = f.read()

        #: Find the MainApplication.java
        main_app_java_path = 'android/app/src/main/java/{}/MainApplication.java'.format(
            bundle_id.replace(".", "/"))
        with open(main_app_java_path) as f:
            main_application_java = f.read()

        try:
            #: Now link all the EnamlPackages we can find in the new "package"
            new_packages = Link.find_packages(join(path, pkg))
            if not new_packages:
                print("\t[Android] {} No EnamlPackages found to link!".format(pkg))
                return

            #: Link settings.gradle
            if not Link.is_settings_linked(settings_gradle, pkg):
                #: Add two statements
                new_settings = settings_gradle.split("\n")
                new_settings.append("") # Blank line
                new_settings.append("include ':{name}'".format(name=pkg))
                new_settings.append("project(':{name}').projectDir = "
                                    "new File(rootProject.projectDir, '../{path}/{name}/android')"
                                    .format(name=pkg, path=self.package_dir))

                with open('android/settings.gradle', 'w') as f:
                    f.write("\n".join(new_settings))
                print("\t[Android] {} linked in settings.gradle!".format(pkg))
            else:
                print("\t[Android] {} was already linked in settings.gradle!".format(pkg))

            #: Link app/build.gradle
            if not Link.is_build_linked(build_gradle, pkg):
                #: Add two statements
                new_build = build_gradle.split("\n")

                #: Find correct line number
                found = False
                for i, line in enumerate(new_build):
                    if re.match(r"dependencies\s*{", line):
                        found = True
                        continue
                    if found and "}" in line:
                        #: Hackish way to find line of the closing bracket after
                        #: the dependencies { block is found
                        break
                if not found:
                    raise ValueError("Unable to find dependencies in android/app/build.gradle!")

                #: Insert before the closing bracket
                new_build.insert(i, "    compile project(':{name}')".format(name=pkg))

                with open('android/app/build.gradle', 'w') as f:
                    f.write("\n".join(new_build))
                print("\t[Android] {} linked in app/build.gradle!".format(pkg))
            else:
                print("\t[Android] {} was already linked in app/build.gradle!".format(pkg))

            new_app_java = []
            for package in new_packages:
                #: Add our import statement
                javacls = os.path.splitext(package)[0].replace("/", ".")

                if not Link.is_app_linked(main_application_java, pkg, javacls):
                    #: Reuse previous if avialable
                    new_app_java = new_app_java or main_application_java.split("\n")

                    #: Find last import statement
                    j = 0
                    for i, line in enumerate(new_app_java):
                        if fnmatch.fnmatch(line, "import *;"):
                            j = i

                    new_app_java.insert(j+1, "import {};".format(javacls))

                    #: Add the package statement
                    j = 0
                    for i, line in enumerate(new_app_java):
                        if fnmatch.fnmatch(line.strip(), "new *Package()"):
                            j = i
                    if j == 0:
                        raise ValueError("Could not find the correct spot to add package {}"
                                         .format(javacls))
                    else:
                        #: Get indent and add to previous line
                        #: Add comma to previous line
                        new_app_java[j] = new_app_java[j]+ ","

                        #: Insert new line
                        new_app_java.insert(j+1, "                new {}()"
                                            .format(javacls.split(".")[-1]))

                else:
                    print("\t[Android] {} was already linked in {}!".format(pkg, main_app_java_path))

            if new_app_java:
                with open(main_app_java_path, 'w') as f:
                    f.write("\n".join(new_app_java))

            print("\t[Android] {} linked successfully!".format(pkg))
        except Exception as e:
            print("\t[Android] {} Failed to link. Reverting due to error: {}".format(pkg, e))

            #: Undo any changes
            with open('android/settings.gradle', 'w') as f:
                f.write(settings_gradle)
            with open('android/app/build.gradle', 'w') as f:
                f.write(build_gradle)
            with open(main_app_java_path, 'w') as f:
                f.write(main_application_java)

            #: Now blow up
            raise

    def link_ios(self, path, pkg):
        print("\t[iOS] Link TODO:...")


class Unlink(Command):
    """ The "Unlink" command tries to undo the modifications done by the linker..
          
        A custom unlinkiner can be used by adding a "enaml_native_unlinker" entry_point which
        shall be a function that receives the app package.json (context) an argument. 
        
        Example
        ----------
        
        def unlinker(ctx):
            # Unlink android and ios projects here
            return True #: To tell the cli the unlinking was handled and should return
    
    """
    title = set_default("unlink")
    help = set_default("Unlink an enaml-native package")
    args = set_default([
        ('names', dict(help="Package name", nargs="+")),
    ])

    def run(self, args=None):
        """ The name IS required here. """
        print("Unlinking {}...".format(args.names))
        for name in args.names:
            self.unlink(Link.package_dir, name)

    def unlink(self, path, pkg):
        """ Unlink the package in the current directory.
        """
        #: Check if a custom unlinker exists to handle unlinking this package
        for ep in pkg_resources.iter_entry_points(group="enaml_native_unlinker"):
            if ep.name.replace("-", '_') == pkg.replace("-", '_'):
                unlinker = ep.load()
                print("Custom unlinker {} found for '{}'. Unlinking...".format(unlinker, pkg))
                if unlinker(self.ctx, path):
                    return

        if exists(join(path, pkg, 'android', 'build.gradle')):
            self.unlink_android(path, pkg)
        else:
            print("Android project does not need unlinked for {}".format(pkg))
        if exists(join(path, pkg, 'ios', 'Podfile')):
            self.link_ios(path, pkg)
        else:
            print("iOS project does not need unlinked for {}".format(pkg))

    def unlink_android(self, path, pkg):
        """ Unlink's the android project to this library.

            1. In the app's android/settings.gradle, it removes the following lines (if they exist):
                    include ':<project-name>'
                    project(':<project-name>').projectDir = new File(rootProject.projectDir, '../venv/packages/<project-name>/android')

            2. In the app's android/app/build.gradle, it removes the following line (if present)
                    compile project(':<project-name>')

            3. In the app's android/app/src/main/java/<bundle/id>/MainApplication.java, it removes:
                    import <package>.<Name>Package;
                     new <Name>Package(), 
                     
                  If no comma exists it will remove the comma from the previous line.
                    
        """
        bundle_id = self.ctx['bundle_id']

        #: Check if it's already linked
        with open('android/settings.gradle') as f:
            settings_gradle = f.read()
        with open('android/app/build.gradle') as f:
            build_gradle = f.read()

        #: Find the MainApplication.java
        main_app_java_path = 'android/app/src/main/java/{}/MainApplication.java'.format(
            bundle_id.replace(".", "/"))
        with open(main_app_java_path) as f:
            main_application_java = f.read()

        try:
            #: Now link all the EnamlPackages we can find in the new "package"
            new_packages = Link.find_packages(join(path, pkg))
            if not new_packages:
                print("\t[Android] {} No EnamlPackages found to unlink!".format(pkg))
                return

            #: Unlink settings.gradle
            if Link.is_settings_linked(settings_gradle, pkg):
                #: Remove the two statements
                new_settings = [
                    line for line in settings_gradle.split("\n")
                    if line.strip() not in [
                        "include ':{name}'".format(name=pkg),
                        "project(':{name}').projectDir = "
                        "new File(rootProject.projectDir, '../{path}/{name}/android')"
                            .format(name=pkg, path=Link.package_dir)
                    ]
                ]

                with open('android/settings.gradle', 'w') as f:
                    f.write("\n".join(new_settings))
                print("\t[Android] {} unlinked settings.gradle!".format(pkg))
            else:
                print("\t[Android] {} was not linked in settings.gradle!".format(pkg))

            #: Unlink app/build.gradle
            if Link.is_build_linked(build_gradle, pkg):
                #: Add two statements
                new_build = [
                    line for line in build_gradle.split("\n")
                    if line.strip() not in ["compile project(':{name}')".format(name=pkg)]
                ]

                with open('android/app/build.gradle', 'w') as f:
                    f.write("\n".join(new_build))

                print("\t[Android] {} unlinked in app/build.gradle!".format(pkg))
            else:
                print("\t[Android] {} was not linked in app/build.gradle!".format(pkg))

            new_app_java = []
            for package in new_packages:
                #: Add our import statement
                javacls = os.path.splitext(package)[0].replace("/", ".")

                if Link.is_app_linked(main_application_java, pkg, javacls):
                    #: Reuse previous if avialable
                    new_app_java = new_app_java or main_application_java.split("\n")

                    new_app_java = [
                        line for line in new_app_java
                        if line.strip() not in [
                            "import {};".format(javacls),
                            "new {}()".format(javacls.split(".")[-1]),
                            "new {}(),".format(javacls.split(".")[-1]),
                        ]
                    ]

                    #: Now find the last package and remove the comma if it exists
                    found = False
                    j = 0
                    for i, line in enumerate(new_app_java):
                        if fnmatch.fnmatch(line.strip(), "new *Package()"):
                            found = True
                        elif fnmatch.fnmatch(line.strip(), "new *Package(),"):
                            j = i

                    if not found:  #: We removed the last package so add a comma
                        #: This kills any whitespace...
                        new_app_java[j] = new_app_java[j][:new_app_java[j].rfind(',')]

                else:
                    print("\t[Android] {} was not linked in {}!".format(pkg, main_app_java_path))

            if new_app_java:
                with open(main_app_java_path, 'w') as f:
                    f.write("\n".join(new_app_java))

            print("\t[Android] {} unlinked successfully!".format(pkg))

        except Exception as e:
            print("\t[Android] {} Failed to unlink. Reverting due to error: {}".format(pkg, e))

            #: Undo any changes
            with open('android/settings.gradle', 'w') as f:
                f.write(settings_gradle)
            with open('android/app/build.gradle', 'w') as f:
                f.write(build_gradle)
            with open(main_app_java_path, 'w') as f:
                f.write(main_application_java)

            #: Now blow up
            raise


class AllPython(Command):
    title = set_default("all-python")
    help = set_default("Does clean, build, trim, and bundle")

    def run(self, args=None):
        for cmd in ['build-python',
                    'trim-assets',
                    'bundle-assets']:
            self.cmds[cmd].run()


class BuildAndroid(Command):
    title = set_default("build-android")
    help = set_default("Build android project")
    args = set_default([
        ('--release', dict(action='store_true', help="Release mode")),
    ])

    def run(self, args=None):
        with cd("android"):
            gradlew = sh.Command('./gradlew')
            if args and args.release:
                shprint(gradlew, 'assembleRelease', _debug=True)
            else:
                shprint(gradlew, 'assembleDebug', _debug=True)


class CleanAndroid(Command):
    title = set_default("clean-android")
    help = set_default("Clean the android project")

    def run(self, args=None):
        with cd('android'):
            gradlew = sh.Command('./gradlew')
            shprint(gradlew, 'clean', _debug=True)


class RunAndroid(Command):
    title = set_default("run-android")
    help = set_default("Build android project, install it, and run")
    args = set_default([
        ('--release', dict(action='store_true', help="Build in Release mode")),
    ])

    def run(self, args=None):
        ctx = self.ctx
        bundle_id = ctx['bundle_id']
        with cd("android"):
            release_apk = os.path.abspath(join('.', 'app', 'build', 'outputs', 'apk',
                                               'app-release-unsigned.apk'))
            gradlew = sh.Command('./gradlew')

            #: If no devices are connected, start the simulator
            if len(sh.adb('devices').stdout.strip())==1:
                device = sh.emulator('-list-avds').stdout.split("\n")[0]
                shprint(sh.emulator, '-avd', device)
            if args and args.release:
                shprint(gradlew, 'assembleRelease', _debug=True)
                #shprint(sh.adb,'uninstall','-k','"{}"'.format(bundle_id))
                shprint(sh.adb,'install',release_apk)
            else:
                shprint(gradlew, 'installDebug', _debug=True)
            shprint(sh.adb, 'shell', 'am', 'start', '-n',
                    '{bundle_id}/{bundle_id}.MainActivity'.format(bundle_id=bundle_id))


class CleanIOS(Command):
    title = set_default("clean-ios")
    help = set_default("Clean the ios project")

    def run(self, args=None):
        with cd('ios'):
            shprint(sh.xcodebuild, 'clean', '-project', 'App.xcodeproj',
                    '-configuration', 'ReleaseAdhoc', '-alltargets')


class RunIOS(Command):
    title = set_default("run-ios")
    help = set_default("Build and run the ios project")
    args = set_default([
        ('--release', dict(action='store_true', help="Build in Release mode")),
    ])

    def run(self, args=None):
        ctx = self.ctx
        env = ctx['ios']
        with cd('ios'):
            shprint(sh.xcrun, 'xcodebuild',
                    '-scheme', env['project'],
                    '-workspace', '{project}.xcworkspace'.format(**env),
                    '-configuration', 'Release' if args and args.release else 'Debug',
                    #'-destination', 'platform=iOS Simulator,name=iPhone 7 Plus,OS=10.2',
                    '-derivedDataPath',
                    'run')
            #shprint(sh.xcrun, 'simctl', 'install', 'booted',
            #        'build/Build/Products/Debug-iphonesimulator/{project}.app'.format(**env))
            shprint(sh.xcrun, 'simctl', 'launch', 'booted', ctx['bundle_id'])


class BuildIOS(Command):
    title = set_default("build-ios")
    help = set_default("Build the ios project")
    args = set_default([
        ('--release', dict(action='store_true', help="Build in Release mode")),
    ])

    def run(self, args=None):
        ctx = self.ctx
        env = ctx['ios']
        with cd('ios'):
            shprint(sh.xcrun, 'xcodebuild',
                    '-scheme', env['project'],
                    '-workspace', '{project}.xcworkspace'.format(**env),
                    '-configuration', 'Release' if args and args.release else 'Debug',
                    #'-destination', 'platform=iOS Simulator,name=iPhone 7 Plus,OS=10.2',
                    '-derivedDataPath',
                    'build')


class Server(Command):
    """ Run a dev server to host files. Only view files can be reloaded at the moment. """
    title = set_default("start")
    help = set_default("Start a debug server for serving files to the app")
    #: Dev server index page to render
    index_page = Unicode("enaml-native dev server. "
                         "When you change a source file it pushes to the app.")
    #: Server port
    port = Int(8888)

    #: Time in ms to wait before triggering a reload
    reload_delay = Float(1)
    _reload_count = Int() #: Pending reload requests

    #: Watchdog  observer
    observer = Instance(object)

    #: Watchdog handler
    watcher = Instance(object)

    #: Websocket handler implementation
    handler = Instance(object)

    #: Callable to add a callback from a thread into the event loop
    add_callback = Callable()

    #: Callable to add a callback at some later time
    call_later = Callable()

    #: Changed file events
    changes = List()

    def run(self, args=None):
        ctx = self.ctx
        #: Look for tornado or twisted in reqs
        use_twisted = 'twisted' in ctx['android']['dependencies']

        #: Setup observer
        try:
            from watchdog.observers import Observer
            from watchdog.events import LoggingEventHandler
        except ImportError:
            print("Watchdog is required the dev server: pip install watchdog")
            return
        self.observer = Observer()
        server = self

        class AppNotifier(LoggingEventHandler):
            def on_any_event(self, event):
                super(AppNotifier, self).on_any_event(event)
                #: Use add callback to push to event loop thread
                server.add_callback(server.on_file_changed, event)

        with cd('src'):
            print("Watching {}".format(abspath('.')))
            self.watcher = AppNotifier()
            self.observer.schedule(self.watcher, abspath('.'), recursive=True)
            self.observer.start()

            if use_twisted:
                self.run_twisted(args)
            else:
                self.run_tornado(args)

    def run_tornado(self, args):
        """ Tornado dev server implementation """
        server = self
        import tornado.ioloop
        import tornado.web
        import tornado.websocket

        ioloop = tornado.ioloop.IOLoop.current()

        class DevWebSocketHandler(tornado.websocket.WebSocketHandler):
            def open(self):
                super(DevWebSocketHandler, self).open()
                server.on_open(self)

            def on_message(self, message):
                server.on_message(message)

            def on_close(self):
                super(DevWebSocketHandler, self).on_close()
                server.on_close(self)

        class MainHandler(tornado.web.RequestHandler):
            def get(self):
                self.write(server.index_page)

        #: Set the call later method
        server.call_later = ioloop.call_later
        server.add_callback = ioloop.add_callback

        app = tornado.web.Application([
            (r"/", MainHandler),
            (r"/dev", DevWebSocketHandler),
        ])

        app.listen(self.port)
        print("Tornado Dev server started on {}".format(self.port))
        ioloop.start()

    def run_twisted(self, args):
        """ Twisted dev server implementation """
        server = self

        from twisted.internet import reactor
        from twisted.web import resource
        from twisted.web.static import File
        from twisted.web.server import Site
        from autobahn.twisted.websocket import WebSocketServerFactory, WebSocketServerProtocol
        from autobahn.twisted.resource import WebSocketResource

        class DevWebSocketHandler(WebSocketServerProtocol):
            def onConnect(self, request):
                super(DevWebSocketHandler, self).onConnect(request)
                server.on_open(self)

            def onMessage(self, payload, isBinary):
                server.on_message(payload)

            def onClose(self, wasClean, code, reason):
                super(DevWebSocketHandler,self).onClose(wasClean, code, reason)
                server.on_close(self)

            def write_message(self, message):
                self.sendMessage(message, False)

        #: Set the call later method
        server.call_later = reactor.callLater
        server.add_callback = reactor.callFromThread

        factory = WebSocketServerFactory(u"ws://0.0.0.0:{}".format(self.port))
        factory.protocol = DevWebSocketHandler

        class MainHandler(resource.Resource):
            def render_GET(self, req):
                return str(server.index_page)

        root = resource.Resource()
        root.putChild("", MainHandler())
        root.putChild("dev", WebSocketResource(factory))
        reactor.listenTCP(self.port, Site(root))
        print("Twisted Dev server started on {}".format(self.port))
        reactor.run()

    #: ========================================================
    #: Shared protocol implementation
    #: ========================================================
    def on_open(self, handler):
        self._reload_count = 0
        print("Client connected!")
        self.handler = handler

    def on_message(self, msg):
        print(msg)

    def send_message(self, msg):
        """ Send a message to the client """
        if self.handler is None:
            return #: Client not connected
        self.handler.write_message(msg)

    def on_close(self, handler):
        print("Client left!")
        self.handler = None

    def on_file_changed(self, event):
        """ """
        print(event)
        self._reload_count +=1
        self.changes.append(event)
        self.call_later(self.reload_delay, self._trigger_reload, event)

    def _trigger_reload(self, event):
        self._reload_count -=1
        if self._reload_count == 0:
            files = {}
            for event in self.changes:
                path = os.path.relpath(event.src_path, os.getcwd())
                if os.path.splitext(path)[-1] not in ['.py', '.enaml']:
                    continue
                with open(event.src_path) as f:
                    data = f.read()

                #: Add to changed files
                files[path] = data

            if files:
                #: Send the reload request
                msg = {
                    'type':'reload',
                    'files':files
                }
                print("Reloading: {}".format(files.keys()))
                self.send_message(json.dumps(msg))

            #: Clear changes
            self.changes = []


def find_commands(cls):
    """ Finds commands by finding the subclasses of Command"""
    cmds = []
    for subclass in cls.__subclasses__():
        cmds.append(subclass)
        cmds.extend(find_commands(subclass))
    return cmds


class EnamlNativeCli(Atom):
    #: Root parser
    parser = Instance(ArgumentParser)

    #: Loaded from package
    ctx = Dict()

    #: Parsed args
    args = Instance(Namespace)

    #: Location of package file
    package = Unicode("package.json")

    #: If enaml-native is being run within an app directory
    in_app_directory = Bool()

    #: Commands
    commands = List(Command)

    def _default_commands(self):
        """ Build the list of CLI commands by finding subclasses of the Command class

        Also allows commands to be installed using the "enaml_native_command" entry point.

        This entry point should return a Command subclass

        """
        commands = [c() for c in find_commands(Command)]

        #: Get commands installed via entry points
        for ep in pkg_resources.iter_entry_points(group="enaml_native_command"):
            c = ep.load()
            if not issubclass(c, Command):
                print("Warning: entry point {} did not return a valid enaml cli command! "
                      "This command will be ignored!".format(ep.name))
            commands.append(c())

        return commands

    def _default_in_app_directory(self):
        """ Return if we are in a directory that contains the package.json file 
            which should indicate it's in the root directory of an enaml-native app.
        """
        return exists(self.package)

    def _default_ctx(self):
        """ Return the package config or context and normalize some of the values """
        if not self.in_app_directory:
            print("Warning: {} does not exist. Using the default.".format(self.package))
            ctx = {
                "name": "Enaml-Native Demo",
                "bundle_id": "com.codelv.enamlnative.demo",
                "version": "1.8",
                "private": True,
                "sources": ["src"],
                "android": {
                    "ndk":"~/Android/Crystax/crystax-ndk-10.3.2",
                    "sdk":"~/Android/Sdk",
                    "arches": ["x86", "armeabi-v7a"],
                    "dependencies": {
                        "python2crystax": "2.7.10",
                        "enaml-native": ">=2.1",
                        "ply": "==3.10"
                    },
                    "excluded": [],
                },
                "ios": {
                    "project": "demo",
                    "arches": ["x86_64", "i386", "armv7", "arm64"],
                    "dependencies": {
                        "openssl": "1.0.2l",
                        "python": "2.7.13",
                        "ply": "==3.10"
                    },
                    "excluded": []
                }
            }

        else:
            with open(self.package) as f:
                ctx = json.load(f, object_pairs_hook=OrderedDict)

        #: Add p4a to ctx if not in enaml-native git root
        ctx['cli_root'] = os.environ.get('ENAML_NATIVE_ROOT')
        ctx['android']['p4a'] = os.environ.get('P4A_ROOT')
        ctx['ios']['p4i'] = os.environ.get('P4I_ROOT')

        #: Force ndk and sdk to expand user
        for k in ['ndk', 'sdk']:
            ctx['android'][k] = os.path.expanduser(ctx['android'][k])

        if self.in_app_directory:
            #: Ensure build dir exists for each env
            for env in [ctx['ios'], ctx['android']]:
                #: Add default
                if 'python_build_dir' not in env:
                    env['python_build_dir'] = os.path.expanduser(os.path.abspath('build/python'))

        return ctx

    def _default_parser(self):
        """ Generate a parser using the command list """
        parser = ArgumentParser(prog='enaml-native')

        #: Build commands by name
        cmds = {c.title:c for c in self.commands}

        #: Build parser, prepare commands
        subparsers = parser.add_subparsers()
        for c in self.commands:
            p = subparsers.add_parser(c.title, help=c.help)
            c.parser = p
            for (flags,kwargs) in c.args:
                p.add_argument(flags,**kwargs)
            p.set_defaults(cmd=c)
            c.ctx = self.ctx
            c.cmds = cmds

        return parser

    def start(self):
        """ Run the commands"""
        self.args = self.parser.parse_args()

        cmd = self.args.cmd
        try:
            if cmd.app_dir_required and not self.in_app_directory:
                raise EnvironmentError(
                    "'enaml-native {}' must be run within an app root directory not: {}"
                        .format(cmd.title, os.getcwd()))
            cmd.run(self.args)
        except sh.ErrorReturnCode as e:
            print(e.stderr)
            print(e.stdout)
            raise


def configure_path():
    #: Hack to configure the path correctly when running from source or pip installed versions
    try:
        import site
        user_base = site.USER_BASE
    except:
        user_base = join(expanduser('~'), '.local')

    for root in [join(dirname(realpath(__file__))),
                 join(sys.prefix, 'packages', 'enaml-native-cli'),  #: venv or system install
                 join(user_base, 'packages', 'enaml-native-cli')]:  #: user dir
        if exists(join(root, 'python-for-android')):
            os.environ['ENAML_NATIVE_ROOT'] = abspath(expanduser(root))
            os.environ['P4A_ROOT'] = join(root, 'python-for-android')
            os.environ['P4I_ROOT'] = join(root, 'python-for-ios')

            sys.path.append(os.environ['P4A_ROOT'])
            return
    raise ValueError("Could not find the enaml-native-cli/python-for-android directory")


def main():
    configure_path()
    global shprint
    from pythonforandroid.logger import shprint
    EnamlNativeCli().start()

if __name__ == '__main__':
    main()
