/*
 * Symphony - A modern community (forum/SNS/blog) platform written in Java.
 * Copyright (C) 2012-2017,  b3log.org & hacpai.com
 *
 * 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 3 of the License, or
 * (at your option) any later version.
 *
 * 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, see <http://www.gnu.org/licenses/>.
 */
package org.b3log.symphony.service;

import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.time.DateFormatUtils;
import org.b3log.latke.Keys;
import org.b3log.latke.Latkes;
import org.b3log.latke.ioc.inject.Inject;
import org.b3log.latke.logging.Level;
import org.b3log.latke.logging.Logger;
import org.b3log.latke.model.Pagination;
import org.b3log.latke.model.User;
import org.b3log.latke.repository.*;
import org.b3log.latke.service.LangPropsService;
import org.b3log.latke.service.ServiceException;
import org.b3log.latke.service.annotation.Service;
import org.b3log.latke.util.CollectionUtils;
import org.b3log.latke.util.Locales;
import org.b3log.latke.util.Paginator;
import org.b3log.latke.util.Stopwatchs;
import org.b3log.symphony.model.*;
import org.b3log.symphony.processor.advice.validate.UserRegisterValidation;
import org.b3log.symphony.repository.ArticleRepository;
import org.b3log.symphony.repository.CommentRepository;
import org.b3log.symphony.repository.UserRepository;
import org.b3log.symphony.util.*;
import org.json.JSONArray;
import org.json.JSONObject;
import org.jsoup.Jsoup;
import org.jsoup.safety.Whitelist;

import java.util.*;

/**
 * Comment management service.
 *
 * @author <a href="http://88250.b3log.org">Liang Ding</a>
 * @version 2.11.10.1, Dec 8, 2017
 * @since 0.2.0
 */
@Service
public class CommentQueryService {

    /**
     * Logger.
     */
    private static final Logger LOGGER = Logger.getLogger(CommentQueryService.class);

    /**
     * Revision query service.
     */
    @Inject
    private RevisionQueryService revisionQueryService;

    /**
     * Comment repository.
     */
    @Inject
    private CommentRepository commentRepository;

    /**
     * Article repository.
     */
    @Inject
    private ArticleRepository articleRepository;

    /**
     * User repository.
     */
    @Inject
    private UserRepository userRepository;

    /**
     * User query service.
     */
    @Inject
    private UserQueryService userQueryService;

    /**
     * Avatar query service.
     */
    @Inject
    private AvatarQueryService avatarQueryService;

    /**
     * Language service.
     */
    @Inject
    private LangPropsService langPropsService;

    /**
     * Short link query service.
     */
    @Inject
    private ShortLinkQueryService shortLinkQueryService;

    /**
     * Gets the page number of a comment.
     *
     * @param articleId the specified article id
     * @param commentId the specified comment id
     * @param sortMode  the specified sort mode
     * @param pageSize  the specified comment page size
     * @return page number, return {@code 1} if occurs exception
     */
    public int getCommentPage(final String articleId, final String commentId, final int sortMode, final int pageSize) {
        final Query numQuery = new Query()
                .setPageSize(Integer.MAX_VALUE).setCurrentPageNum(1).setPageCount(1);

        switch (sortMode) {
            case UserExt.USER_COMMENT_VIEW_MODE_C_TRADITIONAL:
                numQuery.setFilter(CompositeFilterOperator.and(
                        new PropertyFilter(Comment.COMMENT_ON_ARTICLE_ID, FilterOperator.EQUAL, articleId),
                        new PropertyFilter(Keys.OBJECT_ID, FilterOperator.LESS_THAN, commentId)
                )).addSort(Keys.OBJECT_ID, SortDirection.ASCENDING);

                break;
            case UserExt.USER_COMMENT_VIEW_MODE_C_REALTIME:
                numQuery.setFilter(CompositeFilterOperator.and(
                        new PropertyFilter(Comment.COMMENT_ON_ARTICLE_ID, FilterOperator.EQUAL, articleId),
                        new PropertyFilter(Keys.OBJECT_ID, FilterOperator.GREATER_THAN, commentId)
                )).addSort(Keys.OBJECT_ID, SortDirection.DESCENDING);

                break;
        }

        Stopwatchs.start("Get comment page");
        try {
            final long num = commentRepository.count(numQuery);
            return (int) ((num / pageSize) + 1);
        } catch (final Exception e) {
            LOGGER.log(Level.ERROR, "Gets comment page failed", e);

            return 1;
        } finally {
            Stopwatchs.end();
        }
    }

