/** Copyright (c) 2020-2022 The Creators of Simphone

    class ContactsModel (QAbstractTableModel): provide data for ContactsTableView
    class LogsModel (BaseModel): provide data for ChatTableView (console log messages)
    class MessagesModel (BaseModel): provide data for ChatTableView (chat messages)
    class BaseModel (QAbstractTableModel): parent of MessagesModel and LogsModel

    See the file COPYING.LESSER.txt for copying permission.
**/

#define _CRT_SECURE_NO_WARNINGS //#ifdef _WIN32

#include "qtfix.h"
#include "sounds.h"
#include "contacts.h"

#include <QApplication>
#include <QFontDatabase>

#include <time.h>

void BaseModel::setFoundIndex(int ndx)
{
  int old = m_foundIndex;
  m_foundIndex = ndx;

  if (old >= 0) {
    emit dataChanged(index(old, 0), index(old, msgcol_nCols));
  }
  if (ndx >= 0) {
    emit dataChanged(index(ndx, 0), index(ndx, msgcol_nCols));
  }
}

void BaseModel::notifyRowsInserted(int start, int end)
{
  beginInsertRows(QModelIndex(), start, end);
  endInsertRows();
}

void BaseModel::notifyRowsDeleted(int start, int end)
{
  beginRemoveRows(QModelIndex(), start, end);
  endRemoveRows();
}

MessagesModel::MessagesModel(int contactId)
  : BaseModel(msgcol_msg, msgcol_timestamp)
  , m_contactId(contactId)
  , m_editIndex(-1)
  , m_editCount(0)
  , m_size(0)
  , m_oldSize(0)
  , m_lastIndex(-1)
{
  connect(SimCore::get(), SIGNAL(signalMessageReceived(unsigned, int, bool)),
          this, SLOT(onSignalMessageReceived(unsigned, int, bool)));
  connect(SimCore::get(), SIGNAL(signalMessageSent(unsigned, int, int)),
          this, SLOT(onSignalMessageSent(unsigned, int, int)));
  connect(SimCore::get(), SIGNAL(signalMessageEdited(unsigned, int)), this, SLOT(onSignalMessageEdited(unsigned, int)));

  m_lineBold = SimParam::get("ui.chat.linebold") != 0;
}

MessagesModel::~MessagesModel()
{
}

void MessagesModel::readSettings()
{
  m_editReceivedColor = SimParam::getColor("ui.chat.editreceived");
  m_editSentColor = SimParam::getColor("ui.chat.editsent");
  m_systemColor = SimParam::getColor("ui.chat.system");
  m_missedColor = SimParam::getColor("ui.chat.missed");
  m_receivedColor = SimParam::getColor("ui.chat.received");
  m_sentColor = SimParam::getColor("ui.chat.sent");
  m_notsentColor = SimParam::getColor("ui.chat.notsent");
  m_showTime = SimParam::get("ui.chat.showtime");
  m_showStatus = SimParam::get("ui.chat.showstatus") != 0;
  m_showDuration = SimParam::get("ui.chat.duration");
  m_xferMaxSize = SimParam::get("ui.chat.xfersize");
  m_oldTime = SimParam::get("ui.chat.oldtime");
  m_oldDate = SimParam::get("ui.chat.olddate");
  m_lineColor = SimParam::getColor("ui.chat.line");
  m_dateColor = SimParam::getColor("ui.chat.date");
  m_backtimeColor = SimParam::getColor("ui.chat.backtime");
  m_backdateColor = SimParam::getColor("ui.chat.backdate");
}

