/***************************************************************************
 *   Copyright (C) 2005-09 by the Quassel Project                          *
 *   devel@quassel-irc.org                                                 *
 *                                                                         *
 *   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) version 3.                                           *
 *                                                                         *
 *   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.,                                       *
 *   59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.             *
 ***************************************************************************/
#include <QApplication>

#include "buffersettings.h"
#include "iconloader.h"
#include "qssparser.h"
#include "quassel.h"
#include "uistyle.h"
#include "uisettings.h"
#include "util.h"

QHash<QString, UiStyle::FormatType> UiStyle::_formatCodes;
QString UiStyle::_timestampFormatString;

UiStyle::UiStyle(QObject *parent)
: QObject(parent),
  _channelJoinedIcon(SmallIcon("irc-channel-active")),
  _channelPartedIcon(SmallIcon("irc-channel-inactive")),
  _userOfflineIcon(SmallIcon("im-user-offline")),
  _userOnlineIcon(SmallIcon("im-user")),
  _userAwayIcon(SmallIcon("im-user-away")),
  _categoryOpIcon(SmallIcon("irc-operator")),
  _categoryVoiceIcon(SmallIcon("irc-voice")),
  _opIconLimit(UserCategoryItem::categoryFromModes("o")),
  _voiceIconLimit(UserCategoryItem::categoryFromModes("v"))
{
  // register FormatList if that hasn't happened yet
  // FIXME I don't think this actually avoids double registration... then again... does it hurt?
  if(QVariant::nameToType("UiStyle::FormatList") == QVariant::Invalid) {
    qRegisterMetaType<FormatList>("UiStyle::FormatList");
    qRegisterMetaTypeStreamOperators<FormatList>("UiStyle::FormatList");
    Q_ASSERT(QVariant::nameToType("UiStyle::FormatList") != QVariant::Invalid);
  }

  _uiStylePalette = QVector<QBrush>(NumRoles, QBrush());

  // Now initialize the mapping between FormatCodes and FormatTypes...
  _formatCodes["%O"] = Base;
  _formatCodes["%B"] = Bold;
  _formatCodes["%S"] = Italic;
  _formatCodes["%U"] = Underline;
  _formatCodes["%R"] = Reverse;

  _formatCodes["%DN"] = Nick;
  _formatCodes["%DH"] = Hostmask;
  _formatCodes["%DC"] = ChannelName;
  _formatCodes["%DM"] = ModeFlags;
  _formatCodes["%DU"] = Url;

  setTimestampFormatString("[hh:mm:ss]");

  // BufferView / NickView settings
  UiStyleSettings s;
  _showBufferViewIcons = _showNickViewIcons = s.value("ShowItemViewIcons", true).toBool();
  s.notify("ShowItemViewIcons", this, SLOT(showItemViewIconsChanged(QVariant)));

  _allowMircColors = s.value("AllowMircColors", true).toBool();
  s.notify("AllowMircColors", this, SLOT(allowMircColorsChanged(QVariant)));

  loadStyleSheet();
}

UiStyle::~UiStyle() {
  qDeleteAll(_metricsCache);
}

void UiStyle::reload() {
  loadStyleSheet();
}

void UiStyle::loadStyleSheet() {
  qDeleteAll(_metricsCache);
  _metricsCache.clear();
  _formatCache.clear();
  _formats.clear();

  UiStyleSettings s;

  QString styleSheet;
  styleSheet += loadStyleSheet("file:///" + Quassel::findDataFilePath("stylesheets/default.qss"));
  styleSheet += loadStyleSheet("file:///" + Quassel::configDirPath() + "settings.qss");
  if(s.value("UseCustomStyleSheet", false).toBool())
    styleSheet += loadStyleSheet("file:///" + s.value("CustomStyleSheetPath").toString(), true);
  styleSheet += loadStyleSheet("file:///" + Quassel::optionValue("qss"), true);

  if(!styleSheet.isEmpty()) {
    QssParser parser;
    parser.processStyleSheet(styleSheet);
    QApplication::setPalette(parser.palette());

    _uiStylePalette = parser.uiStylePalette();
    _formats = parser.formats();
    _listItemFormats = parser.listItemFormats();

    styleSheet = styleSheet.trimmed();
    if(!styleSheet.isEmpty())
      qApp->setStyleSheet(styleSheet); // pass the remaining sections to the application
  }

  emit changed();
}

