import redis.clients.jedis.Jedis;
import redis.clients.jedis.ZParams;

import java.util.*;

/**
 * Redis 实战
 * Chapter01 - 实现网站投票功能
 * 具体功能如下:
 * 1)发布文章 - postArticle
 * 2)对文章进行投票 - articleVote
 * 3)根据score值由大到小,分页获取文章列表 - getArticles
 * 4) 将文章加入分组 - addGroups
 * 5) 获取当前分组中根据分值由大到小,第几页的文章列表 - getGroupArticles
 * 6) 打印文章列表详细信息 - printArticles
 */

public class Chapter01 {
    private static final int ONE_WEEK_IN_SECONDS = 7 * 86400;
    private static final int VOTE_SCORE = 432;
    private static final int ARTICLES_PER_PAGE = 25;

    public static final void main(String[] args) {
        new Chapter01().run();
    }

    public void run() {
        Jedis conn = new Jedis("localhost");
        conn.select(15);

        // liuxianzhishou 发布一篇文章,名为 《Redis整理》,链接为“https://blog.nowcoder.net/n/3d525a6454ee45b2ace4eb9e9ebab23a”, 返回结果为文章ID
        String articleId = postArticle(
            conn, "liuxianzhishou", "Redis整理", "https://blog.nowcoder.net/n/3d525a6454ee45b2ace4eb9e9ebab23a");
        System.out.println("We posted a new article with id: " + articleId); // 打印文章ID
        System.out.println("Its HASH looks like:");
        // 获取文章详细信息
        Map<String,String> articleData = conn.hgetAll("article:" + articleId);
        for (Map.Entry<String,String> entry : articleData.entrySet()){
            System.out.println("  " + entry.getKey() + ": " + entry.getValue()); // 打印文章详细信息
        }

        System.out.println();

        // tun 对 刚刚 liuxianzhishou 发布的文章进行了投票
        articleVote(conn, "tun", "article:" + articleId);
        String votes = conn.hget("article:" + articleId, "votes"); // 获取文章当前获票数
        System.out.println("We voted for the article, it now has votes: " + votes); // 打印该文当前票数
        assert Integer.parseInt(votes) > 1; // assert关键字,断言,用于调试程序,TRUE时继续执行,FALSE时抛出异常,并终止执行

        // 获取第一页,也就是分值最高的文章列表
        System.out.println("The currently highest-scoring articles are:");
        List<Map<String,String>> articles = getArticles(conn, 1);
        printArticles(articles); // 打印文章列表
        assert articles.size() >= 1;

        // 对刚刚 liuxianzhishou 发布的文章加入到分组Redis中
        addGroups(conn, articleId, new String[]{"Redis"});
        System.out.println("We added the article to a new group, other articles include:"); // 打印
        // 获取分组Redis中的文章列表
        articles = getGroupArticles(conn, "Redis", 1);
        printArticles(articles); // 打印
        assert articles.size() >= 1;
    }

    /**
     * 发布文章
     * @param conn - Redis
     * @param user - 发布者
     * @param title - 文章标题
     * @param link - 文章链接
     * @return - 文章编号
     */
    public String postArticle(Jedis conn, String user, String title, String link) {
        // 给当前文章分配ID
        String articleId = String.valueOf(conn.incr("article:"));

        String voted = "voted:" + articleId; // 将该篇文章放入可投票set中
        conn.sadd(voted, user); // 默认发布者已经对自己的文章进行投票
        conn.expire(voted, ONE_WEEK_IN_SECONDS); // 对这篇文章的投票时间设置为一周,因为只会发布一次文章,因此过期时间只设置了一次,并不会出现多次延长过期时间的行为

        long now = System.currentTimeMillis() / 1000; // 获取当前时间,以秒记
        String article = "article:" + articleId;
        // 记录该文详细信息
        HashMap<String,String> articleData = new HashMap<String,String>();
        articleData.put("title", title); // 标题
        articleData.put("link", link); // 链接
        articleData.put("user", user); // 发布者
        articleData.put("now", String.valueOf(now)); // 时间戳
        articleData.put("votes", "1"); // 已有投票数1
        // 将该文详细信息作为对象,存入Redis中
        conn.hmset(article, articleData);
        // 将该文的分值存到zset中
        conn.zadd("score:", now + VOTE_SCORE, article);
        // 将该文的时间信息存入zset中
        conn.zadd("time:", now, article);

        return articleId;
    }