QVariant MessagesModel::data(const QModelIndex & index, int role) const
{
  switch (role) {
    case Qt::DisplayRole:
      switch (index.column()) {
        case msgcol_msg:
          return getMessageText(index.row());

          //case msgcol_timestamp: return getMessageTime(index.row());

        case msgcol_nick:
          int status = getMessageStatus(index.row());
          if (status < 0) break;

          if (!m_lastNick.isEmpty() && !m_usedNicks.contains(m_lastNick)) {
            m_usedNicks.insert(m_lastNick);
            emit signalFoundUnseenNick(m_lastNick);
          }

          if (!m_showStatus) {
            if (isSameNick(index.row()) && !m_lastDateDiff) {
              if ((status == SIM_MSG_INCOMING) == (m_prevStatus == SIM_MSG_INCOMING)) return "";
            }
            if (!m_lastDateDiff) return getMessageNick(index.row());
            return "\n" + getMessageNick(index.row());
          }

          switch (status) {
            case SIM_MSG_INCOMING: // message was received from contact
              if (isSameNick(index.row()) && !m_lastDateDiff && status == m_prevStatus) return "";
              if (!m_lastDateDiff) return getMessageNick(index.row());
              return "\n" + getMessageNick(index.row());

            case SIM_MSG_PENDING:
            case SIM_MSG_SENT: // message was sent to contact but is not yet acknowledged
              return m_lastDateDiff ? "\n?" : "?";

            case SIM_MSG_ACKNOWLEDGED:
            case SIM_MSG_DELIVERED: // message was sent to contact and contact has received it
              if (isSameNick(index.row()) && !m_lastDateDiff && status == m_prevStatus) return "";
              if (!m_lastDateDiff) return getMessageNick(index.row());
              return "\n" + getMessageNick(index.row());

            case SIM_MSG_NOTSENT: // message cannot yet be sent to contact
              return m_lastDateDiff ? "\n!" : "!";

            case SIM_MSG_UNSENT:
            case SIM_MSG_UNDELIVERED:
            case SIM_MSG_NOTDELIVERED: // message was reported in SIM_EVENT_NOTSENT
              return m_lastDateDiff ? "\n*" : "*";
          }
          break;
      }
      break;

    case Qt::BackgroundColorRole:
      if (getMessageStatus(index.row()) < 0) break;
      if ((m_lastIndex == m_editIndex || m_lastIndex == m_foundIndex) && index.column() == msgcol_msg) {
        return QApplication::palette((QWidget *)0).highlight();
      }

      switch (m_lastStatus) {
        case SIM_MSG_INCOMING:
          return m_receivedColor.isValid() ? m_receivedColor : QApplication::palette((QWidget *)0).base();

        case SIM_MSG_UNSENT:
        case SIM_MSG_UNDELIVERED:
        case SIM_MSG_NOTDELIVERED:
          return m_notsentColor.isValid() ? m_notsentColor : QApplication::palette((QWidget *)0).toolTipBase();
      }
      return m_sentColor.isValid() ? m_sentColor : QApplication::palette((QWidget *)0).alternateBase();

    case Qt::TextColorRole:
      if (getMessageStatus(index.row()) < 0) break;
      if ((m_lastIndex == m_editIndex || m_lastIndex == m_foundIndex) && index.column() == msgcol_msg) {
        return QApplication::palette((QWidget *)0).highlightedText();
      }
      if (m_lastType == 'S' && m_missedColor.isValid()) return m_missedColor;
      break;

    case Qt::FontRole:
      if (index.column() == msgcol_msg && getMessageStatus(index.row()) >= 0) {
        if (m_lastType == 's' || m_lastType == 'S') {
          QFont boldFont;
          boldFont.setBold(true);
          return boldFont;
        }
      }
      break;

    case Qt::TextAlignmentRole:
      switch (index.column()) {
        case msgcol_nick: //return Qt::AlignLeft + Qt::AlignTop;
        case msgcol_timestamp: return Qt::AlignLeft + Qt::AlignTop;
        default: return Qt::AlignTop;
      }
      break;

    case Qt::ToolTipRole:
      if (index.column() == msgcol_timestamp) {
        QString time = getMessageTimes(index.row());
        if (!time.isNull()) return time;
      } else if (index.column() == msgcol_msg && getMessageStatus(index.row()) >= 0) {
        if (m_lastType == 's' || m_lastType == 'S') {
          Contact * contact = SimCore::getContact(m_contactId);
          if (contact) {
            simtype msg = sim_msg_get_(contact->m_simId, index.row() + 1);
            if (sim_get_pointer(msg)) {
              QString qs = sim_table_get_pointer(msg, SIM_CMD_MSG_TEXT);
              QStringList list = qs.split(' ');
              bool ok = false;

              sim_msg_free_(msg);
              if (list.size() >= 3) qs = SimCore::getErrorBuffer(list[2].toInt(&ok));
              if (ok) {
                if (list[1] == "SEND") return tr("File size = %n byte(s)", 0, int(list[2].toLongLong()));
                if (list[2] != "0") return qs.append(tr(" (error %1)").arg(list[2]));
              }
            }
          }
        }
      }
      return "";
  }
  return QVariant();
}

QVariant MessagesModel::headerData(int section, Qt::Orientation orientation, int role) const
{
  if (role == Qt::DisplayRole) {
    if (orientation == Qt::Horizontal) {
      switch (section) {
        case msgcol_nick: return "who";
        case msgcol_msg: return "message";
      }
    }
  }
  return QVariant();
}

