#!/usr/bin/python3
# -*- coding: utf-8 -*-

# Copyright (C) 2014-2016 Canonical Ltd.
# Author: Christopher Townsend <christopher.townsend@canonical.com>

# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; version 3 of the License.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

import argparse
import fcntl
import json
import libertine.utils
import lsb_release
import getpass
import os
import platform
import sys

from apt.debfile import DebPackage
from distro_info import UbuntuDistroInfo, DistroDataOutdated
from libertine import LibertineContainer


def read_container_config_file():
    container_list = {}
    container_config_file = libertine.utils.get_libertine_database_file_path()

    if (os.path.exists(container_config_file) and
        os.path.getsize(container_config_file) != 0):
        with open(container_config_file, 'r') as fd:
            container_list = json.load(fd)

    return container_list


def write_container_config_file(container_list):
    container_config_file = libertine.utils.get_libertine_database_file_path()

    with open(container_config_file, 'w') as fd:
        fcntl.lockf(fd, fcntl.LOCK_EX)
        json.dump(container_list, fd, sort_keys=True, indent=4)
        fd.write('\n')
        fcntl.lockf(fd, fcntl.LOCK_UN)


def get_default_container_id():
    default_container_id = None

    container_list = read_container_config_file()

    if "defaultContainer" in container_list:
        default_container_id = container_list['defaultContainer']

    return default_container_id


def select_container_type():
    kernel_release = platform.release().split('.')

    if int(kernel_release[0]) >= 4:
        return "lxc"
    elif int(kernel_release[0]) == 3 and int(kernel_release[1]) >= 13:
        return "lxc"
    else:
        return "chroot"


def get_host_distro_release():
    distinfo = lsb_release.get_distro_information()

    return distinfo.get('CODENAME', 'n/a')


def is_distro_valid(distro, force):
    if force:
       return UbuntuDistroInfo().valid(distro)

    supported_distros = UbuntuDistroInfo().supported()

    try:
        supported_distros.index(distro)
    except ValueError:
        return False

    return True


def get_distro_codename(distro):
    ubuntu_distro_info = UbuntuDistroInfo()

    for row in ubuntu_distro_info._rows:
        if row['series'] == distro:
            return row['codename']

    return None


def update_container_install_status(container_id, new_status):
    container_list = read_container_config_file()

    for container in container_list['containerList']:
        if container['id'] == container_id:
            container['installStatus'] = new_status

            write_container_config_file(container_list)
            break


def update_container_multiarch_support(container_id, multiarch):
    container_list = read_container_config_file()

    if multiarch == 'enabled' and libertine.utils.get_host_architecture() == 'i386':
        multiarch = 'disabled'

    for container in container_list['containerList']:
        if container['id'] == container_id:
            container['multiarch'] = multiarch

            write_container_config_file(container_list)
            break


def get_container_multiarch_support(container_id):
    container_list = read_container_config_file()

    for container in container_list['containerList']:
        if container['id'] == container_id:
            if not 'multiarch' in container:
                return 'disabled'
            else:
                return container['multiarch']


def update_archive_install_status(container_id, archive_name, new_status):
    container_list = read_container_config_file()

    for container in container_list['containerList']:
        if container['id'] == container_id:
            for archive in container['extraArchives']:
                if archive['archiveName'] == archive_name:
                    archive['archiveStatus'] = new_status
                    write_container_config_file(container_list)
                    return


def add_container_archive(container_id, archive_name):
    container_list = read_container_config_file()

    for container in container_list['containerList']:
        if container['id'] == container_id:
            archive_obj = {'archiveName': archive_name, 'archiveStatus': 'new'}

            if 'extraArchives' not in container:
                container['extraArchives'] = [archive_obj]
            else:
                container['extraArchives'].append(archive_obj)

            write_container_config_file(container_list)
            break


