
import json, os, glob, subprocess
from collections import defaultdict

from planutils import settings

PACKAGES = {}

CUR_DIR = os.path.dirname(os.path.abspath(__file__))


def check_package(target, manifest):
    assert os.path.exists(manifest), "Error: Manifest must be defined for %s" % target
    with open(manifest, 'r') as f:
        config = json.load(f)
    for key in ['name', 'description', 'dependencies', 'install-size']:
        assert key in config, "Error: Manifest for %s must include '%s'" % (base, key)


for conf_file in glob.glob(os.path.join(CUR_DIR, 'packages', '*')):
    base = os.path.basename(conf_file)
    if base not in ['README.md', 'TEMPLATE']:
        assert base not in PACKAGES, "Error: Duplicate package config -- %s" % base
        check_package(base, os.path.join(conf_file, 'manifest.json'))
        with open(os.path.join(conf_file, 'manifest.json'), 'r') as f:
            config = json.load(f)
        PACKAGES[base] = config
        PACKAGES[base]['runnable'] = os.path.exists(os.path.join(conf_file, 'run'))


def check_installed(target):
    return target in settings.load()['installed']


def uninstall(targets):

    for target in targets:
        if target not in PACKAGES:
            print("Error: Package not found -- %s" % target)
            return

    to_check = []
    for target in targets:
        if check_installed(target):
            to_check.append(target)
        else:
            print("%s isn't installed." % target)

    if not to_check:
        return

    s = settings.load()
    # map a package to all those that depend on it
    dependency_mapping = defaultdict(set)
    for p in s['installed']:
        for dep in PACKAGES[p]['dependencies']:
            dependency_mapping[dep].add(p)

    # compute all the packages that will be removed
    to_remove = set()
    while to_check:
        check = to_check.pop(0)
        to_remove.add(check)
        to_check.extend(list(dependency_mapping[check]))

    print("\nAbout to remove the following packages: %s" % ', '.join(to_remove))
    if input("  Proceed? [y/N] ").lower() in ['y', 'yes']:
        for package in to_remove:
            print ("Uninstalling %s..." % package)
            subprocess.call('./uninstall', cwd=os.path.join(CUR_DIR, 'packages', package))
            print ("Finished uninstalling %s" % package)
            s['installed'].remove(package)

    # Search for any packages that may be no longer required
    dependency_mapping = defaultdict(set)
    for p in PACKAGES:
        for dep in PACKAGES[p]['dependencies']:
            dependency_mapping[dep].add(p)

    possible_deletions = [d for p in to_remove for d in PACKAGES[p]['dependencies']]
    while possible_deletions:
        package = possible_deletions.pop(0)
        # Consider removing if (1) it's installed; and (2) nothing that requires this is installed
        if (package in s['installed']) and (not any([p in s['installed'] for p in dependency_mapping[package]])):
            print("\nPackage may no longer be required: %s" % package)
            if input("  Remove? [y/N] ").lower() in ['y', 'yes']:
                print ("Uninstalling %s..." % package)
                subprocess.call('./uninstall', cwd=os.path.join(CUR_DIR, 'packages', package))
                print ("Finished uninstalling %s" % package)
                s['installed'].remove(package)
                possible_deletions.extend(PACKAGES[package]['dependencies'])

    settings.save(s)


def package_list():
    print("\nInstalled:")
    installed = set(settings.load()['installed'])
    for p in installed:
        print("  %s: %s" % (p, PACKAGES[p]['name']))

    print("\nAvailable:")
    for p in PACKAGES:
        if p not in installed:
            print("  %s: %s" % (p, PACKAGES[p]['name']))
    print()

def upgrade():
    s = settings.load()
    for package in s['installed']:
        print("Upgrading %s..." % package)
        subprocess.call('./uninstall', cwd=os.path.join(CUR_DIR, 'packages', package))
        subprocess.call('./install', cwd=os.path.join(CUR_DIR, 'packages', package))

def install(targets, forced=False):
    for target in targets:
        if target not in PACKAGES:
            print("Error: Package not found -- %s" % target)
            return False

    # Compute all those that will need to be installed
    to_check = []
    for target in targets:
        if check_installed(target):
            print("%s is already installed." % target)
        else:
            to_check.append(target)

    done = set()
    to_install = []
    while to_check:
        check = to_check.pop(0)
        if check not in done:
            done.add(check)
            if not check_installed(check):
                to_install.append(check)
                to_check.extend(PACKAGES[check]['dependencies'])

    to_install.reverse()

    if to_install:
        to_install_desc = ["%s (%s)" % (pkg, PACKAGES[pkg]['install-size']) for pkg in to_install]
        print("\nAbout to install the following packages: %s" % ', '.join(to_install_desc))

        if forced:
            user_response = True
        else:
            user_response = input("  Proceed? [Y/n] ").lower() in ['', 'y', 'yes']

        if user_response:
            installed = []
            for package in to_install:
                package_path = os.path.join(CUR_DIR, 'packages', package)
                print("Installing %s..." % package)
                try:
                    installed.append(package)
                    subprocess.check_call('./install', cwd=package_path, shell=True)
                    size = subprocess.check_output('du -sh .',
                                                   cwd=package_path,
                                                   shell=True,
                                                   encoding='utf-8').split('\t')[0]
                    print("Finished installing %s (size: %s)" % (package, size))
                except subprocess.CalledProcessError:
                    print("\nError installing %s. Rolling back changes..." % package)
                    for p in installed:
                        subprocess.call('./uninstall', cwd=os.path.join(CUR_DIR, 'packages', p))
                    return False

            s = settings.load()
            s['installed'].extend(installed)
            settings.save(s)
            return True
        else:
            print("Aborting installation.")
    else:
        print("Nothing left to install.")

    return False