QString UiStyle::loadStyleSheet(const QString &styleSheet, bool shouldExist) {
  QString ss = styleSheet;
  if(ss.startsWith("file:///")) {
    ss.remove(0, 8);
    if(ss.isEmpty())
      return QString();

    QFile file(ss);
    if(file.open(QFile::ReadOnly)) {
      QTextStream stream(&file);
      ss = stream.readAll();
      file.close();
    } else {
      if(shouldExist)
        qWarning() << "Could not open stylesheet file:" << file.fileName();
      return QString();
    }
  }
  return ss;
}

void UiStyle::setTimestampFormatString(const QString &format) {
  if(_timestampFormatString != format) {
    _timestampFormatString = format;
    // FIXME reload
  }
}

void UiStyle::allowMircColorsChanged(const QVariant &v) {
  _allowMircColors = v.toBool();
  emit changed();
}

/******** ItemView Styling *******/

void UiStyle::showItemViewIconsChanged(const QVariant &v) {
  _showBufferViewIcons = _showNickViewIcons = v.toBool();
}

QVariant UiStyle::bufferViewItemData(const QModelIndex &index, int role) const {
  BufferInfo::Type type = (BufferInfo::Type)index.data(NetworkModel::BufferTypeRole).toInt();
  bool isActive = index.data(NetworkModel::ItemActiveRole).toBool();

  if(role == Qt::DecorationRole) {
    if(!_showBufferViewIcons)
      return QVariant();

    switch(type) {
      case BufferInfo::ChannelBuffer:
        if(isActive)
          return _channelJoinedIcon;
        else
          return _channelPartedIcon;
      case BufferInfo::QueryBuffer:
        if(!isActive)
          return _userOfflineIcon;
        if(index.data(NetworkModel::UserAwayRole).toBool())
          return _userAwayIcon;
        else
          return _userOnlineIcon;
      default:
        return QVariant();
    }
  }

  quint32 fmtType = BufferViewItem;
  switch(type) {
    case BufferInfo::StatusBuffer:
      fmtType |= NetworkItem;
      break;
    case BufferInfo::ChannelBuffer:
      fmtType |= ChannelBufferItem;
      break;
    case BufferInfo::QueryBuffer:
      fmtType |= QueryBufferItem;
      break;
    default:
      return QVariant();
  }

  QTextCharFormat fmt = _listItemFormats.value(BufferViewItem);
  fmt.merge(_listItemFormats.value(fmtType));

  BufferInfo::ActivityLevel activity = (BufferInfo::ActivityLevel)index.data(NetworkModel::BufferActivityRole).toInt();
  if(activity & BufferInfo::Highlight) {
    fmt.merge(_listItemFormats.value(BufferViewItem | HighlightedBuffer));
    fmt.merge(_listItemFormats.value(fmtType | HighlightedBuffer));
  } else if(activity & BufferInfo::NewMessage) {
    fmt.merge(_listItemFormats.value(BufferViewItem | UnreadBuffer));
    fmt.merge(_listItemFormats.value(fmtType | UnreadBuffer));
  } else if(activity & BufferInfo::OtherActivity) {
    fmt.merge(_listItemFormats.value(BufferViewItem | ActiveBuffer));
    fmt.merge(_listItemFormats.value(fmtType | ActiveBuffer));
  } else if(!isActive) {
    fmt.merge(_listItemFormats.value(BufferViewItem | InactiveBuffer));
    fmt.merge(_listItemFormats.value(fmtType | InactiveBuffer));
  } else if(index.data(NetworkModel::UserAwayRole).toBool()) {
    fmt.merge(_listItemFormats.value(BufferViewItem | UserAway));
    fmt.merge(_listItemFormats.value(fmtType | UserAway));
  }

  return itemData(role, fmt);
}

