#  ___________________________________________________________________________
#
#  Pyomo: Python Optimization Modeling Objects
#  Copyright (c) 2008-2022
#  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.
#  ___________________________________________________________________________

__all__ = ['CounterLabeler', 'NumericLabeler', 'CNameLabeler', 'TextLabeler',
           'AlphaNumericTextLabeler','NameLabeler', 'CuidLabeler',
           'ShortNameLabeler']

import re

from pyomo.common.deprecation import deprecated
from pyomo.core.base.componentuid import ComponentUID

# This module provides some basic functionality for generating labels
# from pyomo names, which often contain characters such as "[" and "]"
# (e.g., in my_var[1]).  These characters generally cause issues with
# optimization input file formats, e.g., CPLEX LP files.  The purpose of
# this module is to provide a simple remap function, that will satisfy
# broadly problematic symbols. if solver-specific remaps are required,
# they should be handled in the corresponding solver plugin.

class _CharMapper(object):
    def __init__(self, preserve, translate, other):
        """
        Arguments::
           preserve: a string of characters to preserve
           translate: a dict or key/value list of characters to translate
           other: the character to return for all characters not in
                  preserve or translate
        """
        self.table = {k if isinstance(k, int) else ord(k): v
            for k,v in dict(translate).items() }
        for c in preserve:
            _c = ord(c)
            if _c in self.table and self.table[_c] != c:
                raise RuntimeError("Duplicate character '%s' appears in both "
                                   "translate table and preserve list" % (c,))
            self.table[_c] = c
        self.other = other

    def __getitem__(self, c):
        # Most of the time c should be known.  For the rare cases we
        # encounter a new character, remember it by adding a new entry
        # into the translation table and return the default character
        try:
            return self.table[c]
        except:
            self.table[c] = self.other
            return self.other

    def make_table(self):
        return ''.join(self[i] for i in range(256))

_alpha = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJLKMNOPQRSTUVWXYZ'
_digit = '1234567890'
_cpxlp_translation_table = _CharMapper( preserve=_alpha+_digit+'()_',
                                        translate = zip('[]{}', '()()'),
                                        other='_' ).make_table()
def cpxlp_label_from_name(name):

    if name is None:
        raise RuntimeError("Illegal name=None supplied to "
                           "cpxlp_label_from_name function")

    return str.translate(name, _cpxlp_translation_table)

_alphanum_translation_table = _CharMapper( preserve=_alpha+_digit+'_',
                                       translate = {},
                                       other='_' ).make_table()
def alphanum_label_from_name(name):

    if name is None:
        raise RuntimeError("Illegal name=None supplied to "
                           "alphanum_label_from_name function")

    return str.translate(name, _alphanum_translation_table)

class CuidLabeler(object):

    def __call__(self, obj=None):
        return ComponentUID(obj)

class CounterLabeler(object):
    def __init__(self, start=0):
        self._id = start

    def __call__(self, obj=None):
        self._id += 1
        return self._id

class NumericLabeler(object):
    def __init__(self, prefix, start=0):
        self.id = start
        self.prefix = prefix

    def __call__(self, obj=None):
        self.id += 1
        return self.prefix + str(self.id)

    @deprecated("The 'remove_obj' method is no longer "
                "necessary now that 'getname' does not "
                "support the use of a name buffer", version="6.4.1")
    def remove_obj(self, obj):
        pass
#
# TODO: [JDS] I would like to rename TextLabeler to LPLabeler - as it
# generated LP-file-compliant labels - and make the CNameLabeler the
# TextLabeler.  This makes sense as the name() is the closest thing we
# have to a human-readable canonical text naming convention (the
# ComponentUID strings are actually unique, but not meant to be human
# readable).  Unfortunately, the TextLabeler is used all over the place
# (particularly PySP), and I don't know how much depends on the labels
# actually being LP-compliant.
#
class CNameLabeler(object):
    def __call__(self, obj):
        return obj.getname(True)

class TextLabeler(object):
    def __call__(self, obj):
        return cpxlp_label_from_name(obj.getname(True))

    @deprecated("The 'remove_obj' method is no longer "
                "necessary now that 'getname' does not "
                "support the use of a name buffer", version="6.4.1")
    def remove_obj(self, obj):
        pass

class AlphaNumericTextLabeler(object):
    def __call__(self, obj):
        return alphanum_label_from_name(obj.getname(True))

class NameLabeler(object):
    def __call__(self, obj):
        return obj.getname(True)

class ShortNameLabeler(object):
    def __init__(self, limit, suffix, start=0, labeler=None,
                 prefix="", caseInsensitive=False, legalRegex=None):
        self.id = start
        self.prefix = prefix
        self.suffix = suffix
        self.limit = limit
        if labeler is not None:
            self.labeler = labeler
        else:
            self.labeler = AlphaNumericTextLabeler()
        self.known_labels = set()
        self.caseInsensitive = caseInsensitive
        if isinstance(legalRegex, str):
            self.legalRegex = re.compile(legalRegex)
        else:
            self.legalRegex = legalRegex

    def __call__(self, obj=None):
        lbl = self.labeler(obj)
        lbl_len = len(lbl)
        shorten = False
        if lbl_len > self.limit:
            shorten = True
        elif lbl_len == self.limit and lbl.startswith(self.prefix) \
             and lbl.endswith(self.suffix):
            shorten = True
        elif (lbl.upper() if self.caseInsensitive else lbl) in self.known_labels:
            shorten = True
        elif self.legalRegex and not self.legalRegex.match(lbl):
            shorten = True
        if shorten:
            self.id += 1
            suffix = "%s%d%s" % (self.suffix, self.id, self.suffix)
            tail = -self.limit + len(suffix) + len(self.prefix)
            if tail >= 0:
                raise RuntimeError(
                    "Too many identifiers.\n\t"
                    "The ShortNameLabeler cannot generate a guaranteed unique "
                    "label limited to %d characters" % (self.limit,))
            lbl = self.prefix + lbl[tail:] + suffix
        if self.known_labels is not None:
            self.known_labels.add(lbl.upper() if self.caseInsensitive else lbl)
        return lbl