    /**
     * Gets original comment of a comment specified by the given comment id.
     *
     * @param avatarViewMode  the specified avatar view mode
     * @param commentViewMode the specified comment view mode
     * @param commentId       the given comment id
     * @return original comment, return {@code null} if not found
     */
    public JSONObject getOriginalComment(final int avatarViewMode, final int commentViewMode, final String commentId) {
        try {
            final JSONObject comment = commentRepository.get(commentId);

            organizeComment(avatarViewMode, comment);

            final int pageSize = Symphonys.getInt("articleCommentsPageSize");

            final JSONObject ret = new JSONObject();

            final JSONObject commentAuthor = comment.optJSONObject(Comment.COMMENT_T_COMMENTER);
            if (UserExt.USER_XXX_STATUS_C_PRIVATE == commentAuthor.optInt(UserExt.USER_UA_STATUS)) {
                ret.put(Comment.COMMENT_UA, "");
            }

            ret.put(Comment.COMMENT_T_AUTHOR_NAME, comment.optString(Comment.COMMENT_T_AUTHOR_NAME));
            ret.put(Comment.COMMENT_T_AUTHOR_THUMBNAIL_URL, comment.optString(Comment.COMMENT_T_AUTHOR_THUMBNAIL_URL));
            ret.put(Common.TIME_AGO, comment.optString(Common.TIME_AGO));
            ret.put(Comment.COMMENT_CREATE_TIME_STR, comment.optString(Comment.COMMENT_CREATE_TIME_STR));
            ret.put(Common.REWARED_COUNT, comment.optString(Common.REWARED_COUNT));
            ret.put(Common.REWARDED, comment.optBoolean(Common.REWARDED));
            ret.put(Keys.OBJECT_ID, commentId);
            ret.put(Comment.COMMENT_CONTENT, comment.optString(Comment.COMMENT_CONTENT));
            ret.put(Pagination.PAGINATION_CURRENT_PAGE_NUM, getCommentPage(
                    comment.optString(Comment.COMMENT_ON_ARTICLE_ID), commentId,
                    commentViewMode, pageSize));

            return ret;
        } catch (final RepositoryException e) {
            LOGGER.log(Level.ERROR, "Get replies failed", e);

            return null;
        }
    }

    /**
     * Gets replies of a comment specified by the given comment id.
     *
     * @param avatarViewMode  the specified avatar view mode
     * @param commentViewMode the specified comment view mode
     * @param commentId       the given comment id
     * @return a list of replies, return an empty list if not found
     */
    public List<JSONObject> getReplies(final int avatarViewMode, final int commentViewMode, final String commentId) {
        final Query query = new Query().addSort(Keys.OBJECT_ID, SortDirection.DESCENDING).
                setPageSize(Integer.MAX_VALUE).setCurrentPageNum(1).setPageCount(1)
                .setFilter(CompositeFilterOperator.and(
                        new PropertyFilter(Comment.COMMENT_ORIGINAL_COMMENT_ID, FilterOperator.EQUAL, commentId),
                        new PropertyFilter(Comment.COMMENT_STATUS, FilterOperator.EQUAL, Comment.COMMENT_STATUS_C_VALID)
                ));
        try {
            final List<JSONObject> comments = CollectionUtils.jsonArrayToList(
                    commentRepository.get(query).optJSONArray(Keys.RESULTS));

            organizeComments(avatarViewMode, comments);

            final int pageSize = Symphonys.getInt("articleCommentsPageSize");

            final List<JSONObject> ret = new ArrayList<>();
            for (final JSONObject comment : comments) {
                final JSONObject reply = new JSONObject();
                ret.add(reply);

                final JSONObject commentAuthor = comment.optJSONObject(Comment.COMMENT_T_COMMENTER);
                if (UserExt.USER_XXX_STATUS_C_PRIVATE == commentAuthor.optInt(UserExt.USER_UA_STATUS)) {
                    reply.put(Comment.COMMENT_UA, "");
                }

                reply.put(Comment.COMMENT_T_AUTHOR_NAME, comment.optString(Comment.COMMENT_T_AUTHOR_NAME));
                reply.put(Comment.COMMENT_T_AUTHOR_THUMBNAIL_URL, comment.optString(Comment.COMMENT_T_AUTHOR_THUMBNAIL_URL));
                reply.put(Common.TIME_AGO, comment.optString(Common.TIME_AGO));
                reply.put(Comment.COMMENT_CREATE_TIME_STR, comment.optString(Comment.COMMENT_CREATE_TIME_STR));
                reply.put(Common.REWARED_COUNT, comment.optString(Common.REWARED_COUNT));
                reply.put(Common.REWARDED, comment.optBoolean(Common.REWARDED));
                reply.put(Keys.OBJECT_ID, comment.optString(Keys.OBJECT_ID));
                reply.put(Comment.COMMENT_CONTENT, comment.optString(Comment.COMMENT_CONTENT));
                reply.put(Pagination.PAGINATION_CURRENT_PAGE_NUM, getCommentPage(
                        comment.optString(Comment.COMMENT_ON_ARTICLE_ID), reply.optString(Keys.OBJECT_ID),
                        commentViewMode, pageSize));
            }

            return ret;
        } catch (final RepositoryException e) {
            LOGGER.log(Level.ERROR, "Get replies failed", e);

            return Collections.emptyList();
        }
    }

