"""pIDLy 0.2.7: IDL within Python.

Control ITT's IDL (Interactive Data Language) from within Python.

https://github.com/anthonyjsmith/pIDLy
http://pypi.python.org/pypi/pIDLy/

Requirements:

* Pexpect
* NumPy

Usage:

>>> import pidly
>>> idl = pidly.IDL()

>>  print(idl.__doc__)

Consult the docstrings or README.txt in the source distribution for
further information.

Copyright (c) 2008-2017, Anthony Smith
anthonysmith80@gmail.com

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

"""
from __future__ import print_function
import sys
import re
import weakref
import atexit
import unittest
import tempfile
import os
import errno
from datetime import datetime

import numpy
import pexpect
from scipy.io import idl as idlio

now = datetime.now
__version__ = '0.2.6'
STR_DELIMITER = '!@#'  # To distinguish items in an array of strings
try:
    __IPYTHON__
    _ipython = True
except NameError:
    _ipython = False


# List of weak references to IDL sessions (primarily for IPython)
try:
    weakrefs_to_pidly_sessions  # Allow IDL to continue when module is reloaded
except NameError:
    weakrefs_to_pidly_sessions = []


def close_all():
    for pidly_weakref in weakrefs_to_pidly_sessions: #[:]:
        if pidly_weakref() is not None:
            pidly_weakref().close()


# Clean exit in IPython
atexit.register(close_all)


class IDLInputOverflowError(Exception):
    """Expression too long for IDL to receive."""