char MessagesModel::getMessage(int ndx) const
{
  if (ndx < 0) return 0;

  Contact * contact = SimCore::getContact(m_contactId);
  if (!contact) return 0;
  simtype msg = sim_msg_get_(contact->m_simId, ndx + 1);
  if (!sim_get_pointer(msg)) return 0;

  if (!ndx) {
    m_lastRecvtime = m_lastTimestamp = 0;
    m_lastStatus = SIM_MSG_INCOMING;
    m_lastType = 0;
  } else if (m_lastIndex != ndx - 1) {
    m_lastIndex = ndx - 2; // stop recursion
    getMessage(ndx - 1);
  }

  m_prevTimestamp = m_lastTimestamp;
  m_prevRecvtime = m_lastRecvtime;
  m_prevStatus = m_lastStatus;
  m_prevType = m_lastType;
  m_lastIndex = ndx;
  m_lastTimestamp = sim_table_get_number(msg, SIM_CMD_MSG_TIME);
  m_lastRecvtime = sim_table_get_number(msg, SIM_CMD_MSG_RECEIVED);
  m_lastStatus = int(sim_table_get_number(msg, SIM_CMD_MSG_STATUS));
  simtype str = sim_table_get_string(msg, SIM_CMD_MSG_NICK);
  QString nick = qtfix::getString(sim_get_pointer(str) ? sim_get_pointer(str) : "");
  str = sim_table_get_string(msg, SIM_CMD_MSG_TEXT);
  if (sim_table_get_number(msg, SIM_CMD_MSG_TYPE) == SIM_MSG_TYPE_SYSTEM) {
    m_editedTstamp = sim_table_get_number(msg, SIM_CMD_MSG_HANDLE);
    m_lastType = 's';
  } else {
    m_editedTstamp = sim_table_get_number(msg, SIM_CMD_MSG_EDIT);
    m_lastType = m_lastStatus == SIM_MSG_INCOMING ? 'i' : 'o';
  }
  if (!sim_get_pointer(str)) {
    m_lastMessage.erase();
    m_lastMessageRemoved = true;
  } else {
    const char * text = sim_get_pointer(str);
    m_lastMessage.assign(text);
    m_lastMessageRemoved = false;
    if (m_lastType == 's') {
      bool ok = true;
      if (text[0] == '\n') text++;

      if (!strcmp(text, "STATUS ON")) {
        m_lastMessage.assign(tr("%1 has NOT logged you off").arg(nick).toStdString());
      } else if (!strcmp(text, "STATUS OFF")) {
        m_lastMessage.assign(tr("%1 has logged you off").arg(nick).toStdString());
      } else if (!SIM_STRING_CHECK_DIFF_CONST(text, "FILE ")) {
        const char * restText = text + SIM_STRING_GET_LENGTH_CONST("FILE ");
        const char * s = 0;
        QString qs;
        ok = false;
        if (!SIM_STRING_CHECK_DIFF_CONST(restText, "SEND ")) {
          s = strchr(restText += SIM_STRING_GET_LENGTH_CONST("SEND "), ' ');
          if (s) {
            static const char * sizes[] = {
              QT_TR_NOOP("byte"), QT_TR_NOOP("kilobyte"), QT_TR_NOOP("megabyte"), QT_TR_NOOP("gigabyte"),
              QT_TR_NOOP("terabyte"), QT_TR_NOOP("petabyte"), QT_TR_NOOP("exabyte")
            };
            qulonglong n = QString::fromUtf8(restText, s - restText).toULongLong(&ok);
            int i;
            int k = 0;
            for (i = 0; n >= m_xferMaxSize; i++) {
              k = int((n >> 9) & 1);
              n >>= 10;
            }
            if (m_lastStatus == SIM_MSG_INCOMING) {
              qs = tr("File transfer from %1 started, %2-%3 file %4");
            } else {
              qs = tr("File transfer to %1 started, %2-%3 file %4");
            }
            qs = qs.arg(nick).arg(n + k).arg(tr(sizes[i], 0, int(n) + k));
          }
        } else if (!SIM_STRING_CHECK_DIFF_CONST(restText, "RECV ")) {
          s = strchr(restText += SIM_STRING_GET_LENGTH_CONST("RECV "), ' ');
          if (s) {
            int n = QString::fromUtf8(restText, s - restText).toInt(&ok);
            if (n) {
              if (m_lastStatus == SIM_MSG_INCOMING) {
                qs = tr("File transfer to %1 finished, error %2, file %3");
              } else {
                qs = tr("File transfer from %1 finished, error %2, file %3");
              }
              qs = qs.arg(nick).arg(n);
            } else {
              if (m_lastStatus == SIM_MSG_INCOMING) {
                qs = tr("File transfer to %1 finished, file %2");
              } else {
                qs = tr("File transfer from %1 finished, file %2");
              }
              qs = qs.arg(nick);
            }
          }
        } else if (!SIM_STRING_CHECK_DIFF_CONST(restText, "CANCEL ") ||
                   !SIM_STRING_CHECK_DIFF_CONST(restText, "REJECT ")) {
          s = strchr(restText += SIM_STRING_GET_LENGTH_CONST("CANCEL "), ' ');
          if (s) {
            int n = QString::fromUtf8(restText, s - restText).toInt(&ok);
            if ((m_lastStatus == SIM_MSG_INCOMING) ^ !SIM_STRING_CHECK_DIFF_CONST(text, "FILE CANCEL ")) {
              qs = n ? tr("File transfer to %1 failed, file %2") : tr("File transfer to %1 cancelled, file %2");
            } else {
              qs = n ? tr("File transfer from %1 failed, file %2") : tr("File transfer from %1 cancelled, file %2");
            }
            qs = qs.arg(nick);
          }
        }
        if (ok) {
          const char * p = strrchr(s, '/');
          m_lastMessage.assign(qs.arg(qtfix::getString(p ? p + 1 : s + 1)).toStdString());
        }
      } else if (!SIM_STRING_CHECK_DIFF_CONST(text, "CALL ")) {
        const char * restText = text + SIM_STRING_GET_LENGTH_CONST("CALL ");
        if (!strcmp(restText, "START")) {
          if (m_lastStatus == SIM_MSG_INCOMING) {
            m_lastMessage.assign(tr("Call from %1 started").arg(nick).toStdString());
          } else {
            m_lastMessage.assign(tr("Call to %1 started").arg(nick).toStdString());
          }
        } else if (!strcmp(restText, "HANGUP") || !SIM_STRING_CHECK_DIFF_CONST(restText, "HANGUP ")) {
          m_lastMessage.assign(tr("Call to %1 finished").arg(nick).toStdString());
        } else if (!strcmp(restText, "HUNGUP") || !SIM_STRING_CHECK_DIFF_CONST(restText, "HUNGUP ")) {
          m_lastMessage.assign(tr("Call from %1 finished").arg(nick).toStdString());
        } else if (!strcmp(restText, "FAILED") || !SIM_STRING_CHECK_DIFF_CONST(restText, "FAILED ") ||
                   !strcmp(restText, "ABORT") || !SIM_STRING_CHECK_DIFF_CONST(restText, "ABORT ")) {
          if (m_lastStatus == SIM_MSG_INCOMING) {
            m_lastMessage.assign(tr("Missed call from %1").arg(nick).toStdString());
            if (unsigned(ndx) >= m_oldSize) m_lastType = 'S'; // show this message in red
          } else {
            m_lastMessage.assign(tr("Call to %1, no answer").arg(nick).toStdString());
          }
        } else if (!strcmp(restText, "BUSY")) {
          if (m_lastStatus == SIM_MSG_INCOMING) {
            m_lastMessage.assign(tr("Call to %1, busy").arg(nick).toStdString());
          } else {
            m_lastMessage.assign(tr("Call from %1").arg(nick).toStdString());
          }
        } else if (!SIM_STRING_CHECK_DIFF_CONST(restText, "BUSY ")) {
          if (m_lastStatus == SIM_MSG_INCOMING) {
            m_lastMessage.assign(tr("Call to %1").arg(nick).toStdString());
          } else {
            m_lastMessage.assign(tr("Call from %1").arg(nick).toStdString());
          }
          m_lastMessage.append(tr(", error %1").arg(restText + SIM_STRING_GET_LENGTH_CONST("BUSY ")).toStdString());
        } else if (!SIM_STRING_CHECK_DIFF_CONST(restText, "ERROR ")) {
          const char * s = restText + SIM_STRING_GET_LENGTH_CONST("ERROR ");
          m_lastMessage.assign(tr("Call to %1, ERROR %2").arg(nick, s).toStdString());
        } else {
          ok = false;
        }
        if (!SIM_STRING_CHECK_DIFF_CONST(restText, "HANGUP ") || !SIM_STRING_CHECK_DIFF_CONST(restText, "HUNGUP ")) {
          if (m_lastStatus == SIM_MSG_INCOMING) {
            restText += SIM_STRING_GET_LENGTH_CONST("HANGUP ");
            m_lastMessage.append(tr(", error %1").arg(restText).toStdString());
          } else {
            m_lastMessage.append(tr(", disconnected").toStdString());
          }
        }
      } else {
        ok = false;
      }

      simnumber seconds = m_lastRecvtime;
      if (seconds) seconds = m_lastTimestamp - seconds;
      if (m_showDuration && seconds > 0) {
        if (m_showDuration > 1 || !SIM_STRING_CHECK_DIFF_CONST(text, "STATUS ") ||
            !SIM_STRING_CHECK_DIFF_CONST(text, "CALL HANGUP ") || !strcmp(text, "CALL HANGUP") ||
            !SIM_STRING_CHECK_DIFF_CONST(text, "CALL HUNGUP ") || !strcmp(text, "CALL HUNGUP")) {
          m_lastMessage.append(tr(" (duration %1)").arg(Contact::convertTimeToString(seconds)).toStdString());
        }
      }
      if (ok) m_lastMessage.append(".");
    }
  }

  str = sim_table_get_string(msg, SIM_CMD_MSG_SENDER);
  m_lastNickSame = false;
  if (sim_get_pointer(str)) {
    nick = qtfix::getString(sim_get_pointer(str));
    if (m_lastNick == nick) {
      m_lastNickSame = true;
    } else {
      m_lastNick = nick;
    }
  } else {
    m_lastNick.clear();
  }

  sim_msg_free_(msg);

  char buff1[64];
  char buff2[64];
  sim_convert_time_to_string(m_lastTimestamp, buff1);
  buff1[SIM_SIZE_TIME - 10] = 0;
  sim_convert_time_to_string(m_prevTimestamp, buff2);
  buff2[SIM_SIZE_TIME - 10] = 0;

  simnumber delta = m_prevTimestamp - m_lastTimestamp;
  if (m_prevType == 's' || m_prevType == 'S') {
    if (m_prevStatus != SIM_MSG_INCOMING || m_prevRecvtime <= m_prevTimestamp) delta = 0;
  }

  if (m_oldDate && delta >= m_oldDate) {
    m_lastDateDiff = 'd';
    m_lastTimeDiff = true;
  } else if (m_oldTime && delta >= m_oldTime) {
    m_lastDateDiff = 't';
    m_lastTimeDiff = true;
  } else if (strcmp(buff1, buff2)) {
    m_lastDateDiff = 'n';
    m_lastTimeDiff = true;
  } else {
    m_lastDateDiff = 0;
    buff1[16] = buff2[16] = 0;
    m_lastTimeDiff = strcmp(buff1 + 11, buff2 + 11) != 0;
  }

  if (m_lastDateDiff) m_lastMessage.insert(0, 1, '\n');

  return m_lastType;
}