    /**
     * Gets nice comments of an article specified by the given article id.
     *
     * @param avatarViewMode  the specified avatar view mode
     * @param commentViewMode the specified comment view mode
     * @param articleId       the given article id
     * @param fetchSize       the specified fetch size
     * @return a list of nice comments, return an empty list if not found
     */
    public List<JSONObject> getNiceComments(final int avatarViewMode, final int commentViewMode,
                                            final String articleId, final int fetchSize) {
        Stopwatchs.start("Gets nice comments");
        try {
            final Query query = new Query().addSort(Comment.COMMENT_SCORE, SortDirection.DESCENDING).
                    setPageSize(fetchSize).setCurrentPageNum(1).setPageCount(1)
                    .setFilter(CompositeFilterOperator.and(
                            new PropertyFilter(Comment.COMMENT_ON_ARTICLE_ID, FilterOperator.EQUAL, articleId),
                            new PropertyFilter(Comment.COMMENT_SCORE, FilterOperator.GREATER_THAN, 0D),
                            new PropertyFilter(Comment.COMMENT_STATUS, FilterOperator.EQUAL, Comment.COMMENT_STATUS_C_VALID)
                    ));
            try {
                final List<JSONObject> ret = CollectionUtils.jsonArrayToList(
                        commentRepository.get(query).optJSONArray(Keys.RESULTS));

                organizeComments(avatarViewMode, ret);

                final int pageSize = Symphonys.getInt("articleCommentsPageSize");

                for (final JSONObject comment : ret) {
                    comment.put(Pagination.PAGINATION_CURRENT_PAGE_NUM, getCommentPage(
                            articleId, comment.optString(Keys.OBJECT_ID),
                            commentViewMode, pageSize));
                }

                return ret;
            } catch (final RepositoryException e) {
                LOGGER.log(Level.ERROR, "Get nice comments failed", e);

                return Collections.emptyList();
            }
        } finally {
            Stopwatchs.end();
        }
    }

    /**
     * Gets comment count of the specified day.
     *
     * @param day the specified day
     * @return comment count
     */
    public int getCommentCntInDay(final Date day) {
        final long time = day.getTime();
        final long start = Times.getDayStartTime(time);
        final long end = Times.getDayEndTime(time);

        final Query query = new Query().setFilter(CompositeFilterOperator.and(
                new PropertyFilter(Keys.OBJECT_ID, FilterOperator.GREATER_THAN_OR_EQUAL, start),
                new PropertyFilter(Keys.OBJECT_ID, FilterOperator.LESS_THAN, end),
                new PropertyFilter(Comment.COMMENT_STATUS, FilterOperator.EQUAL, Comment.COMMENT_STATUS_C_VALID)
        ));

        try {
            return (int) commentRepository.count(query);
        } catch (final RepositoryException e) {
            LOGGER.log(Level.ERROR, "Count day comment failed", e);

            return 1;
        }
    }

    /**
     * Gets comment count of the specified month.
     *
     * @param day the specified month
     * @return comment count
     */
    public int getCommentCntInMonth(final Date day) {
        final long time = day.getTime();
        final long start = Times.getMonthStartTime(time);
        final long end = Times.getMonthEndTime(time);

        final Query query = new Query().setFilter(CompositeFilterOperator.and(
                new PropertyFilter(Keys.OBJECT_ID, FilterOperator.GREATER_THAN_OR_EQUAL, start),
                new PropertyFilter(Keys.OBJECT_ID, FilterOperator.LESS_THAN, end),
                new PropertyFilter(Comment.COMMENT_STATUS, FilterOperator.EQUAL, Comment.COMMENT_STATUS_C_VALID)
        ));

        try {
            return (int) commentRepository.count(query);
        } catch (final RepositoryException e) {
            LOGGER.log(Level.ERROR, "Count month comment failed", e);

            return 1;
        }
    }

