/******************************************************************************
  This source file is part of the Avogadro project.
  This source code is released under the 3-Clause BSD License, (see "LICENSE").
******************************************************************************/

#include "coordinateeditordialog.h"
#include "coordinatetextedit.h"
#include "ui_coordinateeditordialog.h"

#include <avogadro/qtgui/molecule.h>
#include <avogadro/qtgui/rwmolecule.h>

#include <avogadro/core/avogadrocore.h>
#include <avogadro/core/coordinateblockgenerator.h>
#include <avogadro/core/crystaltools.h>
#include <avogadro/core/elements.h>
#include <avogadro/core/vector.h>

#include <QtGui/QClipboard>
#include <QtGui/QFont>
#include <QtGui/QIcon>
#include <QtGui/QRegularExpressionValidator>
#include <QtGui/QTextCursor>
#include <QtGui/QTextDocument>
#include <QtWidgets/QApplication>
#include <QtWidgets/QMessageBox>
#include <QtWidgets/QToolTip>

#include <QtCore/QDebug>
#include <QtCore/QMimeData>
#include <QtCore/QMutableListIterator>
#include <QtCore/QRegularExpression>
#include <QtCore/QString>
#include <QtCore/QTimer>

// Define this to print out details of the format detection algorithm.
#undef ENABLE_FORMAT_DEBUG

#ifdef ENABLE_FORMAT_DEBUG
#define FORMAT_DEBUG(x) x
#else // ENABLE_FORMAT_DEBUG
#define FORMAT_DEBUG(x)
#endif // ENABLE_FORMAT_DEBUG

using Avogadro::Vector3;
using Avogadro::Core::Elements;
using Avogadro::QtGui::Molecule;

namespace {

// Ensure a cross-platform monospaced font
#if defined(Q_OS_WIN) || defined(Q_OS_MACOS)
static const QString EDITOR_FONT = "Courier";
#else // Linux and other OSes
static const QString EDITOR_FONT = "Monospace";
#endif

// Various integer constants.
enum
{
  CustomPreset = 0
};

// Distance unit indices -- keep in sync with the .ui file.
enum DistanceUnitIndex
{
  Angstrom = 0,
  Bohr
};

// Types of tokens, used while parsing.
enum TokenType
{
  Integer = 0,
  Double,
  String
};

// Some frequently used regexes:
static const QRegularExpression TOKEN_SEPARATOR("[\\s,;]+");
static const QRegularExpression VALID_TOKEN("[^\\s,;]+");
// These two need to be exact
static const QRegularExpression INT_CHECKER(
  QRegularExpression::anchoredPattern("(:?[+-])?\\d+"));
static const QRegularExpression DOUBLE_CHECKER(
  QRegularExpression::anchoredPattern(
    "(:?[+-])?"             // Leading sign
    "(:?"                   // Must match one of the following:
    "\\d*\\.\\d*"           // Fractional part
    "|"                     // or
    "\\d+[Ee](:?[+-])?\\d+" // Exponential part
    "|"                     // or
    "\\d*\\.\\d*"           // Fractional part and
    "[Ee](:?[+-])?\\d+"     // Exponential part
    ")"));

struct AtomStruct
{
  unsigned char atomicNumber;
  Vector3 pos;
};

} // namespace

namespace Avogadro::QtPlugins {

// Storage class used to hold state while validating input.
class CoordinateEditorDialog::ValidateStorage
{
public:
  ValidateStorage()
    : isValidating(false), restartWhenFinished(false), collectAtoms(false),
      convertDistance(false), latticePositions(false), distanceConversion(1.f)
  {
  }

  bool isValidating;
  bool restartWhenFinished;
  bool collectAtoms;
  bool convertDistance;
  bool latticePositions;
  float distanceConversion;

  // Format specification
  QString spec;

  // Text cursors
  QTextCursor lineCursor;
  QTextCursor tokenCursor;

