import os
from PySide import QtGui
from PySide import QtCore
import FreeCADGui # just used for FreeCADGui.updateGui()
from SearchBoxLight import SearchBoxLight

globalIgnoreFocusOut = False

genericToolIcon = QtGui.QIcon(QtGui.QIcon(os.path.dirname(__file__) + '/Tango-Tools-spanner-hammer.svg'))

def easyToolTipWidget(html):
  foo = QtGui.QTextEdit()
  foo.setReadOnly(True)
  foo.setAlignment(QtCore.Qt.AlignTop)
  foo.setText(html)
  return foo

class SearchBox(QtGui.QLineEdit):
  # The following block of code is present in the lightweight proxy SearchBoxLight
  '''
  resultSelected = QtCore.Signal(int, int)
  '''
  @staticmethod
  def lazyInit(self):
    if self.isInitialized:
      return self
    getItemGroups = self.getItemGroups
    getToolTip = self.getToolTip
    getItemDelegate = self.getItemDelegate
    maxVisibleRows = self.maxVisibleRows
    # The following block of code is executed by the lightweight proxy SearchBoxLight
    '''
    # Call parent constructor
    super(SearchBoxLight, self).__init__(parent)
    # Connect signals and slots
    self.textChanged.connect(self.filterModel)
    # Thanks to https://saurabhg.com/programming/search-box-using-qlineedit/ for indicating a few useful options
    ico = QtGui.QIcon(':/icons/help-browser.svg')
    #ico = QtGui.QIcon(':/icons/WhatsThis.svg')
    self.addAction(ico, QtGui.QLineEdit.LeadingPosition)
    self.setClearButtonEnabled(True)
    self.setPlaceholderText('Search tools, prefs & tree')
    self.setFixedWidth(200) # needed to avoid a change of width when the clear button appears/disappears
    '''

    # Save arguments
    #self.model = model
    self.getItemGroups = getItemGroups
    self.getToolTip = getToolTip
    self.itemGroups = None # Will be initialized by calling getItemGroups() the first time the search box gains focus, through focusInEvent and refreshItemGroups
    self.maxVisibleRows = maxVisibleRows # TODO: use this to compute the correct height
    # Create proxy model
    self.proxyModel = QtCore.QIdentityProxyModel()
    # Filtered model to which items are manually added. Store it as a property of the object instead of a local variable, to prevent grbage collection.
    self.mdl = QtGui.QStandardItemModel()
    #self.proxyModel.setModel(self.model)
    # Create list view
    self.listView = QtGui.QListView(self)
    self.listView.setWindowFlags(QtGui.Qt.ToolTip)
    self.listView.setWindowFlag(QtGui.Qt.FramelessWindowHint)
    self.listView.setSelectionMode(QtGui.QAbstractItemView.SingleSelection)
    self.listView.setModel(self.proxyModel)
    self.listView.setItemDelegate(getItemDelegate()) # https://stackoverflow.com/a/65930408/324969
    # make the QListView non-editable
    self.listView.setEditTriggers(QtGui.QAbstractItemView.NoEditTriggers)
    # Create pane for showing extra info about the currently-selected tool
    #self.extraInfo = QtGui.QLabel()
    self.extraInfo = QtGui.QWidget()
    self.extraInfo.setWindowFlags(QtGui.Qt.ToolTip)
    self.extraInfo.setWindowFlag(QtGui.Qt.FramelessWindowHint)
    self.extraInfo.setLayout(QtGui.QVBoxLayout())
    self.extraInfo.layout().setContentsMargins(0,0,0,0)
    self.setExtraInfoIsActive = False
    self.pendingExtraInfo = None
    self.currentExtraInfo = None
    # Connect signals and slots
    self.listView.clicked.connect(lambda x: self.selectResult('select', x))
    self.listView.selectionModel().selectionChanged.connect(self.onSelectionChanged)

    # Note: should probably use the eventFilter method instead...
    wdgctx = QtCore.Qt.ShortcutContext.WidgetShortcut

    QtGui.QShortcut(QtGui.QKeySequence(QtCore.Qt.Key_Down), self, context = wdgctx).activated.connect(self.listDown)
    QtGui.QShortcut(QtGui.QKeySequence(QtCore.Qt.Key_Up), self, context = wdgctx).activated.connect(self.listUp)
    QtGui.QShortcut(QtGui.QKeySequence(QtCore.Qt.Key_PageDown), self, context = wdgctx).activated.connect(self.listPageDown)
    QtGui.QShortcut(QtGui.QKeySequence(QtCore.Qt.Key_PageUp), self, context = wdgctx).activated.connect(self.listPageUp)

    # Home and End do not work, for some reason.
    #QtGui.QShortcut(QtGui.QKeySequence.MoveToEndOfDocument, self, context = wdgctx).activated.connect(self.listEnd)
    #QtGui.QShortcut(QtGui.QKeySequence.MoveToStartOfDocument, self, context = wdgctx).activated.connect(self.listStart)
    #QtGui.QShortcut(QtGui.QKeySequence(QtCore.Qt.Key_End), self, context = wdgctx).activated.connect(self.listEnd)
    #QtGui.QShortcut(QtGui.QKeySequence('Home'), self, context = wdgctx).activated.connect(self.listStart)
    
    QtGui.QShortcut(QtGui.QKeySequence(QtCore.Qt.Key_Enter), self, context = wdgctx).activated.connect(self.listAccept)
    QtGui.QShortcut(QtGui.QKeySequence(QtCore.Qt.Key_Return), self, context = wdgctx).activated.connect(self.listAccept)
    QtGui.QShortcut(QtGui.QKeySequence('Ctrl+Return'), self, context = wdgctx).activated.connect(self.listAcceptToggle)
    QtGui.QShortcut(QtGui.QKeySequence('Ctrl+Enter'), self, context = wdgctx).activated.connect(self.listAcceptToggle)
    QtGui.QShortcut(QtGui.QKeySequence('Ctrl+Space'), self, context = wdgctx).activated.connect(self.listAcceptToggle)
    
    QtGui.QShortcut(QtGui.QKeySequence(QtCore.Qt.Key_Escape), self, context = wdgctx).activated.connect(self.listCancel)

    # Initialize the model with the full list (assuming the text() is empty)
    #self.proxyFilterModel(self.text()) # This is done by refreshItemGroups on focusInEvent, because the initial loading from cache can take time
    self.firstShowList = True
    self.isInitialized = True
    return self

  @staticmethod
  def refreshItemGroups(self):
    self.itemGroups = self.getItemGroups()
    self.proxyFilterModel(self.text())

  @staticmethod
  def proxyFocusInEvent(self, qFocusEvent):
    if self.firstShowList:
      mdl = QtGui.QStandardItemModel()
      mdl.appendRow([QtGui.QStandardItem(genericToolIcon, 'Please wait, loading results from cacheā€¦'),
                     QtGui.QStandardItem('0'),
                     QtGui.QStandardItem('-1')])
      self.proxyModel.setSourceModel(mdl)
      self.showList()
      self.firstShowList = False
      FreeCADGui.updateGui()
    global globalIgnoreFocusOut
    if not globalIgnoreFocusOut:
      self.refreshItemGroups()
    self.showList()
    super(SearchBoxLight, self).focusInEvent(qFocusEvent)

  @staticmethod
  def proxyFocusOutEvent(self, qFocusEvent):
    global globalIgnoreFocusOut
    if not globalIgnoreFocusOut:
      self.hideList()
      super(SearchBoxLight, self).focusOutEvent(qFocusEvent)

  @staticmethod
  def movementKey(self, rowUpdate):
    currentIndex = self.listView.currentIndex()
    self.showList()
    if self.listView.isEnabled():
        currentRow = currentIndex.row()
        nbRows = self.listView.model().rowCount()
        if nbRows > 0:
          newRow = rowUpdate(currentRow, nbRows)
          index = self.listView.model().index(newRow, 0)
          self.listView.setCurrentIndex(index)

  @staticmethod
  def proxyListDown(self): self.movementKey(lambda current, nbRows: (current + 1) % nbRows)
  @staticmethod
  def proxyListUp(self): self.movementKey(lambda current, nbRows: (current - 1) % nbRows)
  @staticmethod
  def proxyListPageDown(self): self.movementKey(lambda current, nbRows: min(current + max(1, self.maxVisibleRows / 2), nbRows - 1))
  @staticmethod
  def proxyListPageUp(self): self.movementKey(lambda current, nbRows: max(current - max(1, self.maxVisibleRows / 2), 0))
  @staticmethod
  def proxyListEnd(self): self.movementKey(lambda current, nbRows: nbRows - 1)
  @staticmethod
  def proxyListStart(self): self.movementKey(lambda current, nbRows: 0)

  @staticmethod
  def acceptKey(self, mode):
    currentIndex = self.listView.currentIndex()
    self.showList()
    if currentIndex.isValid():
      self.selectResult(mode, currentIndex)

  @staticmethod
  def proxyListAccept(self): self.acceptKey('select')
  @staticmethod
  def proxyListAcceptToggle(self): self.acceptKey('toggle')

  @staticmethod
  def cancelKey(self):
    self.hideList()
    self.clearFocus()

  # QKeySequence::Cancel
  @staticmethod
  def proxyListCancel(self): self.cancelKey()

  @staticmethod
  def proxyKeyPressEvent(self, qKeyEvent):
    key = qKeyEvent.key()
    modifiers = qKeyEvent.modifiers()
    self.showList()
    if key == QtCore.Qt.Key_Home and modifiers & QtCore.Qt.CTRL != 0:
      self.listStart()
    elif key == QtCore.Qt.Key_End and modifiers & QtCore.Qt.CTRL != 0:
      self.listEnd()
    else:
      super(SearchBoxLight, self).keyPressEvent(qKeyEvent)

  @staticmethod
  def showList(self):
    self.setFloatingWidgetsGeometry()
    if not self.listView.isVisible():
      self.listView.show()
    self.showExtraInfo()

  @staticmethod
  def hideList(self):
    self.listView.hide()
    self.hideExtraInfo()

  @staticmethod
  def hideExtraInfo(self):
    self.extraInfo.hide()

  @staticmethod
  def selectResult(self, mode, index):
    groupId = int(index.model().itemData(index.siblingAtColumn(2))[0])
    self.hideList()
    # TODO: allow other options, e.g. some items could act as combinators / cumulative filters
    self.setText('')
    self.proxyFilterModel(self.text())
    # TODO: emit index relative to the base model
    self.resultSelected.emit(index, groupId)

  @staticmethod
  def proxyFilterModel(self, userInput):
    # TODO: this will cause a race condition if it is accessed while being modified
    def matches(s):
      return userInput.lower() in s.lower()
    def filterGroup(group):
      if matches(group['text']):
        # If a group matches, include the entire subtree (might need to disable this if it causes too much noise)
        return group
      else:
        subitems = filterGroups(group['subitems'])
        if len(subitems) > 0 or matches(group['text']):
          return { 'id': group['id'], 'text': group['text'], 'icon': group['icon'], 'action': group['action'], 'toolTip':group['toolTip'], 'subitems': subitems }
        else:
          return None
    def filterGroups(groups):
      groups = (filterGroup(group) for group in groups)
      return [group for group in groups if group is not None]
    self.mdl = QtGui.QStandardItemModel()
    self.mdl.appendColumn([])
    def addGroups(filteredGroups, depth=0):
      for group in filteredGroups:
        # TODO: this is not very clean, we should memorize the index from the original itemgroups
        self.mdl.appendRow([QtGui.QStandardItem(group['icon'] or genericToolIcon, group['text']),
                            QtGui.QStandardItem(str(depth)),
                            QtGui.QStandardItem(str(group['id']))])
        addGroups(group['subitems'], depth+1)
    addGroups(filterGroups(self.itemGroups))
    self.proxyModel.setSourceModel(self.mdl)
    self.currentExtraInfo = None # Unset this so that the ExtraInfo can be updated
    # TODO: try to find the already-highlighted item
    nbRows = self.listView.model().rowCount()
    if nbRows > 0:
      index = self.listView.model().index(0, 0)
      self.listView.setCurrentIndex(index)
      self.setExtraInfo(index)
    else:
      self.clearExtraInfo()
    #self.showList()

  @staticmethod
  def setFloatingWidgetsGeometry(self):
    def getScreenPosition(widget):
      geo = widget.geometry()
      parent = widget.parent()
      parentPos = getScreenPosition(parent) if parent is not None else QtCore.QPoint(0,0)
      return QtCore.QPoint(geo.x() + parentPos.x(), geo.y() + parentPos.y())
    pos = getScreenPosition(self)
    siz = self.size()
    screen = QtGui.QGuiApplication.screenAt(pos)
    x = pos.x()
    y = pos.y() + siz.height()
    hint_w = self.listView.sizeHint().width()
    # TODO: this can still bump into the bottom of the screen, in that case we should flip
    w = max(siz.width(), hint_w)
    h = 200 # TODO: set height / size here according to desired number of items
    extraw = w # choose a preferred width that doesn't change all the time,
                # self.extraInfo.sizeHint().width() would change for every item.
    extrax = x - extraw
    if screen is not None:
      scr = screen.geometry()
      x = min(scr.x() + scr.width() - hint_w, x)
      extraleftw = x - scr.x()
      extrarightw = scr.x() + scr.width() - x
      # flip the extraInfo if it doesn't fit on the screen
      if extraleftw < extraw and extrarightw > extraleftw:
        extrax = x + w
        extraw = min(extrarightw, extraw)
      else:
        extrax = x - extraw
        extraw = min(extraleftw, extraw)
    self.listView.setGeometry(x, y, w, h)
    self.extraInfo.setGeometry(extrax, y, extraw, h)

  @staticmethod
  def proxyOnSelectionChanged(self, selected, deselected):
    # The list has .setSelectionMode(QtGui.QAbstractItemView.SingleSelection),
    # so there is always at most one index in selected.indexes() and at most one
    # index in deselected.indexes()
    selected = selected.indexes()
    deselected = deselected.indexes()
    if len(selected) > 0:
      index = selected[0]
      self.setExtraInfo(index)
      # Poor attempt to circumvent a glitch where the extra info pane stays visible after pressing Return
      if not self.listView.isHidden():
        self.showExtraInfo()
    elif len(deselected) > 0:
      self.hideExtraInfo()

  @staticmethod
  def setExtraInfo(self, index):
    if self.currentExtraInfo == (index.row(), index.column(), index.model()):
      # avoid useless updates of the extra info window; this also prevents segfaults when the widget
      # is replaced when selecting an option from the right-click context menu
      return
    self.currentExtraInfo = (index.row(), index.column(), index.model())
    # TODO: use an atomic swap or mutex if possible
    if self.setExtraInfoIsActive:
      self.pendingExtraInfo = index
      #print("boom")
    else:
      self.setExtraInfoIsActive = True
      #print("lock")
      # setExtraInfo can be called multiple times while this function is running,
      # so just before existing we check for the latest pending call and execute it,
      # if during that second execution some other calls are made the latest of those will
      # be queued by the code a few lines above this one, and the loop will continue processing
      # until an iteration during which no further call was made.
      while True:
        groupId = str(index.model().itemData(index.siblingAtColumn(2))[0])
        # TODO: move this outside of this class, probably use a single metadata
        # This is a hack to allow some widgets to set the parent and recompute their size
        # during their construction.
        parentIsSet = False
        def setParent(toolTipWidget):
          nonlocal parentIsSet
          parentIsSet = True
          w = self.extraInfo.layout().takeAt(0)
          while w:
            if hasattr(w.widget(), 'finalizer'):
              # The 3D viewer segfaults very easily if it is used after being destroyed, and some Python/C++ interop seems to overzealously destroys some widgets, including this one, too soon?
              # Ensuring that we properly detacth the 3D viewer widget before discarding its parent seems to avoid these crashes.
              #print('FINALIZER')
              w.widget().finalizer()
            if w.widget() is not None:
              w.widget().hide() # hide before detaching, or we have widgets floating as their own window that appear for a split second in some cases.
              w.widget().setParent(None)
            w = self.extraInfo.layout().takeAt(0)
          self.extraInfo.layout().addWidget(toolTipWidget)
          self.setFloatingWidgetsGeometry()
        toolTipWidget = self.getToolTip(groupId, setParent)
        if isinstance(toolTipWidget, str):
            toolTipWidget = easyToolTipWidget(toolTipWidget)
        if not parentIsSet:
          setParent(toolTipWidget)
        if self.pendingExtraInfo is not None:
          index = self.pendingExtraInfo
          self.pendingExtraInfo = None
        else:
          break
      #print("unlock")
      self.setExtraInfoIsActive = False

  @staticmethod
  def clearExtraInfo(self):
    # TODO: just clear the contents but keep the widget visible.
    self.extraInfo.hide()

  @staticmethod
  def showExtraInfo(self):
    self.extraInfo.show()