QVariant UiStyle::nickViewItemData(const QModelIndex &index, int role) const {
  NetworkModel::ItemType type = (NetworkModel::ItemType)index.data(NetworkModel::ItemTypeRole).toInt();

  if(role == Qt::DecorationRole) {
    if(!_showNickViewIcons)
      return QVariant();

    switch(type) {
      case NetworkModel::UserCategoryItemType:
      {
        int categoryId = index.data(TreeModel::SortRole).toInt();
        if(categoryId <= _opIconLimit)
          return _categoryOpIcon;
        if(categoryId <= _voiceIconLimit)
          return _categoryVoiceIcon;
        return _userOnlineIcon;
      }
      case NetworkModel::IrcUserItemType:
        if(index.data(NetworkModel::ItemActiveRole).toBool())
          return _userOnlineIcon;
        else
          return _userAwayIcon;
      default:
        return QVariant();
    }
  }

  QTextCharFormat fmt = _listItemFormats.value(NickViewItem);

  switch(type) {
    case NetworkModel::IrcUserItemType:
      fmt.merge(_listItemFormats.value(NickViewItem | IrcUserItem));
      if(!index.data(NetworkModel::ItemActiveRole).toBool()) {
        fmt.merge(_listItemFormats.value(NickViewItem | UserAway));
        fmt.merge(_listItemFormats.value(NickViewItem | IrcUserItem | UserAway));
      }
      break;
    case NetworkModel::UserCategoryItemType:
      fmt.merge(_listItemFormats.value(NickViewItem | UserCategoryItem));
      break;
    default:
      return QVariant();
  }

  return itemData(role, fmt);
}

QVariant UiStyle::itemData(int role, const QTextCharFormat &format) const {
  switch(role) {
    case Qt::FontRole:
      return format.font();
    case Qt::ForegroundRole:
      return format.property(QTextFormat::ForegroundBrush);
    case Qt::BackgroundRole:
      return format.property(QTextFormat::BackgroundBrush);
    default:
      return QVariant();
  }
}

/******** Caching *******/

QTextCharFormat UiStyle::format(quint64 key) const {
  return _formats.value(key, QTextCharFormat());
}

QTextCharFormat UiStyle::cachedFormat(quint32 formatType, quint32 messageLabel) const {
  return _formatCache.value(formatType | ((quint64)messageLabel << 32), QTextCharFormat());
}

void UiStyle::setCachedFormat(const QTextCharFormat &format, quint32 formatType, quint32 messageLabel) const {
  _formatCache[formatType | ((quint64)messageLabel << 32)] = format;
}

QFontMetricsF *UiStyle::fontMetrics(quint32 ftype, quint32 label) const {
  // QFontMetricsF is not assignable, so we need to store pointers :/
  quint64 key = ftype | ((quint64)label << 32);

  if(_metricsCache.contains(key))
    return _metricsCache.value(key);

  return (_metricsCache[key] = new QFontMetricsF(format(ftype, label).font()));
}

/******** Generate formats ********/

// NOTE: This and the following functions are intimately tied to the values in FormatType. Don't change this
//       until you _really_ know what you do!
QTextCharFormat UiStyle::format(quint32 ftype, quint32 label_) const {
  if(ftype == Invalid)
    return QTextCharFormat();

  quint64 label = (quint64)label_ << 32;

  // check if we have exactly this format readily cached already
  QTextCharFormat fmt = cachedFormat(ftype, label_);
  if(fmt.properties().count())
    return fmt;

  mergeFormat(fmt, ftype, label & Q_UINT64_C(0xffff000000000000));

  for(quint64 mask = Q_UINT64_C(0x0000000100000000); mask <= (quint64)Selected << 32; mask <<=1) {
    if(label & mask)
      mergeFormat(fmt, ftype, mask | Q_UINT64_C(0xffff000000000000));
  }

  setCachedFormat(fmt, ftype, label_);
  return fmt;
}

