#!/usr/bin/python

import inspect, os, shutil, subprocess, glob, sys, re, argparse, json

scriptdir = os.path.dirname(os.path.realpath(inspect.getfile(inspect.currentframe())))
rootdir = os.path.dirname(scriptdir)

import argparse

parser = argparse.ArgumentParser(description='gitg osx bundler')

parser.add_argument('-d', '--debug', type=bool, help='enable debugging')
parser.add_argument('bundle', type=str, metavar='FILE', help='bundle json config')

args = parser.parse_args()

class Application:
    def __init__(self, name, variables):
        self.name = name
        self.path = os.path.join(rootdir, name + '.app')

        self.install_path = os.path.join('/Applications', self.path)

        self.variables = dict(variables)

        self.variables['name'] = name
        self.variables['path'] = self.path
        self.variables['rootdir'] = rootdir
        self.variables['contents'] = os.path.join(self.path, 'Contents')
        self.variables['resources'] = os.path.join(self.variables['contents'], 'Resources')
        self.variables['lib'] = os.path.join(self.variables['resources'], 'lib')
        self.variables['macos'] = os.path.join(self.variables['contents'], 'MacOS')

        shutil.rmtree(self.path, ignore_errors=True)

        for p in (self.variables['contents'], self.variables['resources'], self.variables['macos']):
            try:
                os.makedirs(p)
            except:
                pass

        self._resolved_libs = {}
        self._pkg_cache = {}

    def repl(self, s):
        def replace(x):
            m = re.match('^pkg:([^:]+):([^:]+)$', x.group(1))

            if m:
                cachename = m.group(1) + ':' + m.group(2)

                if cachename in self._pkg_cache:
                    return self._pkg_cache[cachename]

                out = subprocess.Popen(['pkg-config', '--variable', m.group(2), m.group(1)], stdout=subprocess.PIPE).communicate()[0].strip()
                self._pkg_cache[cachename] = out

                return out
            
            m = re.match('^pkg-root:([^:]+)$', x.group(1))

            if m:
                paths = []

                if 'PKG_CONFIG_PATH' in os.environ:
                    paths.extend(os.environ['PKG_CONFIG_PATH'].split(':'))

                paths.extend(subprocess.Popen(['pkg-config', '--variable', 'pc_path', 'pkg-config'], stdout=subprocess.PIPE).communicate()[0].strip().split(':'))

                for path in paths:
                    if os.path.isfile(os.path.join(path, m.group(1) + '.pc')):
                        return os.path.dirname(os.path.dirname(path))

                sys.stderr.write('Warning: failed to find package ' + m.group(1) + '\n')
                return s
            
            return self.variables[x.group(1)]

        return re.sub("\\${([^}]+)}", replace, s)

    def needs_copy(self, p):
        prefixes = [self.variables['prefix'], '/usr/local'];

        for prefix in prefixes:
            if p.startswith(prefix):
                return True

        return False

    def future_path(self, p):
        if not os.path.isabs(p):
            return os.path.join(self.install_path, p)

        prefixes = [self.variables['prefix'], '/usr/local'];

        for prefix in prefixes:
            if p.startswith(prefix):
                return os.path.join(self.install_path, p[len(prefix) + 1:])
        
        return p

    def copy_binary(self, binary, target):
        binary = self.repl(binary)
        target = self.repl(target)

        target = self._copy(binary, target)

        future = self.future_path(target)
        self._resolved_libs[os.path.realpath(binary)] = future

        os.chmod(target, 0755)

        # Set the new id of the library
        if binary.endswith('.so') or binary.endswith('.dylib'):
            if not args.debug:
                subprocess.call(['strip', '-x', target])

            # Set the new id
            subprocess.call(['install_name_tool', '-id', future, target])
        else:
            if not args.debug:
                subprocess.call(['strip', '-u', '-r', target])

        # Resolve and copy external dependencies
        self.resolve_deps(target)

    def otool_deps(self, path):
        out = subprocess.Popen(['otool', '-L', path], stdout=subprocess.PIPE).communicate()[0]
        return [x.strip().split(' ')[0] for x in out.splitlines()[1:]]

    def resolve_deps(self, libname):
        # Run otool to get the deps
        deps = self.otool_deps(libname)

        for dep in deps:
            rdep = os.path.realpath(dep)

            if not self.needs_copy(rdep) or rdep == libname:
                continue

            if not rdep in self._resolved_libs and rdep != libname:
                # Copy the dependency
                name = os.path.basename(rdep)
                target = os.path.join(self.variables['lib'], name)

                # Go deep
                self.copy_binary(rdep, target)

            newname = self._resolved_libs[rdep].replace(self.variables['contents'], '@executable_path/..')
            subprocess.call(['install_name_tool', '-change', dep, newname, libname])

    def _copy_file_name(self, source, target):
        if target.endswith('/'):
            return target + os.path.basename(source)
        else:
            return target

    def _copy(self, source, target):
        target = self._copy_file_name(source, target)

        try:
            os.makedirs(os.path.dirname(target))
        except:
            pass

        if os.path.isdir(source):
            shutil.copytree(source, target)
        else:
            shutil.copyfile(source, target)

        return target

    def copy_data(self, data, target):
        self._copy(data, target)

    def _interpolate_file(self, filename):
        data = open(filename).read()
        newdata = self.repl(data)

        if newdata != data:
            f = open(filename, 'w')
            f.write(newdata)
            f.flush()
            f.close()

    def copy_data_interpolated(self, data, target):
        target = self._copy(data, target)

        if os.path.isdir(target):
            for root, dirnames, filenames in os.walk(target):
                for filename in filenames:
                    fullname = os.path.join(root, filename)
                    self._interpolate_file(fullname)
        else:
            self._interpolate_file(target)

    def copy_script(self, script, target):
        target = self._copy(script, target)
        os.chmod(target, 0755)

    def copy_glob(self, items, fn):
        for k in items:
            g = self.repl(k)
            files = glob.glob(g)

            if len(files) == 0:
                print('Warning: The glob `{0}\' did not result in any files'.format(g))
                continue

            target = items[k]

            if isinstance(target, dict) and 'files' in target and 'target' in target:
                newfiles = []

                for f in files:
                    newfiles.extend([os.path.join(f, x) for x in target['files']])
                
                files = newfiles
                target = target['target']

            if not isinstance(target, list):
                target = [target]

            for t in target:
                t = self.repl(t)

                for f in files:
                    fn(f, t)

    def link_main(self, main):
        launcher = open(os.path.join(scriptdir, 'launcher'), 'r').read()

        launcher = self.repl(launcher)
        main = self.repl(main)

        lpath = os.path.join(self.variables['macos'], self.name)

        with open(lpath, 'w') as f:
            f.write(launcher)

        os.chmod(lpath, 0755)

        relpath = os.path.relpath(main, os.path.join(self.variables['macos']))
        os.symlink(relpath, os.path.join(self.variables['macos'], self.name + '-bin'))

    def link_binaries(self, binaries):
        p = os.path.join(self._root, 'bin')
        shutil.rmtree(p, ignore_errors=True)

        try:
            os.makedirs(p)
        except:
            pass

        for b in binaries:
            files = glob.glob(self.repl(b))

            for f in files:
                os.symlink(self.future_path(f), os.path.join(p, os.path.basename(f)))

    def copy_pixbuf_loaders(self):
        moduledir = self.repl('${pkg:gdk-pixbuf-2.0:gdk_pixbuf_moduledir}')
        loaders = glob.glob(os.path.join(moduledir, '*.so'))

        target_moduledir = self.repl('gdk-pixbuf-2.0/${pkg:gdk-pixbuf-2.0:gdk_pixbuf_binary_version}')

        for loader in loaders:
            self.copy_binary(loader, os.path.join(self.variables['lib'], target_moduledir, 'loaders', os.path.basename(loader)))

        args = ['gdk-pixbuf-query-loaders']
        args.extend(loaders)

        cache = subprocess.Popen(args, stdout=subprocess.PIPE).communicate()[0]
        cache = cache.replace(moduledir, os.path.join('@executable_path/../Resources/lib', target_moduledir, 'loaders'))

        with open(os.path.join(self.variables['lib'], target_moduledir, 'loaders.cache'), 'w') as f:
            f.write(cache)

    def glib_compile_schemas(self):
        subprocess.call(['glib-compile-schemas', os.path.join(self.variables['resources'], 'share', 'glib-2.0', 'schemas')])

    def copy_schemas(self, schemas):
        if not schemas:
            return

        self.copy_glob(schemas, application.copy_data)
        self.glib_compile_schemas()

    def copy_icon_themes(self, themes):
        if not themes:
            return

        self.copy_glob(themes, application.copy_data)

        for theme_path in themes.values():
            subprocess.call(['gtk3-update-icon-cache', '-f', '-t', '--quiet', self.repl(theme_path)])

bundle = json.load(open(args.bundle, 'r'))

# Create the framework
application = Application(bundle['name'], bundle['variables'])

# Copy binaries
application.copy_glob(bundle['binaries'], application.copy_binary)

# Link main
application.link_main(bundle['main'])

# Copy pixbuf loaders
application.copy_pixbuf_loaders()

# Copy data
application.copy_glob(bundle['data'], application.copy_data)

# Copy data interpolated
application.copy_glob(bundle['data_interpolated'], application.copy_data_interpolated)

# Compile glib schemas
application.copy_schemas(bundle['schemas'])

# Compile icon themes
application.copy_icon_themes(bundle['icon-themes'])

print('Application created in {0}.app'.format(bundle['name']))

# vi:ts=4:et