def delete_container_archive(container_id, archive_name):
    container_list = read_container_config_file()

    for container in container_list['containerList']:
        if container['id'] == container_id:
            for archive in container['extraArchives']:
                if archive['archiveName'] == archive_name:
                    container['extraArchives'].remove(archive)
                    write_container_config_file(container_list)
                    return

    print("%s does not exist." % archive_name)
    sys.exit(1)


def archive_exists(container_id, archive_name):
    container_list = read_container_config_file()

    for container in container_list['containerList']:
        if container['id'] == container_id:
            if 'extraArchives' not in container:
                return False
            else:
                for archive in container['extraArchives']:
                    if archive['archiveName'] == archive_name:
                        return True

    return False


def add_new_container(id, name, type, distro):
    if not os.path.exists(libertine.utils.get_libertine_database_dir_path()):
        os.makedirs(libertine.utils.get_libertine_database_dir_path())

    container_list = read_container_config_file()

    container_obj = {'id': id, 'installStatus': 'new', 'type': type,
                     'distro': distro, 'name': name, 'installedApps': []}

    if "defaultContainer" not in container_list:
        container_list['defaultContainer'] = id

    if "containerList" not in container_list:
        container_list['containerList'] = [container_obj]
    else:
        container_list['containerList'].append(container_obj)

    write_container_config_file(container_list)


def delete_container(container_id):
    container_list = read_container_config_file()

    if not container_list:
        print("Unable to delete container.  No containers defined.")
        sys.exit(1)

    for container in container_list['containerList']:
        if container['id'] == container_id:
            container_list['containerList'].remove(container)

            # Set a new defaultContainer if the current default is being deleted.
            if container_list['defaultContainer'] == container_id and container_list['containerList']:
                container_list['defaultContainer'] = container_list['containerList'][0]['id']
            # Remove the defaultContainer if there are no more containers left.
            elif not container_list['containerList']:
                del container_list['defaultContainer']

            write_container_config_file(container_list)
            break


def package_exists(container_id, package_name):
    container_list = read_container_config_file()

    if not container_list:
        return False

    for container in container_list['containerList']:
        if container['id'] == container_id:
            for package in container['installedApps']:
                if package['packageName'] == package_name:
                    return True

    return False


def update_package_install_status(container_id, package_name, new_status):
    container_list = read_container_config_file()

    for container in container_list['containerList']:
        if container['id'] == container_id:
            for package in container['installedApps']:
                if package['packageName'] == package_name:
                    package['appStatus'] = new_status
                    write_container_config_file(container_list)
                    return


def add_new_package(container_id, package_name):
    container_list = read_container_config_file()

    if not container_list:
        print("No containers defined.  Please create a new container before installing a package.")
        sys.exit(1)

    for container in container_list['containerList']:
        if container['id'] == container_id:
            package_obj = {'packageName': package_name, 'appStatus': 'new'}

            if not container['installedApps']:
                container['installedApps'] = [package_obj]
            else:
                container['installedApps'].append(package_obj)

            write_container_config_file(container_list)
            break


def delete_package(container_id, package_name):
    container_list = read_container_config_file()

    if not container_list:
        print("No containers defined.  Please create a new container before installing a package.")
        sys.exit(1)

    for container in container_list['containerList']:
        if container['id'] == container_id:
            for package in container['installedApps']:
                if package['packageName'] == package_name:
                    container['installedApps'].remove(package)
                    write_container_config_file(container_list)
                    return

    print("Package \'%s\' does not exist." % package_name)
    sys.exit(1)