const char * MessagesModel::getSearchText(int ndx) const
{
  const char * result = getMessageText(ndx);

  static std::string qs;
  if (m_lastDateDiff && result && result[0] == '\n') { // insert date
    qs = result;
    result = getMsgDate(ndx);
    if (result) qs.insert(0, result);
    result = qs.c_str();
  }

  return result;
}

QString MessagesModel::getSelectedText(int ndx, bool textOnly) const
{
  char buffer[64];
  QString qs;

  if (!textOnly) {
    qs = getMessageNick(ndx);
    *buffer = 0;
    if (qs.isNull() || !qs.isEmpty()) sim_convert_time_to_string(m_lastTimestamp, buffer);
    qs.insert(0, " ");
    qs.insert(0, buffer);
    qs.append(": ");
  }

  const char * result = getMessageText(ndx);
  if (result) qs.append(m_lastDateDiff && result[0] == '\n' ? result + 1 : result);

  return qs;
}

bool MessagesModel::isEditAllowed(int ndx) const
{
  int status = getMessageStatus(ndx);
  if (status < 0 || m_lastType != 'o' || m_lastMessageRemoved) return false;
  if (status == SIM_MSG_NOTSENT || status == SIM_MSG_NOTDELIVERED) return true;

  Contact * contact = SimCore::getContact(m_contactId);
  for (int i = contact ? contact->m_editMax : 0; i--;) {
    status = getMessageStatus(++ndx);
    if (status < 0 || status == SIM_MSG_NOTSENT || status == SIM_MSG_NOTDELIVERED) return true;
  }
  return false;
}

const char * MessagesModel::getEditText(unsigned * rowNdxBack, int delta)
{
  unsigned ndx = *rowNdxBack;
  Contact * contact = SimCore::getContact(m_contactId);
  if (!contact) return 0;

  if (delta <= 0) {
    const char * result = getMessageText(count() - ndx);
    if (!delta || !result) {
      if (m_lastDateDiff && result && result[0] == '\n') ++result;
      return m_lastMessageRemoved ? 0 : result;
    }
  }

  while (ndx || delta >= 0) {
    ndx += delta;
    if (!ndx || count() < ndx) break;
    const char * result = getMessageText(count() - ndx);
    if (!result) break;

    if (m_lastStatus != SIM_MSG_NOTSENT && m_lastStatus != SIM_MSG_NOTDELIVERED) {
      if (delta > 0 && m_editCount >= contact->m_editMax) break;
      m_editCount += delta;
      if (m_editCount < 0) m_editCount = 0;
    }

    if (m_lastType == 'o' && !m_lastMessageRemoved) {
      *rowNdxBack = ndx;
      if (m_lastDateDiff && result[0] == '\n') ++result;
      return result;
    }
  }
  return 0;
}

void MessagesModel::setEditIndex(int ndx)
{
  int old = m_editIndex;
  m_editIndex = ndx;

  if (old >= 0) {
    emit dataChanged(index(old, 0), index(old, msgcol_nCols));
  }
  if (ndx >= 0) {
    emit dataChanged(index(ndx, 0), index(ndx, msgcol_nCols));
  } else {
    m_editCount = 0;
  }
}

const char * MessagesModel::getMessageText(int ndx) const
{
  if (ndx < 0 || unsigned(ndx) >= count()) return 0;
  if (ndx != m_lastIndex && !getMessage(ndx)) return 0;

  return m_lastMessage.c_str();
}

