#!/usr/bin/env python

# python3 status: started

import sys, os

# verify system libraries
from afnipy import module_test_lib
g_testlibs = ['os', 'gc', 'numpy', 'wx', 'matplotlib']
if module_test_lib.num_import_failures(g_testlibs,details=0):
   print("""
     -- for details, consider xmat_tool -test_libs
     -- also, many computations do not require the GUI
        (e.g. 'xmat_tool -load_xmat X.xmat.1D -show_cormat_warnings')
   """)
   sys.exit(1)

import numpy as N
from afnipy import afni_xmat as AM
import gc

# must use matplotlib with wx, not pylab
import wx
import matplotlib
matplotlib.use('WX')

# set some resource font values
matplotlib.rc('axes',titlesize=11)
matplotlib.rc('axes',labelsize=9)
matplotlib.rc('xtick',labelsize=8)
matplotlib.rc('ytick',labelsize=7)

from matplotlib.backends.backend_wx import FigureCanvasWx as FigureCanvas
from matplotlib.backends.backend_wx import NavigationToolbar2Wx
from matplotlib.figure import Figure

from matplotlib.ticker import FormatStrFormatter

from afnipy import afni_util as UTIL

# ======================================================================
# general plotting routine:
# 

def plot(data, title='', verb=1):
   """Convert data to an AfniMatrix and plot.

            data  : data to plot, one of:     o  AfniMatrix
                                              o  Numpy Matrix
                                              o  array (of arrays ?)
                                              o  1D filename
            title : optional window title
            verb  : verbose level

      return err, result
                err    : 0 = success, else failure
                result : CanvasFrame (if err == 0)
                         error message (otherwise)
   """

   if isinstance(data, AM.AfniXmat):
      amat = data
   elif isinstance(data, N.ndarray) or type(data) == type([]):
      amat = AM.AfniXmat('', from_mat=1, matrix=data, verb=verb)
   elif type(data) == type(''):
      amat = AM.AfniXmat(filename=data)
   else:
      mesg = "** plot: cannot convert %s to AfniXmat" % type(data)
      if verb > 1: print(mesg)
      return 1, mesg

   if not isinstance(amat, AM.AfniXmat):
      mesg = "** plot: failed to convert %s to AfniXmat" % type(data)
      if verb > 1: print(mesg)
      return 2, mesg

   # we have a valid AfniXmat, plot it

   frame = CanvasFrame(title=title, as_one=1, verb=verb)

   if title == '': title = 'AfniXmat'
   new_title = '%s   (%d)' % (title, frame.counter)
   frame.SetTitle(new_title)

   frame.Show(True)
   frame.plot_mat_by_cols(amat)

   return 0, frame


# ======================================================================
# main plotting canvas class