def create(args):
    password = None

    if args.distro and not is_distro_valid(args.distro, args.force):
        print("Invalid distro %s" % args.distro, file=sys.stderr)
        sys.exit(1)

    if args.id and libertine.utils.container_exists(args.id):
        print("Container id \'%s\' is already used." % args.id, file=sys.stderr)
        sys.exit(1)
    elif not args.id:
        args.id = get_unique_container_id(distro)

    if not args.distro:
        args.distro = get_host_distro_release()

    if not args.name:
        args.name = "Ubuntu \'" + get_distro_codename(args.distro) + "\'"

    if not args.type:
        container_type = select_container_type()
    else:
        container_type = args.type

    if container_type == "lxc":
        if args.password:
            password = args.password
        elif sys.stdin.isatty():
            print("Your user password is required for creating a Libertine container.")
            password = getpass.getpass()
        else:
            password = sys.stdin.readline().rstrip()

    add_new_container(args.id, args.name, container_type, args.distro)
    
    multiarch = 'disabled'
    if args.multiarch:
        multiarch = 'enabled'
    update_container_multiarch_support(args.id, multiarch)

    container = LibertineContainer(args.id)
    update_container_install_status(args.id, "installing")
    if not container.create_libertine_container(password, args.multiarch, args.verbosity):
        delete_container(args.id)
        sys.exit(1)
    update_container_install_status(args.id, "ready")


def destroy(args):
    if args.id and not libertine.utils.container_exists(args.id):
        print("Container id \'%s\' does not exist." % args.id, file=sys.stderr)
        sys.exit(1)
    elif not args.id:
        args.id = get_default_container_id()

    container = LibertineContainer(args.id)
    update_container_install_status(args.id, "removing")
    container.destroy_libertine_container()
    update_container_install_status(args.id, "removed")
    delete_container(args.id)


def install_package(args):
    if args.id and not libertine.utils.container_exists(args.id):
        print("Container id \'%s\' does not exist." % args.id, file=sys.stderr)
        sys.exit(1)
    elif not args.id:
        args.id = get_default_container_id()

    is_debian_package = args.package.endswith('.deb')

    if is_debian_package:
        if os.path.exists(args.package):
            package = DebPackage(args.package).pkgname
        else:
            print("%s does not exist." % args.package)
            sys.exit(1)
    else:
        package = args.package

    if package_exists(args.id, package):
        if not is_debian_package:
            print("Package \'%s\' is already installed." % package)
            sys.exit(1)
    else:
        add_new_package(args.id, package)

    container = LibertineContainer(args.id)

    update_package_install_status(args.id, package, "installing")
    if not container.install_package(args.package, args.verbosity):
        delete_package(args.id, package)
        sys.exit(1)

    update_package_install_status(args.id, package, "installed")


def remove_package(args):
    if args.id and not libertine.utils.container_exists(args.id):
        print("Container id \'%s\' does not exist." % args.id, file=sys.stderr)
        sys.exit(1)
    elif not args.id:
        args.id = get_default_container_id()

    if not package_exists(args.id, args.package):
        print("Package \'%s\' is not installed." % args.package)
        sys.exit(1)

    container = LibertineContainer(args.id)
    update_package_install_status(args.id, args.package, "removing")

    container.remove_package(args.package, args.verbosity)
    update_package_install_status(args.id, args.package, "removed")

    delete_package(args.id, args.package)


def search_cache(args):
    if args.id and not libertine.utils.container_exists(args.id):
        print("Container id \'%s\' does not exist." % args.id, file=sys.stderr)
        sys.exit(1)
    elif not args.id:
        args.id = get_default_container_id()

    container = LibertineContainer(args.id)
    container.search_package_cache(args.search_string)


def update(args):
    if args.id and not libertine.utils.container_exists(args.id):
        print("Container id \'%s\' does not exist." % args.id, file=sys.stderr)
        sys.exit(1)
    elif not args.id:
        args.id = get_default_container_id()

    container = LibertineContainer(args.id)
    container.update_libertine_container(args.verbosity)


def list(args):
    containers = libertine.utils.Libertine.list_containers()
    for container in containers:
        print("%s" % container)


def list_apps(args):
    if args.id and not libertine.utils.container_exists(args.id):
        print("Container id \'%s\' does not exist." % args.id, file=sys.stderr)
        sys.exit(1)
    elif not args.id:
        args.id = get_default_container_id()

    container = LibertineContainer(args.id)
    print(container.list_app_launchers(use_json=args.json))


