# vPresent is an immersive presentation creation and display application.
# Copyright (C) 2007-2008 by Infiscape Corporation
#
# This program is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the Free
# Software Foundation; either version 2 of the License, or (at your option)
# any later version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.

import types

from PyQt4 import QtCore, QtGui
import panoramaui
import gui.util.environment as environ
import arcdial
import opendocument
import gui.common.imagerefedit

class Panorama(QtGui.QWidget, panoramaui.Ui_PanoramaBase):
   def __init__(self, panoramaElement, parent=None):
      QtGui.QWidget.__init__(self, parent)
      self.setupUi(self)

      env = environ.Environment()
      self.mDocument = env.curDocument.document
      self.undoStack = env.curDocument.document.getUndoStack()

      self.mPanoramaElement = panoramaElement

      self.mTextureModel = TextureModel(self.mPanoramaElement, self.undoStack)
      self.connect(self.mTextureModel,
                   QtCore.SIGNAL("rowsInserted(const QModelIndex&,int,int)"),
                   self.onImageRefInserted)

      self.mPanoramaView.setModel(self.mTextureModel)
      self.mPanoramaView.setSelectionMode(QtGui.QAbstractItemView.ExtendedSelection)
      self.mPanoramaView.setDragEnabled(True)
      self.mPanoramaView.setAcceptDrops(True)
      self.mPanoramaView.setDropIndicatorShown(True)
      #self.mPanoramaView.setDragDropMode(QtGui.QAbstractItemView.InternalMove)

      self.connect(self.mPanoramaView.selectionModel(),
         QtCore.SIGNAL("selectionChanged(QItemSelection,QItemSelection)"),
         self.onSelectionChanged)

      self._updateUi()

      # Update the valid buttons.
      self.onSelectionChanged()

   def setupUi(self, widget):
      panoramaui.Ui_PanoramaBase.setupUi(self, widget)

      # Replace the placeholder with the arc dial.
      ph_index = self.gridlayout.indexOf(self.mPlaceholderDial)
      ph_location = self.gridlayout.getItemPosition(ph_index)
      self.gridlayout.removeWidget(self.mPlaceholderDial)
      self.mPlaceholderDial.setParent(None)
      self.mPlaceholderDial = None
      self.mArcDial = arcdial.ArcDial()
      self.gridlayout.addWidget(self.mArcDial, *ph_location)


      self.widgets = [self.mRadius, self.mHeight, self.mStartArc,
                      self.mEndArc, self.mNumSides, self.mArcDial]

      self.connect(self.mRadius, QtCore.SIGNAL("valueChanged(double)"),
                   self.onValueChanged)
      self.connect(self.mHeight, QtCore.SIGNAL("valueChanged(double)"),
                   self.onValueChanged)
      self.connect(self.mStartArc, QtCore.SIGNAL("valueChanged(double)"),
                   self.onValueChanged)
      self.connect(self.mEndArc, QtCore.SIGNAL("valueChanged(double)"),
                   self.onValueChanged)
      self.connect(self.mNumSides, QtCore.SIGNAL("valueChanged(int)"),
                   self.onValueChanged)
      self.connect(self.mArcDial, QtCore.SIGNAL("valuesChanged"),
                   self.onValuesChanged)

      self.actionAddImageRef.setShortcuts(QtGui.QKeySequence.New)
      self.actionRemoveImageRef.setShortcuts(QtGui.QKeySequence.Delete)

      self.mAddBtn.setDefaultAction(self.actionAddImageRef)
      self.mRemoveBtn.setDefaultAction(self.actionRemoveImageRef)
      self.mEditBtn.setDefaultAction(self.actionEditImageRef)

      self.connect(self.actionAddImageRef, QtCore.SIGNAL("triggered()"),
                   self.onAddImageRef)
      self.connect(self.actionRemoveImageRef, QtCore.SIGNAL("triggered()"),
                   self.onRemoveImageRef)
      self.connect(self.actionEditImageRef, QtCore.SIGNAL("triggered()"),
                   self.onEditImageRef)

      # Setup a custom context menu callback.
      self.mPanoramaView.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
      self.connect(self.mPanoramaView,
                   QtCore.SIGNAL("customContextMenuRequested(QPoint)"),
                   self.onPanoramaContextMenu)

   def _updateUi(self):
      # Disable signals while changing values.
      for w in self.widgets:
         w.blockSignals(True)

      self.mRadius.setValue(self.mPanoramaElement.vpresent_radius)
      self.mHeight.setValue(self.mPanoramaElement.vpresent_height)
      self.mStartArc.setValue(self.mPanoramaElement.vpresent_start_arc)
      self.mEndArc.setValue(self.mPanoramaElement.vpresent_end_arc)
      self.mNumSides.setValue(self.mPanoramaElement.vpresent_num_sides_per_texture)
      self.mArcDial.setStartArc(self.mStartArc.value())
      self.mArcDial.setEndArc(self.mEndArc.value())

      for w in self.widgets:
         w.blockSignals(False)

   def _updateElement(self):
      self.mPanoramaElement.vpresent_radius = self.mRadius.value()
      self.mPanoramaElement.vpresent_height = self.mHeight.value()
      self.mPanoramaElement.vpresent_start_arc = self.mStartArc.value()
      self.mPanoramaElement.vpresent_end_arc = self.mEndArc.value()
      self.mPanoramaElement.vpresent_num_sides_per_texture = self.mNumSides.value()

      self.mPanoramaElement._update()
      env = environ.Environment()
      env.updateGL()

   def onSelectionChanged(self, selected=QtGui.QItemSelection(),
                          deselected=QtGui.QItemSelection()):
      valid = not selected.isEmpty()
      widgets = []
      for w in [self.actionRemoveImageRef, self.actionEditImageRef]:
         w.setEnabled(valid)

      # Don't allow the user to remove the single remaining texture.
      if self.mTextureModel.rowCount() <= 1:
         self.actionRemoveImageRef.setEnabled(False)

   def onValueChanged(self, value):
      self.mArcDial.setStartArc(self.mStartArc.value())
      self.mArcDial.setEndArc(self.mEndArc.value())
      self._updateElement()

   def onValuesChanged(self, startArc, endArc):
      self.mStartArc.setValue(startArc)
      self.mEndArc.setValue(endArc)
      self._updateElement()

   def onImageRefInserted(self, parent, start, end):
      """
      Selectes an ImageRef after it is inserted into the model.
      """
      new_idx = self.mTextureModel.index(start)
      self.mPanoramaView.setCurrentIndex(new_idx)
      self.mPanoramaView.scrollTo(new_idx)

   def onAddImageRef(self):
      img_ref = opendocument.createNode('vpresent:fill-image-ref')
      add_image = AddPanoramaImage(self.mPanoramaElement, self.mTextureModel, img_ref)
      self.undoStack.push(add_image)

      self.mPanoramaElement._childrenAddedOrRemoved()
      self.mPanoramaElement._update()

      # Scroll to the new image ref.
      new_idx = self.mTextureModel.index(self.mTextureModel.rowCount()-1)
      self.mPanoramaView.setCurrentIndex(new_idx)
      self.mPanoramaView.scrollTo(new_idx)
      #self.mPanoramaElement._update()

      # Edit the new FillImageRef
      self.onEditImageRef(True)

   def onRemoveImageRef(self):
      img_ref = self._getCurrentImageRef()
      if img_ref is not None:
         remove_image = RemovePanoramaImage(self.mPanoramaElement,
                                            self.mTextureModel, img_ref)
         self.undoStack.push(remove_image)

   def onPanoramaContextMenu(self, point):
      """ Create a pop-up menu listing all valid operations for selection. """
      # Get the currently selected FillImageRef.
      if self.mPanoramaView.currentIndex().isValid():
         # Create a menu
         menu = QtGui.QMenu("Format", self)
         menu.addAction(self.actionEditImageRef)

         # Show the context menu.
         menu.exec_(self.mPanoramaView.mapToGlobal(point))

   def onEditImageRef(self, addOperation=False):
      img_ref = self._getCurrentImageRef()

      if img_ref is None:
         return

      # Store the current undo index to revert changes later.
      undo_index = self.undoStack.index()

      # Since we are doing an add operation, we really want to undo the last
      # QUndoCommand too when the dialog is rejected.
      if addOperation:
         undo_index -= 1

      # Create a dialog to edit the image reference.
      self.mCurrentDialog = gui.common.imagerefedit.ImageRefEdit(img_ref, self.undoStack, parent=self)

      new_idx = self.mPanoramaView.currentIndex()
      idx_rect = self.mPanoramaView.visualRect(new_idx)

      # Display the dialog at the lower right corner of the new image ref.
      p = QtCore.QPoint(idx_rect.x()+idx_rect.width(), idx_rect.y()+idx_rect.height())
      p = self.mapToGlobal(p)
      self.mCurrentDialog.setGeometry(QtCore.QRect(p, QtCore.QSize(400,400)))
      result = self.mCurrentDialog.exec_()

      # If the add operation was accepted, push the undo commands.
      if QtGui.QDialog.Accepted != result:
         self.undoStack.setIndex(undo_index)
      else:
         # XXX: Manually update the panorama element since the ImageRef
         #      points to something else now.
         # XXX: This does not work correctly with undo/redo.
         self.mPanoramaElement._materialsDirty = True

      self.mPanoramaElement._update()
      # Update the OpenSG widget.
      env = environ.Environment()
      env.updateGL()

   def _getCurrentImageRef(self):
      idx = self.mPanoramaView.currentIndex()
      img_ref = None
      if idx.isValid() and self.mTextureModel is not None:
         img_ref = self.mTextureModel.data(idx, QtCore.Qt.UserRole)
      return img_ref


