#!/usr/bin/python3
#
# This file is part of Checkbox.
#
# Copyright 2009 Canonical Ltd.
#
# Checkbox is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 3,
# as published by the Free Software Foundation.

#
# Checkbox is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Checkbox.  If not, see <http://www.gnu.org/licenses/>.
#
import re
import sys
import dbus
import string
import posixpath

from checkbox_support.lib.pci import Pci
from checkbox_support.lib.usb import Usb


class UnknownName:
    def __init__(self, function):
        self._function = function

    def __get__(self, instance, cls=None):
        self._instance = instance
        return self

    def __call__(self, *args, **kwargs):
        name = self._function(self._instance, *args, **kwargs)
        if name and name.startswith("Unknown ("):
            name = None

        return name


class DeviceResource:
    __slots__ = ("_properties")

    def __init__(self, properties):
        self._properties = properties

    @property
    def bus(self):
        return self._properties.get("linux.subsystem")

    @property
    def category(self):
        if "system.hardware.vendor" in self._properties:
            return "SYSTEM"

        if "net.interface" in self._properties:
            return "NETWORK"

        if "pci.device_class" in self._properties:
            class_id = self._properties["pci.device_class"]
            subclass_id = self._properties["pci.device_subclass"]

            if class_id == Pci.BASE_CLASS_NETWORK:
                return "NETWORK"

            if class_id == Pci.BASE_CLASS_DISPLAY:
                return "VIDEO"

            if class_id == Pci.BASE_CLASS_SERIAL \
               and subclass_id == Pci.CLASS_SERIAL_USB:
                return "USB"

            if class_id == Pci.BASE_CLASS_STORAGE:
                if subclass_id == Pci.CLASS_STORAGE_SCSI:
                    return "SCSI"

                if subclass_id == Pci.CLASS_STORAGE_IDE:
                    return "IDE"

                if subclass_id == Pci.CLASS_STORAGE_FLOPPY:
                    return "FLOPPY"

                if subclass_id == Pci.CLASS_STORAGE_RAID:
                    return "RAID"

            if class_id == Pci.BASE_CLASS_COMMUNICATION \
               and subclass_id == Pci.CLASS_COMMUNICATION_MODEM:
                return "MODEM"

            if class_id == Pci.BASE_CLASS_INPUT \
               and subclass_id == Pci.CLASS_INPUT_SCANNER:
                return "SCANNER"

            if class_id == Pci.BASE_CLASS_MULTIMEDIA:
                if subclass_id == Pci.CLASS_MULTIMEDIA_VIDEO:
                    return "CAPTURE"

                if subclass_id == Pci.CLASS_MULTIMEDIA_AUDIO \
                   or subclass_id == Pci.CLASS_MULTIMEDIA_AUDIO_DEVICE:
                    return "AUDIO"

            if class_id == Pci.BASE_CLASS_SERIAL \
               and subclass_id == Pci.CLASS_SERIAL_FIREWIRE:
                return "FIREWIRE"

            if class_id == Pci.BASE_CLASS_BRIDGE \
               and (subclass_id == Pci.CLASS_BRIDGE_PCMCIA \
                    or subclass_id == Pci.CLASS_BRIDGE_CARDBUS):
                return "SOCKET"

        if "usb.interface.class" in self._properties:
            interface_class = self._properties["usb.interface.class"]
            interface_subclass = self._properties["usb.interface.subclass"]

            if interface_class == Usb.BASE_CLASS_AUDIO:
                return "AUDIO"

            if interface_class == Usb.BASE_CLASS_PRINTER:
                return "PRINTER"

            if interface_class == Usb.BASE_CLASS_STORAGE:
                if interface_subclass == Usb.CLASS_STORAGE_FLOPPY:
                    return "FLOPPY"

                if interface_subclass == Usb.CLASS_STORAGE_SCSI:
                    return "SCSI"

            if interface_class == Usb.BASE_CLASS_VIDEO:
                return "VIDEO"

            if interface_class == Usb.BASE_CLASS_WIRELESS:
                return "NETWORK"

        if "info.capabilities" in self._properties:
            capabilities = self._properties["info.capabilities"]
            if "input.keyboard" in capabilities:
                return "KEYBOARD"

            if "input.mouse" in capabilities:
                return "MOUSE"

        if "storage.drive_type" in self._properties:
            drive_type = self._properties["storage.drive_type"]
            if drive_type == "cdrom":
                return "CDROM"

            if drive_type == "disk":
                return "DISK"

            if drive_type == "floppy":
                return "FLOPPY"

        if "scsi.type" in self._properties:
            type = self._properties["scsi.type"]
            if type == "disk":
                return "DISK"

            if type == "tape":
                return "TAPE"

            if type == "printer":
                return "PRINTER"

            if type == "cdrom":
                return "CDROM"

            if type == "scanner":
                return "SCANNER"

            if type == "raid":
                return "RAID"

        if self.product_id:
            return "OTHER"

        return None

    @property
    def driver(self):
        return self._properties.get("info.linux.driver")

    @property
    def path(self):
        return self._properties.get("linux.sysfs_path", "").replace("/sys", "")

    @property
    def product_id(self):
        if "info.subsystem" in self._properties:
            product_id = "%s.product_id" % self._properties["info.subsystem"]
            if product_id in self._properties:
                return self._properties[product_id]

        # pnp
        if "pnp.id" in self._properties:
            match = re.match(r"^(?P<vendor_name>.*)(?P<product_id>[%s]{4})$"
                % string.hexdigits, self._properties["pnp.id"])
            if match:
                return int(match.group("product_id"), 16)

        return None

    @property
    def vendor_id(self):
        if "info.subsystem" in self._properties:
            vendor_id = "%s.vendor_id" % self._properties["info.subsystem"]
            if vendor_id in self._properties:
                return self._properties[vendor_id]

        return None

    @property
    def subproduct_id(self):
        return self._properties.get("pci.subsys_product_id")

    @property
    def subvendor_id(self):
        return self._properties.get("pci.subsys_vendor_id")

    @property
    def product(self):
        return self._get_product()

    @UnknownName
    def _get_product(self):
        bus = self.bus

        # Ignore subsystems using parent or generated names
        if bus in ("drm", "net", "pci", "pnp", "scsi_generic",
                   "scsi_host", "tty", "usb", "video4linux"):
            return None

        # Treat the floppy device specifically
        if bus == "platform":
            if self.driver == "floppy":
                return "Platform Device"
            else:
                return None

        if "usb.interface.number" in self._properties:
            return None

        if self._properties.get("info.category") == "ac_adapter":
            return None

        for property in ("alsa.device_id",
                         "alsa.card_id",
                         "sound.card_id",
                         "battery.model",
                         "ieee1394.product",
                         "killswitch.name",
                         "oss.device_id",
                         "scsi.model",
                         "system.hardware.product",
                         "info.product"):
            if property in self._properties:
                return self._properties[property]

        return None

    @property
    def vendor(self):
        return self._get_vendor()

    @UnknownName
    def _get_vendor(self):
        bus = self.bus

        # Ignore subsystems using parent or generated names
        if bus in ("drm", "pci", "rfkill", "usb"):
            return None

        # pnp
        if "pnp.id" in self._properties:
            match = re.match(r"^(?P<vendor_name>.*)(?P<product_id>[%s]{4})$"
                % string.hexdigits, self._properties["pnp.id"])
            if match:
                return match.group("vendor_name")

        for property in ("battery.vendor",
                         "ieee1394.vendor",
                         "scsi.vendor",
                         "system.hardware.vendor",
                         "info.vendor"):
            if property in self._properties:
                return self._properties[property]

        return None