def exec(args):
    if args.id and not libertine.utils.container_exists(args.id):
        print("Container id \'%s\' does not exist." % args.id, file=sys.stderr)
        sys.exit(1)
    elif not args.id:
        args.id = get_default_container_id()

    container = LibertineContainer(args.id)

    sys.exit(container.exec_command(args.command))


def configure(args):
    if args.id and not libertine.utils.container_exists(args.id):
        print("Container id \'%s\' does not exist." % args.id, file=sys.stderr)
        sys.exit(1)
    elif not args.id:
        args.id = get_default_container_id()

    container = LibertineContainer(args.id)

    if args.multiarch and libertine.utils.get_host_architecture() == 'amd64':
        multiarch = 'disabled'
        if args.multiarch == 'enable':
            multiarch = 'enabled'

        current_multiarch = get_container_multiarch_support(args.id)
        if current_multiarch == multiarch:
            print("i386 multiarch support is already %s" % multiarch)
            sys.exit(1)

        container.configure_command('multiarch', args.multiarch)
        update_container_multiarch_support(args.id, multiarch)

    elif args.add_archive:
        if archive_exists(args.id, args.add_archive):
            print("%s already added in container." % args.add_archive)
            sys.exit(1)

        add_container_archive(args.id, args.add_archive)
        update_archive_install_status(args.id, args.add_archive, 'installing')
        if container.configure_command('add-archive', args.add_archive) != 0:
            delete_container_archive(args.id, args.add_archive)
            sys.exit(1)

        update_archive_install_status(args.id, args.add_archive, 'installed')


    elif args.delete_archive:
        if not archive_exists(args.id, args.delete_archive):
            print("%s is not added in container." % args.delete_archive)
            sys.exit(1)

        update_archive_install_status(args.id, args.delete_archive, 'removing')
        if container.configure_command('delete-archive', args.delete_archive) != 0:
            sys.exit(1)

        delete_container_archive(args.id, args.delete_archive)