    /**
     * Gets a comment with {@link #organizeComment(int, JSONObject)} by the specified comment id.
     *
     * @param avatarViewMode the specified avatar view mode
     * @param commentId      the specified comment id
     * @return comment, returns {@code null} if not found
     * @throws ServiceException service exception
     */
    public JSONObject getCommentById(final int avatarViewMode, final String commentId) throws ServiceException {

        try {
            final JSONObject ret = commentRepository.get(commentId);
            if (null == ret) {
                return null;
            }

            organizeComment(avatarViewMode, ret);

            return ret;
        } catch (final RepositoryException e) {
            LOGGER.log(Level.ERROR, e.getMessage(), e);

            throw new ServiceException("Gets comment[id=" + commentId + "] failed");
        }
    }

    /**
     * Gets a comment by the specified id.
     *
     * @param commentId the specified id
     * @return comment, return {@code null} if not found
     * @throws ServiceException service exception
     */
    public JSONObject getComment(final String commentId) throws ServiceException {
        try {
            final JSONObject ret = commentRepository.get(commentId);

            if (null == ret) {
                return null;
            }

            return ret;
        } catch (final RepositoryException e) {
            LOGGER.log(Level.ERROR, "Gets a comment [commentId=" + commentId + "] failed", e);
            throw new ServiceException(e);
        }
    }

    /**
     * Gets the latest comments with the specified fetch size.
     * <p>
     * <p>
     * The returned comments content is plain text.
     * </p>
     *
     * @param avatarViewMode the specified avatar view mode
     * @param fetchSize      the specified fetch size
     * @return the latest comments, returns an empty list if not found
     * @throws ServiceException service exception
     */
    public List<JSONObject> getLatestComments(final int avatarViewMode, final int fetchSize) throws ServiceException {
        final Query query = new Query().addSort(Comment.COMMENT_CREATE_TIME, SortDirection.DESCENDING)
                .setCurrentPageNum(1).setPageSize(fetchSize).setPageCount(1);
        try {
            final JSONObject result = commentRepository.get(query);
            final List<JSONObject> ret = CollectionUtils.<JSONObject>jsonArrayToList(result.optJSONArray(Keys.RESULTS));

            for (final JSONObject comment : ret) {
                comment.put(Comment.COMMENT_CREATE_TIME, comment.optLong(Comment.COMMENT_CREATE_TIME));
                final String articleId = comment.optString(Comment.COMMENT_ON_ARTICLE_ID);
                final JSONObject article = articleRepository.get(articleId);
                comment.put(Comment.COMMENT_T_ARTICLE_TITLE, Emotions.clear(article.optString(Article.ARTICLE_TITLE)));
                comment.put(Comment.COMMENT_T_ARTICLE_PERMALINK, article.optString(Article.ARTICLE_PERMALINK));

                final String commenterId = comment.optString(Comment.COMMENT_AUTHOR_ID);
                final JSONObject commenter = userRepository.get(commenterId);

                if (UserExt.USER_STATUS_C_INVALID == commenter.optInt(UserExt.USER_STATUS)
                        || Comment.COMMENT_STATUS_C_INVALID == comment.optInt(Comment.COMMENT_STATUS)) {
                    comment.put(Comment.COMMENT_CONTENT, langPropsService.get("commentContentBlockLabel"));
                }

                if (Article.ARTICLE_TYPE_C_DISCUSSION == article.optInt(Article.ARTICLE_TYPE)) {
                    comment.put(Comment.COMMENT_CONTENT, "....");
                }

                String content = comment.optString(Comment.COMMENT_CONTENT);
                content = Emotions.clear(content);
                content = Jsoup.clean(content, Whitelist.none());
                if (StringUtils.isBlank(content)) {
                    comment.put(Comment.COMMENT_CONTENT, "....");
                } else {
                    comment.put(Comment.COMMENT_CONTENT, content);
                }

                final String commenterEmail = commenter.optString(User.USER_EMAIL);
                String avatarURL = Symphonys.get("defaultThumbnailURL");
                if (!UserExt.DEFAULT_CMTER_EMAIL.equals(commenterEmail)) {
                    avatarURL = avatarQueryService.getAvatarURLByUser(avatarViewMode, commenter, "20");
                }
                commenter.put(UserExt.USER_AVATAR_URL, avatarURL);

                comment.put(Comment.COMMENT_T_COMMENTER, commenter);
            }

            return ret;
        } catch (final RepositoryException e) {
            LOGGER.log(Level.ERROR, "Gets user comments failed", e);
            throw new ServiceException(e);
        }
    }