void UiStyle::mergeFormat(QTextCharFormat &fmt, quint32 ftype, quint64 label) const {
  mergeSubElementFormat(fmt, ftype & 0x00ff, label);

  // TODO: allow combinations for mirc formats and colors (each), e.g. setting a special format for "bold and italic"
  //       or "foreground 01 and background 03"
  if((ftype & 0xfff00)) { // element format
    for(quint32 mask = 0x00100; mask <= 0x40000; mask <<= 1) {
      if(ftype & mask) {
        mergeSubElementFormat(fmt, ftype & (mask | 0xff), label);
      }
    }
  }

  // Now we handle color codes
  // We assume that those can't be combined with subelement and message types.
  if(_allowMircColors) {
    if(ftype & 0x00400000)
      mergeSubElementFormat(fmt, ftype & 0x0f400000, label); // foreground
    if(ftype & 0x00800000)
      mergeSubElementFormat(fmt, ftype & 0xf0800000, label); // background
    if((ftype & 0x00c00000) == 0x00c00000)
      mergeSubElementFormat(fmt, ftype & 0xffc00000, label); // combination
  }

  // URL
  if(ftype & Url)
    mergeSubElementFormat(fmt, ftype & (Url | 0x000000ff), label);
}

// Merge a subelement format into an existing message format
void UiStyle::mergeSubElementFormat(QTextCharFormat& fmt, quint32 ftype, quint64 label) const {
  quint64 key = ftype | label;
  fmt.merge(format(key & Q_UINT64_C(0x0000ffffffffff00)));  // label + subelement
  fmt.merge(format(key & Q_UINT64_C(0x0000ffffffffffff)));  // label + subelement + msgtype
  fmt.merge(format(key & Q_UINT64_C(0xffffffffffffff00)));  // label + subelement + nickhash
  fmt.merge(format(key & Q_UINT64_C(0xffffffffffffffff)));  // label + subelement + nickhash + msgtype
}

UiStyle::FormatType UiStyle::formatType(Message::Type msgType) {
  switch(msgType) {
    case Message::Plain:
      return PlainMsg;
    case Message::Notice:
      return NoticeMsg;
    case Message::Action:
      return ActionMsg;
    case Message::Nick:
      return NickMsg;
    case Message::Mode:
      return ModeMsg;
    case Message::Join:
      return JoinMsg;
    case Message::Part:
      return PartMsg;
    case Message::Quit:
      return QuitMsg;
    case Message::Kick:
      return KickMsg;
    case Message::Kill:
      return KillMsg;
    case Message::Server:
      return ServerMsg;
    case Message::Info:
      return InfoMsg;
    case Message::Error:
      return ErrorMsg;
    case Message::DayChange:
      return DayChangeMsg;
    case Message::Topic:
      return TopicMsg;
    case Message::NetsplitJoin:
      return NetsplitJoinMsg;
    case Message::NetsplitQuit:
      return NetsplitQuitMsg;
    case Message::Invite:
      return InviteMsg;
  }
  //Q_ASSERT(false); // we need to handle all message types
  qWarning() << Q_FUNC_INFO << "Unknown message type:" << msgType;
  return ErrorMsg;
}

UiStyle::FormatType UiStyle::formatType(const QString & code) {
  if(_formatCodes.contains(code)) return _formatCodes.value(code);
  return Invalid;
}

QString UiStyle::formatCode(FormatType ftype) {
  return _formatCodes.key(ftype);
}

QList<QTextLayout::FormatRange> UiStyle::toTextLayoutList(const FormatList &formatList, int textLength, quint32 messageLabel) const {
  QList<QTextLayout::FormatRange> formatRanges;
  QTextLayout::FormatRange range;
  int i = 0;
  for(i = 0; i < formatList.count(); i++) {
    range.format = format(formatList.at(i).second, messageLabel);
    range.start = formatList.at(i).first;
    if(i > 0) formatRanges.last().length = range.start - formatRanges.last().start;
    formatRanges.append(range);
  }
  if(i > 0) formatRanges.last().length = textLength - formatRanges.last().start;
  return formatRanges;
}