  // Accumulate atom data
  QVector<AtomStruct> atoms;
};

CoordinateEditorDialog::CoordinateEditorDialog(QWidget* parent_)
  : QDialog(parent_), m_ui(new Ui::CoordinateEditorDialog), m_molecule(nullptr),
    m_validate(new ValidateStorage), m_defaultSpec("SZxyz#N")
{
  m_ui->setupUi(this);

  // Set up text editor
  m_ui->text->setFont(QFont(EDITOR_FONT, qApp->font().pointSize()));
  connect(m_ui->text->document(), SIGNAL(modificationChanged(bool)),
          SLOT(textModified(bool)));

  // Setup spec edit
  QRegularExpression specRegExp("[#ZGSLNabcxyz01_]*");
  auto* specValidator = new QRegularExpressionValidator(specRegExp, this);
  m_ui->spec->setValidator(specValidator);
  connect(m_ui->presets, SIGNAL(currentIndexChanged(int)),
          SLOT(presetChanged(int)));
  connect(m_ui->spec, SIGNAL(textChanged(QString)), SLOT(specChanged()));
  connect(m_ui->spec, SIGNAL(textEdited(QString)), SLOT(specEdited()));

  connect(m_ui->distanceUnit, SIGNAL(currentIndexChanged(int)),
          SLOT(updateText()));

  connect(m_ui->help, SIGNAL(clicked()), SLOT(helpClicked()));
  connect(m_ui->cut, SIGNAL(clicked()), SLOT(cutClicked()));
  connect(m_ui->copy, SIGNAL(clicked()), SLOT(copyClicked()));
  connect(m_ui->paste, SIGNAL(clicked()), SLOT(pasteClicked()));
  connect(m_ui->revert, SIGNAL(clicked()), SLOT(revertClicked()));
  connect(m_ui->clear, SIGNAL(clicked()), SLOT(clearClicked()));
  connect(m_ui->apply, SIGNAL(clicked()), SLOT(applyClicked()));

  m_ui->cut->setIcon(QIcon::fromTheme("edit-cut"));
  m_ui->copy->setIcon(QIcon::fromTheme("edit-copy"));
  m_ui->paste->setIcon(QIcon::fromTheme("edit-paste"));

  buildPresets();
  listenForTextEditChanges(true);
}

CoordinateEditorDialog::~CoordinateEditorDialog()
{
  delete m_ui;
}

void CoordinateEditorDialog::setMolecule(QtGui::Molecule* mol)
{
  if (mol != m_molecule) {
    if (m_molecule)
      m_molecule->disconnect(this);
    m_molecule = mol;
    connect(m_molecule, SIGNAL(changed(uint)), SLOT(moleculeChanged(uint)));
    updateText();
  }
}

void CoordinateEditorDialog::moleculeChanged(uint change)
{
  if (static_cast<Molecule::MoleculeChange>(change) & Molecule::Atoms ||
      static_cast<Molecule::MoleculeChange>(change) & Molecule::UnitCell) {
    updateText();
  }
}

void CoordinateEditorDialog::presetChanged(int ind)
{
  QVariant itemData(m_ui->presets->itemData(ind));
  bool isCustom(itemData.type() != QVariant::String);

  // Changing the spec text will update the editor text.
  m_ui->spec->setText(isCustom ? m_defaultSpec : itemData.toString());
}

void CoordinateEditorDialog::specChanged()
{
  // Store the spec if custom preset is selected.
  if (m_ui->presets->currentIndex() == CustomPreset)
    m_defaultSpec = m_ui->spec->text();
  updateText();
}

void CoordinateEditorDialog::specEdited()
{
  // Editing the spec switches to and updates the custom preset.
  if (m_ui->presets->currentIndex() != CustomPreset) {
    m_defaultSpec = m_ui->spec->text();
    m_ui->presets->setCurrentIndex(CustomPreset);
  }
}

void CoordinateEditorDialog::updateText()
{
  if (m_ui->text->document()->isModified()) {
    int reply = QMessageBox::question(
      this, tr("Overwrite changes?"),
      tr("The text document has been modified. Would "
         "you like to discard your changes and revert "
         "to the current molecule?"),
      QMessageBox::Yes | QMessageBox::No, QMessageBox::No);
    if (reply != QMessageBox::Yes)
      return;
  }

  Core::CoordinateBlockGenerator gen;
  gen.setMolecule(m_molecule);
  gen.setSpecification(m_ui->spec->text().toStdString());
  switch (m_ui->distanceUnit->currentIndex()) {
    default:
    case Angstrom:
      gen.setDistanceUnit(Core::CoordinateBlockGenerator::Angstrom);
      break;
    case Bohr:
      gen.setDistanceUnit(Core::CoordinateBlockGenerator::Bohr);
      break;
  }

  // Disable markup for the generated text.
  listenForTextEditChanges(false);
  m_ui->text->document()->setPlainText(
    QString::fromStdString(gen.generateCoordinateBlock()));
  listenForTextEditChanges(true);
  m_ui->text->document()->setModified(false);
}

void CoordinateEditorDialog::helpClicked()
{
  // Give the spec lineedit focus and show its tooltip.
  m_ui->spec->setFocus(Qt::MouseFocusReason);
  QPoint point(m_ui->spec->pos() + pos());
  point.setY(point.y() + m_ui->spec->frameGeometry().height() + 5);
  QToolTip::showText(point, m_ui->spec->toolTip(), m_ui->spec);
}

void CoordinateEditorDialog::validateInput()
{
  if (m_validate->isValidating) {
    m_validate->restartWhenFinished = true;
    return;
  }

  // Reset formatting. Stop listening for changes since format changes will
  // retrigger validation.
  listenForTextEditChanges(false);
  m_ui->text->resetMarks();
  listenForTextEditChanges(true);

  // No text, nothing to do! Trim the plain text - this fixes a crashing
  // bug if the user accidentally presses return in a blank document.
  if (m_ui->text->document()->toPlainText().trimmed().isEmpty()) {
    emit validationFinished(true);
    return;
  }

  // Try to detect the input format
  QString inputFormat(detectInputFormat());
  if (inputFormat.isEmpty()) {
    emit validationFinished(false);
    return;
  }

  // Initialize
  m_validate->latticePositions = inputFormat.contains('a');
  m_validate->isValidating = true;
  m_validate->spec = inputFormat;
  m_validate->lineCursor = QTextCursor(m_ui->text->document());

  // Start the worker
  validateInputWorker();
}

void CoordinateEditorDialog::validateInputWorker()
{
  if (!m_validate->isValidating)
    return;

  // Disable revalidation due to formatting changes.
  listenForTextEditChanges(false);

  // Setup some aliases to keep code concise:
  const QString& spec(m_validate->spec);
  QTextCursor& lineCursor(m_validate->lineCursor);
  QTextCursor& tokenCursor(m_validate->tokenCursor);

  QTextDocument* doc(m_ui->text->document());
  QString::const_iterator begin(spec.constBegin());
  QString::const_iterator end(spec.constEnd());
  QString::const_iterator iter;

  // Only do a few lines at a time, then return control to the event loop.
  int lineThisIteration = 0;

  while (++lineThisIteration <= 10 && !lineCursor.atEnd()) {
    // Place the entire line in the line cursor's selection.
    lineCursor.movePosition(QTextCursor::StartOfLine, QTextCursor::MoveAnchor);
    lineCursor.movePosition(QTextCursor::EndOfLine, QTextCursor::KeepAnchor);

    // Skip empty lines - this is nice if the user accidentally left some
    // empty lines at the end of the input
    if (lineCursor.selectedText().trimmed().isEmpty()) {
      // So that we don't have an infinite loop...
      if (!lineCursor.atEnd()) {
        lineCursor.movePosition(QTextCursor::Down);
        lineCursor.movePosition(QTextCursor::StartOfLine);
      }
      continue;
    }

    // Start the token cursor at the beginning of the current line.
    tokenCursor.setPosition(lineCursor.anchor(), QTextCursor::MoveAnchor);

    // This is used when applying changes to store the atom specifications.
    AtomStruct atom;

    // Iterate through spec characters
    for (iter = begin; iter != end; ++iter) {
      // Place the next valid token in tokenCursor's selection:
      tokenCursor = doc->find(VALID_TOKEN, tokenCursor);

      // If the token cursor has moved off of the current line, mark the entire
      // line as invalid and move on.
      if (tokenCursor.isNull() ||
          tokenCursor.position() > lineCursor.position()) {
        m_ui->text->markInvalid(lineCursor, tr("Too few entries on line."));
        break;
      }

      switch (iter->toLatin1()) {
        case '?': // Nothing to validate other than that this is a valid token.
          break;

        case 'N': {
          // Validate name:
          QString cleanToken(tokenCursor.selectedText().toLower());
          if (!cleanToken.isEmpty())
            cleanToken.replace(0, 1, cleanToken[0].toUpper());
          std::string tokenStd(cleanToken.toStdString());
          atom.atomicNumber = Elements::atomicNumberFromName(tokenStd);
          if (atom.atomicNumber == Avogadro::InvalidElement)
            m_ui->text->markInvalid(tokenCursor, tr("Invalid element name."));
          else
            m_ui->text->markValid(tokenCursor, tr("Element name."));
          break;
        }

        case 'S': {
          // Validate symbol:
          QString cleanToken(tokenCursor.selectedText().toLower());
          if (!cleanToken.isEmpty())
            cleanToken.replace(0, 1, cleanToken[0].toUpper());
          std::string tokenStd(cleanToken.toStdString());
          atom.atomicNumber = Elements::atomicNumberFromSymbol(tokenStd);
          if (atom.atomicNumber == Avogadro::InvalidElement)
            m_ui->text->markInvalid(tokenCursor, tr("Invalid element symbol."));
          else
            m_ui->text->markValid(tokenCursor, tr("Element symbol."));
          break;
        }

        case 'L': {
          // Validate label (symbol + number)
          QString cleanToken(tokenCursor.selectedText().toLower());
          if (!cleanToken.isEmpty())
            cleanToken.replace(0, 1, cleanToken[0].toUpper());

          // Split the label into symbol and number
          QRegularExpression labelSplitter("([A-Z][a-z]?)(\\d+)");
          QRegularExpressionMatch match = labelSplitter.match(cleanToken);
          if (match.hasMatch()) {
            m_ui->text->markInvalid(tokenCursor, tr("Invalid atom label."));
            break;
          }
          // check the symbol
          std::string tokenStd(match.captured(1).toStdString());
          atom.atomicNumber = Elements::atomicNumberFromSymbol(tokenStd);
          if (atom.atomicNumber == Avogadro::InvalidElement)
            m_ui->text->markInvalid(tokenCursor, tr("Invalid element symbol."));
          else
            m_ui->text->markValid(tokenCursor, tr("Element symbol."));
          break;
        }

        case '#': {
          // Validate integer:
          bool isInt;
          [[maybe_unused]] int index = tokenCursor.selectedText().toInt(&isInt);
          if (!isInt)
            m_ui->text->markInvalid(tokenCursor, tr("Invalid atomic index."));
          else
            m_ui->text->markValid(tokenCursor, tr("Atomic index."));
          break;
        }

        case 'Z': {
          // Validate integer:
          bool isInt;
          atom.atomicNumber = static_cast<unsigned char>(
            tokenCursor.selectedText().toInt(&isInt));
          if (!isInt)
            m_ui->text->markInvalid(tokenCursor, tr("Invalid atomic number."));
          else
            m_ui->text->markValid(tokenCursor, tr("Atomic number."));
          break;
        }

        case 'x': {
          // Validate real:
          bool isReal;
          atom.pos.x() = tokenCursor.selectedText().toDouble(&isReal);
          if (!isReal)
            m_ui->text->markInvalid(tokenCursor, tr("Invalid coordinate."));
          else
            m_ui->text->markValid(tokenCursor, tr("X coordinate."));
          break;
        }

        case 'y': {
          // Validate real:
          bool isReal;
          atom.pos.y() = tokenCursor.selectedText().toDouble(&isReal);
          if (!isReal)
            m_ui->text->markInvalid(tokenCursor, tr("Invalid coordinate."));
          else
            m_ui->text->markValid(tokenCursor, tr("Y coordinate."));
          break;
        }

        case 'z': {
          // Validate real:
          bool isReal;
          atom.pos.z() = tokenCursor.selectedText().toDouble(&isReal);
          if (!isReal)
            m_ui->text->markInvalid(tokenCursor, tr("Invalid coordinate."));
          else
            m_ui->text->markValid(tokenCursor, tr("Z coordinate."));
          break;
        }

        case 'a': {
          // Validate real:
          bool isReal;
          atom.pos.x() = tokenCursor.selectedText().toDouble(&isReal);
          if (!isReal)
            m_ui->text->markInvalid(tokenCursor, tr("Invalid coordinate."));
          else
            m_ui->text->markValid(tokenCursor, tr("'a' lattice coordinate."));
          break;
        }

        case 'b': {
          // Validate real:
          bool isReal;
          atom.pos.y() = tokenCursor.selectedText().toDouble(&isReal);
          if (!isReal)
            m_ui->text->markInvalid(tokenCursor, tr("Invalid coordinate."));
          else
            m_ui->text->markValid(tokenCursor, tr("'b' lattice coordinate."));
          break;
        }

        case 'c': {
          // Validate real:
          bool isReal;
          atom.pos.z() = tokenCursor.selectedText().toDouble(&isReal);
          if (!isReal)
            m_ui->text->markInvalid(tokenCursor, tr("Invalid coordinate."));
          else
            m_ui->text->markValid(tokenCursor, tr("'c' lattice coordinate."));
          break;
        }

        default:
          qWarning() << "Unhandled character in detected spec: " << *iter;
          break;
      }
    }

    // Store this atom info if collecting.
    if (m_validate->collectAtoms) {
      if (m_validate->convertDistance && !m_validate->latticePositions)
        atom.pos *= m_validate->distanceConversion;
      m_validate->atoms << atom;
    }

    // Move down to the next line if we are not at the end. Moving to the
    // start of the line after moving down is necessary in case moving down
    // puts us at the end of the document.
    if (!lineCursor.atEnd()) {
      lineCursor.movePosition(QTextCursor::Down);
      lineCursor.movePosition(QTextCursor::StartOfLine);
    }
  }

  // Reenable validation.
  listenForTextEditChanges(true);

  // If we're not at the end, post this method back into the event loop.
  if (!lineCursor.atEnd()) {
    QTimer::singleShot(0, this, SLOT(validateInputWorker()));
  } else {
    // Otherwise emit the finished signal.
    emit validationFinished(!m_ui->text->hasInvalidMarks());
    m_validate->isValidating = false;

    // If a validation request came in while already validating, revalidate.
    if (m_validate->restartWhenFinished) {
      m_validate->restartWhenFinished = false;
      validateInput();
    }
  }
}

void CoordinateEditorDialog::applyClicked()
{
  if (!m_molecule)
    return;

  // If we're in the middle of a validation, abort it
  if (m_validate->isValidating) {
    m_validate->isValidating = false;
    qApp->processEvents();
  }

  m_validate->collectAtoms = true;
  m_validate->atoms.clear();

  switch (m_ui->distanceUnit->currentIndex()) {
    case Bohr:
      m_validate->convertDistance = true;
      m_validate->distanceConversion = BOHR_TO_ANGSTROM_F;
      break;
    default:
      m_validate->convertDistance = false;
      m_validate->distanceConversion = 1.f;
      break;
  }

  connect(this, SIGNAL(validationFinished(bool)), SLOT(applyFinish(bool)));
  validateInput();
}

void CoordinateEditorDialog::applyFinish(bool valid)
{
  // Clean up
  m_validate->collectAtoms = false;
  QVector<AtomStruct> atoms(m_validate->atoms);
  m_validate->atoms.clear();
  disconnect(this, SIGNAL(validationFinished(bool)), this,
             SLOT(applyFinish(bool)));

  if (!valid) {
    QMessageBox::critical(this, tr("Error applying geometry"),
                          tr("Could not parse geometry specification. Fix the "
                             "highlighted errors and try again.\n\n"
                             "(Hint: Hold the mouse over red text for a "
                             "description of the error.)"));
    return;
  }

  bool hadAtoms(m_molecule->atomCount() > 0);
  bool hadBonds(m_molecule->bondCount() > 0);

  // Create a new molecule so we can eventually store both in the undo command
  Molecule newMolecule = *m_molecule;
  newMolecule.clearAtoms();
  foreach (const AtomStruct& atom, atoms)
    newMolecule.addAtom(atom.atomicNumber).setPosition3d(atom.pos);
  if (m_validate->latticePositions) {
    Core::CrystalTools::setFractionalCoordinates(newMolecule,
                                                 newMolecule.atomPositions3d());
  } else {
    newMolecule.perceiveBondsSimple();
    newMolecule.perceiveBondOrders();
  }

  m_ui->text->document()->setModified(false);

  Molecule::MoleculeChanges change = Molecule::NoChange;
  if (hadAtoms)
    change |= Molecule::Atoms | Molecule::Removed;
  if (hadBonds)
    change |= Molecule::Bonds | Molecule::Removed;
  if (newMolecule.atomCount() > 0)
    change |= Molecule::Atoms | Molecule::Added;
  if (newMolecule.bondCount() > 0)
    change |= Molecule::Bonds | Molecule::Added;

  QString undoText = tr("Edit Atomic Coordinates");
  m_molecule->undoMolecule()->modifyMolecule(newMolecule, change, undoText);

  // We went from no atoms to something, don't edit by default
  // PR#394
  if (!hadAtoms)
    emit pastedMolecule();
}

void CoordinateEditorDialog::textModified(bool modified)
{
  m_ui->apply->setEnabled(modified);
  m_ui->revert->setEnabled(modified);
}

void CoordinateEditorDialog::buildPresets()
{
  // Custom must be first:
  m_ui->presets->addItem(tr("Custom"), QVariant());
  m_ui->presets->addItem(tr("XYZ format (symbols)"), QVariant("Sxyz"));
  m_ui->presets->addItem(tr("XYZ format (names)"), QVariant("Nxyz"));
  m_ui->presets->addItem(tr("XYZ format (atomic numbers)"), QVariant("Zxyz"));
  m_ui->presets->addItem(tr("Lattice coordinates (symbols)"), QVariant("Sabc"));
  m_ui->presets->addItem(tr("Lattice coordinates (names)"), QVariant("Nabc"));
  m_ui->presets->addItem(tr("Lattice coordinates (atomic numbers)"),
                         QVariant("Zabc"));
  m_ui->presets->addItem(tr("GAMESS format (symbols)"), QVariant("SGxyz"));
  m_ui->presets->addItem(tr("GAMESS format (names)"), QVariant("NGxyz"));
  m_ui->presets->addItem(tr("Turbomole format"), QVariant("xyzS"));

  // show Sxyz initially
  m_ui->presets->setCurrentIndex(1);
}

void CoordinateEditorDialog::listenForTextEditChanges(bool enable)
{
  if (enable)
    connect(m_ui->text, SIGNAL(textChanged()), this, SLOT(validateInput()));
  else
    disconnect(m_ui->text, SIGNAL(textChanged()), this, SLOT(validateInput()));
}

QString CoordinateEditorDialog::detectInputFormat() const
{
  if (m_ui->text->document()->isEmpty())
    return QString();

  if (!m_ui->spec->text().isEmpty())
    return m_ui->spec->text();

  // Extract the first non-empty line of text from the document.
  QTextCursor cur(m_ui->text->document());
  QString sample;
  while (sample.isEmpty()) {
    cur.movePosition(QTextCursor::StartOfLine, QTextCursor::MoveAnchor);
    cur.movePosition(QTextCursor::EndOfLine, QTextCursor::KeepAnchor);
    sample = cur.selectedText();
    cur.movePosition(QTextCursor::Down);
  }

  FORMAT_DEBUG(qDebug() << "\n\nExamining sample:" << sample;)

  // Split the string into tokens, and identify the type of each.
  QList<QString> tokens(sample.split(TOKEN_SEPARATOR, Qt::SkipEmptyParts));
  QList<TokenType> tokenTypes;
  tokenTypes.reserve(tokens.size());
  size_t tokenTypeCounts[3] = { 0, 0, 0 };

  foreach (const QString& token, tokens) {
    TokenType tokenType = String;
    if (INT_CHECKER.match(token).hasMatch())
      tokenType = Integer;
    else if (DOUBLE_CHECKER.match(token).hasMatch())
      tokenType = Double;
    ++tokenTypeCounts[tokenType];
    tokenTypes << tokenType;
  }

  FORMAT_DEBUG(qDebug() << "\nDetected types:"; qDebug() << tokens;
               qDebug() << tokenTypes;);

  // If less than three doubles are present, promote some integers to doubles.
  if (tokenTypeCounts[Double] < 3 &&
      tokenTypeCounts[Double] + tokenTypeCounts[Integer] >= 3) {

    // If numInts + numDoubles is greater than 3, leave the first integer as is,
    // we'll assume it's the atomic number.
    bool skipNextInt(tokenTypeCounts[Integer] + tokenTypeCounts[Double] > 3);

    size_t intsToPromote = 3 - tokenTypeCounts[Double];
    QMutableListIterator<TokenType> tokenTypeIter(tokenTypes);
    while (intsToPromote > 0 && tokenTypeIter.hasNext()) {
      if (tokenTypeIter.next() == Integer) {
        if (!skipNextInt) {
          tokenTypeIter.setValue(Double);
          --intsToPromote;
          --tokenTypeCounts[Integer];
          ++tokenTypeCounts[Double];
        } else {
          skipNextInt = false;
        }
      }
    }
  }

  FORMAT_DEBUG(qDebug() << "\nAfter promotion:"; qDebug() << tokens;
               qDebug() << tokenTypes;)

  // If there are no strings or integers, bail out -- we can't determine the
  // atom types. Likewise if there are less than 3 doubles, the coordinates
  // are incomplete.
  if ((tokenTypeCounts[Integer] == 0 && tokenTypeCounts[String] == 0) ||
      tokenTypeCounts[Double] < 3) {
    return "";
  }

  // Start assigning meaning to the values:
  QString resultSpec;
  bool atomTypeSet(false);
  int numCoordsSet(0);
  const int numberOfElements(static_cast<int>(Core::Elements::elementCount()));

  for (int i = 0; i < tokens.size() && (!atomTypeSet || numCoordsSet < 3);
       ++i) {
    QChar current = '?';

    switch (tokenTypes[i]) {
      case Integer:
        if (!atomTypeSet) {
          int tokenAsInt = tokens[i].toInt();
          if (tokenAsInt >= 0 && tokenAsInt <= numberOfElements) {
            current = 'Z';
            atomTypeSet = true;
          }
        }
        break;

      case Double:
        switch (numCoordsSet) {
          case 0:
            current = 'x';
            ++numCoordsSet;
            break;
          case 1:
            current = 'y';
            ++numCoordsSet;
            break;
          case 2:
            current = 'z';
            ++numCoordsSet;
            break;
          default:
            break;
        }
        break;

      case String:
        if (!atomTypeSet) {
          QString cleanToken(tokens[i].toLower());
          if (!cleanToken.isEmpty())
            cleanToken.replace(0, 1, cleanToken[0].toUpper());

          if (cleanToken.size() <= 3) {
            if (Elements::atomicNumberFromSymbol(cleanToken.toStdString()) !=
                Avogadro::InvalidElement) {
              current = 'S';
              atomTypeSet = true;
            }
          } else {
            if (Elements::atomicNumberFromName(cleanToken.toStdString()) !=
                Avogadro::InvalidElement) {
              current = 'N';
              atomTypeSet = true;
            }
          }
        }
        break;
    }

    FORMAT_DEBUG(qDebug() << current << tokens[i];)
    resultSpec += current;
  }

  // Check the current specification -- if a|b|c appears before x|y|z, assume
  // that the specified coordinates are lattice coords
  static QRegularExpression cartesianSniffer("x|y|z");
  static QRegularExpression fractionalSniffer("a|b|c");
  const QString currentSpec(m_ui->spec->text());
  int cartesianIndex = currentSpec.indexOf(cartesianSniffer);
  int fractionalIndex = currentSpec.indexOf(fractionalSniffer);
  if (fractionalIndex != -1 &&
      (cartesianIndex == -1 || fractionalIndex < cartesianIndex)) {
    resultSpec.replace('x', 'a');
    resultSpec.replace('y', 'b');
    resultSpec.replace('z', 'c');
    FORMAT_DEBUG(qDebug() << "Interpreting positions as lattice coordinates.";)
  }

  FORMAT_DEBUG(qDebug() << "Detected format:" << resultSpec);

  return (!atomTypeSet || numCoordsSet < 3) ? QString() : resultSpec;
}

void CoordinateEditorDialog::cutClicked()
{
  copyClicked();
  clearClicked();
}

void CoordinateEditorDialog::copyClicked()
{
  qApp->clipboard()->setText(m_ui->text->document()->toPlainText());
}

void CoordinateEditorDialog::pasteClicked()
{
  const QMimeData* mimeData = qApp->clipboard()->mimeData();
  m_ui->text->document()->setPlainText(
    (mimeData && mimeData->hasText()) ? mimeData->text() : "");
}

void CoordinateEditorDialog::revertClicked()
{
  updateText();
}

void CoordinateEditorDialog::clearClicked()
{
  m_ui->text->document()->clear();
}

} // namespace Avogadro::QtPlugins
