#!/usr/bin/env python3

# Codacy D203 and D211 conflict, I choose D203
# Codacy D212 and D213 conflict, I choose D212

# This file is Copyright 2010 by the GPSD project
# SPDX-License-Identifier: BSD-2-Clause
# This code runs compatibly under Python 2 and 3.x for x >= 2.
# Preserve this property!

"""Uninstall cleanup."""

from __future__ import absolute_import, print_function, division

import glob
import os
import subprocess
import sys

GPS_LIB_NAME = 'gps'

BINARY_ENCODING = 'latin-1'

if bytes is str:

    polystr = str

else:  # Otherwise we do something real

    def polystr(o):
        """Convert bytes or str to str with proper encoding."""
        if isinstance(o, str):
            return o
        if isinstance(o, bytes):
            return str(o, encoding=BINARY_ENCODING)
        raise ValueError


def DoCommand(cmd_list):
    """Perform external command, returning exit code and stdout."""
    pipe = subprocess.PIPE
    proc = subprocess.Popen(cmd_list, stdin=pipe, stdout=pipe)
    result, _ = proc.communicate()
    return proc.returncode, polystr(result)


class PythonCommand(object):
    """Object for one system Python command."""

    PYTHON_GLOB = 'python*'
    TEXT_PREFIX = b'#!'
    PATH_ENV = 'PATH'
    PATH_ENV_SEP = ':'
    PYTHON_EXE_COMMANDS = [
        'import sys',
        'print(sys.executable)',
        ]

    instances = []

    def __init__(self, command):
        """Set up PythonCommand."""
        self.command = command

    @classmethod
    def FindPythons(cls, pdir):
        """Create PythonCommand objects by scanning directory."""
        pattern = pdir + os.path.sep + cls.PYTHON_GLOB
        pythons = glob.glob(pattern)
        for python in pythons:
            with open(python, 'rb') as f:
                if f.read(2) == cls.TEXT_PREFIX:
                    continue
            cls.instances.append(cls(python))
        return cls.instances

    @classmethod
    def FindAllPythons(cls):
        """Create PythonCommand objects by scanning command PATH."""
        paths = os.getenv(cls.PATH_ENV)
        for pdir in paths.split(cls.PATH_ENV_SEP):
            cls.FindPythons(pdir)
        return cls.instances

    def GetExecutable(self):
        """Obtain executable path from this Python."""
        command = [self.command, '-c', ';'.join(self.PYTHON_EXE_COMMANDS)]
        status, result = DoCommand(command)
        if status:
            return None
        return result.strip()


class PythonExecutable(object):
    """Object for one Python executable, deduped."""

    PYTHON_LIBDIR_COMMANDS = [
        'from distutils import sysconfig',
        'print(sysconfig.get_python_lib())',
        ]

    _by_path = {}

    def __new__(cls, command):
        """Create or update one PythonExecutable from PythonCommand."""
        path = command.GetExecutable()
        existing = cls._by_path.get(path)
        if existing:
            existing.commands.append(command)
            return existing
        self = super(PythonExecutable, cls).__new__(cls)
        self.commands = [command]
        self.path = path
        self.libdir = None
        cls._by_path[path] = self
        return self

    def __lt__(self, other):
        """Allow sorting."""
        return self.path < other.path

    @classmethod
    def GetAllExecutables(cls, command_list):
        """Build list of executables from list of commands."""
        for command in command_list:
            cls(command)
        return sorted(cls._by_path.values())

    def GetLibdir(self):
        """Obtain libdir path from this Python."""
        if self.libdir:
            return self.libdir
        command = [self.path, '-c', ';'.join(self.PYTHON_LIBDIR_COMMANDS)]
        status, result = DoCommand(command)
        if status:
            return None
        return result.strip()

    def CleanLib(self, name):
        """Clean up given package from this Python."""
        libdir = os.path.join(self.GetLibdir(), name)
        if not name or not os.path.exists(libdir):
            return
        try:
            os.rmdir(libdir)
        except OSError:
            print('Unable to remove %s' % libdir)
        else:
            print('Removed empty %s' % libdir)

    @classmethod
    def CleanAllLibs(cls, name):
        """Clean up given package from all executables."""
        for exe in cls._by_path.values():
            exe.CleanLib(name)


def main():
    """Main function."""
    commands = PythonCommand.FindAllPythons()
    PythonExecutable.GetAllExecutables(commands)
    PythonExecutable.CleanAllLibs(GPS_LIB_NAME)
    return 0


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