# Copyright 2015 Hewlett-Packard Development Company, L.P.
#
# 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.

from proliantutils import exception
from proliantutils.hpssa import constants

FILTER_CRITERIA = ['disk_type', 'interface_type', 'model', 'firmware']


def _get_criteria_matching_disks(logical_disk, physical_drives):
    """Finds the physical drives matching the criteria of logical disk.

    This method finds the physical drives matching the criteria
    of the logical disk passed.

    :param logical_disk: The logical disk dictionary from raid config
    :param physical_drives: The physical drives to consider.
    :returns: A list of physical drives which match the criteria
    """
    matching_physical_drives = []
    criteria_to_consider = [x for x in FILTER_CRITERIA
                            if x in logical_disk]

    for physical_drive_object in physical_drives:
        for criteria in criteria_to_consider:
            logical_drive_value = logical_disk.get(criteria)
            physical_drive_value = getattr(physical_drive_object, criteria)
            if logical_drive_value != physical_drive_value:
                break
        else:
            matching_physical_drives.append(physical_drive_object)

    return matching_physical_drives


def allocate_disks(logical_disk, server, raid_config):
    """Allocate physical disks to a logical disk.

    This method allocated physical disks to a logical
    disk based on the current state of the server and
    criteria mentioned in the logical disk.

    :param logical_disk: a dictionary of a logical disk
        from the RAID configuration input to the module.
    :param server: An objects.Server object
    :param raid_config: The target RAID configuration requested.
    :raises: PhysicalDisksNotFoundError, if cannot find
        physical disks for the request.
    """
    size_gb = logical_disk['size_gb']
    raid_level = logical_disk['raid_level']
    number_of_physical_disks = logical_disk.get(
        'number_of_physical_disks', constants.RAID_LEVEL_MIN_DISKS[raid_level])
    share_physical_disks = logical_disk.get('share_physical_disks', False)

    # Try to create a new independent array for this request.
    for controller in server.controllers:
        physical_drives = controller.unassigned_physical_drives
        physical_drives = _get_criteria_matching_disks(logical_disk,
                                                       physical_drives)

        if size_gb != "MAX":
            # If we want to allocate for a logical disk for which size_gb is
            # mentioned, we take the smallest physical drives which is required
            # to match the criteria.
            reverse_sort = False
            physical_drives = [x for x in physical_drives
                               if x.size_gb >= size_gb]
        else:
            # If we want to allocate for a logical disk for which size_gb is
            # MAX, we take the largest physical drives available.
            reverse_sort = True

        if len(physical_drives) >= number_of_physical_disks:
            selected_drives = sorted(physical_drives, key=lambda x: x.size_gb,
                                     reverse=reverse_sort)
            selected_drive_ids = [x.id for x in selected_drives]
            logical_disk['controller'] = controller.id
            physical_disks = selected_drive_ids[:number_of_physical_disks]
            logical_disk['physical_disks'] = physical_disks
            return

    # We didn't find physical disks to create an independent array.
    # Check if we can get some shared arrays.
    if share_physical_disks:
        sharable_disk_wwns = []
        for sharable_logical_disk in raid_config['logical_disks']:
            if (sharable_logical_disk.get('share_physical_disks', False)
                    and 'root_device_hint' in sharable_logical_disk):
                wwn = sharable_logical_disk['root_device_hint']['wwn']
                sharable_disk_wwns.append(wwn)

        for controller in server.controllers:
            sharable_arrays = [x for x in controller.raid_arrays if
                               x.logical_drives[0].wwn in sharable_disk_wwns]

            for array in sharable_arrays:

                # Check if criterias for the logical disk match the ones with
                # physical disks in the raid array.
                criteria_matched_disks = _get_criteria_matching_disks(
                    logical_disk, array.physical_drives)

                # Check if all disks in the array don't match the criteria
                if len(criteria_matched_disks) != len(array.physical_drives):
                    continue

                # Check if raid array can accomodate the logical disk.
                if array.can_accomodate(logical_disk):
                    logical_disk['controller'] = controller.id
                    logical_disk['array'] = array.id
                    return

    # We check both options and couldn't get any physical disks.
    raise exception.PhysicalDisksNotFoundError(size_gb=size_gb,
                                               raid_level=raid_level)
