########################################################################
""" ipythonsupport.py -- Initial IPython support

Copyright (c) 1999-2012, Archaeopteryx Software, Inc.  All rights reserved.

Written by Stephan R.A. Deibel and John P. Ehresman

"""
#########################################################################

from __future__ import absolute_import
from __future__ import print_function


import itertools
import os
import sys

kCapabilities = ( 'ipython.general', )

kWingHome = os.path.normpath(os.path.abspath(__file__) + '../../../../..')

for src_dir in [os.path.join(kWingHome, 'src'), kWingHome]:
  if src_dir not in sys.path:
    sys.path.append(src_dir)
    
if 'WINGDB_ACTIVE' in os.environ:
  # os.environ['WINGDB_EXITONFAILURE'] = '1'
  os.environ.pop('WINGDB_ACTIVE', None)
  import wingdbstub

  
from wingutils import mainloop

# Exit cleanly with an error if IPython can't be imported
try:
  import IPython
except:
  sys.stderr.write("Cannot import ipython\n")
  sys.stderr.write("sys.executable: %r\n" % sys.executable)
  raise 

from IPython.utils.traitlets import Type
import IPython.kernel.manager
import IPython.kernel.channels
import IPython.core.usage

from wingbase import channel

from wingbase import destroyable
from wingutils import remoteops
from wingutils import callutils

import threading
if sys.platform != 'win32':
  import fcntl
else:
  fcntl = None

# Old code to work around timeout_add not being thread safe; keep for
# now in case it's needed (jpe 9/24/14)
kUseEventPosting = False
if kUseEventPosting:
  from guiutils import wgtk
  class CMainThreadReceiverObject(wgtk.QObject):
    """ A receiver object to get callbacks to run on the main thread.  Works
    by having CCrossThreadEvent objects posted to an object created on the
    main thread (it can be a global).  The fCallback is simply called from
    the event method. """
  
    def event(self, ev):
      
      if isinstance(ev, CCrossThreadEvent):
        ev.fCallback()
        
  _gMainThreadReceiver = CMainThreadReceiverObject()
  
  class CCrossThreadEvent(wgtk.QEvent):
    """ Event subclass to send a schedule a callback to run on the main 
    thread """
    
    def __init__(self, callback):
      
      wgtk.QEvent.__init__(self, wgtk.QEvent.Type.User)
      
      self.fCallback = callback
  


class CMainThreadCallbackMixin(object):
  """ Mixin used to define the four IPython channels to pass all messages
  to a callback that is called on the main thread.  Each channel is actually
  its own thread and call_handlers method is called on the subthread
  
  XXX there must be a better way to do things, but IPython code seems to
  assume that the right thing to do is to use threads.  To avoid threads,
  it probably would require reaching through to the zmq layer and sending
  & receiving messages directly.
  """
  
  fMessageCallback = None
  
  def call_handlers(self, msg):
    
    print('Got in threade:', msg.get('msg_type'))
    
    def in_idle():
      self.fMessageCallback(msg)
    
    if kUseEventPosting:
      wgtk.gQtApp.postEvent(_gMainThreadReceiver, CCrossThreadEvent(in_idle))
    else:
      remoteops.GetMainloop().timeout_add_while_alive(0, in_idle, None)
      
      
class CMainThreadCallbackIOPubChannel(CMainThreadCallbackMixin, IPython.kernel.channels.IOPubChannel):
    pass

class CMainThreadCallbackShellChannel(CMainThreadCallbackMixin, IPython.kernel.channels.ShellChannel):
    pass

class CMainThreadCallbackStdInChannel(CMainThreadCallbackMixin, IPython.kernel.channels.StdInChannel):
    pass

class CMainThreadCallbackHBChannel(CMainThreadCallbackMixin, IPython.kernel.channels.HBChannel):
    pass