if __name__ == '__main__':
    parser = argparse.ArgumentParser(description="Legacy X application support for Unity 8")
    parser.add_argument('-q', '--quiet',
                        action='store_const', dest='verbosity', const=0,
                        help=('do not print status updates on stdout'))
    parser.add_argument('-v', '--verbose',
                        action='store_const', dest='verbosity', const=2,
                        help=('extra verbose output'))
    subparsers = parser.add_subparsers(dest="subparser_name")

    # Handle the create command and its options
    parser_create = subparsers.add_parser(
        'create',
        help=("Create a new Libertine container."))
    parser_create.add_argument(
        '-i', '--id',
        required=True,
        help=("Container identifier. Required."))
    parser_create.add_argument(
        '-t', '--type',
        help=("Type of Libertine container to create. Either 'lxc' or 'chroot'."))
    parser_create.add_argument(
        '-d', '--distro',
        help=("Ubuntu distro series to create."))
    parser_create.add_argument(
        '-n', '--name',
        help=("User friendly container name."))
    parser_create.add_argument(
        '--force', action='store_true',
        help=("Force the installation of the given valid Ubuntu distro even if "
              "it is no longer supported."))
    parser_create.add_argument(
        '-m', '--multiarch', action='store_true',
        help=("Add i386 support to amd64 Libertine containers.  This option has "
              "no effect when the Libertine container is i386."))
    parser_create.add_argument(
        '--password',
        help=("Pass in the user's password when creating an LXC container.  This "
              "is intended for testing only and is very insecure."))
    parser_create.set_defaults(func=create)

    # Handle the destroy command and its options
    parser_destroy = subparsers.add_parser(
        'destroy',
        help=("Destroy any existing environment entirely."))
    parser_destroy.add_argument(
        '-i', '--id',
        help=("Container identifier.  Default container is used if omitted."))
    parser_destroy.set_defaults(func=destroy)

    # Handle the install-package command and its options
    parser_install = subparsers.add_parser(
        'install-package',
        help=("Install a package in the specified Libertine container."))
    parser_install.add_argument(
        '-p', '--package',
        required=True,
        help=("Name of package to install or full path to a Debian package. Required."))
    parser_install.add_argument(
        '-i', '--id',
        help=("Container identifier.  Default container is used if omitted."))
    parser_install.set_defaults(func=install_package)

    # Handle the remove-package command and its options
    parser_install = subparsers.add_parser(
        'remove-package',
        help=("Remove a package in the specified Libertine container."))
    parser_install.add_argument(
        '-p', '--package',
        required=True,
        help=("Name of package to remove. Required."))
    parser_install.add_argument(
        '-i', '--id',
        help=("Container identifier.  Default container is used if omitted."))
    parser_install.set_defaults(func=remove_package)

    # Handle the search-cache command and its options
    parser_search = subparsers.add_parser(
        'search-cache',
        help=("Search for packages based on the search string in the specified Libertine container."))
    parser_search.add_argument(
        '-s', '--search-string',
        required=True,
        help=("String to search for in the package cache. Required."))
    parser_search.add_argument(
        '-i', '--id',
        help=("Container identifier.  Default container is used if omitted."))
    parser_search.set_defaults(func=search_cache)

    # Handle the update command and its options
    parser_update = subparsers.add_parser(
        'update',
        help=("Update the packages in the Libertine container."))
    parser_update.add_argument(
        '-i', '--id',
        help=("Container identifier.  Default container is used if omitted."))
    parser_update.set_defaults(func=update)

    # Handle the list command
    parser_list = subparsers.add_parser(
        "list",
        help=("List all Libertine containers."))
    parser_list.set_defaults(func=list)

    # Handle the list-apps command and its options
    parser_list_apps = subparsers.add_parser(
        'list-apps',
        help=("List available app launchers in a container."))
    parser_list_apps.add_argument(
        '-i', '--id',
        help=("Container identifier.  Default container is used if omitted."))
    parser_list_apps.add_argument(
        '-j', '--json',
        action='store_true',
        help=("use JSON output format."))
    parser_list_apps.set_defaults(func=list_apps)

    # Handle the execute command and it's options
    parser_exec = subparsers.add_parser(
        'exec',
        help=("Run an arbitrary command in the specified Libertine container."))
    parser_exec.add_argument(
        '-i', '--id',
        help=("Container identifier.  Default container is used if omitted."))
    parser_exec.add_argument(
        '-c', '--command',
        help=("The command to run in the specified container."))
    parser_exec.set_defaults(func=exec)

    # Handle the configure command and it's options
    parser_configure = subparsers.add_parser(
        'configure',
        help=("Configure various options in the specified Libertine container."))
    parser_configure.add_argument(
        '-i', '--id',
        help=("Container identifier.  Default container is used if omitted."))
    parser_configure.add_argument(
        '-m', '--multiarch',
        choices=['enable', 'disable'],
        help=("Enables or disables i386 multiarch support for amd64 Libertine "
              "containers.   This option has no effect when the Libertine "
              "container is i386."))
    parser_configure.add_argument(
        '-a', '--add-archive',
        metavar='Archive name',
        help=("Adds an archive (PPA) in the specified Libertine container.  Needs to be "
              "in the form of \"ppa:user/ppa-name\"."))
    parser_configure.add_argument(
        '-d', '--delete-archive',
        metavar='Archive name',
        help=("Deletes an existing archive (PPA) in the specified Libertine container.  "
              "Needs to be in the form of \"ppa:user/ppa-name\"."))
    parser_configure.set_defaults(func=configure)

    # Actually parse the args
    args = parser.parse_args()
    if args.verbosity is None:
        args.verbosity = 1

    args.func(args)
