#
#    Licensed under the Apache License, Version 2.0 (the "License"); you may
#    not use this file except in compliance with the License. You may obtain
#    a copy of the License at
#
#         http://www.apache.org/licenses/LICENSE-2.0
#
#    Unless required by applicable law or agreed to in writing, software
#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
#    License for the specific language governing permissions and limitations
#    under the License.

import ast

from nova import exception
from nova.i18n import _
from nova.pci import utils

MAX_VENDOR_ID = 0xFFFF
MAX_PRODUCT_ID = 0xFFFF
MAX_FUNC = 0x7
MAX_DOMAIN = 0xFFFF
MAX_BUS = 0xFF
MAX_SLOT = 0x1F
ANY = '*'


def get_value(v):
    return ast.literal_eval("0x" + v)


def get_pci_dev_info(pci_obj, property, max, hex_value):
    a = getattr(pci_obj, property)
    if a == ANY:
        return
    v = get_value(a)
    if v > max:
        raise exception.PciConfigInvalidWhitelist(
            reason=_("invalid %(property)s %(attr)s") %
                     {'property': property, 'attr': a})
    setattr(pci_obj, property, hex_value % v)


class PciAddress(object):
    """Manages the address fields of the whitelist.

    This class checks the address fields of the pci_passthrough_whitelist
    configuration option, validating the address fields.
    Example config are:

        | pci_passthrough_whitelist = {"address":"*:0a:00.*",
        |                         "physical_network":"physnet1"}
        | pci_passthrough_whitelist = {"vendor_id":"1137","product_id":"0071"}

    This function class will validate the address fields, check for wildcards,
    and insert wildcards where the field is left blank.
    """
    def __init__(self, pci_addr, is_physical_function):
        self.domain = ANY
        self.bus = ANY
        self.slot = ANY
        self.func = ANY
        self.is_physical_function = is_physical_function
        self._init_address_fields(pci_addr)

    def _check_physical_function(self):
        if ANY in (self.domain, self.bus, self.slot, self.func):
            return
        self.is_physical_function = utils.is_physical_function(
            self.domain, self.bus, self.slot, self.func)

    def _init_address_fields(self, pci_addr):
        if self.is_physical_function:
            (self.domain, self.bus, self.slot,
             self.func) = utils.get_pci_address_fields(pci_addr)
            return
        dbs, sep, func = pci_addr.partition('.')
        if func:
            fstr = func.strip()
            if fstr != ANY:
                try:
                    f = get_value(fstr)
                except SyntaxError:
                    raise exception.PciDeviceWrongAddressFormat(
                        address=pci_addr)
                if f > MAX_FUNC:
                    raise exception.PciDeviceInvalidAddressField(
                        address=pci_addr, field="function")
                self.func = "%1x" % f
        if dbs:
            dbs_fields = dbs.split(':')
            if len(dbs_fields) > 3:
                raise exception.PciDeviceWrongAddressFormat(address=pci_addr)
            # If we got a partial address like ":00.", we need to turn this
            # into a domain of ANY, a bus of ANY, and a slot of 00. This code
            # allows the address bus and/or domain to be left off
            dbs_all = [ANY for x in range(3 - len(dbs_fields))]
            dbs_all.extend(dbs_fields)
            dbs_checked = [s.strip() or ANY for s in dbs_all]
            self.domain, self.bus, self.slot = dbs_checked
            get_pci_dev_info(self, 'domain', MAX_DOMAIN, '%04x')
            get_pci_dev_info(self, 'bus', MAX_BUS, '%02x')
            get_pci_dev_info(self, 'slot', MAX_SLOT, '%02x')
            self._check_physical_function()

    def match(self, pci_addr, pci_phys_addr):
        """Match a device to this PciAddress.  Assume this is called given
        pci_addr and pci_phys_addr reported by libvirt, no attempt is made to
        verify if pci_addr is a VF of pci_phys_addr.

        :param pci_addr: PCI address of the device to match.
        :param pci_phys_addr: PCI address of the parent of the device to match
                              (or None if the device is not a VF).
        """

        # Try to match on the parent PCI address if the PciDeviceSpec is a
        # PF (sriov is available) and the device to match is a VF.  This
        # makes possible to specify the PCI address of a PF in the
        # pci_passthrough_whitelist to match any of it's VFs PCI devices.
        if self.is_physical_function and pci_phys_addr:
            domain, bus, slot, func = (
                utils.get_pci_address_fields(pci_phys_addr))
            if (self.domain == domain and self.bus == bus and
                    self.slot == slot and self.func == func):
                return True

        # Try to match on the device PCI address only.
        domain, bus, slot, func = (
            utils.get_pci_address_fields(pci_addr))
        conditions = [
            self.domain in (ANY, domain),
            self.bus in (ANY, bus),
            self.slot in (ANY, slot),
            self.func in (ANY, func)
        ]
        return all(conditions)


class PciDeviceSpec(object):
    def __init__(self, dev_spec):
        self.tags = dev_spec
        self._init_dev_details()

    def _init_dev_details(self):
        self.vendor_id = self.tags.pop("vendor_id", ANY)
        self.product_id = self.tags.pop("product_id", ANY)
        self.address = self.tags.pop("address", None)
        self.dev_name = self.tags.pop("devname", None)

        self.vendor_id = self.vendor_id.strip()
        get_pci_dev_info(self, 'vendor_id', MAX_VENDOR_ID, '%04x')
        get_pci_dev_info(self, 'product_id', MAX_PRODUCT_ID, '%04x')

        if self.address and self.dev_name:
            raise exception.PciDeviceInvalidDeviceName()
        if not self.dev_name:
            address_str = self.address or "*:*:*.*"
            self.address = PciAddress(address_str, False)

    def match(self, dev_dict):
        if self.dev_name:
            address_str, pf = utils.get_function_by_ifname(
                self.dev_name)
            if not address_str:
                return False
            address_obj = PciAddress(address_str, pf)
        elif self.address:
            address_obj = self.address
        return all([
            self.vendor_id in (ANY, dev_dict['vendor_id']),
            self.product_id in (ANY, dev_dict['product_id']),
            address_obj.match(dev_dict['address'],
                dev_dict.get('parent_addr'))])

    def match_pci_obj(self, pci_obj):
        return self.match({'vendor_id': pci_obj.vendor_id,
                            'product_id': pci_obj.product_id,
                            'address': pci_obj.address,
                            'parent_addr': pci_obj.parent_addr})

    def get_tags(self):
        return self.tags