    /**
     * Gets the user comments with the specified user id, page number and page size.
     *
     * @param avatarViewMode the specified avatar view mode
     * @param userId         the specified user id
     * @param anonymous      the specified comment anonymous
     * @param currentPageNum the specified page number
     * @param pageSize       the specified page size
     * @param viewer         the specified viewer, may be {@code null}
     * @return user comments, return an empty list if not found
     * @throws ServiceException service exception
     */
    public List<JSONObject> getUserComments(final int avatarViewMode, final String userId, final int anonymous,
                                            final int currentPageNum, final int pageSize, final JSONObject viewer) throws ServiceException {
        final Query query = new Query().addSort(Comment.COMMENT_CREATE_TIME, SortDirection.DESCENDING)
                .setCurrentPageNum(currentPageNum).setPageSize(pageSize).
                        setFilter(CompositeFilterOperator.and(
                                new PropertyFilter(Comment.COMMENT_AUTHOR_ID, FilterOperator.EQUAL, userId),
                                new PropertyFilter(Comment.COMMENT_ANONYMOUS, FilterOperator.EQUAL, anonymous)
                        ));
        try {
            final JSONObject result = commentRepository.get(query);
            final List<JSONObject> ret = CollectionUtils.<JSONObject>jsonArrayToList(result.optJSONArray(Keys.RESULTS));
            if (ret.isEmpty()) {
                return ret;
            }

            final JSONObject pagination = result.optJSONObject(Pagination.PAGINATION);
            final int recordCount = pagination.optInt(Pagination.PAGINATION_RECORD_COUNT);
            final int pageCount = pagination.optInt(Pagination.PAGINATION_PAGE_COUNT);

            final JSONObject first = ret.get(0);
            first.put(Pagination.PAGINATION_RECORD_COUNT, recordCount);
            first.put(Pagination.PAGINATION_PAGE_COUNT, pageCount);

            for (final JSONObject comment : ret) {
                comment.put(Comment.COMMENT_CREATE_TIME, new Date(comment.optLong(Comment.COMMENT_CREATE_TIME)));

                final String articleId = comment.optString(Comment.COMMENT_ON_ARTICLE_ID);
                final JSONObject article = articleRepository.get(articleId);

                comment.put(Comment.COMMENT_T_ARTICLE_TITLE,
                        Article.ARTICLE_STATUS_C_INVALID == article.optInt(Article.ARTICLE_STATUS)
                                ? langPropsService.get("articleTitleBlockLabel")
                                : Emotions.convert(article.optString(Article.ARTICLE_TITLE)));
                comment.put(Comment.COMMENT_T_ARTICLE_TYPE, article.optInt(Article.ARTICLE_TYPE));
                comment.put(Comment.COMMENT_T_ARTICLE_PERMALINK, article.optString(Article.ARTICLE_PERMALINK));
                comment.put(Comment.COMMENT_T_ARTICLE_PERFECT, article.optInt(Article.ARTICLE_PERFECT));

                final JSONObject commenter = userRepository.get(userId);
                comment.put(Comment.COMMENT_T_COMMENTER, commenter);

                final String articleAuthorId = article.optString(Article.ARTICLE_AUTHOR_ID);
                final JSONObject articleAuthor = userRepository.get(articleAuthorId);
                final String articleAuthorName = articleAuthor.optString(User.USER_NAME);
                if (Article.ARTICLE_ANONYMOUS_C_PUBLIC == article.optInt(Article.ARTICLE_ANONYMOUS)) {
                    comment.put(Comment.COMMENT_T_ARTICLE_AUTHOR_NAME, articleAuthorName);
                    comment.put(Comment.COMMENT_T_ARTICLE_AUTHOR_URL, "/member/" + articleAuthor.optString(User.USER_NAME));
                    final String articleAuthorThumbnailURL = avatarQueryService.getAvatarURLByUser(
                            avatarViewMode, articleAuthor, "48");
                    comment.put(Comment.COMMENT_T_ARTICLE_AUTHOR_THUMBNAIL_URL, articleAuthorThumbnailURL);
                } else {
                    comment.put(Comment.COMMENT_T_ARTICLE_AUTHOR_NAME, UserExt.ANONYMOUS_USER_NAME);
                    comment.put(Comment.COMMENT_T_ARTICLE_AUTHOR_URL, "");
                    comment.put(Comment.COMMENT_T_ARTICLE_AUTHOR_THUMBNAIL_URL, avatarQueryService.getDefaultAvatarURL("48"));
                }

                if (Article.ARTICLE_TYPE_C_DISCUSSION == article.optInt(Article.ARTICLE_TYPE)
                        && Article.ARTICLE_ANONYMOUS_C_PUBLIC == article.optInt(Article.ARTICLE_ANONYMOUS)) {
                    final String msgContent = langPropsService.get("articleDiscussionLabel").
                            replace("{user}", "<a href='" + Latkes.getServePath()
                                    + "/member/" + articleAuthorName + "'>" + articleAuthorName + "</a>");

                    if (null == viewer) {
                        comment.put(Comment.COMMENT_CONTENT, msgContent);
                    } else {
                        final String commenterName = commenter.optString(User.USER_NAME);
                        final String viewerUserName = viewer.optString(User.USER_NAME);
                        final String viewerRole = viewer.optString(User.USER_ROLE);

                        if (!commenterName.equals(viewerUserName) && !Role.ROLE_ID_C_ADMIN.equals(viewerRole)) {
                            final String articleContent = article.optString(Article.ARTICLE_CONTENT);
                            final Set<String> userNames = userQueryService.getUserNames(articleContent);

                            boolean invited = false;
                            for (final String userName : userNames) {
                                if (userName.equals(viewerUserName)) {
                                    invited = true;

                                    break;
                                }
                            }

                            if (!invited) {
                                comment.put(Comment.COMMENT_CONTENT, msgContent);
                            }
                        }
                    }
                }

                processCommentContent(comment);
            }

            return ret;
        } catch (final RepositoryException e) {
            LOGGER.log(Level.ERROR, "Gets user comments failed", e);
            throw new ServiceException(e);
        }
    }