class TextureModel(QtCore.QAbstractListModel):
   mime_type = 'application/vpresent-panorama-texture'

   def __init__(self, panorama, undoStack, parent=None):
      QtCore.QAbstractListModel.__init__(self, parent)
      self.mPanorama = panorama
      self.undoStack = undoStack
      self.mErrorThumbnail = None

   def flags(self, index):
      default_flags = QtCore.QAbstractListModel.flags(self, index)

      # NOTE: Row 0 is for the special None texture.
      if index.isValid():
         return QtCore.Qt.ItemIsDragEnabled | QtCore.Qt.ItemIsDropEnabled | \
                QtCore.Qt.ItemIsEditable | default_flags
      else:
         return QtCore.Qt.ItemIsDropEnabled | default_flags

   def data(self, index, role=QtCore.Qt.DisplayRole):
      """ Returns the data representation of each node in the cluster.
      """
      if not index.isValid():
         return QtCore.QVariant()

      # Get the ImageRef we want data for.
      img_ref = self.mPanorama._children[index.row()]

      if role == QtCore.Qt.UserRole:
         return img_ref

      fill_image = None
      fin = img_ref.draw_fill_image_name
      if fin is not None:
         env = environ.Environment()
         fill_image = env.curDocument.document.getFillImage(fin)

      if role == QtCore.Qt.UserRole+1:
         return fill_image

      # Return a thumbnail for the image.
      if role == QtCore.Qt.DecorationRole:
         thumbnail = None
         if fill_image is not None:
            thumbnail = fill_image._getThumbnail()
         # If we could not get a valid thumbnail.
         if thumbnail is None:
            # Lazily create an error thumbnail.
            if self.mErrorThumbnail is None:
               pixmap = QtGui.QPixmap(":/vpresent/gui/images/help32.png")
               self.mErrorThumbnail = pixmap.scaled(64, 64)
            thumbnail = self.mErrorThumbnail
         # Return the correct thumbnail.
         return QtCore.QVariant(thumbnail)
      return QtCore.QVariant()

   def setData(self, index, value, role):
      return False

   def addImageRef(self, newImageRef):
      self.insertImageRef(self.rowCount(), newImageRef)

   def insertImageRef(self, row, newImageRef):
      """
      Insert a image ref into the panorama.
      @param row Index into the list of image refs.
      """
      assert row >= 0

      # Update the model with our row, but the presentation with
      # the child index.
      self.beginInsertRows(QtCore.QModelIndex(), row, row)
      self.mPanorama.insertChild(row, newImageRef)
      self.endInsertRows()

   def removeImageRef(self, rowOrImgRef):
      """
      Remove an ImageRef from the panorama.
      @param rowOrImgRef Row index or a ImageRef object. If it is a row index then it
                         is an index into the list of children.
      """
      if rowOrImgRef is None:
         return

      # Handle both cases of index and ImageRef object.
      if type(rowOrImgRef) == types.IntType:
         row = rowOrImgRef
         imgRef = self.mPanorama._children[row]
      else:
         imgRef = rowOrImgRef
         row = self.mPanorama._children.index(imgRef)

      # Update the model with our row, but the presentation with
      # the child index.
      self.beginRemoveRows(QtCore.QModelIndex(), row, row)
      self.mPanorama.subChild(row)
      self.endRemoveRows()


   def rowCount(self, parent=QtCore.QModelIndex()):
      """ Returns the number of nodes in the current cluster configuration.
      """
      # If the parent is not valid, then we have no children.
      if parent.isValid():
         return 0
      else:
         return len(self.mPanorama._children)

   def supportedDropActions(self):
      return QtCore.Qt.MoveAction | QtCore.Qt.CopyAction

   def mimeTypes(self):
      """ List of types we can represent. """
      types = QtCore.QStringList()
      types.append(TextureModel.mime_type)
      return types

   def mimeData(self, indexes):
      assert len(indexes) == 1
      img_ref = self.data(indexes[0], QtCore.Qt.UserRole)
      assert img_ref is not None

      mat_data   = QtCore.QByteArray()
      data_stream = QtCore.QDataStream(mat_data, QtCore.QIODevice.WriteOnly)
      text = "img-ref:%s" % indexes[0].row()
      txt_qstr = QtCore.QString(text)

      data_stream << txt_qstr

      # Create the MIME data object that will hold the serialized form of the
      # item being dragged.
      mime_data = QtCore.QMimeData()
      mime_data.setData(TextureModel.mime_type, mat_data)

      return mime_data

   def dropMimeData(self, mimeData, action, row, column, parent):
      """
      Called when moving a image ref to a new location.

      If row and col == (-1, -1) then we need to place at the beginning.
      Otherwise row specifies the new index for the image ref.
      """
      if not parent.isValid():
         return False
      if not mimeData.hasFormat(TextureModel.mime_type):
         return False
      if action == QtCore.Qt.IgnoreAction:
         return True
      if column > 0:
         return False

      # Extract the data from the dragged object.
      style_data = mimeData.data(TextureModel.mime_type)

      # Deserialzied the dragged object into the style name.
      data_stream = QtCore.QDataStream(style_data,
                                       QtCore.QIODevice.ReadOnly)
      old_row_str = QtCore.QString()
      data_stream >> old_row_str
      old_row_str = str(old_row_str).lstrip('image-ref:')
      old_row = int(old_row_str)

      print "parent Row: %s Col: %s" % (parent.row(), parent.column())
      print "Row: %s Col: %s" % (row, column)
      print "Moving %s -> %s" % (old_row, row)
      row = parent.row()
      if row < 0:
         row = 0

      img_ref = self.data(self.index(old_row), QtCore.Qt.UserRole)
      assert img_ref is not None

      self.removeImageRef(old_row)
      self.insertImageRef(row, img_ref)
      self.mPanorama._childMoved()
      self.mPanorama._update()

      env = environ.Environment()
      env.updateGL()

      return True

