# Source code for tecplot.tecutil.util

    import collections
    import contextlib
    import ctypes
    import inspect
    import logging
    import os
    import re
    import tempfile
    import textwrap
    import warnings

    from collections.abc import Iterable
    from ctypes import cast, c_int, POINTER

    from ..exception import *
    from ..constant import Color

    log = logging.getLogger(__name__)

    maxint64 = 2**(64 - 1) - 1
    minint64 = -maxint64 - 1
    maxuint64 = 2**64 - 1

    IndexRange = collections.namedtuple('IndexRange', 'min max step')
    IndexRange.__new__.__defaults__ = (None, None, None)

    class Index(int):
        """Position identifier type.

        This type is used internally to represent a position in a list. It is
        used to indicate that a change between zero-based indexing and one-based
        indexing must occur at the TecUtil boundary.

        This type can be treated exactly like a Python native `int` and is only
        meaningful internally to the tecplot Python module.
        """

    XY = collections.namedtuple('XY', ('x', 'y'))
    XY.__new__.__defaults__ = (None, None)

    XYZ = collections.namedtuple('XYZ', ('x', 'y', 'z'))
    XYZ.__new__.__defaults__ = (None, None, None)

    def flatten_args(*args):
        flatargs = []
        for a in args:
            if isinstance(a, Iterable) and not isinstance(a, str):
                flatargs += list(a)
            else:
                flatargs.append(a)
        return tuple(flatargs)

    def array_to_enums(array_pointer, array_size, enum_type):
        indexes = cast(array_pointer, POINTER(c_int))
        return tuple(enum_type(indexes[i]) for i in range(array_size))

    def inherited_property(cls):
        def _copy_property(prop):
            attr = getattr(cls, prop.__name__)
            return property(attr.fget, attr.fset, attr.fdel, prop.__doc__)
        return _copy_property

    def lock_attributes(cls):
        """
        As a decorator of a class, this ensures that no new attributes are created
        after __init__() is called.
        """
        if __debug__:
            def _setattr(self, name, value):
                if not name.startswith('_') and name not in dir(self):
                    stacknames = [f[0].f_code.co_name for f in inspect.stack()[:3]]
                    if '__init__' not in stacknames:
                        msg = 'No attribute: {}.{}'
                        classname = self.__class__.__name__
                        raise TecplotAttributeError(msg.format(classname, name))
                return super(cls, self).__setattr__(name, value)
            cls.__setattr__ = _setattr
        return cls

    _VarInfo = collections.namedtuple('_VarInfo', ('types', 'values', 'names'))

    def check_arglist_argtypes(function_name, *args):
        for arg in args:
            vinfo = _VarInfo(*arg)
            for name, value in zip(vinfo.names, vinfo.values):
                if value is not None:
                    if not isinstance(value, tuple(vinfo.types)):
                        errfmt = '{}: Type of  parameter {} must be one of: {}'
                        types = ', '.join(t.__name__ for t in vinfo.types)
                        errmsg = errfmt.format(function_name, name, types)
                        raise TecplotTypeError(errmsg)

    def color_spec(color, plot=None):
        """
            color_spec(Color.Blue, plot)          --> Color.Blue
            color_spec(Color.MultiColor, plot)    --> plot.contour(0)
            color_spec(Color.MultiColor2, plot)   --> plot.contour(1)
            color_spec(Color.Blue)                --> Color.Blue
            color_spec(plot.contour(0))           --> Color.MultiColor
            color_spec(plot.contour(1))           --> Color.MultiColor2

            color_spec(plot.rgb_coloring)         --> Color.RGBColor
            color_spec(Color.RGBColor, plot)      --> plot.rgb_coloring
        """
        if isinstance(color, Iterable):
            return tuple([color_spec(c, plot) for c in color])
        color_spec._indexes = {
            Color.RGBColor: Index(Color.RGBColor.value),
            Color.MultiColor: Index(0),
            Color.MultiColor2: Index(1),
            Color.MultiColor3: Index(2),
            Color.MultiColor4: Index(3),
            Color.MultiColor5: Index(4),
            Color.MultiColor6: Index(5),
            Color.MultiColor7: Index(6),
            Color.MultiColor8: Index(7)}
        color_spec._multicolors = {v: k for k, v in color_spec._indexes.items()}
        try:
            if plot:
                if color == Color.RGBColor:
                    return plot.rgb_coloring
                else:
                    return plot.contour(color_spec._indexes[Color(color)])
            else:
                return color_spec._multicolors[Index(color.index)]
        except (AttributeError, KeyError):
            return Color(color)

    def filled_slice(slice_, maxlen):
        """Convert start, stop, step in slice to real integers.

        None and negative values are converted to actual default values depending
        on the maxlen given.
        """
        if slice_.start is None:
            start = 0
        elif slice_.start < 0:
            start = maxlen + slice_.start
        else:
            start = slice_.start
        start = min(max(start, 0), maxlen)

        if slice_.stop is None:
            stop = maxlen
        elif slice_.stop < 0:
            stop = maxlen + slice_.stop
        else:
            stop = slice_.stop
        stop = min(max(stop, 0), maxlen)

        if slice_.step is None:
            step = 1
        else:
            step = min(max(slice_.step, 1), maxlen)

        return slice(start, stop, step)

    def array_to_str(arr, maxlen=10):
        try:
            itr = iter(arr)
            item = next(itr)
            ret = '[' + str(item)
            for i, item in enumerate(itr, start=2):
                if i > maxlen:
                    ret += ' ...'
                    break
                ret += ', {}'.format(item)
            return ret + ']'
        except StopIteration:
            return '[]'
        except TypeError:
            return str(arr)

    class ListWrapper(object):
        """Converts a list to a wrapped paragraph of items.

        Unlike textwrap.TextWrapper, items in the list are not broken over
        multiple lines even if they contain spaces.
        """
        def __init__(self, initial_indent='', subsequent_indent='    ',
                     initial_width=70, subsequent_width=70, delim=',',
                     prefix='', suffix=''):
            assert (len(prefix) + len(initial_indent)) < initial_width
            assert (len(suffix) + len(subsequent_indent)) < subsequent_width
            self.initial_indent = initial_indent
            self.subsequent_indent = subsequent_indent
            self.initial_width = initial_width
            self.subsequent_width = subsequent_width
            self.delim = delim
            self.prefix = prefix
            self.suffix = suffix

        def wrap(self, str_list):
            ret = []
            max_space = self.subsequent_width - len(self.subsequent_indent)

            line = "{}{}".format(self.prefix, self.initial_indent)

            itr = iter(str_list)
            try:
                item = next(itr)
                line += "'{}'".format(item)
            except StopIteration:
                pass  # no items in list

            space_left = self.initial_width - len(line)

            for item in itr:
                s = "{} '{}'".format(self.delim, str(item))
                if len(s) < space_left or max_space <= space_left:
                    line += s
                    space_left -= len(s)
                else:
                    ret.append(line + self.delim)
                    line = "{}'{}'".format(self.subsequent_indent, item)
                    space_left = self.subsequent_width - len(line)

            if len(self.suffix) <= space_left:
                ret.append(line + self.suffix)
            else:
                ret.append(line)
                ret.append("{}{}".format(self.subsequent_indent, self.suffix))
            return ret

        def fill(self, str_list):
            return '\n'.join(self.wrap(str_list))

    @contextlib.contextmanager
    def optional(cls, args):
        """Context for optional arguments that can be None.

        In this example, variables is an optional parameter. If not `None`, then
        ``varset`` will be the result of ``IndexSet(*variables)``, if `None`, then
        ``varset`` will be `None`::

            def fn(variables=None):
                with optional(IndexSet, variables) as varset:
                    _tecutil.Fn(varset)

        The ``cls`` parameter must be a class and must have the ``__enter__`` and
        ``__exit__`` methods implemented.
        """
        if args is None:
            yield None
        else:
            with cls(*flatten_args(args)) as obj:
                yield obj

    def split_macro(source):
        """Split macro code into a list of commands."""
        comments = re.compile(r'(?<!\\)(\".*?\"|\'.*?\')|(#[^\r\n]*$)', re.MULTILINE)
        pattern = re.compile(r'(\$!.*?)(?=\$!)|(\$!.*)', re.MULTILINE | re.DOTALL)
        source = comments.sub(lambda m: m.group(1) or '', source)
        for match in pattern.finditer(source):
            yield (match.group(1) or match.group(2)).strip()

    def normalize_path(nodepath):
        """Convert relative paths to absolute and normalize.

        Always use Python's current working directory to convert to absolute paths.
        However, relative paths are disallowed when connected to a non-local
        instance of the TecUtil Server.
        """
        if nodepath is not None:
            nodepath = str(nodepath)
            from tecplot.tecutil import _tecutil_connector
            if _tecutil_connector.connected:
                if _tecutil_connector.client.host not in ['localhost', '127.0.0.1']:
                    if not os.path.isabs(nodepath):
                        msg = 'paths must be absolute when connected to Tecplot 360'
                        raise TecplotLogicError(msg)
            return os.path.abspath(nodepath)

    @contextlib.contextmanager
    def temporary_closed_file(suffix=None):
        """A named temporary file that has been created and closed.

        The file will be removed on exit of the context.
        """
        ftmp = tempfile.NamedTemporaryFile(suffix=suffix, delete=False)
        try:
            ftmp.close()
            yield ftmp.name
        finally:
            try:
                os.remove(ftmp.name)
            except OSError as e:
                msg = 'Temp file I/O error: {}'.format(e)
                warnings.warn(msg)

    def api_changed(msg, version_changed, sdk_version_changed, warning=False):
        msg = textwrap.dedent('''\
        PyTecplot API Change (version {})
        Tecplot 360 version: {}
        {}
        ''').format(version_changed, sdk_version_changed, textwrap.fill(msg))
        if warning:
            if __debug__:
                msg += 'Please update your code as the legacy interface will\n' \
                    +  'be removed in the next major release of PyTecplot.'
                warnings.warn(msg, TecplotFutureWarning)
        else:
            raise TecplotInterfaceChangeError(msg)

    def api_moved(old, new, version_changed, sdk_version_changed, warning=False):
        msg = textwrap.dedent('''\
        PyTecplot API Change (version {})
        Tecplot 360 version: {}
            {} has been moved to {}
        ''').format(version_changed, sdk_version_changed, old, new)
        if warning:
            if __debug__:
                msg += 'Please update your code as the legacy interface will\n' \
                    +  'be removed in the next major release of PyTecplot.'
                warnings.warn(msg, TecplotFutureWarning)
        else:
            raise TecplotInterfaceChangeError(msg)