class IDL(pexpect.spawn):
    """pidly.IDL() : Launch IDL session within Python.

    The IDL class inherits from pexpect.spawn.  Consult pexpect
    documentation for details of further methods.

    Usage:

    Initiate:
    >>> import pidly
    >>> idl = pidly.IDL()

    Or:
        idl = pidly.IDL('/path/to/idl')

    Execute commands:
    >>> idl('x = total([1, 1], /int)')

    Retrieve values:
    >>> print(idl.ev('x'))
    2

    Or (slightly slower):
    >>> print(idl.x)
    2

    Evaluate expressions:
    >>> print(idl.ev('x ^ 2'))
    4

    Use cache (IDL save) to handle large arrays:
    >>> idl('x=[1,2,3,4,5,6]')
    >>> print(idl.ev('x', use_cache=True))
    [1 2 3 4 5 6]

    Transfer a list of IDL variables, using cache:
    >>> idl('y=[1,2,3,4,5,6]')
    >>> xy = idl.ev_list(['x','y'], use_cache=True)
    >>> print(sorted(xy.keys()))
    ['x', 'y']
    >>> print(xy['x'])
    [1 2 3 4 5 6]

    Assign value from Python expression:
    >>> idl.x = 2 + 2
    >>> print(idl.x)
    4

    Or:
    >>> idl('x', 2 + 2)
    >>> print(idl.x)
    4

    Perform IDL function on Python expression(s):
    >>> idl.func('reform', range(4), 2, 2)
    array([[0, 1],
           [2, 3]])

    Or (slightly slower):
    >>> idl.reform(range(4), 2, 2)
    array([[0, 1],
           [2, 3]])

    With keywords (/L64 -> L64=True or L64=1)
    >>> x = idl.histogram(range(4), binsize=3, L64=True)
    >>> print(x)
    [3 1]
    >>> print(x.dtype)
    int64

    IDL procedure with Python argument(s):
    >>> idl.pro('plot', range(10), range(10), xstyle=True, ystyle=True)

    Interactive mode:
    >>  idl.interact()
    IDL> print, x
         4
    IDL> ^D
    >>>

    Close:
    >>> idl.close()

    pIDLy supports the transfer of:
    * ints, longs, ...
    * floats, doubles, ...
    * strings
    * arrays of the above types, with arbitrary size and shape
    * dictionaries <-> structures & lists of dicts <-> arrays of structures
      but with certain limitations on transfer from Python to IDL

    [NB if getting Syntax Errors when passing large arrays to IDL, try using
    >>  idl = pidly.IDL(long_delay=0.05)
    default is 0.02.]

    """

    def __init__(self, *arguments, **kwargs):
        # NB set .logfile_send = sys.stdout to monitor commands sent

        # The speed Py -> IDL is limited by the IDL input buffer,
        # which can overload.  self.delaybeforesend is increased
        # before sending long arrays.
        self.short_delay = kwargs.pop('short_delay', 0)
        # As small as possible! (try 0.014)
        self.long_delay = kwargs.pop('long_delay', 0.02)
        # Temp dir for get_from_save
        self._cache_dir = kwargs.pop("cache_dir", None)
        self.use_cache = kwargs.pop("use_cache", False)

        # There are limits to how data may be passed to IDL:
        # max_sendline is the number of bytes that may be sent in one line
        # (Final, additional, byte is the end-of-line)
        self.max_sendline = kwargs.pop('max_sendline', 1023)

        # max_idl_code_area is the maximum number of bytes that may
        # be input as an IDL command. This limit may be reached by
        # splitting the line and sending as more than one send()
        # command
        # (Must be 1244 or greater, otherwise command in ev() fails)
        self.max_idl_code_area = kwargs.pop('max_idl_code_area', 2046) # 2048?

        # Number of array elements in IDL code area limited to 249 (don't ask)
        # (Was 331 on IDL 6.x and 7[?], but 249 on 7.1.1)
        self.max_n_elements_code_area = kwargs.pop('max_n_elements_code_area',
                                                   249)

        # Custom IDL prompt
        self.idl_prompt = kwargs.pop('idl_prompt', 'IDL> ')

        # Begin
        if len(arguments) == 0:
            arguments = ('idl',)
        if 'timeout' not in kwargs:
            kwargs['timeout'] = None
        pexpect.spawn.__init__(self, *arguments, **kwargs)
        self.delaybeforesend = self.short_delay  # Changed for long commands
        self._wait_for_prompt()

        # For clean exit in IPython and for close_all()
        weakrefs_to_pidly_sessions.append(weakref.ref(self))

        self.ready = True  # For __setattr__


    def close(self):
        """
        Close IDL session.

        Try to call IDL exit function - this way you may still be able
        to terminate IDL if it is a called indirectly through a script.
        """
        if self.isalive():
            try:
                self.ex('exit', print_output=False, ret=True)
            except OSError:
                pass
        super(IDL, self).close()


    def ex(self, expression, assignment_value=None,
           print_output=True, ret=False):
        """Execute a command in IDL.

        If assignment_value is set (to a Python expression), this value is
        assigned to the IDL variable named in expression.

        """

        # Assign value to expression?
        if assignment_value is not None:
            expression = self._python_to_idl_input(assignment_value,
                                                   expression)

        # List of commands to execute?
        if hasattr(expression, '__iter__') and not isinstance(expression, (str, bytes, bytearray)):
            # Long assignments are broken down into lists: iterate then return
            # Or can receive a list of commands directly
            output = []
            print("Sending", len(expression), "commands to IDL:", end=' ')
            sys.stdout.flush()
            for exp in expression:
                sys.stdout.write(".")
                sys.stdout.flush()
                out = self.ex(exp, print_output=print_output, ret=ret)
                if out:
                    output.append(out)
                self.delaybeforesend = self.long_delay
            self.delaybeforesend = self.short_delay
            print("done.")
            if ret:
                return ''.join(output)
            else:
                return

        # Send expression to IDL
        if self._send_expression_to_idl(expression):  # Any bytes sent?
            # Wait for command to be completed, and optionally print output
            self.readline()  # First line of output = IDL command repeated
            idl_output = self._wait_for_prompt(print_output=print_output)

            # Return IDL output
            if ret and idl_output:
                return idl_output


    def ev(self, expression, print_output=True, use_cache=None):
        """Return the value of an IDL expression as a numpy.ndarray."""

        # Evaluate expression and store as an IDL variable
        if expression != 'pidly_tmp':
            self.ex('pidly_tmp=' + expression, print_output=print_output)

        if use_cache is None:
            use_cache = self.use_cache
        if use_cache:
            return self._get_from_save(['pidly_tmp'])['pidly_tmp']
        else:
            return self._get_from_print()


    def ev_list(self, names, print_output=True, use_cache=None):
        """Return a dictionary containing values of IDL variables in list names."""
        if not isinstance(names, (list, tuple)):
            raise ValueError("input should be a list or tuple, not ", type(names))
        if use_cache is None:
            use_cache = self.use_cache
        if use_cache:
            return self._get_from_save(names)
        else:
            result = {}
            for eachname in names:
                result[eachname] = self.ev(eachname, print_output=print_output)
            return result


    def interact(self, show_prompt=True, **kwargs):
        """Interactive IDL shell. Press ^D to return to Python."""

        if show_prompt:
            print(self.idl_prompt, end=' ')
        sys.stdout.flush()
        if 'escape_character' not in kwargs:
            kwargs['escape_character'] = '\x04'
        pexpect.spawn.interact(self, **kwargs)
        if not _ipython:
            print("")
    interact.__doc__ += "\n\n        " + pexpect.spawn.interact.__doc__


    def variables(self):