// This method expects a well-formatted string, there is no error checking!
// Since we create those ourselves, we should be pretty safe that nobody does something crappy here.
UiStyle::StyledString UiStyle::styleString(const QString &s_, quint32 baseFormat) {
  QString s = s_;
  if(s.length() > 65535) {
    qWarning() << QString("String too long to be styled: %1").arg(s);
    return StyledString();
  }
  StyledString result;
  result.formatList.append(qMakePair((quint16)0, baseFormat));
  quint32 curfmt = baseFormat;
  int pos = 0; quint16 length = 0;
  for(;;) {
    pos = s.indexOf('%', pos);
    if(pos < 0) break;
    if(s[pos+1] == '%') { // escaped %, we just remove one and continue
      s.remove(pos, 1);
      pos++;
      continue;
    }
    if(s[pos+1] == 'D' && s[pos+2] == 'c') { // color code
      if(s[pos+3] == '-') {  // color off
        curfmt &= 0x003fffff;
        length = 4;
      } else {
        int color = 10 * s[pos+4].digitValue() + s[pos+5].digitValue();
        //TODO: use 99 as transparent color (re mirc color "standard")
        color &= 0x0f;
        if(s[pos+3] == 'f') {
          curfmt &= 0xf0ffffff;
          curfmt |= (quint32)(color << 24) | 0x00400000;
        } else {
          curfmt &= 0x0fffffff;
          curfmt |= (quint32)(color << 28) | 0x00800000;
        }
        length = 6;
      }
    } else if(s[pos+1] == 'O') { // reset formatting
      curfmt &= 0x000000ff; // we keep message type-specific formatting
      length = 2;
    } else if(s[pos+1] == 'R') { // reverse
      // TODO: implement reverse formatting

      length = 2;
    } else { // all others are toggles
      QString code = QString("%") + s[pos+1];
      if(s[pos+1] == 'D') code += s[pos+2];
      FormatType ftype = formatType(code);
      if(ftype == Invalid) {
        pos++;
        qWarning() << (QString("Invalid format code in string: %1").arg(s));
        continue;
      }
      curfmt ^= ftype;
      length = code.length();
    }
    s.remove(pos, length);
    if(pos == result.formatList.last().first)
      result.formatList.last().second = curfmt;
    else
      result.formatList.append(qMakePair((quint16)pos, curfmt));
  }
  result.plainText = s;
  return result;
}

QString UiStyle::mircToInternal(const QString &mirc_) {
  QString mirc = mirc_;
  mirc.replace('%', "%%");      // escape % just to be sure
  mirc.replace('\t', "        ");      // tabs break layout, also this is italics in Konversation
  mirc.replace('\x02', "%B");
  mirc.replace('\x0f', "%O");
  mirc.replace('\x12', "%R");
  mirc.replace('\x16', "%R");
  mirc.replace('\x1d', "%S");
  mirc.replace('\x1f', "%U");

  // Now we bring the color codes (\x03) in a sane format that can be parsed more easily later.
  // %Dcfxx is foreground, %Dcbxx is background color, where xx is a 2 digit dec number denoting the color code.
  // %Dc- turns color off.
  // Note: We use the "mirc standard" as described in <http://www.mirc.co.uk/help/color.txt>.
  //       This means that we don't accept something like \x03,5 (even though others, like WeeChat, do).
  int pos = 0;
  for(;;) {
    pos = mirc.indexOf('\x03', pos);
    if(pos < 0) break; // no more mirc color codes
    QString ins, num;
    int l = mirc.length();
    int i = pos + 1;
    // check for fg color
    if(i < l && mirc[i].isDigit()) {
      num = mirc[i++];
      if(i < l && mirc[i].isDigit()) num.append(mirc[i++]);
      else num.prepend('0');
      ins = QString("%Dcf%1").arg(num);

      if(i+1 < l && mirc[i] == ',' && mirc[i+1].isDigit()) {
        i++;
        num = mirc[i++];
        if(i < l && mirc[i].isDigit()) num.append(mirc[i++]);
        else num.prepend('0');
        ins += QString("%Dcb%1").arg(num);
      }
    } else {
      ins = "%Dc-";
    }
    mirc.replace(pos, i-pos, ins);
  }
  return mirc;
}

