# ******************************************************************************
# Copyright 2017-2020 Intel Corporation
#
# 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.
# ******************************************************************************

"""! Helper functions for validating user input."""

import logging
from typing import Any, Callable, Dict, Iterable, List, Optional, Tuple, Type

import numpy as np

from ngraph.exceptions import UserInputError

log = logging.getLogger(__name__)


def assert_list_of_ints(value_list: Iterable[int], message: str) -> None:
    """! Verify that the provided value is an iterable of integers."""
    try:
        for value in value_list:
            if not isinstance(value, int):
                raise TypeError
    except TypeError:
        log.warning(message)
        raise UserInputError(message, value_list)


def _check_value(op_name, attr_key, value, val_type, cond=None):
    # type: (str, str, Any, Type, Optional[Callable[[Any], bool]]) -> bool
    """! Check whether provided value satisfies specified criteria.

    @param      op_name:        The operator name which attributes are checked.
    @param      attr_key:       The attribute name.
    @param      value:          The value to check.
    @param      val_type:       Required value type.
    @param      cond:           The optional function running additional checks.

    :raises     UserInputError:
    @return    True if attribute satisfies all criterias. Otherwise False.
    """
    if not np.issubdtype(type(value), val_type):
        raise UserInputError(
            '{} operator attribute "{}" value must by of type {}.'.format(
                op_name, attr_key, val_type
            )
        )
    if cond is not None and not cond(value):
        raise UserInputError(
            '{} operator attribute "{}" value does not satisfy provided condition.'.format(
                op_name, attr_key
            )
        )
    return True


def check_valid_attribute(op_name, attr_dict, attr_key, val_type, cond=None, required=False):
    # type: (str, dict, str, Type, Optional[Callable[[Any], bool]], Optional[bool]) -> bool
    """! Check whether specified attribute satisfies given criteria.

    @param  op_name:    The operator name which attributes are checked.
    @param attr_dict:   Dictionary containing key-value attributes to check.
    @param attr_key:    Key value for validated attribute.
    @param val_type:    Value type for validated attribute.
    @param cond:        Any callable wich accept attribute value and returns True or False.
    @param required:    Whether provided attribute key is not required. This mean it may be missing
                        from provided dictionary.

    :raises     UserInputError:

    @return True if attribute satisfies all criterias. Otherwise False.
    """
    result = True

    if required and attr_key not in attr_dict:
        raise UserInputError(
            'Provided dictionary is missing {} operator required attribute "{}"'.format(
                op_name, attr_key
            )
        )

    if attr_key not in attr_dict:
        return result

    attr_value = attr_dict[attr_key]

    if np.isscalar(attr_value):
        result = result and _check_value(op_name, attr_key, attr_value, val_type, cond)
    else:
        for v in attr_value:
            result = result and _check_value(op_name, attr_key, v, val_type, cond)

    return result


def check_valid_attributes(
    op_name,  # type: str
    attributes,  # type: Dict[str, Any]
    requirements,  # type: List[Tuple[str, bool, Type, Optional[Callable]]]
):
    # type: (...) -> bool
    """! Perform attributes validation according to specified type, value criteria.

    @param  op_name:        The operator name which attributes are checked.
    @param  attributes:     The dictionary with user provided attributes to check.
    @param  requirements:   The list of tuples describing attributes' requirements. The tuple should
                            contain following values:
                            (attr_name: str,
                             is_required: bool,
                             value_type: Type,
                             value_condition: Callable)

    :raises     UserInputError:
    @return True if all attributes satisfies criterias. Otherwise False.
    """
    for attr, required, val_type, cond in requirements:
        check_valid_attribute(op_name, attributes, attr, val_type, cond, required)
    return True


def is_positive_value(x):  # type: (Any) -> bool
    """! Determine whether the specified x is positive value.

    @param      x:    The value to check.

    @return   True if the specified x is positive value, False otherwise.
    """
    return x > 0


def is_non_negative_value(x):  # type: (Any) -> bool
    """! Determine whether the specified x is non-negative value.

    @param      x:    The value to check.

    @return   True if the specified x is non-negative value, False otherwise.
    """
    return x >= 0