class AddPanoramaImage(QtGui.QUndoCommand):
   def __init__(self, panoramaElement, textureModel, imageRef, parent=None):
      QtGui.QUndoCommand.__init__(self, "Add Panorama Image", parent)
      self.mPanoramaElement = panoramaElement
      self.mTextureModel = textureModel
      self.mNewImageRef = imageRef
      
   def redo(self):
      self.mTextureModel.addImageRef(self.mNewImageRef)
      self.mPanoramaElement._childrenAddedOrRemoved()
      self.mPanoramaElement._update()

      # Update the OpenSG widget.
      env = environ.Environment()
      env.updateGL()

   def undo(self):
      self.mTextureModel.removeImageRef(self.mNewImageRef)
      self.mPanoramaElement._childrenAddedOrRemoved()
      self.mPanoramaElement._update()

      # Update the OpenSG widget.
      env = environ.Environment()
      env.updateGL()

class RemovePanoramaImage(QtGui.QUndoCommand):
   def __init__(self, panoramaElement, textureModel, imageRef, parent=None):
      QtGui.QUndoCommand.__init__(self, "Remove Panorama Image", parent)
      self.mPanoramaElement = panoramaElement
      self.mTextureModel = textureModel
      self.mOldImageRef = imageRef
      self.mOldIndex = self.mPanoramaElement._children.index(self.mOldImageRef)

   def redo(self):
      self.mTextureModel.removeImageRef(self.mOldImageRef)
      self.mPanoramaElement._childrenAddedOrRemoved()
      self.mPanoramaElement._update()

      # Update the OpenSG widget.
      env = environ.Environment()
      env.updateGL()

   def undo(self):
      self.mTextureModel.insertImageRef(self.mOldIndex, self.mOldImageRef)
      self.mPanoramaElement._childrenAddedOrRemoved()
      self.mPanoramaElement._update()

      # Update the OpenSG widget.
      env = environ.Environment()
      env.updateGL()