class CMainThreadCallbackKernelClient(IPython.kernel.KernelClient):
  
  fMessageCallback = None

  shell_channel_class = Type(CMainThreadCallbackShellChannel)
  iopub_channel_class = Type(CMainThreadCallbackIOPubChannel)
  stdin_channel_class = Type(CMainThreadCallbackStdInChannel)
  hb_channel_class = Type(CMainThreadCallbackHBChannel)
  
  @property
  def shell_channel(self):
    if self._shell_channel:
      return self._shell_channel
    
    channel = super(CMainThreadCallbackKernelClient, self).shell_channel
    channel.fMessageCallback = self.fMessageCallback
    return channel

  @property
  def iopub_channel(self):
    if self._iopub_channel:
      return self._iopub_channel
    
    channel = super(CMainThreadCallbackKernelClient, self).iopub_channel
    channel.fMessageCallback = self.fMessageCallback
    return channel

  @property
  def stdin_channel(self):
    if self._stdin_channel:
      return self._stdin_channel
    
    channel = super(CMainThreadCallbackKernelClient, self).stdin_channel
    channel.fMessageCallback = self.fMessageCallback
    return channel

  @property
  def hb_channel(self):
    if self._hb_channel:
      return self._hb_channel
    
    channel = super(CMainThreadCallbackKernelClient, self).hb_channel
    channel.fMessageCallback = self.fMessageCallback
    return channel

class CIPyRemoteSingleton():
  
  def CreateIPyOperations(self, callback_obj):
    
    ops = CIPyOperations(callback_obj)
    ctx = remoteops.GetContext(self)
    ctx.AddNetworkAccessibleObject(ops)
    return ops

class CIPyOperations(remoteops.destroyable.CDestroyable):
  
  def __init__(self, callback_obj):
    
    remoteops.destroyable.CDestroyable.__init__(self)
    
    self._fCallbackObj = callback_obj
    
    self._fKernelClient = None
    self._fManagedKernel = None
    
  def _destroy_impl(self):
    
    self.ShutdownKernel()
  
  def ShutdownKernel(self):

    if self._fManagedKernel:
      self._fManagedKernel.shutdown_kernel()
    
    self._fManagedKernel = None
    self._fKernelClient = None
        
  def StartKernel(self, options=None): # -> int
    
    kernel_mgr = IPython.kernel.manager.KernelManager()
    kernel_mgr.client_factory = CMainThreadCallbackKernelClient
    env = os.environ.copy()
    try:
      # start_kernel() always uses os.environ as of ipython 1.2.1
      os.environ['PYTHONPATH'] = ':'.join(sys.path[-3:])
      # Wing specific -- unset the WINGDB_ACTIVE env variable so wingdbstub works
      os.environ.pop('WINGDB_ACTIVE', None)
      kernel_mgr.start_kernel()
    finally:
      os.environ = env
      
    client = kernel_mgr.client()
    client.fMessageCallback = self.__CB_Message
    client.start_channels()
    
    self._fKernelClient = client
    self._fManagedKernel = kernel_mgr
    
  def ConnectToExistingKernel(self, connection_file): # -> int
    
    #kernel_mgr = IPython.kernel.manager.KernelManager()
    #kernel_mgr.client_factory = CMainThreadCallbackKernelClient
    #kernel_mgr.connection_file = connection_file
      
    connection_file = IPython.kernel.connect.find_connection_file(connection_file)
      
    client = CMainThreadCallbackKernelClient()
    client.fMessageCallback = self.__CB_Message

    client.connection_file = connection_file
    client.load_connection_file()
    client.start_channels()
    client.connect_hb()
    
    self._fKernelClient = client
    self._fManagedKernel = None

  def GetQuickGuideText(self):
    
    return IPython.core.usage.quick_guide
  
  def GetKernelFilename(self):

    if self._fKernelClient is None:
      return None
    return self._fKernelClient.connection_file

  def RequestKernelInfo(self):
    
    self._fKernelClient.shell_channel.kernel_info()

  def RequestCompletions(self, expr, line_text, col):

    msg_id = self._fKernelClient.shell_channel.complete(expr, line_text, col)
    return msg_id

  def RequestIntrospection(self, name, code, cursor_pos, level):
    
    msg_id = self._fKernelClient.shell_channel.object_info(name, level)
    return msg_id
  
  def RequestExecStart(self, expr, silent=False):
    
    print('exec-start', expr)
    msg_id = self._fKernelClient.shell_channel.execute(expr, silent=silent)
    print('end exec-start')
    return msg_id
    
  def RequestDeliveryToStdin(self, line):
    
    self._fKernelClient.input(line)
    
  def RequestHistory(self, session=0, start=0, stop=None):
    
    self._fKernelClient.shell_channel.history(session=session, start=start,
                                              stop=stop)
    
  def SendInterrupt(self):
    
    self._fManagedKernel.interrupt_kernel()
    
  def __CB_Message(self, msg):
    
    if self.destroyed():
      print('Late message received:', msg.get('type'))
      return
    
    self._fCallbackObj.ProcessWireMessages([msg], callback=None,
                                           exception=None)
        
