/*
 * Copyright (c) 2013-2017 Isode Limited.
 * All rights reserved.
 * See the COPYING file for more information.
 */

#include <Swift/Controllers/Chat/ChatMessageParser.h>

#include <algorithm>
#include <memory>
#include <utility>
#include <vector>

#include <boost/algorithm/string.hpp>
#include <boost/regex.hpp>

#include <Swiften/Base/Regex.h>
#include <Swiften/Base/String.h>

#include <SwifTools/Linkify.h>

namespace Swift {

    ChatMessageParser::ChatMessageParser(const std::map<std::string, std::string>& emoticons, std::shared_ptr<HighlightConfiguration> highlightConfiguration, Mode mode) : emoticons_(emoticons), highlightConfiguration_(highlightConfiguration), mode_(mode) {
    }

    typedef std::pair<std::string, std::string> StringPair;

    ChatWindow::ChatMessage ChatMessageParser::parseMessageBody(const std::string& body, const std::string& senderNickname, bool senderIsSelf) {
        ChatWindow::ChatMessage parsedMessage;

        std::string remaining = body;
        if (boost::starts_with(body, "/me ")) {
           remaining = String::getSplittedAtFirst(body, ' ').second;
           parsedMessage.setIsMeCommand(true);
        }

        /* Parse one, URLs */
        while (!remaining.empty()) {
            bool found = false;
            std::pair<std::vector<std::string>, size_t> links = Linkify::splitLink(remaining);
            remaining = "";
            for (size_t i = 0; i < links.first.size(); i++) {
                const std::string& part = links.first[i];
                if (found) {
                    // Must be on the last part, then
                    remaining = part;
                }
                else {
                    if (i == links.second) {
                        found = true;
                        parsedMessage.append(std::make_shared<ChatWindow::ChatURIMessagePart>(part));
                    }
                    else {
                        parsedMessage.append(std::make_shared<ChatWindow::ChatTextMessagePart>(part));
                    }
                }
            }
        }

        /* do emoticon substitution */
        parsedMessage = emoticonHighlight(parsedMessage);

        if (!senderIsSelf) { /* do not highlight our own messsages */
            // Highlight keywords and own mentions.
            parsedMessage = splitHighlight(parsedMessage);

            // Highlight full message events like, specific sender, general
            // incoming group message, or general incoming direct message.
            parsedMessage = fullMessageHighlight(parsedMessage, senderNickname);
        }

        return parsedMessage;
    }

    ChatWindow::ChatMessage ChatMessageParser::emoticonHighlight(const ChatWindow::ChatMessage& message) {
        ChatWindow::ChatMessage parsedMessage = message;

        std::string regexString;
        /* Parse two, emoticons */
        for (StringPair emoticon : emoticons_) {
            /* Construct a regexp that finds an instance of any of the emoticons inside a group
             * at the start or end of the line, or beside whitespace.
             */
            regexString += regexString.empty() ? "" : "|";
            std::string escaped = "(" + Regex::escape(emoticon.first) + ")";
            regexString += "^" + escaped + "|";
            regexString += escaped + "$|";
            regexString += "\\s" + escaped + "|";
            regexString += escaped + "\\s";

        }
        if (!regexString.empty()) {
            regexString += "";
            boost::regex emoticonRegex(regexString);

            ChatWindow::ChatMessage newMessage;
            for (const auto& part : parsedMessage.getParts()) {
                std::shared_ptr<ChatWindow::ChatTextMessagePart> textPart;
                if ((textPart = std::dynamic_pointer_cast<ChatWindow::ChatTextMessagePart>(part))) {
                    try {
                        boost::match_results<std::string::const_iterator> match;
                        const std::string& text = textPart->text;
                        std::string::const_iterator start = text.begin();
                        while (regex_search(start, text.end(), match, emoticonRegex)) {
                            int matchIndex = 0;
                            for (matchIndex = 1; matchIndex < static_cast<int>(match.size()); matchIndex++) {
                                if (match[matchIndex].length() > 0) {
                                    //This is the matching subgroup
                                    break;
                                }
                            }
                            std::string::const_iterator matchStart = match[matchIndex].first;
                            std::string::const_iterator matchEnd = match[matchIndex].second;
                            if (start != matchStart) {
                                /* If we're skipping over plain text since the previous emoticon, record it as plain text */
                                newMessage.append(std::make_shared<ChatWindow::ChatTextMessagePart>(std::string(start, matchStart)));
                            }
                            std::shared_ptr<ChatWindow::ChatEmoticonMessagePart> emoticonPart = std::make_shared<ChatWindow::ChatEmoticonMessagePart>();
                            std::string matchString = match[matchIndex].str();
                            std::map<std::string, std::string>::const_iterator emoticonIterator = emoticons_.find(matchString);
                            assert (emoticonIterator != emoticons_.end());
                            const StringPair& emoticon = *emoticonIterator;
                            emoticonPart->imagePath = emoticon.second;
                            emoticonPart->alternativeText = emoticon.first;
                            newMessage.append(emoticonPart);
                            start = matchEnd;
                        }
                        if (start != text.end()) {
                            /* If there's plain text after the last emoticon, record it */
                            newMessage.append(std::make_shared<ChatWindow::ChatTextMessagePart>(std::string(start, text.end())));
                        }

                    }
                    catch (const std::runtime_error&) {
                        /* Basically too expensive to compute the regex results and it gave up, so pass through as text */
                        newMessage.append(part);
                    }
                }
                else {
                    newMessage.append(part);
                }
            }
            parsedMessage.setParts(newMessage.getParts());
        }

        return parsedMessage;
    }

