#!/usr/bin/env python3

import argparse
import sys
import shutil
import subprocess
import operator
from enum import Enum
from functools import reduce
import concurrent.futures
import json
import io

# for apt
import urllib.request

neededFiles = [
    "Qt5CoreConfig.cmake",
    "Qt5QuickConfig.cmake",
    "Qt5WebEngineConfig.cmake",

    "ECMConfig.cmake",
    "KF5*Config.cmake",
]

neededBinaries =  [
    "gcc",
    "g++",
    "cmake",
    "cmake-gui",
    "git",
    "gdb",
    "clang",
    "make",
    "ninja",
    "msgfmt" #it's needed when including ki18n
    #"clazy",
]

suggestedBinaries = [
    "plasmaengineexplorer",
    "plasmawindowed",
    "cuttlefish",
    "kate",
    "kdevelop"
]

def progressCallback(progress, type, user_data):
    pass
    #print('  ', type.value_name, 'package:', progress.props.package_id, ':', progress.props.percentage, '%')

class RequirementType(Enum):
    Needed = 0
    Suggested = 1

class Contents:
    class Connection:
        def __init__(self, urlBase = 'https://contents.neon.kde.org/v2'):
            self.urlBase = urlBase

        def getJSON(self, path):
            url = "%s/%s" % (self.urlBase, path)
            response = urllib.request.urlopen(url)
            ret = json.loads(response.read().decode('utf-8'))
            return ret

    class Archive:
        overrides = {
            "usr/bin/gcc": "build-essential",
            "usr/bin/g++": "build-essential",
            "usr/bin/make": "build-essential"
        }

        def __init__(self, id):
            self.id = id
            self.connection = Contents.Connection()

        def findFile(self, file):
            return self.connection.getJSON(('findFirst/%s?q=*%s' % (self.id, file)))

        def findPackageForFile(self, file):
            try:
                return self.overrides[file]
            except:
                pass
            payload = self.findFile(file)
            if not payload:
                return None
            assert len(payload.keys()) == 1
            # Nested list since we don't resolve files, simply pick first.
            packages = list(payload.values())[0]
            assert len(packages) == 1, "Cannot find which package to use for " + file
            return packages[0]

        def findAllPackages(self, file):
            payload = self.findFile(file)
            packages = payload.values()
            return [item for sublist in packages for item in sublist] #make sure it's just a list, not a list of lists

    poolsCache = None

    @classmethod
    def pools(klass, connection = Connection()):
        if not klass.poolsCache:
            klass.poolsCache = connection.getJSON('pools')
        return klass.poolsCache

class Global:
    repositories = None
    backend = "debian"

    packages = {}
    packagesNotFound = []

    def __init__(self):
        values  = [ (v, RequirementType.Needed, False) for v in neededFiles]
        values += [ (v, RequirementType.Needed, True)  for v in neededBinaries]
        values += [ (v, RequirementType.Suggested, True)  for v in suggestedBinaries]

        self.process(values)

    def addResults(self, fileName, req, pkgs):
        if not pkgs:
            self.packagesNotFound.append(fileName)
        else:
            for p in pkgs:
                self.packages[p] = (req, fileName)

    def printVerbose(self):
        if self.packagesNotFound:
            print("Could not find packages for", self.packagesNotFound)
            print()

        print("We suggest the following packages:")
        for pkg, props in self.packages.items():
            print("* ", pkg, "<-", props)

    def printPackagesJson(self):
        ret = {
            "required": [],
            "suggested": []
        }
        for pkg, (required, filename) in self.packages.items():
            if required == RequirementType.Needed:
                ret["required"].append(pkg)
            else:
                ret["suggested"].append(pkg)
        ret["required"].sort()
        ret["suggested"].sort()
        print(json.dumps(ret, sort_keys=True, indent=4, separators=(',', ': ')))

    def printPackages(self):
        print(" ".join(self.packages.keys()))

    def installPackages(self):
        subprocess.call(["pkcon", "install"] + list(self.packages.keys()))

    def searchFile(fileName, requirement, isExecutable):
        packages = None

        #ArchLinux
        if Global.backend == "archlinux":
            cmd = ["pacman", "--machinereadable", "-F", fileName]
            output = subprocess.check_output(cmd)
            process = output.split(b'\0')
            packages = [ process[1].decode("utf-8") ] # only use the first alternative, pacman will sort them by repository precedence
        #Debian
        elif Global.backend == "debian":
            prefix = "usr/bin/" if isExecutable else "/"
            packages = []
            for repo in Contents.pools():
                pkg = Contents.Archive(repo).findPackageForFile(prefix + fileName)
                if pkg:
                    packages = [pkg]
                    break
            # print("pac", fileName, package)

        return (fileName, requirement, packages)

    def searchGlobFiles(fileName, requirement, isExecutable):
        packages = None

        #ArchLinux
        if Global.backend == "archlinux":
            fileNamerx = fileName.replace("*", ".*")
            cmd = ["pacman", "--machinereadable", "-Fx", fileNamerx]
            output = subprocess.check_output(cmd)
            lines = output.split(b'\n')
            packages = [line.split(b'\0')[1].decode("utf-8") for line in lines if len(line)>0]
        #Debian
        elif Global.backend == "debian":
            prefix = "usr/bin/" if isExecutable else "/"
            packages = []
            for repo in Contents.pools():
                packages = Contents.Archive(repo).findAllPackages(prefix + fileName)
                if packages:
                    break

        return (fileName, requirement, packages)

    def locate(arg):
        (fileName, requirement, isExecutable) = arg
        ret = None
        if '*' in fileName:
            ret = Global.searchGlobFiles(fileName, requirement, isExecutable)
        else:
            ret = Global.searchFile(fileName, requirement, isExecutable)
        return ret

    def process(self, values):
        with concurrent.futures.ProcessPoolExecutor() as executor:
            #for fileName, req, packages in map(Global.locate, values):
            for fileName, req, packages in executor.map(Global.locate, values):
                self.addResults(fileName, req, packages)

def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("command", help="[install|packages|packages-json|list]")
    parser.add_argument("backend", choices=["debian", "archlinux"])
    parser.add_argument("--verbose", action='store_true', help="increase output verbosity")
    args = parser.parse_args()

    Global.backend = args.backend

    g = Global()

    if args.verbose:
        g.printVerbose()

    if args.command == "install":
        g.installPackages()
    elif args.command == "packages":
        g.printPackages()
    elif args.command == "packages-json":
        g.printPackagesJson()
    else:
        correctCommand = args.command=="list"
        if not correctCommand:
            print("Wrong command '%s'" % args.command)
            print()

        print(" install:       triggers an install of the needed packages.")
        print(" packages:      lists the suggested packages, for distributors mostly.")
        print(" packages-json: lists the suggested packages, for distributors mostly.")
        print(" list:          this list")
        return 0 if correctCommand else 1

    return 0

if __name__ == "__main__":
    sys.exit(main())