    /**
     * Gets the article comments with the specified article id, page number and page size.
     *
     * @param avatarViewMode the specified avatar view mode
     * @param articleId      the specified article id
     * @param currentPageNum the specified page number
     * @param pageSize       the specified page size
     * @param sortMode       the specified sort mode (traditional: 0, real time: 1)
     * @return comments, return an empty list if not found
     * @throws ServiceException service exception
     */
    public List<JSONObject> getArticleComments(final int avatarViewMode,
                                               final String articleId, final int currentPageNum, final int pageSize, final int sortMode)
            throws ServiceException {
        Stopwatchs.start("Get comments");

        final Query query = new Query()
                .setPageCount(1).setCurrentPageNum(currentPageNum).setPageSize(pageSize)
                .setFilter(new PropertyFilter(Comment.COMMENT_ON_ARTICLE_ID, FilterOperator.EQUAL, articleId));

        if (UserExt.USER_COMMENT_VIEW_MODE_C_REALTIME == sortMode) {
            query.addSort(Keys.OBJECT_ID, SortDirection.DESCENDING);
        } else {
            query.addSort(Keys.OBJECT_ID, SortDirection.ASCENDING);
        }

        try {
            Stopwatchs.start("Query comments");
            JSONObject result;
            try {
                result = commentRepository.get(query);
            } finally {
                Stopwatchs.end();
            }
            final List<JSONObject> ret = CollectionUtils.<JSONObject>jsonArrayToList(result.optJSONArray(Keys.RESULTS));

            organizeComments(avatarViewMode, ret);

            Stopwatchs.start("Revision, paging, original");
            try {
                for (final JSONObject comment : ret) {
                    final String commentId = comment.optString(Keys.OBJECT_ID);

                    // Fill revision count
                    comment.put(Comment.COMMENT_REVISION_COUNT,
                            revisionQueryService.count(commentId, Revision.DATA_TYPE_C_COMMENT));

                    final String originalCmtId = comment.optString(Comment.COMMENT_ORIGINAL_COMMENT_ID);
                    if (StringUtils.isBlank(originalCmtId)) {
                        continue;
                    }

                    // Fill page number
                    comment.put(Pagination.PAGINATION_CURRENT_PAGE_NUM,
                            getCommentPage(articleId, originalCmtId, sortMode, pageSize));

                    // Fill original comment
                    final JSONObject originalCmt = commentRepository.get(originalCmtId);
                    organizeComment(avatarViewMode, originalCmt);
                    comment.put(Comment.COMMENT_T_ORIGINAL_AUTHOR_THUMBNAIL_URL,
                            originalCmt.optString(Comment.COMMENT_T_AUTHOR_THUMBNAIL_URL));
                }
            } finally {
                Stopwatchs.end();
            }

            return ret;
        } catch (final RepositoryException e) {
            LOGGER.log(Level.ERROR, "Gets article [" + articleId + "] comments failed", e);
            throw new ServiceException(e);
        } finally {
            Stopwatchs.end();
        }
    }