int MessagesModel::getMessageStatus(int ndx) const
{
  if (ndx < 0 || unsigned(ndx) >= count()) return -1;
  if (ndx != m_lastIndex && !getMessage(ndx)) return -1;

  return m_lastStatus;
}

const char * MessagesModel::getMessageTime(int ndx) const
{
  if (ndx < 0 || unsigned(ndx) >= count()) return "";
  if (ndx != m_lastIndex && !getMessage(ndx)) return "";

  static char buffer[64];

  if (ndx) { // ndx == 0 if we are at the very first line in chat
    switch (m_showTime) {
      case 0:
        if (!m_lastTimeDiff) return "";
        break;

      case 1:
        if (!m_lastTimeDiff && (m_lastStatus == SIM_MSG_INCOMING) == (m_prevStatus == SIM_MSG_INCOMING)) return "";
        break;

      case 2:
        if (!m_lastTimeDiff && m_lastStatus == m_prevStatus) return "";
        break;

        // case 3: is default:
    }
  }

  sim_convert_time_to_string(m_lastTimestamp, buffer);
  buffer[SIM_SIZE_TIME - 4] = 0;
  buffer[SIM_SIZE_TIME - 10] = ' ';
  if (!m_lastDateDiff) return buffer + SIM_SIZE_TIME - 10;

  buffer[9] = '\n';
  return buffer + SIM_SIZE_TIME - 11;
}

QString MessagesModel::getMessageTimes(int ndx) const
{
  if (ndx < 0 || unsigned(ndx) >= count()) return "";
  if (ndx != m_lastIndex && !getMessage(ndx)) return "";

  char buffer[64];
  QString result;
  bool system = m_lastType == 's' || m_lastType == 'S';

  result = "<table>";
  simnumber ts = !system ? m_editedTstamp : 0;
  if (m_lastMessageRemoved) {
    result.append("<tr><td align=\"right\">***&nbsp;</td><td>").append(tr("DELETED")).append("&nbsp;***</td></tr>");
  }
  if (ts) {
    result.append("<tr><td align=\"right\">").append(tr("edited"));
    sim_convert_time_to_string(ts, buffer);
    result.append(":&nbsp;</td><td>").append(QString(buffer).replace(" ", "&nbsp;")).append("</td></tr>");
  }
  ts = m_lastRecvtime;
  if (ts) {
    result.append("<tr><td align=\"right\">");
    result.append(system && ts <= m_lastTimestamp ? tr("begun") : tr("received"));
    sim_convert_time_to_string(ts, buffer);
    result.append(":&nbsp;</td><td>").append(QString(buffer).replace(" ", "&nbsp;")).append("</td></tr>");
  }
  ts = m_lastTimestamp;
  if (ts) {
    result.append("<tr><td align=\"right\">");
    result.append(system && ts >= m_lastRecvtime && m_lastRecvtime ? tr("ended") : tr("created"));
    sim_convert_time_to_string(ts, buffer);
    result.append(":&nbsp;</td><td>").append(QString(buffer).replace(" ", "&nbsp;")).append("</td></tr>");
  }

  return result == "<table>" ? QString() : result.append("</table>");
}

char MessagesModel::isNewDate(QColor * background, QColor * foreground, bool * bold) const
{
  switch (m_lastDateDiff) {
    case 'd': *background = m_backdateColor; break;
    case 't': *background = m_backtimeColor; break;
    case 0: *background = QColor(); break;
    default: *background = m_dateColor;
  }
  *foreground = m_lineColor;
  *bold = m_lineBold;
  return m_lastDateDiff;
}

char MessagesModel::isEdited(QColor * color) const
{
  switch (m_lastType) {
    case 'S':
    case 's': *color = m_systemColor; return '.';
    case 'o': *color = m_editSentColor; break;
    default: *color = m_editReceivedColor;
  }
  return m_editedTstamp ? '*' : 0;
}

bool MessagesModel::isRemoved(int ndx) const
{
  const char * result = getMessageText(ndx);

  return result && m_lastMessageRemoved;
}

char MessagesModel::isReceived(int ndx) const
{
  switch (getMessageStatus(ndx)) {
    case SIM_MSG_DELIVERED:
    case SIM_MSG_ACKNOWLEDGED:
      return 'y';

    case -1:
    case SIM_MSG_INCOMING:
    case SIM_MSG_UNSENT:
    case SIM_MSG_NOTSENT:
    case SIM_MSG_NOTDELIVERED:
      return 'n';
  }

  return 0;
}

char MessagesModel::getMsgType(int ndx) const
{
  if (ndx < 0 || unsigned(ndx) >= count()) return 0;
  if (ndx != m_lastIndex && !getMessage(ndx)) return 0;

  return m_lastType == 's' && m_editedTstamp > 1 ? 'x' : m_lastType;
}

const char * MessagesModel::getMsgDate(int ndx) const
{
  if (ndx < 0 || unsigned(ndx) >= count()) return "";
  if (ndx != m_lastIndex && !getMessage(ndx)) return "";

  static char buffer[64];

  sim_convert_time_to_string(m_lastTimestamp, buffer);
  buffer[SIM_SIZE_TIME - 10] = 0;
  return buffer;
}

QString MessagesModel::getMessageNick(int ndx) const
{
  if (ndx < 0 || unsigned(ndx) >= count()) return "";
  if (ndx != m_lastIndex && !getMessage(ndx)) return "";

  return m_lastNick;
}

bool MessagesModel::isSameNick(int ndx) const
{
  if (ndx < 0 || unsigned(ndx) >= count()) return false;
  if (ndx != m_lastIndex && !getMessage(ndx)) return false;

  return m_lastNickSame;
}

void MessagesModel::notifyRowsChanged()
{
  unsigned old = m_size;
  Contact * contact = SimCore::getContact(m_contactId);
  if (!contact) return;

  m_size = unsigned(sim_msg_count_(contact->m_simId));
  if (m_size > old) {
    notifyRowsInserted(old, m_size - 1);
  } else if (m_size < old) {
    notifyRowsDeleted(m_size, old - 1);
  }
}

