#!/usr/bin/env python
#
# -*- coding: utf-8 -*-
#

import sys
import os

import chewy
import chewy.session

import argparse
import urllib.parse
import tempfile


try:
    import portage.output
    log = portage.output.EOutput()

except ImportError:
    class FakeLogger(object):
        def einfo(self, msg):
            print(msg)
        def eerror(self, msg):
            print(msg, file=sys.stderr)
        def ewarn(self, msg):
            print(msg)

    log = FakeLogger()



def rcv_manifests(repobase_list):
    # Retrieve manifests for all used chewy repositories
    manifest_map = {}
    with chewy.session.Factory() as sf:
        for repobase in repobase_list:
            if repobase not in manifest_map:                   # Manifest still not retrieved
                ep = chewy.HttpEndpoint(repobase)
                manifest_map[repobase] = sf.get_session(ep).get_manifest()
    return manifest_map



class Application(object):

    __modules_dir = None
    cmd_parser = None
    args = None


    def __init__(self):
        self.cmd_parser = argparse.ArgumentParser(description='Manage the project specific CMake modules')
        self.cmd_parser.add_argument(
            '-m'
          , '--modules-dir'
          , metavar='PATH'
          , help='CMake modules directory to operate with'
          )


        subparsers = self.cmd_parser.add_subparsers(help='Available sub-commands:', dest='cmd')

        list_parser = subparsers.add_parser(
            'list'
          , help="Show available modules by repositores' URLs."
          , aliases=['ls']
          )
        list_parser.add_argument(
            'rep_url'
          , metavar='REPO-URL'
          , help='Repositories URL list.'
                 'Empty one means to list all repositories which URLs are '
                 'founded in installed modules'
          , nargs='*'
          )
        list_parser.set_defaults(func=self.do_list)

        install_parser = subparsers.add_parser(
            'install'
          , help='Install module from repository and add it to the project as new one.'
          , aliases=['in']
          )
        install_parser.add_argument('file_url', metavar='FILE-URL', help='Files URL list', nargs='+')
        install_parser.set_defaults(func=self.do_install)

        update_parser = subparsers.add_parser(
            'update'
          , help='Update [all] installed modules from their repo-sources'
          , aliases=['up']
          )
        update_parser.add_argument(
            'file_url'
          , metavar='FILE-URL'
          , help='Files URI list. Empty one means to update all installed modules'
          , nargs='*'
          )
        update_parser.set_defaults(func=self.do_update)

        status_parser = subparsers.add_parser('status', help='Check status of [all] installed files', aliases=['st'])
        status_parser.add_argument(
            'file_url'
          , metavar='FILE-URL'
          , help='Files URI list. Empty one means to check status of all installed modules'
          , nargs='*'
          )
        status_parser.set_defaults(func=self.do_status)

        uninstall_parser = subparsers.add_parser(
            'uninstall'
          , help='Uninstall installed modules'
          , aliases=['un']
          )
        uninstall_parser.add_argument(
            'file_url'
          , metavar='FILE-URL'
          , help='Module paths list'
          , nargs='+'
          )
        uninstall_parser.set_defaults(func=self.do_uninstall)



    def get_modules_dir(self):
        if not self.__modules_dir:
            if self.args.modules_dir:
                # TODO Check for symlinked directory
                if os.path.isdir(self.args.modules_dir):
                    self.__modules_dir = self.args.modules_dir
                else:
                    raise RuntimeError("Specified path is not a directory: `{}'".format(self.args.modules_dir))
            else:
                self.__modules_dir = chewy.modules_dir_lookup()
                log.einfo("Guessed CMake modules directory: `{}'".format(self.__modules_dir))

        return self.__modules_dir



    def uninstall(self, mod):
        '''
            Uninstall the chewy module and all related files
            Note uninstall doesn't clean empty directories possible after uninstall process
        '''
        for path in [ os.path.join(self.get_modules_dir(), f) for f in mod.addons + [mod.path] ]:
            abspath = chewy.sandbox_path(self.get_modules_dir(), path)
            # TODO: os.path.exists returns False for zero-sized files
            if os.path.exists(abspath):
                os.unlink(abspath)



    def rcv_module(self, obj, sf):
        # "Overloading" by the first argument
        local_module = None
        if isinstance(obj, chewy.Module):
            local_module = obj
            url = os.path.join(local_module.repobase, local_module.path)
        elif isinstance(obj, str):
            url = obj
        else:
            assert(False)

        # Doesn't catch any exception because any error means module receiving is fall
        ep = chewy.HttpEndpoint(url)
        cs = sf.get_session(ep)
        log.einfo('Receiving the module file {}'.format(url))
        data = cs.retrieve_remote_file(ep.geturl())             # Get a remote file into string
        mod = chewy.Module(data)

        def create_subtree(prefix, name):
            abs_path = chewy.sandbox_path(prefix, name)
            abs_dirname = os.path.dirname(abs_path)
            if not os.path.exists(abs_dirname):
                os.makedirs(
                    abs_dirname
                  , exist_ok=True
                  )

        with tempfile.TemporaryDirectory() as tmpdirname:
            # Create necessary temporary directorie's sub tree
            create_subtree(tmpdirname, mod.path)
            # Write module to file
            with open(chewy.sandbox_path(tmpdirname, mod.path), 'wt', encoding='utf-8') as f:
                f.write(data)


            # Receive all module-related files
            for relpath in mod.addons:
                url = os.path.join(mod.repobase, relpath)
                ep = chewy.HttpEndpoint(url)
                cs = sf.get_session(ep)
                log.einfo('Receiving the addon file {}'.format(url))
                data = cs.retrieve_remote_file(ep.geturl())             # Get a remote file into string

                # Create necessary temporary directorie's sub tree
                create_subtree(tmpdirname, relpath)

                # Going to write just received data to the temporary dir
                # TODO: Strip repobasename
                with open(chewy.sandbox_path(tmpdirname, relpath), 'wt', encoding='utf-8') as f:
                    f.write(data)
                    # TODO: Version compare is required as well. Is it?
            if local_module:
                self.uninstall(local_module)

            # If success, install module to real modules path
            chewy.copytree(tmpdirname, self.get_modules_dir())



    def get_statuses(self):
        '''
            Return: Dictionary by unique repobase to module-related statuses' list
        '''
        # Get modules installed in a given dir
        status_map = chewy.collect_installed_modules(self.get_modules_dir())

        # Retrieve remote_modules_map for all used chewy repositories
        manifest_map = rcv_manifests(status_map.keys())

        # Iterate over installed modules (statuses actually) grouped by repository
        for repobase, modules in status_map.items():
            # Iterate over list of module statuses
            for status in modules:
                local_mod = status.module
                # Find local module in a remote repository
                remote_mod = chewy.find(manifest_map[repobase].modules, lambda x: x.path == local_mod.path)
                # If not found, mark current module as deleted
                if not remote_mod:
                    status.set_remote_version(None)
                else:
                    assert(remote_mod.path == local_mod.path)
                    # Remember the remote version
                    status.set_remote_version(remote_mod.version)
        return status_map



    def do_list(self):
        '''
            Execute `list' command
            If no repo given, the function try to find a modules base,
            scan it and collect used repo bases
        '''
        url_list = self.args.rep_url
        if not url_list:
            url_list = chewy.collect_installed_modules(self.get_modules_dir()).keys()

        # Make URLs unique
        urls = set(url_list)

        if not urls:
            raise RuntimeError('No chewy repository URLs was found and nothing provided in the command line')

        for repobase, manifest in rcv_manifests(urls).items():
            log.einfo("Modules from the `{}' repository".format(repobase))
            print(chewy.FancyGrid(
                [ [mod.path, mod.version, mod.description] for mod in manifest.modules ]
              ))



    def do_install(self):
        '''Execute `install' command'''
        url_list = self.args.file_url
        if not url_list:
            raise RuntimeError('At least one url should be given')

        with chewy.session.Factory() as sf:
            for url in url_list:
                try:
                    self.rcv_module(url, sf)

                # TODO pass 'can't create modules dir' exception through
                except RuntimeError as ex:
                    log.eerror("Can't receive the file `{}': {}".format(url, ex))



    def do_status(self):
        '''Execute `status' command'''
        status_map = self.get_statuses().items()

        if not status_map:
            raise RuntimeError('No chewy repository URLs was found and nothing provided at the command line')

        for repobase, status_list in status_map:
            log.einfo('List of installed modules from the repository {}'.format(repobase))
            # TODO Colorise output (especially new versions)
            print(
                chewy.FancyGrid([
                    [
                        m.status_as_string()
                      , m.module.path
                      , m.module.version
                      , m.available_version()
                      , m.module.description
                    ]
                    for m in status_list
                  ])
              )



    def do_update(self):
        '''Execute `update' command'''
        # TODO: Use it
        file_list = self.args.file_url
        with chewy.session.Factory() as sf:
            for repobase, status_list in self.get_statuses().items():
                for mod in [ st.module for st in status_list if st.needs_update() ]:
                    try:
                        self.rcv_module(mod, sf)
                    # TODO pass 'can't create modules dir' exception through
                    except RuntimeError as ex:
                        log.eerror("Can't update the module `{}': {}".format(url, ex))



    def do_uninstall(self):
        '''Execute `uninstall' command'''
        for f in self.args.file_url:
            try:
                abspath = os.path.abspath(f)
                if not abspath.startswith(self.get_modules_dir()):
                    abspath = chewy.sandbox_path(self.get_modules_dir(), f)
                self.uninstall(chewy.open_module(abspath))
            except (RuntimeError, IOError) as ex:
                log.eerror("Can't uninstall `{}': {}".format(f, ex))
                continue



    def run(self):
        self.args = self.cmd_parser.parse_args()
        if self.args.cmd is not None:
            self.args.func()
        else:
            log.eerror('No command given')



if __name__ == "__main__":
    try:
        a = Application()
        a.run()
    except KeyboardInterrupt:
        log.ewarn('Terminated by user request')
        sys.exit(1)
    except RuntimeError as ex:
        log.eerror('Error: {}'.format(ex))
        sys.exit(1)
    sys.exit(0)