class DmiDeviceResource(DeviceResource):

    _category_to_property = {
        "BIOS": "system.firmware",
        "BOARD": "system.board",
        "CHASSIS": "system.chassis"}

    def __init__(self, properties, category):
        super(DmiDeviceResource, self).__init__(properties)
        if category not in self._category_to_property:
            raise Exception("Unsupported category: %s" % category)

        self._category = category

    @property
    def _property(self):
        return self._category_to_property[self._category]

    @property
    def category(self):
        return self._category

    @property
    def path(self):
        path = super(DmiDeviceResource, self).path
        return posixpath.join(path, self._category.lower())

    @property
    def product(self):
        for subproperty in "product", "type", "version":
            property = "%s.%s" % (self._property, subproperty)
            product = self._properties.get(property)
            if product and product != "Not Available":
                return product

        return None

    @property
    def vendor(self):
        for subproperty in "vendor", "manufacturer":
            property = "%s.%s" % (self._property, subproperty)
            if property in self._properties:
                return self._properties[property]

        return None


class HalResource:
    """Resource for HAL information.

    Each item contained in this resource consists of the udi as key and
    the corresponding device resource as value.
    """

    # See also section "Deprecated Properties"
    # of the "HAL 0.5.10 Specification",
    # available from
    # http://people.freedesktop.org/~david/hal-spec/hal-spec.html
    _deprecated_expressions = (
        (r"info\.bus",                             "info.subsystem"),
        (r"([^\.]+)\.physical_device",             "\1.originating_device"),
        (r"power_management\.can_suspend_to_ram",  "power_management.can_suspend"),
        (r"power_management\.can_suspend_to_disk", "power_management.can_hibernate"),
        (r"smbios\.system\.manufacturer",          "system.hardware.vendor"),
        (r"smbios\.system\.product",               "system.hardware.product"),
        (r"smbios\.system\.version",               "system.hardware.version"),
        (r"smbios\.system\.serial",                "system.hardware.serial"),
        (r"smbios\.system\.uuid",                  "system.hardware.uuid"),
        (r"smbios\.bios\.vendor",                  "system.firmware.vendor"),
        (r"smbios\.bios\.version",                 "system.firmware.version"),
        (r"smbios\.bios\.release_date",            "system.firmware.release_date"),
        (r"smbios\.chassis\.manufacturer",         "system.chassis.manufacturer"),
        (r"smbios\.chassis\.type",                 "system.chassis.type"),
        (r"system\.vendor",                        "system.hardware.vendor"),
        (r"usb_device\.speed_bcd",                 "usb_device.speed"),
        (r"usb_device\.version_bcd",               "usb_device.version"))

    _conversion_types = {
        dbus.Boolean: bool,
        dbus.Int16: int,
        dbus.UInt16: int,
        dbus.Int32: int,
        dbus.UInt32: int,
        dbus.Int64: int,
        dbus.UInt64: int,
        dbus.Double: float,
        dbus.Array: list,
        dbus.Dictionary: dict,
        dbus.String: str,
        dbus.UTF8String: str}

    def __init__(self, *args, **kwargs):
        super(HalResource, self).__init__(*args, **kwargs)
        self._deprecated_patterns = ((re.compile("^%s$" % a), b)
            for (a, b) in self._deprecated_expressions)

    def _get_key(self, key):
        key = str(key)
        for (old, new) in self._deprecated_patterns:
            key = old.sub(new, key)

        return key

    def _get_value(self, value):
        return self._conversion_types[type(value)](value)

    def _ignore_device(self, device):
        # Ignore devices without bus information
        if not device.bus:
            return True

        # Ignore devices without product information
        if not device.product and device.product_id is None:
            return True

        # Ignore invalid subsystem information
        if (device.subproduct_id is None and device.subvendor_id is not None) \
           or (device.subproduct_id is not None
               and device.subvendor_id is None):
            return True

        # Ignore virtual devices except for dmi information
        if device.bus != "dmi" \
           and "virtual" in device.path.split(posixpath.sep):
            return True

        return False

    @property
    def devices(self):
        devices = []
        bus = dbus.SystemBus()
        manager_obj = bus.get_object("org.freedesktop.Hal",
                                     "/org/freedesktop/Hal/Manager")
        manager = dbus.Interface(manager_obj, "org.freedesktop.Hal.Manager")
        for udi in manager.GetAllDevices():
            name = udi.split(posixpath.sep)[-1]
            object = bus.get_object("org.freedesktop.Hal", udi)
            interface = dbus.Interface(object, "org.freedesktop.Hal.Device")

            properties = {}
            for key, value in interface.GetAllProperties().items():
                key = self._get_key(key)
                value = self._get_value(value)
                properties[key] = value

            if name == "computer":
                properties["linux.subsystem"] = "dmi"
                properties["linux.sysfs_path"] = "/sys/devices/virtual/dmi/id"

                device = DeviceResource(properties)
                devices.append(device)
                for category in "BIOS", "BOARD", "CHASSIS":
                    device = DmiDeviceResource(properties, category)
                    devices.append(device)
            else:
                device = DeviceResource(properties)
                devices.append(device)

        return [d for d in devices if not self._ignore_device(d)]


def main():
    attributes = ("path", "bus", "category", "driver", "product_id",
        "vendor_id", "subproduct_id", "subvendor_id", "product", "vendor",)

    hal = HalResource()
    for device in hal.devices:
        for attribute in attributes:
            value = getattr(device, attribute)
            if value is not None:
                print("%s: %s" % (attribute, value))

        # Empty line
        print()

    return 0


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