void MessagesModel::onSignalMessageReceived(unsigned id, int, bool)
{
  if (m_contactId == int(id)) notifyRowsChanged(); // if it is for us, then perhaps number of rows has changed
}

void MessagesModel::onSignalMessageSent(unsigned id, int msgNdx, int)
{
  if (m_contactId == int(id)) {
    --msgNdx;
    m_lastIndex = -1;
    emit dataChanged(index(msgNdx, msgcol_nick), index(msgNdx, msgcol_nick));
    emit dataChanged(index(msgNdx, msgcol_msg), index(msgNdx, msgcol_msg));
    emit dataChanged(index(msgNdx, msgcol_timestamp), index(msgNdx, msgcol_timestamp));
  }
}

void MessagesModel::onSignalMessageEdited(unsigned id, int msgNdx)
{
  if (m_contactId == int(id)) {
    --msgNdx;
    m_lastIndex = -1;
    emit dataChanged(index(msgNdx, msgcol_msg), index(msgNdx, msgcol_msg));
    emit dataChanged(index(msgNdx, msgcol_timestamp), index(msgNdx, msgcol_timestamp));
  }
}

LogsModel::LogsModel()
  : BaseModel(logscol_log, logscol_nCols)
{
  Logs::getLogs()->attachModel(this);

  m_lineBold = SimParam::get("ui.console.linebold") != 0;
}

LogsModel::~LogsModel()
{
  Logs::getLogs()->detachModel(this);
}

void LogsModel::readSettings()
{
  m_lineColor = SimParam::getColor("ui.console.line");
  m_dateColor = SimParam::getColor("ui.console.date");
  m_timeColor = SimParam::getColor("ui.console.time");
}

int LogsModel::rowCount(const QModelIndex & /*parent*/) const
{
  return Logs::getLogs()->count();
}

QVariant LogsModel::data(const QModelIndex & index, int role) const
{
  switch (role) {
    case Qt::DisplayRole:
      switch (index.column()) {
        case logscol_level: return Logs::getLogs()->getLogLevel(index.row());
        case logscol_log: return Logs::getLogs()->getLogLine(index.row());
        case logscol_timestamp: return Logs::getLogs()->getLogTime(index.row());
      }
      break;

    case Qt::FontRole: {
      QString level = Logs::getLogs()->getLogLevel(index.row());
      if ((level.isEmpty() || level == "\n") && index.column() == logscol_log) {
        QFont fixedFont = QFontDatabase::systemFont(QFontDatabase::FixedFont);
        fixedFont.setBold(true);
        return qtfix::fixFontSize(fixedFont, "QAbstractItemView", true);
      }

      if (!m_usedLevels.contains(level)) {
        m_usedLevels.insert(level);
        emit signalFoundUnseenNick(level);
      }
    } break;

    case Qt::TextAlignmentRole:
      switch (index.column()) {
        case logscol_level: return Qt::AlignRight + Qt::AlignTop;
        case logscol_log: return Qt::AlignLeft + Qt::AlignTop;
        default: return Qt::AlignTop;
      }
      break;

    case Qt::TextColorRole:
      if ((index.row() == m_foundIndex) && index.column() == logscol_log) {
        return QApplication::palette((QWidget *)0).highlightedText();
      }
      break;

    case Qt::BackgroundColorRole:
      if ((index.row() == m_foundIndex) && index.column() == logscol_log) {
        return QApplication::palette((QWidget *)0).highlight();
      }
      return QApplication::palette((QWidget *)0).base();
  }
  return QVariant();
}

QVariant LogsModel::headerData(int section, Qt::Orientation orientation, int role) const
{
  if (role == Qt::DisplayRole) {
    if (orientation == Qt::Horizontal) {
      switch (section) {
        case logscol_level: return "lvl";
        case logscol_log: return "log";
      }
    }
  }
  return QVariant();
}

const char * LogsModel::getSearchText(int ndx) const
{
  QString result = Logs::getLogs()->getLogLine(ndx);
  static QByteArray bytes;

  if (!result.isEmpty() && result[0] == '\n') {
    result[0] = ' ';
  } else {
    result.insert(0, " ");
  }
  result.insert(0, Logs::getLogs()->getLogLevel(ndx)); // insert level
  if (Logs::getLogs()->isNewDate() && !result.isEmpty() && result[0] == '\n') {
    result.insert(0, getMsgDate(ndx)); // insert date
  }

  bytes = result.toUtf8();
  return bytes.data();
}

QString LogsModel::getSelectedText(int ndx, bool textOnly) const
{
  return Logs::getLogText(ndx, textOnly);
}

char LogsModel::isNewDate(QColor * background, QColor * foreground, bool * bold) const
{
  char diffDate = Logs::getLogs()->isNewDate();
  switch (diffDate) {
    case 't': *background = m_timeColor; break;
    case 'd': *background = m_dateColor; break;
    default: *background = QColor();
  }
  *foreground = m_lineColor;
  *bold = m_lineBold;
  return diffDate;
}

const char * LogsModel::getMsgDate(int ndx) const
{
  return Logs::getLogDate(ndx);
}

void LogsModel::notifyRowsChanged()
{
  beginResetModel();
  endResetModel();
}

ContactsModel::ContactsModel()
  : m_showDeleted(true), m_sort(sort_level), m_curContactId(-1), m_noAvatar(false), m_showInfoLine(false)
{
  connect(SimCore::get(), SIGNAL(signalContactAdded(int)), this, SLOT(onSignalContactAdded(int)));
  connect(SimCore::get(), SIGNAL(signalContactAudioChanged(unsigned, SimAudioState)),
          this, SLOT(onSignalContactAudioChanged(unsigned, SimAudioState)));
  connect(SimCore::get(), SIGNAL(signalContactStatusChanged(unsigned, int, int)),
          this, SLOT(onSignalContactStatusChanged(unsigned, int, int)));
  connect(SimCore::get(), SIGNAL(signalContactChanged(unsigned)), this, SLOT(onSignalContactChanged(unsigned)));
}

