# Copyright (c) 2011 Openstack, LLC.
# All Rights Reserved.
#
#    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.
"""
Least Cost is an algorithm for choosing which host machines to
provision a set of resources to. The input is a WeightedHost object which
is decided upon by a set of objective-functions, called the 'cost-functions'.
The WeightedHost contains a combined weight for each cost-function.

The cost-function and weights are tabulated, and the host with the least cost
is then selected for provisioning.
"""


from nova import flags
from nova import log as logging

LOG = logging.getLogger('nova.scheduler.least_cost')

FLAGS = flags.FLAGS
flags.DEFINE_list('least_cost_functions',
        ['nova.scheduler.least_cost.compute_fill_first_cost_fn'],
        'Which cost functions the LeastCostScheduler should use.')


# TODO(sirp): Once we have enough of these rules, we can break them out into a
# cost_functions.py file (perhaps in a least_cost_scheduler directory)
flags.DEFINE_float('noop_cost_fn_weight', 1.0,
             'How much weight to give the noop cost function')
flags.DEFINE_float('compute_fill_first_cost_fn_weight', 1.0,
             'How much weight to give the fill-first cost function')


class WeightedHost(object):
    """Reduced set of information about a host that has been weighed.
    This is an attempt to remove some of the ad-hoc dict structures
    previously used."""

    def __init__(self, weight, host_state=None, blob=None, zone=None):
        self.weight = weight
        self.blob = blob
        self.zone = zone

        # Local members. These are not returned outside of the Zone.
        self.host_state = host_state

    def to_dict(self):
        x = dict(weight=self.weight)
        if self.blob:
            x['blob'] = self.blob
        if self.host_state:
            x['host'] = self.host_state.host
        if self.zone:
            x['zone'] = self.zone
        return x


def noop_cost_fn(host_state, weighing_properties):
    """Return a pre-weight cost of 1 for each host"""
    return 1


def compute_fill_first_cost_fn(host_state, weighing_properties):
    """More free ram = higher weight. So servers will less free
    ram will be preferred."""
    return host_state.free_ram_mb


def weighted_sum(weighted_fns, host_states, weighing_properties):
    """Use the weighted-sum method to compute a score for an array of objects.
    Normalize the results of the objective-functions so that the weights are
    meaningful regardless of objective-function's range.

    host_list - [(host, HostInfo()), ...]
    weighted_fns - list of weights and functions like:
        [(weight, objective-functions), ...]
    weighing_properties is an arbitrary dict of values that can influence
        weights.

    Returns a single WeightedHost object which represents the best
    candidate.
    """

    # Make a grid of functions results.
    # One row per host. One column per function.
    scores = []
    for weight, fn in weighted_fns:
        scores.append([fn(host_state, weighing_properties)
                for host_state in host_states])

    # Adjust the weights in the grid by the functions weight adjustment
    # and sum them up to get a final list of weights.
    adjusted_scores = []
    for (weight, fn), row in zip(weighted_fns, scores):
        adjusted_scores.append([weight * score for score in row])

    # Now, sum down the columns to get the final score. Column per host.
    final_scores = [0.0] * len(host_states)
    for row in adjusted_scores:
        for idx, col in enumerate(row):
            final_scores[idx] += col

    # Super-impose the host_state into the scores so
    # we don't lose it when we sort.
    final_scores = [(final_scores[idx], host_state)
            for idx, host_state in enumerate(host_states)]

    final_scores = sorted(final_scores)
    weight, host_state = final_scores[0]  # Lowest score is the winner!
    return WeightedHost(weight, host_state=host_state)
