#  ___________________________________________________________________________
#
#  Pyomo: Python Optimization Modeling Objects
#  Copyright 2017 National Technology and Engineering Solutions of Sandia, LLC
#  Under the terms of Contract DE-NA0003525 with National Technology and
#  Engineering Solutions of Sandia, LLC, the U.S. Government retains certain
#  rights in this software.
#  This software is distributed under the 3-clause BSD License.
#  ___________________________________________________________________________

import os
import platform

import enum
import six
from pyutilib.misc.config import ConfigBlock, ConfigList, ConfigValue

if 'PYOMO_CONFIG_DIR' in os.environ:
    PYOMO_CONFIG_DIR = os.path.abspath(os.environ['PYOMO_CONFIG_DIR'])
elif platform.system().lower().startswith(('windows','cygwin')):
    PYOMO_CONFIG_DIR = os.path.abspath(
        os.path.join(os.environ.get('LOCALAPPDATA', ''), 'Pyomo'))
else:
    PYOMO_CONFIG_DIR = os.path.abspath(
        os.path.join(os.environ.get('HOME', ''), '.pyomo'))

# Note that alternative platform-independent implementation of the above
# could be to use:
#
#   PYOMO_CONFIG_DIR = os.path.abspath(appdirs.user_data_dir('pyomo'))
#
# But would require re-adding the hard dependency on appdirs.  For now
# (13 Jul 20), the above appears to be sufficiently robust.

USER_OPTION = 0
ADVANCED_OPTION = 1
DEVELOPER_OPTION = 2

def PositiveInt(val):
    ans = int(val)
    # We want to give an error for floating point numbers...
    if ans != float(val) or ans <= 0:
        raise ValueError(
            "Expected positive int, but received %s" % (val,))
    return ans

def NegativeInt(val):
    ans = int(val)
    if ans != float(val) or ans >= 0:
        raise ValueError(
            "Expected negative int, but received %s" % (val,))
    return ans

def NonPositiveInt(val):
    ans = int(val)
    if ans != float(val) or ans > 0:
        raise ValueError(
            "Expected non-positive int, but received %s" % (val,))
    return ans

def NonNegativeInt(val):
    ans = int(val)
    if ans != float(val) or ans < 0:
        raise ValueError(
            "Expected non-negative int, but received %s" % (val,))
    return ans

def PositiveFloat(val):
    ans = float(val)
    if ans <= 0:
        raise ValueError(
            "Expected positive float, but received %s" % (val,))
    return ans

def NegativeFloat(val):
    ans = float(val)
    if ans >= 0:
        raise ValueError(
            "Expected negative float, but received %s" % (val,))
    return ans

def NonPositiveFloat(val):
    ans = float(val)
    if ans > 0:
        raise ValueError(
            "Expected non-positive float, but received %s" % (val,))
    return ans

def NonNegativeFloat(val):
    ans = float(val)
    if ans < 0:
        raise ValueError(
            "Expected non-negative float, but received %s" % (val,))
    return ans


class In(object):
    def __init__(self, domain, cast=None):
        self._domain = domain
        self._cast = cast

    def __call__(self, value):
        if self._cast is not None:
            v = self._cast(value)
        else:
            v = value
        if v in self._domain:
            return v
        raise ValueError("value %s not in domain %s" % (value, self._domain))


class Path(object):
    BasePath = None
    SuppressPathExpansion = False

    def __init__(self, basePath=None):
        self.basePath = basePath

    def __call__(self, path):
        #print "normalizing path '%s' " % (path,),
        path = str(path)
        if path is None or Path.SuppressPathExpansion:
            return path

        if self.basePath:
            base = self.basePath
        else:
            base = Path.BasePath
        if type(base) is ConfigValue:
            base = base.value()
        if base is None:
            base = ""
        else:
            base = str(base).lstrip()

        # We want to handle the CWD variable ourselves.  It should
        # always be in a known location (the beginning of the string)
        if base and base[:6].lower() == '${cwd}':
            base = os.getcwd() + base[6:]
        if path and path[:6].lower() == '${cwd}':
            path = os.getcwd() + path[6:]

        ans = os.path.normpath(os.path.abspath(os.path.join(
            os.path.expandvars(os.path.expanduser(base)),
            os.path.expandvars(os.path.expanduser(path)))))
        #print "to '%s'" % (ans,)
        return ans

class PathList(Path):
    def __call__(self, data):
        if hasattr(data, "__iter__") and not isinstance(data, six.string_types):
            return [ super(PathList, self).__call__(i) for i in data ]
        else:
            return [ super(PathList, self).__call__(data) ]


def add_docstring_list(docstring, configblock, indent_by=4):
    """Returns the docstring with a formatted configuration arguments listing."""
    return docstring + (" " * indent_by).join(
        configblock.generate_documentation(
            block_start="Keyword Arguments\n-----------------\n",
            block_end="",
            item_start="%s\n",
            item_body="  %s",
            item_end="",
            indent_spacing=0,
            width=256
        ).splitlines(True))


class ConfigEnum(enum.Enum):
    @classmethod
    def from_enum_or_string(cls, arg):
        if type(arg) is str:
            return cls[arg]
        else:
            # Handles enum or integer inputs
            return cls(arg)