    ChatWindow::ChatMessage ChatMessageParser::splitHighlight(const ChatWindow::ChatMessage& message) {
        auto keywordToRegEx = [](const std::string& keyword, bool matchCaseSensitive) {
            std::string escaped = Regex::escape(keyword);
            boost::regex::flag_type flags = boost::regex::normal;
            if (!matchCaseSensitive) {
                flags |= boost::regex::icase;
            }
            return boost::regex("\\b" + escaped + "\\b", flags);
        };

        auto highlightKeywordInChatMessage = [&](const ChatWindow::ChatMessage& message, const std::string& keyword, bool matchCaseSensitive, const HighlightAction& action) {
            ChatWindow::ChatMessage resultMessage;

            for (const auto& part : message.getParts()) {
                std::shared_ptr<ChatWindow::ChatTextMessagePart> textPart;
                if ((textPart = std::dynamic_pointer_cast<ChatWindow::ChatTextMessagePart>(part))) {
                    try {
                        boost::match_results<std::string::const_iterator> match;
                        const std::string& text = textPart->text;
                        std::string::const_iterator start = text.begin();
                        while (regex_search(start, text.end(), match, keywordToRegEx(keyword, matchCaseSensitive))) {
                            std::string::const_iterator matchStart = match[0].first;
                            std::string::const_iterator matchEnd = match[0].second;
                            if (start != matchStart) {
                                /* If we're skipping over plain text since the previous emoticon, record it as plain text */
                                resultMessage.append(std::make_shared<ChatWindow::ChatTextMessagePart>(std::string(start, matchStart)));
                            }
                            std::shared_ptr<ChatWindow::ChatHighlightingMessagePart> highlightPart = std::make_shared<ChatWindow::ChatHighlightingMessagePart>();
                            highlightPart->text = match.str();
                            highlightPart->action = action;
                            resultMessage.append(highlightPart);
                            start = matchEnd;
                        }
                        if (start != text.end()) {
                            /* If there's plain text after the last emoticon, record it */
                            resultMessage.append(std::make_shared<ChatWindow::ChatTextMessagePart>(std::string(start, text.end())));
                        }
                    }
                    catch (const std::runtime_error&) {
                        /* Basically too expensive to compute the regex results and it gave up, so pass through as text */
                        resultMessage.append(part);
                    }
                } else {
                    resultMessage.append(part);
                }
            }
            return resultMessage;
        };

        ChatWindow::ChatMessage parsedMessage = message;

        // detect mentions of own nickname
        HighlightAction ownMentionKeywordAction = highlightConfiguration_->ownMentionAction;
        ownMentionKeywordAction.setSoundFilePath(boost::optional<std::string>());
        ownMentionKeywordAction.setSystemNotificationEnabled(false);
        if (!getNick().empty() && !highlightConfiguration_->ownMentionAction.isEmpty()) {
            auto nicknameHighlightedMessage = highlightKeywordInChatMessage(parsedMessage, nick_, false, ownMentionKeywordAction);
            auto highlightedParts = nicknameHighlightedMessage.getParts();
            auto ownNicknamePart = std::find_if(highlightedParts.begin(), highlightedParts.end(), [&](std::shared_ptr<ChatWindow::ChatMessagePart>& part){
                auto highlightPart = std::dynamic_pointer_cast<ChatWindow::ChatHighlightingMessagePart>(part);
                if (highlightPart && highlightPart->text == nick_) {
                    return true;
                }
                return false;
            });
            if (ownNicknamePart != highlightedParts.end()) {
                parsedMessage.setHighlightActionOwnMention(highlightConfiguration_->ownMentionAction);
            }
            parsedMessage.setParts(nicknameHighlightedMessage.getParts());
        }

        // detect keywords
        for (const auto& keywordHighlight : highlightConfiguration_->keywordHighlights) {
            if (keywordHighlight.keyword.empty() || keywordHighlight.action.isEmpty()) {
                continue;
            }
            auto newMessage = highlightKeywordInChatMessage(parsedMessage, keywordHighlight.keyword, keywordHighlight.matchCaseSensitive, keywordHighlight.action);
            parsedMessage.setParts(newMessage.getParts());
        }

        return parsedMessage;
    }

    ChatWindow::ChatMessage ChatMessageParser::fullMessageHighlight(const ChatWindow::ChatMessage& parsedMessage, const std::string& sender) {
        auto fullHighlightedMessage = parsedMessage;

        // contact highlighting
        for (const auto& contactHighlight : highlightConfiguration_->contactHighlights) {
            if (sender == contactHighlight.name) {
                fullHighlightedMessage.setHighlightActionSender(contactHighlight.action);
                break;
            }
        }

        // general incoming messages
        HighlightAction groupAction;
        HighlightAction chatAction;

        switch (mode_) {
            case Mode::GroupChat:
                groupAction.setSoundFilePath(highlightConfiguration_->playSoundOnIncomingGroupchatMessages ? boost::optional<std::string>("") : boost::optional<std::string>());
                groupAction.setSystemNotificationEnabled(highlightConfiguration_->showNotificationOnIncomingGroupchatMessages);
                fullHighlightedMessage.setHighlightActionGroupMessage(groupAction);
                break;

            case Mode::Chat:
                chatAction.setSoundFilePath(highlightConfiguration_->playSoundOnIncomingDirectMessages ? boost::optional<std::string>("") : boost::optional<std::string>());
                chatAction.setSystemNotificationEnabled(highlightConfiguration_->showNotificationOnIncomingDirectMessages);
                fullHighlightedMessage.setHighlightActonDirectMessage(chatAction);
                break;
        }

        return fullHighlightedMessage;
    }
}