def CreateRemoteIPySingleton():
  
  remote_singleton = CIPyRemoteSingleton()
  local_ctx, remote_ctx = remoteops.CreateInProcessRemoteOperations(remote_singleton)

  local_ctx.connect('destroy', wgtk.NoArgCallback(remote_ctx.destroy))
  
  return local_ctx

def Main(args):
  
  mainloop.SetMainloop(mainloop.CNonblockingSocketMainloop())

  debug = False
  if debug:
    remoteops.CContext.kLogActivity = True
    
  add_tracer = False
  if add_tracer:
    tracer = CProfileTracer()
    sys.setprofile(tracer)
    print('added tracer')
    #wgtk.timeout_add(5000, tracer.Print)
  
  ipy_ops = CIPyRemoteSingleton()
  remoteops.Main(ipy_ops, args)
  
import time

class CProfileTracer:
  """ A tracer for the profile hook that records function calls and prints 
  them out iff too much time elapses between idle calls that it schedules. """
  
  
  def __init__(self):
    self.fFrameReprList = [None] * 10000
    self.fDepth = 0
    self.fMaxDepth = 10
    
    self.fInIdle = False
    
    self.fLastIdleTime = None
    
    self.fPrintThreshold = .1
    
    remoteops.GetMainloop().timeout_add_while_alive(0, self._CB_Idle, None)
    
  def __call__(self, frame, event, arg):
    
    if self.fInIdle:
      return
    
    if event in ('call', 'c_call'):
      self.fDepth += 1
      if self.fDepth <= self.fMaxDepth and event == 'call':
        t = time.time()
        co_name = frame.f_code.co_name
        filename = frame.f_code.co_filename
        record = (t, self.fDepth, '%s in %r' % (co_name, filename))
        self.fFrameReprList.append(record)
    else:
      if self.fDepth > 0:
        self.fDepth -= 1

  def _CB_Idle(self):
    
    self.fInIdle = True
    try:
      t = time.time()
      diff = (t - self.fLastIdleTime if self.fLastIdleTime else 0)
      if self.fLastIdleTime is not None and diff > self.fPrintThreshold:
        print('Time since last idle: %.03f' % (diff,))
        self.Print()
        
      self.Clear()
      self.fLastIdleTime = t
    finally:
      self.fInIdle = False

    return True
    
  def Clear(self):
    
    del self.fFrameReprList[:]
    self.fDepth = 0
    
  def Print(self):
    
    for t, depth, r in list(self.fFrameReprList):
      from_last_time = t - self.fLastIdleTime
      line = 't = %.03f' % (from_last_time,) + '  ' * depth + r
      print (line)
      
if __name__ == '__main__':
  sys.exit(Main(sys.argv[1:]))