/***********************************************************************************/
UiStyle::StyledMessage::StyledMessage(const Message &msg)
  : Message(msg)
{
  if(type() == Message::Plain)
    _senderHash = 0xff;
  else
    _senderHash = 0x00;  // this means we never compute the hash for msgs that aren't plain
}

void UiStyle::StyledMessage::style() const {
  QString user = userFromMask(sender());
  QString host = hostFromMask(sender());
  QString nick = nickFromMask(sender());
  QString txt = UiStyle::mircToInternal(contents());
  QString bufferName = bufferInfo().bufferName();
  bufferName.replace('%', "%%"); // well, you _can_ have a % in a buffername apparently... -_-
  host.replace('%', "%%");       // hostnames too...
  user.replace('%', "%%");       // and the username...
  nick.replace('%', "%%");       // ... and then there's totally RFC-violating servers like justin.tv m(
  const int maxNetsplitNicks = 15;

  QString t;
  switch(type()) {
    case Message::Plain:
      //: Plain Message
      t = tr("%1").arg(txt); break;
    case Message::Notice:
      //: Notice Message
      t = tr("%1").arg(txt); break;
    case Message::Action:
      //: Action Message
      t = tr("%DN%1%DN %2").arg(nick).arg(txt);
      break;
    case Message::Nick:
      //: Nick Message
      if(nick == contents()) t = tr("You are now known as %DN%1%DN").arg(txt);
      else t = tr("%DN%1%DN is now known as %DN%2%DN").arg(nick, txt);
      break;
    case Message::Mode:
      //: Mode Message
      if(nick.isEmpty()) t = tr("User mode: %DM%1%DM").arg(txt);
      else t = tr("Mode %DM%1%DM by %DN%2%DN").arg(txt, nick);
      break;
    case Message::Join:
      //: Join Message
      t = tr("%DN%1%DN %DH(%2@%3)%DH has joined %DC%4%DC").arg(nick, user, host, bufferName); break;
    case Message::Part:
      //: Part Message
      t = tr("%DN%1%DN %DH(%2@%3)%DH has left %DC%4%DC").arg(nick, user, host, bufferName);
      if(!txt.isEmpty()) t = QString("%1 (%2)").arg(t).arg(txt);
      break;
    case Message::Quit:
      //: Quit Message
      t = tr("%DN%1%DN %DH(%2@%3)%DH has quit").arg(nick, user, host);
      if(!txt.isEmpty()) t = QString("%1 (%2)").arg(t).arg(txt);
      break;
    case Message::Kick: {
        QString victim = txt.section(" ", 0, 0);
        QString kickmsg = txt.section(" ", 1);
        //: Kick Message
        t = tr("%DN%1%DN has kicked %DN%2%DN from %DC%3%DC").arg(nick).arg(victim).arg(bufferName);
        if(!kickmsg.isEmpty()) t = QString("%1 (%2)").arg(t).arg(kickmsg);
      }
      break;
    //case Message::Kill: FIXME

    case Message::Server:
      //: Server Message
      t = tr("%1").arg(txt); break;
    case Message::Info:
      //: Info Message
      t = tr("%1").arg(txt); break;
    case Message::Error:
      //: Error Message
      t = tr("%1").arg(txt); break;
    case Message::DayChange:
      //: Day Change Message
      t = tr("{Day changed to %1}").arg(timestamp().toString());
      break;
    case Message::Topic:
      //: Topic Message
      t = tr("%1").arg(txt); break;
    case Message::NetsplitJoin: {
      QStringList users = txt.split("#:#");
      QStringList servers = users.takeLast().split(" ");

      for(int i = 0; i < users.count() && i < maxNetsplitNicks; i++)
        users[i] = nickFromMask(users.at(i));

      t = tr("Netsplit between %DH%1%DH and %DH%2%DH ended. Users joined: ").arg(servers.at(0),servers.at(1));
      if(users.count() <= maxNetsplitNicks)
        t.append(QString("%DN%1%DN").arg(users.join(", ")));
      else
        t.append(tr("%DN%1%DN (%2 more)").arg(static_cast<QStringList>(users.mid(0, maxNetsplitNicks)).join(", ")).arg(users.count() - maxNetsplitNicks));
      }
      break;
    case Message::NetsplitQuit: {
      QStringList users = txt.split("#:#");
      QStringList servers = users.takeLast().split(" ");

      for(int i = 0; i < users.count() && i < maxNetsplitNicks; i++)
        users[i] = nickFromMask(users.at(i));

      t = tr("Netsplit between %DH%1%DH and %DH%2%DH. Users quit: ").arg(servers.at(0),servers.at(1));

      if(users.count() <= maxNetsplitNicks)
        t.append(QString("%DN%1%DN").arg(users.join(", ")));
      else
        t.append(tr("%DN%1%DN (%2 more)").arg(static_cast<QStringList>(users.mid(0, maxNetsplitNicks)).join(", ")).arg(users.count() - maxNetsplitNicks));
      }
      break;
    case Message::Invite:
      //: Invite Message
      t = tr("%1").arg(txt); break;
    default:
      t = tr("[%1]").arg(txt);
  }
  _contents = UiStyle::styleString(t, UiStyle::formatType(type()));
}