class CanvasFrame(wx.Frame):
   """create a main plotting canvas
        title   : optional window title
        as_one  : plot as one overlapping graph
                  (else a list of scaled graphs)
        verb    : verbose level (default 1)
   """

   counter = 0
   def __init__(self, title='', as_one=0, verb=1):
      wx.Frame.__init__(self, None, -1, title, size=(400,300))
      self.counter += 1
      self.verb   = verb
      self.figure = Figure()
      self.canvas = FigureCanvas(self, -1, self.figure)
      self.sizer = wx.BoxSizer(wx.VERTICAL)
      self.sizer.Add(self.canvas, 1, wx.LEFT | wx.TOP | wx.GROW)
      self.SetSizer(self.sizer)
      self.Fit()

      self.canvas.mpl_connect('key_press_event', self.cb_keypress)
      self.as_one  = as_one

      self.toolbar = NavigationToolbar2Wx(self.canvas)
      self.toolbar.Realize()
      self.sizer.Add(self.toolbar, 0, wx.LEFT | wx.EXPAND)
      self.toolbar.update()

   def cb_keypress(self, event):
      if event.key == 'q':
         self.Close()

   def plot_scaled_mat(self, amat, verb=1):
      """plot data scaled and shifted to fit on one graph

              data      - AfniMatrix, list of float lists, or 1D file
              title     - optional window title
              verb      - optional verbose level
      """

      # allow matlist, list of float lists, 
      # if type(matlist) == type([]):
      #     e0 = matlist[0]
      #     if type

      if not matlist: return
      nmats = len(matlist)
      if nmats < 1: return

      yformat = FormatStrFormatter('%5.1f')

   def plot_matlist(self, matlist, title='', ylabels=[], ftcolors=0, verb=1):
      """plot AfniMatrix list, one graph per AfniMatrix
              matlist   - list of AfniMatrix elements
              title     - optional window title
              ylabels   - optional list of ylabel per mat
              ftcolors  - flag: use fit/timeseries colors (black blue)
              verb      - optional verbose level
      """

      if not matlist: return
      nmats = len(matlist)
      if nmats < 1: return

      yformat = FormatStrFormatter('%5.1f')
      matplotlib.rcParams['lines.linewidth'] = 2

      # make a label list, and get max length (among any label[0])
      if ylabels: labels = ylabels
      else:       labels = []
      rlen = 0
      nruns = 0
      maxlen = 0
      for ind in range(nmats):
         if ylabels:                lab = ylabels[ind]
         else:
            if matlist[ind].labels: lab = matlist[ind].labels[0]
            else:                   lab = ''
            labels.append(lab)
         if len(lab) > maxlen: maxlen = len(lab)

         # note run info
         if nruns == 0 and matlist[ind].nruns > 1:
            nruns = matlist[ind].nruns
            rlen = matlist[ind].run_len

      for ind in range(nmats):
         amat = matlist[ind]

         if ind == 0: tstr = title
         else:        tstr = ''

         ax = self.figure.add_subplot(nmats,1,ind+1,title=tstr)
         #if ftcolors and amat.ncols == 2:   # use blue over black?
         #   ax.plot(amat.mat[:,0], 'k')
         #   ax.plot(amat.mat[:,1], 'b')
         ax.plot(amat.mat)

         # apply y tickmarks
         ymin, ymax = ax.get_ylim()
         width = ymax - ymin
         if ymin*ymax < 0.0: ymean = 0.0
         else: ymean = round((ymin+ymax)/2.0,2)

         ax.grid(True)

         ax.yaxis.set_major_formatter(yformat)

         # if there are many graphs, reduce the number of yticks
         if nmats > 10:  ax.set_yticks(N.array([ymin,ymax]))
         elif nmats > 2: ax.set_yticks(N.array([ymin,ymean,ymax]))

         if rlen > 0: ax.set_xticks(N.array([r*rlen for r in range(nruns+1)]))
         if ind < nmats - 1: ax.set_xticklabels([])
         else:               ax.set_xlabel('TRs')

         if labels:
            if nmats > 1:
               ax.set_ylabel('%-*s ' % (maxlen,labels[ind]),
                             rotation='horizontal')
               rv, plist = self.axis_posn(ax)
               if rv == 0: ax.set_position((0.15, plist[1], 0.7, 0.7/nmats))
            else:
               ax.set_ylabel('%s' % labels[ind])
               ax.set_position((0.15, 0.2, 0.7, 0.7))

      self.canvas.draw()

      matplotlib.rcParams['lines.linewidth'] = 1   # reset

   def axis_posn(self, ax):
      """return error code, posn array"""

      # first attempt is to look for simple array return
      try: posn = ax.get_position()
      except:
         if self.verb > 1: print('** failed ax.get_position()')
         return 1, None

      # have list, ready for some return
      if type(posn) == type([]):
         if len(posn) < 4:
            if self.verb > 1:
               print('** get_position returns len %d list' % len(posn))
            return 1, None
         if self.verb > 2: print('-- get_position returns list %s' % \
                         UTIL.float_list_string(posn))
         return 0, posn

      # no list, assume Bbox and expect get_points() to return 2x2 numpy array
      try: plist = posn.get_points()
      except:
         if self.verb > 1: print('** failed posn.get_points()')
         return 1, None

      if type(plist) != type(N.array([])):
         if self.verb > 1: print('** posn.get_points() does not return N.array?')
         return 1, None

      try: pnlist = [plist[0][0], plist[0][1], plist[1][0], plist[1][1]]
      except:
         if self.verb > 1: print('** bad plist shape %s' % plist.shape)
         return 1, None

      if self.verb > 2: print('-- get_position returns Bbox list: %s' % \
                        UTIL.float_list_string(pnlist))

      return 0, pnlist

   def plot_mat_by_cols(self, amat, cols=[]):
      """plot the given columns from a single AfniMatrix"""

      if not isinstance(amat, AM.AfniXmat):
         print('** plot_MBC: instance is not AfniXmat')
         return

      if not cols:
         if amat.verb > 1: print('-- plotting default of %d cols' % amat.ncols)
         cols = [i for i in range(amat.ncols)]

      ncols = len(cols)
      if ncols < 1: return

      # create reduced matrix and column labels
      mat = amat.mat[:,cols]

      yformat = FormatStrFormatter('%5.1f')
      if amat.labels:
         labels = N.array(amat.labels)
         labels = labels[cols]
         labels = labels.tolist()

         # create axis formatter string

         striplabs = 1
         for label in labels: # strip trailing '#' if indices are all 0
            ind = regress_index(label)
            if ind > 0: striplabs = 0

         maxlen = 0
         for l in range(len(labels)):
            label = labels[l]
            if striplabs:
               labels[l] = labels[l][0:labels[l].rfind('#')]
            if len(labels[l]) > maxlen: maxlen = len(labels[l])

      for i in range(ncols):
         if i == 0: title = '%s [%s]' % (os.path.basename(amat.fname),
                                         UTIL.encode_1D_ints(cols))
         else     : title = ''

         if self.as_one:
            # then only create one axis
            if i == 0: ax = self.figure.add_subplot(1,1,1,title=title)
         else:
            ax = self.figure.add_subplot(ncols,1,i+1,title=title)
         data = mat[:,i]
         ax.plot(data)

         # apply y tickmarks
         ymin, ymax = ax.get_ylim()
         width = ymax - ymin
         if ymin*ymax < 0.0: ymean = 0.0
         else: ymean = round((ymin+ymax)/2.0,2)

         ax.grid(True)

         ax.yaxis.set_major_formatter(yformat)
         amax = round(data.max(),1)

         if self.as_one:                   pass # use default yticks
         elif amax == 1.0 and ymin == 0.0: ax.set_yticks(N.array([ymin,amax]))
         elif ncols > 10:                  ax.set_yticks(N.array([ymin,ymax]))
         else:                       ax.set_yticks(N.array([ymin,ymean,ymax]))

         # now that yticks are set, prevent tight limits
         if not self.as_one:
            if ymin == data.min():
               ymin -= 0.15 * width
               ax.set_ylim((ymin, ymax))

            if ymax == data.max():
               ymax += 0.15 * width
               ax.set_ylim((ymin, ymax))

            if amat.run_len > 10 and amat.nruns > 1:
               ax.set_xticks(N.array([r*amat.run_len
                                      for r in range(amat.nruns+1)]))

         if i < ncols - 1: ax.set_xticklabels([])
         else:             ax.set_xlabel('TRs')

         if self.as_one: ax.set_ylabel('')
         elif amat.labels:
            ax.set_ylabel('%-*s ' % (maxlen,labels[i]), rotation='horizontal')
            rv, plist = self.axis_posn(ax)
            # rcr - fix
            #if rv == 0: ax.set_position((0.2, plist[1], plist[2], plist[3]))
            if rv == 0: ax.set_position((0.15, plist[1], 0.7, 0.7/ncols))

      self.Fit()
      self.canvas.draw()

def regress_index(label):
   """return integer after trailing '#' if found, else -1"""

   ind = label.rfind('#')
   if ind < 0: return -1

   try:    val = int(label[ind+1:-1])
   except: val = -1

   return val