    /**
     * Gets comments by the specified request json object.
     *
     * @param avatarViewMode    the specified avatar view mode
     * @param requestJSONObject the specified request json object, for example,
     *                          "paginationCurrentPageNum": 1,
     *                          "paginationPageSize": 20,
     *                          "paginationWindowSize": 10,
     *                          , see {@link Pagination} for more details
     * @param commentFields     the specified article fields to return
     * @return for example,      <pre>
     * {
     *     "pagination": {
     *         "paginationPageCount": 100,
     *         "paginationPageNums": [1, 2, 3, 4, 5]
     *     },
     *     "comments": [{
     *         "oId": "",
     *         "commentContent": "",
     *         "commentCreateTime": "",
     *         ....
     *      }, ....]
     * }
     * </pre>
     * @throws ServiceException service exception
     * @see Pagination
     */

    public JSONObject getComments(final int avatarViewMode,
                                  final JSONObject requestJSONObject, final Map<String, Class<?>> commentFields) throws ServiceException {
        final JSONObject ret = new JSONObject();

        final int currentPageNum = requestJSONObject.optInt(Pagination.PAGINATION_CURRENT_PAGE_NUM);
        final int pageSize = requestJSONObject.optInt(Pagination.PAGINATION_PAGE_SIZE);
        final int windowSize = requestJSONObject.optInt(Pagination.PAGINATION_WINDOW_SIZE);
        final Query query = new Query().setCurrentPageNum(currentPageNum).setPageSize(pageSize)
                .addSort(Comment.COMMENT_CREATE_TIME, SortDirection.DESCENDING);
        for (final Map.Entry<String, Class<?>> commentField : commentFields.entrySet()) {
            query.addProjection(commentField.getKey(), commentField.getValue());
        }

        JSONObject result = null;

        try {
            result = commentRepository.get(query);
        } catch (final RepositoryException e) {
            LOGGER.log(Level.ERROR, "Gets comments failed", e);

            throw new ServiceException(e);
        }

        final int pageCount = result.optJSONObject(Pagination.PAGINATION).optInt(Pagination.PAGINATION_PAGE_COUNT);

        final JSONObject pagination = new JSONObject();
        ret.put(Pagination.PAGINATION, pagination);
        final List<Integer> pageNums = Paginator.paginate(currentPageNum, pageSize, pageCount, windowSize);
        pagination.put(Pagination.PAGINATION_PAGE_COUNT, pageCount);
        pagination.put(Pagination.PAGINATION_PAGE_NUMS, pageNums);

        final JSONArray data = result.optJSONArray(Keys.RESULTS);
        final List<JSONObject> comments = CollectionUtils.<JSONObject>jsonArrayToList(data);

        try {
            for (final JSONObject comment : comments) {
                organizeComment(avatarViewMode, comment);

                final String articleId = comment.optString(Comment.COMMENT_ON_ARTICLE_ID);
                final JSONObject article = articleRepository.get(articleId);

                comment.put(Comment.COMMENT_T_ARTICLE_TITLE,
                        Article.ARTICLE_STATUS_C_INVALID == article.optInt(Article.ARTICLE_STATUS)
                                ? langPropsService.get("articleTitleBlockLabel")
                                : Emotions.convert(article.optString(Article.ARTICLE_TITLE)));
                comment.put(Comment.COMMENT_T_ARTICLE_PERMALINK, article.optString(Article.ARTICLE_PERMALINK));
            }
        } catch (final RepositoryException e) {
            LOGGER.log(Level.ERROR, "Organizes comments failed", e);

            throw new ServiceException(e);
        }

        ret.put(Comment.COMMENTS, comments);

        return ret;
    }

    /**
     * Organizes the specified comments.
     *
     * @param avatarViewMode the specified avatar view mode
     * @param comments       the specified comments
     * @throws RepositoryException repository exception
     * @see #organizeComment(int, JSONObject)
     */
    private void organizeComments(final int avatarViewMode, final List<JSONObject> comments) throws RepositoryException {
        Stopwatchs.start("Organizes comments");

        try {
            for (final JSONObject comment : comments) {
                organizeComment(avatarViewMode, comment);
            }
        } finally {
            Stopwatchs.end();
        }
    }