const QString &UiStyle::StyledMessage::plainContents() const {
  if(_contents.plainText.isNull())
    style();

  return _contents.plainText;
}

const UiStyle::FormatList &UiStyle::StyledMessage::contentsFormatList() const {
  if(_contents.plainText.isNull())
    style();

  return _contents.formatList;
}

QString UiStyle::StyledMessage::decoratedTimestamp() const {
  return timestamp().toLocalTime().toString(UiStyle::timestampFormatString());
}

QString UiStyle::StyledMessage::plainSender() const {
  switch(type()) {
    case Message::Plain:
    case Message::Notice:
      return nickFromMask(sender());
    default:
      return QString();
  }
}

QString UiStyle::StyledMessage::decoratedSender() const {
  switch(type()) {
    case Message::Plain:
      return tr("<%1>").arg(plainSender()); break;
    case Message::Notice:
      return tr("[%1]").arg(plainSender()); break;
    case Message::Action:
      return "-*-"; break;
    case Message::Nick:
      return "<->"; break;
    case Message::Mode:
      return "***"; break;
    case Message::Join:
      return "-->"; break;
    case Message::Part:
      return "<--"; break;
    case Message::Quit:
      return "<--"; break;
    case Message::Kick:
      return "<-*"; break;
    case Message::Kill:
      return "<-x"; break;
    case Message::Server:
      return "*"; break;
    case Message::Info:
      return "*"; break;
    case Message::Error:
      return "*"; break;
    case Message::DayChange:
      return "-"; break;
    case Message::Topic:
      return "*"; break;
    case Message::NetsplitJoin:
      return "=>"; break;
    case Message::NetsplitQuit:
      return "<="; break;
    case Message::Invite:
      return "->"; break;
    default:
      return QString("%1").arg(plainSender());
  }
}

// FIXME hardcoded to 16 sender hashes
quint8 UiStyle::StyledMessage::senderHash() const {
  if(_senderHash != 0xff)
    return _senderHash;

  QString nick = nickFromMask(sender()).toLower();
  if(!nick.isEmpty()) {
    int chopCount = 0;
    while(chopCount < nick.size() && nick.at(nick.count() - 1 - chopCount) == '_')
      chopCount++;
    if(chopCount < nick.size())
      nick.chop(chopCount);
  }
  quint16 hash = qChecksum(nick.toAscii().data(), nick.toAscii().size());
  return (_senderHash = (hash & 0xf) + 1);
}

/***********************************************************************************/

QDataStream &operator<<(QDataStream &out, const UiStyle::FormatList &formatList) {
  out << formatList.count();
  UiStyle::FormatList::const_iterator it = formatList.begin();
  while(it != formatList.end()) {
    out << (*it).first << (*it).second;
    ++it;
  }
  return out;
}

QDataStream &operator>>(QDataStream &in, UiStyle::FormatList &formatList) {
  quint16 cnt;
  in >> cnt;
  for(quint16 i = 0; i < cnt; i++) {
    quint16 pos; quint32 ftype;
    in >> pos >> ftype;
    formatList.append(qMakePair((quint16)pos, ftype));
  }
  return in;
}
