#!/usr/bin/env python3

import argparse
import logging
import os
import stat
import re

MASQUERADE_DIR = "/usr/lib/ccache"

GCC_LIBEXEC_DIRS = (
    '/usr/lib/gcc',  # Debian native GCC compilers
    '/usr/lib/gcc-cross',  # Debian GCC cross-compilers
    '/usr/libexec/gcc', # rpm-based distros
)

TOOL_PATH = '/usr/bin/ccache'


def sloppy_listdir(thedir):
    try:
        return os.listdir(thedir)
    except FileNotFoundError:
        pass
    except NotADirectoryError:
        pass
    return []


def scan_symlinks(thedir):
    links = set()
    for name in os.listdir(thedir):
        path = os.path.join(thedir, name)
        if os.path.islink(path):
            if os.access(path, os.X_OK):
                links.add(name)
            else:
                logging.debug('scan_symlinks: removing stale link "%s"', path)
                os.unlink(path)
    return links


def update_symlinks(thedir, old_symlinks=None, new_symlinks=None, dest=TOOL_PATH):
    for link in old_symlinks:
        path = os.path.join(thedir, link)
        if link not in new_symlinks:
                os.unlink(path)
                logging.debug('update_symlinks: rm %s', path)
    for link in new_symlinks:
        path = os.path.join(thedir, link)
        if link not in old_symlinks:
            os.symlink(dest, path)
            logging.debug('update_symlinks: ln -s %s %s', dest, path)


class CompilerLinksUpdater(object):
    def __init__(self, masquerade_dir=MASQUERADE_DIR, tool_path=TOOL_PATH):
        self.masquerade_dir = masquerade_dir
        self.tool_path = tool_path
        self.old_symlinks = set()
        self.new_symlinks = set()

    def consider(self, name):
        if os.access(f"/usr/bin/{name}", os.X_OK):
            self.new_symlinks.add(name)
            logging.debug('consider: %s', name)

    def consider_gcc(self, prefix, suffix=""):
        self.consider(f"{prefix}gcc{suffix}")
        self.consider(f"{prefix}g++{suffix}")

    def scan_gcc_libexec(self, gcc_dir):
        for gnu_host in sloppy_listdir(gcc_dir):
            self.consider_gcc(f"{gnu_host}-")
            for version in sloppy_listdir(gcc_dir + "/" + gnu_host):
                self.consider_gcc("", f"-{version}")
                self.consider_gcc(f"{gnu_host}-", f"-{version}")

    def consider_clang(self, suffix):
        self.consider(f"clang{suffix}")
        self.consider(f"clang++{suffix}")

    def scan_clang(self):
        self.consider_clang("")
        for ent in os.listdir("/usr/lib"):
            if ent.startswith("llvm-"):
                version = ent.split("-")[1]
                if '.' in version:
                    version = version.split('.')[0]
                self.consider_clang(f"-{version}")

    def run(self):
        os.makedirs(self.masquerade_dir, exist_ok=True)
        self.old_symlinks = scan_symlinks(self.masquerade_dir)

        for x in ("cc", "c++", "c89", "c99"):
            self.consider(x)
        for x in ("", "c89-", "c99-"):
            self.consider_gcc(x)

        for gcc_dir in GCC_LIBEXEC_DIRS:
            self.scan_gcc_libexec(gcc_dir)

        self.scan_clang()
        update_symlinks(self.masquerade_dir, self.old_symlinks,
                        self.new_symlinks, dest=self.tool_path)

def main():
    parser = argparse.ArgumentParser(description='Update ccache symlink directory')
    parser.add_argument('-v', '--verbose', action='count', default=0)
    parser.add_argument('-d', '--masquerade-dir', default=MASQUERADE_DIR,
            help=f'manage symlinks in this directory (default: {MASQUERADE_DIR})')
    parser.add_argument('-t', '--tool', default=TOOL_PATH,
            help=f'Symlink target (default: {TOOL_PATH})')
    args = parser.parse_args()
    level = logging.DEBUG if args.verbose > 0 else logging.INFO
    format='%(levelname)s:update-ccache-symlinks: %(message)s'
    logging.basicConfig(format=format, level=level)
    upd = CompilerLinksUpdater(masquerade_dir=args.masquerade_dir,
                               tool_path=args.tool)
    upd.run()


if __name__ == '__main__':
    main()