    /**
     * 对文章进行投票
     * @param conn Redis
     * @param user 投票人
     * @param article 文章
     */
    public void articleVote(Jedis conn, String user, String article) {
        long cutoff = (System.currentTimeMillis() / 1000) - ONE_WEEK_IN_SECONDS;
        if (conn.zscore("time:", article) < cutoff){
            return;
        }

        String articleId = article.substring(article.indexOf(':') + 1);
        if (conn.sadd("voted:" + articleId, user) == 1) {
            conn.zincrby("score:", VOTE_SCORE, article);
            conn.hincrBy(article, "votes", 1);
        }
    }


    /**
     * 根据score值由大到小,分页获取文章列表
     * @param conn - Redis
     * @param page - 页码
     * @return - 文章列表
     */
    public List<Map<String,String>> getArticles(Jedis conn, int page) {
        return getArticles(conn, page, "score:");
    }

    public List<Map<String,String>> getArticles(Jedis conn, int page, String order) {
        int start = (page - 1) * ARTICLES_PER_PAGE;
        int end = start + ARTICLES_PER_PAGE - 1; // 记录startIndex & endIndex

        // 获取分值最高的文章列表ID
        Set<String> ids = conn.zrevrange(order, start, end);
        List<Map<String,String>> articles = new ArrayList<Map<String,String>>();
        // 遍历,根据文章列表ID,找到文章详细信息,放入list中
        for (String id : ids){
            Map<String,String> articleData = conn.hgetAll(id);
            articleData.put("id", id);
            articles.add(articleData);
        }
        return articles;
    }

    /**
     * 将文章加入分组
     * @param conn - Redis
     * @param articleId - 文章ID
     * @param toAdd - 分组名称
     */
    public void addGroups(Jedis conn, String articleId, String[] toAdd) {
        String article = "article:" + articleId;
        for (String group : toAdd) {
            conn.sadd("group:" + group, article);
        }
    }

    /**
     * 获取当前分组中根据分值由大到小,第几页的文章列表
     * @param conn - Redis
     * @param group - 分组名称
     * @param page - 页码
     * @return - 文章带详细信息列表
     */
    public List<Map<String,String>> getGroupArticles(Jedis conn, String group, int page) {
        return getGroupArticles(conn, group, page, "score:");
    }

    public List<Map<String,String>> getGroupArticles(Jedis conn, String group, int page, String order) {
        String key = order + group; // 新的zset
        // 为防止查找时间过长,因此结果设置了过期时间,防止查一次找一次,只有过期时间到了,才会进行重新查找,所以可能有更新延时,但问题不大
        if (!conn.exists(key)) {
            ZParams params = new ZParams().aggregate(ZParams.Aggregate.MAX);
            // 将 无序分组set & score文章zset 求交集,交集结果仍为zset,放入 key 中
            conn.zinterstore(key, params, "group:" + group, order);
            conn.expire(key, 60); // 设置过期时间
        }
        return getArticles(conn, page, key);
    }

    /**
     * 打印文章列表详细信息
     * @param articles - 文章列表
     */
    private void printArticles(List<Map<String,String>> articles){
        for (Map<String,String> article : articles){
            System.out.println("  id: " + article.get("id")); // 打印id
            for (Map.Entry<String,String> entry : article.entrySet()){
                if (entry.getKey().equals("id")){
                    continue; // id 已经打印过,直接跳过
                }
                System.out.println("    " + entry.getKey() + ": " + entry.getValue());
            }
        }
    }
}