    /**
     * Organizes the specified comment.
     * <ul>
     * <li>converts comment create time (long) to date type</li>
     * <li>generates comment author thumbnail URL</li>
     * <li>generates comment author URL</li>
     * <li>generates comment author name</li>
     * <li>generates &#64;username home URL</li>
     * <li>markdowns comment content</li>
     * <li>block comment if need</li>
     * <li>generates emotion images</li>
     * <li>generates time ago text</li>
     * <li>anonymous process</li>
     * </ul>
     *
     * @param avatarViewMode the specified avatar view mode
     * @param comment        the specified comment
     * @throws RepositoryException repository exception
     */
    private void organizeComment(final int avatarViewMode, final JSONObject comment) throws RepositoryException {
        Stopwatchs.start("Organize comment");

        try {
            comment.put(Common.TIME_AGO, Times.getTimeAgo(comment.optLong(Comment.COMMENT_CREATE_TIME), Locales.getLocale()));
            final Date createDate = new Date(comment.optLong(Comment.COMMENT_CREATE_TIME));
            comment.put(Comment.COMMENT_CREATE_TIME, createDate);
            comment.put(Comment.COMMENT_CREATE_TIME_STR, DateFormatUtils.format(createDate, "yyyy-MM-dd HH:mm:ss"));

            final String authorId = comment.optString(Comment.COMMENT_AUTHOR_ID);
            final JSONObject author = userRepository.get(authorId);

            comment.put(Comment.COMMENT_T_COMMENTER, author);
            if (Comment.COMMENT_ANONYMOUS_C_PUBLIC == comment.optInt(Comment.COMMENT_ANONYMOUS)) {
                comment.put(Comment.COMMENT_T_AUTHOR_NAME, author.optString(User.USER_NAME));
                comment.put(Comment.COMMENT_T_AUTHOR_URL, author.optString(User.USER_URL));
                final String thumbnailURL = avatarQueryService.getAvatarURLByUser(avatarViewMode, author, "48");
                comment.put(Comment.COMMENT_T_AUTHOR_THUMBNAIL_URL, thumbnailURL);
            } else {
                comment.put(Comment.COMMENT_T_AUTHOR_NAME, UserExt.ANONYMOUS_USER_NAME);
                comment.put(Comment.COMMENT_T_AUTHOR_URL, "");
                comment.put(Comment.COMMENT_T_AUTHOR_THUMBNAIL_URL, avatarQueryService.getDefaultAvatarURL("48"));
            }

            processCommentContent(comment);
        } finally {
            Stopwatchs.end();
        }
    }

    /**
     * Processes the specified comment content.
     * <p>
     * <ul>
     * <li>Generates &#64;username home URL</li>
     * <li>Markdowns</li>
     * <li>Blocks comment if need</li>
     * <li>Generates emotion images</li>
     * <li>Generates article link with article id</li>
     * </ul>
     *
     * @param comment the specified comment, for example,
     *                "commentContent": "",
     *                ....,
     *                "commenter": {}
     */
    private void processCommentContent(final JSONObject comment) {
        final JSONObject commenter = comment.optJSONObject(Comment.COMMENT_T_COMMENTER);

        final boolean sync = StringUtils.isNotBlank(comment.optString(Comment.COMMENT_CLIENT_COMMENT_ID));
        comment.put(Common.FROM_CLIENT, sync);

        if (Comment.COMMENT_STATUS_C_INVALID == comment.optInt(Comment.COMMENT_STATUS)
                || UserExt.USER_STATUS_C_INVALID == commenter.optInt(UserExt.USER_STATUS)) {
            comment.put(Comment.COMMENT_CONTENT, langPropsService.get("commentContentBlockLabel"));

            return;
        }

        String commentContent = comment.optString(Comment.COMMENT_CONTENT);

        commentContent = shortLinkQueryService.linkArticle(commentContent);
        commentContent = shortLinkQueryService.linkTag(commentContent);
        commentContent = Emotions.convert(commentContent);
        commentContent = Markdowns.toHTML(commentContent);
        commentContent = Markdowns.clean(commentContent, "");
        commentContent = MP3Players.render(commentContent);
        commentContent = VideoPlayers.render(commentContent);

        if (sync) {
            // "<i class='ft-small'>by 88250</i>"
            String syncCommenterName = StringUtils.substringAfter(commentContent, "<i class=\"ft-small\">by ");
            syncCommenterName = StringUtils.substringBefore(syncCommenterName, "</i>");

            if (UserRegisterValidation.invalidUserName(syncCommenterName)) {
                syncCommenterName = UserExt.ANONYMOUS_USER_NAME;
            }

            commentContent = commentContent.replaceAll("<i class=\"ft-small\">by .*</i>", "");

            comment.put(Comment.COMMENT_T_AUTHOR_NAME, syncCommenterName);
        }

        comment.put(Comment.COMMENT_CONTENT, commentContent);
    }
}
