from argparse import ArgumentParser, Namespace
from os import system, devnull, environ, remove, makedirs, listdir, chdir
from typing import List
from sys import exit
from subprocess import Popen, PIPE
from re import search, IGNORECASE
from os.path import splitext, exists, abspath, isdir, isfile, join, basename
from tempfile import NamedTemporaryFile
from shutil import copy2


def add_path_ext(path: str) -> str:
    p, n = splitext(path)
    if n != '':
        return path
    else:
        pext = environ['PATHEXT']
        pextl = pext.split(';')
        for ext in pextl:
            if ext == '':
                continue
            if exists(p + ext):
                return p + ext
    return path


def check_pdb(path: str) -> str:
    p = splitext(path)[0] + '.pdb'
    if exists(p):
        return p


def check_needed_prog():
    if system(f'ldd-rust --help > {devnull}'):
        return False
    if system(f'7z --help > {devnull}'):
        return False
    return True


def check_prog(prog: str) -> List[str]:
    r = Popen(f'ldd-rust {prog}', stdout=PIPE, stderr=PIPE)
    out: bytes = r.communicate()[0]
    r.wait()
    out += r.communicate()[0]
    if not r.returncode:
        sl = out.splitlines(False)
        rl = []
        for r in sl:
            r = r.decode()
            rs = search(r'=?-?> (.+) ?(\(0x[0-9a-f]+\))?$', r, IGNORECASE)
            if rs is not None:
                rl.append(abspath(rs.groups()[0]))
            else:
                raise ValueError(f'Can not find path for {r}.')
        return rl
    return None


def getUnixPath(path: str) -> str:
    rs = search(r'^[A-Z]:', path, IGNORECASE)
    if rs is None:
        return path.replace('\\', '/')
    return '/' + path[0].lower() + path[2:].replace('\\', '/')


def getWindowsPath(path: str) -> str:
    rs = search(r'^[\\/][A-Z][\\/]', path, IGNORECASE)
    if rs is None:
        return path.replace('/', '\\')
    return path[1].upper() + ":" + path[2:].replace('/', '\\')


def listdirs(loc: str, ignore_hidden_files: bool = True):
    bl = listdir(loc)
    r = []
    for i in bl:
        if i.startswith('.'):
            if ignore_hidden_files or i == '.' or i == '..':
                continue
        p = join(loc, i)
        if isfile(p):
            r.append(p)
        elif isdir(p):
            r += listdirs(p)
    return r


def remove_dirs(loc: str):
    bl = listdirs(loc, False)
    for i in bl:
        if isfile(i):
            remove(i)
        elif isdir(i):
            try:
                remove_dirs(i)
            except Exception:
                remove_dirs(i)
    remove(loc)

class Prog:
    def __init__(self):
        self._loc = []
        self.strip = False
        self.num_cpu = None

    def add_dep(self, path: str):
        path_w = getWindowsPath(path)
        if path_w.upper().startswith('C:\\WINDOWS'):
            return
        if path_w not in self._loc:
            print(f'add dependence: "{path_w}"')
            self._loc.append(path_w)

    def add_prog(self, path: str):
        pro = add_path_ext(path)
        pro_w = getWindowsPath(pro)
        if pro_w not in self._loc:
            print(f'add program: "{pro_w}"')
            self._loc.append(pro_w)

    def add_file(self, path: str):
        p = getWindowsPath(path)
        if p not in self._loc:
            print(f'add file: "{p}"')
            self._loc.append(p)

    def to_7z(self, output: str):
        if self.strip:
            makedirs('temp', exist_ok=True)
        p = NamedTemporaryFile(delete=False)
        for i in self._loc:
            if not self.strip:
                p.write((i + '\n').encode('UTF8'))
            else:
                bn = basename(i)
                dest = f'temp/{bn}'
                print(f'Copying {i} to {dest}')
                copy2(i, dest)
                pr = Popen(['strip', dest])
                pr.wait()
                p.write((bn + '\n').encode('UTF8'))
        fp = p.name
        p.close()
        output = getWindowsPath(abspath(output))
        try:
            num_cpu = '' if self.num_cpu is None else f' -mmt{self.num_cpu}'
            if self.strip:
                chdir('temp')
            system(f'7z a{num_cpu} -mx9 -y {output} @{fp}')
            if self.strip:
                chdir('../')
        except Exception:
            remove(fp)
            from traceback import print_exc
            print_exc()
        remove(fp)
        if self.strip:
            try:
                remove_dirs('temp')
            except Exception:
                pass


def main(prog: List[str], output: str = None, adds: List[str] = None,
         pdbs: List[str] = None, args: Namespace = None):
    if output is None:
        output = 'programs.7z'
    if not check_needed_prog():
        print('ldd and 7z is needed.')
    p = Prog()
    for pro in prog:
        pro = abspath(pro)
        pro = add_path_ext(pro)
        # pro_u = getUnixPath(pro)
        rel = check_prog(pro)
        if rel is None:
            print(f'Can not get dependencies for {pro},')
            exit(-1)
        p.add_prog(pro)
        for i in rel:
            p.add_dep(i)
    if adds is not None:
        for f in adds:
            p.add_file(f)
    if pdbs:
        for i in pdbs:
            pro = abspath(i)
            pro = add_path_ext(pro)
            rel = check_prog(pro)
            if rel is None:
                print(f'Can not get dependencies for {pro},')
                exit(-1)
            fn = check_pdb(pro)
            if fn:
                p.add_file(fn)
            for i in rel:
                fn = check_pdb(i)
                if fn:
                    p.add_file(fn)
    if args and args.strip:
        p.strip = True
    if args and args.num_cpu:
        p.num_cpu = args.num_cpu
    p.to_7z(output)


p = ArgumentParser(description='Pack programs into a 7-zip file.')
p.add_argument('PROG', action='append', help='Program to pack', nargs='*', default=[])
p.add_argument('-p', '--pdb', action='append', help='Program\'s PDB file to pack', default=[], dest='pdb')
p.add_argument('-o', '--output', help='Output file', default='programs.7z', dest='output')
p.add_argument('-a', '--add', action='append', help='Additional files to pack', default=[], dest='add')
p.add_argument('-s', '--strip', help='Strip before package', default=False, action='store_true', dest='strip')
p.add_argument('-n', '--num_cpu', help='Number of CPU to use', default=None, type=int, dest='num_cpu')


if __name__ == '__main__':
    try:
        args = p.parse_args()
        main(args.PROG[0], args.output, args.add, args.pdb, args)
    except Exception:
        from traceback import print_exc
        from sys import exit
        print_exc()
        exit(-1)