ContactsModel::~ContactsModel()
{
  m_mapRow2Id.clear();
  m_mapId2Row.clear();
}

QVariant ContactsModel::data(const QModelIndex & index, int role) const
{
  if (Contact * c = getContact(index)) {
    switch (role) {
      case Qt::DisplayRole:
        if (index.column() == ccol_nick) {
          int i = c->m_info.indexOf('\n');
          if (!m_showInfoLine || c->m_info.isEmpty()) return c->m_nick;
          if (m_noAvatar) return c->m_nick + ": " + c->m_info.left(i);
          return c->m_nick + "\n" + c->m_info.left(i);
        }
        break;

      case Qt::FontRole:
        if (index.column() == ccol_nick && !c->isVerified()) {
          QFont italicFont;
          italicFont.setItalic(true);
          return italicFont;
        }
        break;

      case Qt::BackgroundColorRole:
        if (c->m_id == m_curContactId) {
          return QApplication::palette((QWidget *)0).highlight();
        }
        break;

      case Qt::TextColorRole:
        if (c->m_id == m_curContactId) {
          return QApplication::palette((QWidget *)0).highlightedText();
        }
        break;

      case Qt::ToolTipRole:
        QString qs = tr("You are offline.");
        int myStatus = SimCore::getMyStatus();
        unsigned state = c->isMe() ? c->getState() : c->getContactState();
        bool auth = state != Contact::state_new && state != Contact::state_blocked && state != Contact::state_deleted;
        simtype contact = sim_contact_get_(c->m_simId, CONTACT_BIT_DEFAULT);
        simnumber seen = sim_table_get_number(contact, CONTACT_KEY_SEEN);
        time_t now = time(0);

        sim_contact_free_(contact);
        if (c->isMe() || myStatus != SIM_STATUS_OFF || !auth) {
          if (c->isMe() && state == Contact::state_none && myStatus != SIM_STATUS_OFF) {
            SimCore::E_ConnectionState dht = SimCore::getConnectionState(SimCore::get()->getTestContactId());
            if (dht == SimCore::state_connecting) {
              qs = tr("You are still connecting to the DHT network.");
            } else if (dht == SimCore::state_disconnect) {
              qs = tr("You are not connected to the DHT network.");
            } else {
              qs = tr("You are disconnecting from the DHT network.");
            }
          } else {
            qs = c->getStateToolTip(state);
          }
        }

        if (seen && !c->isMe()) {
          QString ts = Contact::convertTimeToText(now - seen);
          if (now < seen) {
            char buffer[64];
            sim_convert_time_to_string(seen, buffer);
            buffer[SIM_SIZE_TIME - 10] = 0;
            ts = buffer;
          }

          if (!c->isTest() || (now != seen && state != Contact::state_deleted)) {
            if (state == Contact::state_deleted) {
              if (now >= seen) {
                qs = tr("%1 was deleted %2 ago.").arg(c->getNickToolTip(true)).arg(ts);
              } else {
                qs = tr("%1 was deleted on %2.").arg(c->getNickToolTip(true)).arg(ts);
              }
            } else if (now >= seen) {
              qs.append("\n").append(tr("Last seen %1 ago.").arg(ts));
            } else {
              qs.append("\n").append(tr("Last seen on %1.").arg(ts));
            }
          }
        }

        if (auth && !c->isVerified()) {
          qs.append("\n").append(tr("%1 is NOT VERIFIED.").arg(c->getNickToolTip(true)));
        }
        if (c->m_nick == c->getDefaultNick()) {
          qs.append("\n").append(tr("The nickname of this user is not known yet."));
        }
        return qs.prepend("<p style='white-space:pre'>");
    }
  }
  return QVariant();
}

QVariant ContactsModel::headerData(int section, Qt::Orientation orientation, int role) const
{
  if (role == Qt::DisplayRole) {
    if (orientation == Qt::Horizontal) {
      switch (section) {
        case ccol_nick: return "n";
        case ccol_state: return "s";
      }
    }
  }
  return QVariant();
}

void ContactsModel::emitDataChanged(int id, int column)
{
  emit dataChanged(index(m_mapId2Row[id], ccol_state), index(m_mapId2Row[id], column));
}

void ContactsModel::onSignalContactAdded(int nContacts)
{
  int first = int(m_mapRow2Id.size());
  int last = first - 1;
  std::vector<Contact *> & contacts = SimCore::get()->m_contacts;
  if (nContacts > 0) {
    m_mapRow2Id.reserve(m_mapRow2Id.size() + nContacts);
  } else { // full rescan
    first = 0;
    m_mapRow2Id.resize(0);
    m_mapRow2Id.reserve(contacts.size());
  }

  m_mapId2Row.resize(contacts.size(), -1);
  for (int i = int(contacts.size()) - nContacts; i < int(contacts.size()); ++i) {
    if (!contacts[i] || contacts[i]->isForgotten() || (!m_showDeleted && contacts[i]->isDeleted())) {
      m_mapId2Row[i] = -1;
    } else {
      m_mapId2Row[i] = m_mapRow2Id.size();
      m_mapRow2Id.push_back(i);
      ++last;
    }
  }

  if (first <= last) {
    beginInsertRows(QModelIndex(), first, last);
    endInsertRows();
  }

  sortContacts();
  layoutChanged();
}

void ContactsModel::onSignalContactStatusChanged(unsigned id, int oldStatus, int newStatus)
{
  if (id >= m_mapId2Row.size() || m_mapId2Row[id] < 0) return;
  Contact * contact = SimCore::getContact(id);

  if (contact) {
    switch (newStatus) {
      case SIM_STATUS_ON:
      case SIM_STATUS_HIDE:
      case SIM_STATUS_BUSY:
      case SIM_STATUS_AWAY:
        if (oldStatus != SIM_STATUS_OFF && oldStatus != SIM_STATUS_INVISIBLE) break;
        if (contact->isMe()) {
          Contacts::get()->showStatus(tr("%1 has logged on.").arg(contact->m_nick), 'e');
        } else {
          Contacts::get()->showStatus(tr("%1 has logged on.").arg(contact->m_nick));
        }
        SoundEffects::playLogonSound(id);
        break;

      case SIM_STATUS_OFF:
        if (contact->isMe()) {
          Contacts::get()->showStatus(tr("%1 has logged off.").arg(contact->m_nick), 'e');
        } else {
          Contacts::get()->showStatus(tr("%1 has logged off.").arg(contact->m_nick));
        }
        SoundEffects::playLogoffSound(id);
        break;
    }
  }

  emitDataChanged(id, ccol_state);
}

void ContactsModel::onSignalContactAudioChanged(unsigned id, SimAudioState)
{
  if (id >= m_mapId2Row.size() || m_mapId2Row[id] < 0) return;

  emitDataChanged(id, ccol_state);
}

void ContactsModel::onSignalContactChanged(unsigned id)
{
  if (id >= m_mapId2Row.size()) return;

  int row = m_mapId2Row[id];
  std::vector<Contact *> & contacts = SimCore::get()->m_contacts;

  if (id >= contacts.size()) return;

  if (contacts[id] && contacts[id]->isForgotten() && row >= 0) {
    m_mapId2Row[id] = -1;
    if (unsigned(row) >= m_mapRow2Id.size() || m_mapRow2Id[row] != id) {
      log_error_("ui", "%s: id %d, row %d, size %d\n", __FUNCTION__, id, row, int(m_mapRow2Id.size()));
    } else {
      beginRemoveRows(QModelIndex(), row, row);
      // just forgotten contact
      m_mapRow2Id.erase(m_mapRow2Id.begin() + row);
      endRemoveRows();
    }
    //layoutChanged();
  }

  refilter();
}

void ContactsModel::clear()
{
  beginRemoveRows(QModelIndex(), 0, m_mapRow2Id.size() - 1);
  m_mapRow2Id.clear();
  m_mapId2Row.clear();
  endRemoveRows();
}

void ContactsModel::refilter()
{
  std::vector<Contact *> & contacts = SimCore::get()->m_contacts;
  std::vector<unsigned> remapRow2Id;

  remapRow2Id.reserve(SimCore::get()->m_contacts.size());
  remapRow2Id.resize(0);

  m_mapId2Row.resize(contacts.size(), -1);
  for (int i = 0; i < int(contacts.size()); ++i) {
    if (!contacts[i] || contacts[i]->isForgotten() || (!m_showDeleted && contacts[i]->isDeleted())) {
      m_mapId2Row[i] = -1;
    } else {
      m_mapId2Row[i] = remapRow2Id.size();
      remapRow2Id.push_back(i);
    }
  }

  unsigned oldrows = m_mapRow2Id.size();

  if (oldrows < remapRow2Id.size()) beginInsertRows(QModelIndex(), oldrows, remapRow2Id.size() - 1);

  m_mapRow2Id.resize(remapRow2Id.size());
  for (unsigned i = remapRow2Id.size(); i--;) m_mapRow2Id[i] = remapRow2Id[i];

  if (oldrows < remapRow2Id.size()) endInsertRows();

  sortContacts();
  layoutChanged();
}

void ContactsModel::setShowDeleted(bool showDeleted)
{
  if (m_showDeleted == showDeleted) return;
  m_showDeleted = showDeleted;
  refilter();
}

void ContactsModel::setSortBy(E_SortType sortBy)
{
  if (m_sort == sortBy) return;
  m_sort = sortBy;
  sortContacts();
  layoutChanged();
}

void ContactsModel::sortContacts(int start, int end)
{
  int i = start;
  int j = end;
  int tmp;

  int pivot = m_mapRow2Id[(start + end) / 2];

  // partition
  while (i <= j) {
    while (compareContacts(m_mapRow2Id[i], pivot)) ++i;
    while (compareContacts(pivot, m_mapRow2Id[j])) --j;
    if (i <= j) {
      tmp = m_mapRow2Id[i];
      m_mapRow2Id[i] = m_mapRow2Id[j];
      m_mapRow2Id[j] = tmp;

      m_mapId2Row[m_mapRow2Id[i]] = i;
      m_mapId2Row[m_mapRow2Id[j]] = j;

      ++i;
      --j;
    }
  }

  // recursion
  if (start < j) sortContacts(start, j);
  if (i < end) sortContacts(i, end);
}

void ContactsModel::sortContacts()
{
  if (m_sort != sort_none && !m_mapRow2Id.empty()) sortContacts(0, m_mapRow2Id.size() - 1);
}

bool ContactsModel::compareContacts(int leftId, int rightId) const
{
  if (leftId == rightId) return false;
  if (m_sort == sort_none) return leftId < rightId;

  Contact * l = SimCore::getContact(leftId);
  Contact * r = SimCore::getContact(rightId);

  // there is only one echo test and it is always the smallest
  if (l->isTest()) return true;
  if (r->isTest()) return false;

  // myself and system come first after echo test
  if (l->isMe() && !r->isMe()) return true;
  if (r->isMe() && !l->isMe()) return false;
  if (l->isSystem() && !r->isSystem()) return true;
  if (r->isSystem() && !l->isSystem()) return false;

  // new contacts are smaller than others
  if (l->isNewContact() && !r->isNewContact()) return true;
  if (r->isNewContact() && !l->isNewContact()) return false;

  // deleted contacts are at the end
  if ((l->isDeleted() || l->isBlocked()) && !r->isDeleted() && !r->isBlocked()) return false;
  if ((r->isDeleted() || r->isBlocked()) && !l->isDeleted() && !l->isBlocked()) return true;

  // blocked (temporarily suppressed) contacts are smaller than (permanently) deleted contacts
  if ((l->isDeleted() || l->isBlocked()) && (r->isDeleted() || r->isBlocked())) {
    if (l->isBlocked() && !r->isBlocked()) return true;
    if (r->isBlocked() && !l->isBlocked()) return false;
  }

  // now we have contacts which have to be sorted according to some criteria
  if (m_sort == sort_nick) return QString::localeAwareCompare(l->m_nick, r->m_nick) < 0;

  return leftId < rightId;
}
