<?php
/**
 *......................位图......................
 *                       _oo0oo_
 *                      o8888888o
 *                      88" . "88
 *                      (| -_- |)
 *                      0\  =  /0
 *                    ___/`---'\___
 *                  .' \\|     |// '.
 *                 / \\|||  :  |||// \
 *                / _||||| -卍-|||||- \
 *               |   | \\\  -  /// |   |
 *               | \_|  ''\---/''  |_/ |
 *               \  .-\__  '-'  ___/-. /
 *             ___'. .'  /--.--\  `. .'___
 *          ."" '<  `.___\_<|>_/___.' >' "".
 *         | | :  `- \`.;`\ _ /`;.`/ - ` : | |
 *         \  \ `_.   \_ __\ /__ _/   .-` /  /
 *     =====`-.____`.___ \_____/___.-`___.-'=====
 *                       `=---='
 *
 *..................佛祖开光 ,永无BUG...................
 *
 * Description: 请简单描述
 * Author: Shuxiaoyuan
 * Email: sxy@shuxiaoyuan.com
 * Website: https://www.shuxiaoyuan.com
 * DateTime: 2023/2/28 15:52
 */

namespace App\Http\Controllers\Redis;

use App\Common\Tools;
use App\Constants\ErrorCode;
use App\Constants\RedisDemo\RedisDemo;
use App\Facades\ToolsFacade;
use Illuminate\Http\Request;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Redis;
use Illuminate\Support\Facades\Validator;

/**
 * Description: Redis 位图
 * Author: Shuxiaoyuan
 * Email: sxy@shuxiaoyuan.com
 * DateTime: 2021/12/29 17:38
 *
 * 基础知识:
 * 1TB = 1024GB
 * 1GB = 1024MB
 * 1MB = 1024KB
 * 1KB = 1024B (byte)
 * 1B  = 8b (bit位)
 *
 * 适用场景:
 * 签到1天送1积分,连续签到2天送2积分,3天送3积分,3天以上均送3积分等。
 * 如果连续签到中断,则重置计数,每月初重置计数。
 * 当月签到满3天领取奖励1,满5天领取奖励2,满7天领取奖励3……等等。
 * 显示用户某个月的签到次数和首次签到时间。
 * 在日历控件上展示用户每月签到情况,可以切换年月显示……等等。
 *
 * 最大位只能设置为 2^32 = (这会将位图限制为 512MB)
 *
 * Class RedisBitmapController
 *
 * @package App\Http\Controllers\Redis
 */
class RedisBitmapController
{
    public string $key = RedisDemo::REDIS_BITMAP;

    public string $tmp_key = RedisDemo::REDIS_BITMAP . ':tmp';

    /**
     * Description: 设置相应位为0或者1
     * Author: Shuxiaoyuan
     * Email: sxy@shuxiaoyuan.com
     * Website: https://www.shuxiaoyuan.com
     * DateTime: 2023/4/14 18:00
     *
     * @param Request $request
     *
     * @return array
     */
    public function setBit(Request $request): array
    {
        $key = $request->input('redis_key');
        if (!$key) {
            $key = $this->tmp_key;
        }

        $bit = (int)$request->input('bit', 66);

        // 返回值:0或1,存储在偏移量的原始位值
        Redis::setbit($key, $bit, 1);

        $status = Redis::getbit($key, $bit);

        $data = [
            'bit'       => $bit,
            'redis_key' => $key,
            'status'    => $status,
            'bitcount'  => Redis::bitcount($key), // 一共有多少位设置成 1
        ];

        Redis::expire($key, 8 * 2400);

        return Tools::outSuccessInfo($data);
    }

    /**
     * Description: 获取相应位是0还是1
     * Author: Shuxiaoyuan
     * Email: sxy@shuxiaoyuan.com
     * Website: https://www.shuxiaoyuan.com
     * DateTime: 2023/4/14 18:07
     *
     * @param Request $request
     *
     * @return array
     */
    public function getBit(Request $request): array
    {
        $key = $request->input('redis_key');
        if (!$key) {
            $key = $this->tmp_key;
        }

        $bit = (int)$request->input('bit', 66);

        $status = Redis::getbit($key, $bit);

        $data = [
            'bit'       => $bit,
            'redis_key' => $key,
            'status'    => $status,
            'bitcount'  => Redis::bitcount($key), // 一共有多少位设置成 1
            // 'value'     => Redis::get($key), // 编码需要转换
        ];

        return Tools::outSuccessInfo($data);
    }

    /**
     * Description: 获取指定范围内1的个数
     * Author: Shuxiaoyuan
     * Email: sxy@shuxiaoyuan.com
     * Website: https://www.shuxiaoyuan.com
     * DateTime: 2023/4/14 18:12
     *
     * @param Request $request
     *
     * @return array
     */
    public function getCountByStartToEnd(Request $request): array
    {
        $key = $request->input('redis_key');
        if (!$key) {
            $key = $this->tmp_key;
        }

        $start_bytes = $request->input('start_bytes', 0);
        $end_bytes   = $request->input('end_bytes', -1);

        // 这里的开始和结束指 byte,从 0 开始算,0到2 查找的是 0到(2+1)*8=24位
        // 0-0 就是 1byte 也指 0-7位
        $bitcount = Redis::bitcount($key, $start_bytes, $end_bytes);

        $data = [
            'redis_key'   => $key,
            'start_bytes' => $start_bytes,
            'end_bytes'   => $end_bytes,
            'bitcount'    => $bitcount,
        ];

        return Tools::outSuccessInfo($data);
    }

    /**
     * Description: 获取指定范围内出现的第一个 0 或 1
     * Author: Shuxiaoyuan
     * Email: sxy@shuxiaoyuan.com
     * Website: https://www.shuxiaoyuan.com
     * DateTime: 2023/4/17 14:10
     *
     * @param Request   $request
     * @param ErrorCode $errorCode
     *
     * @return array|void
     */
    public function getBitPosByStartToEnd(Request $request, ErrorCode $errorCode)
    {
        $validator = Validator::make($request->all(), [
            'start_bytes' => 'required|integer|min:0',
            'end_bytes'   => 'required|integer',
            'status'      => 'required|in:0,1',
        ], $message = [
            'start_bytes.required' => '开始位未设置',
            'start_bytes.integer'  => '开始位必须为整数',
            'start_bytes.min'      => '开始位最小为1',
            'end_bytes.required'   => '结束位未设置',
            'end_bytes.integer'    => '结束位必须为整数',
            'status.required'      => '状态未设置',
            'status.in'            => '状态只能为0或1',
        ]);
        if ($validator->fails()) {
            return $errorCode->getValidatorErrorMessage($validator);
        }

        $key = $request->input('redis_key');
        if (!$key) {
            $key = $this->tmp_key;
        }

        $start_bytes = $request->input('start_bytes', 0);
        $end_bytes   = $request->input('end_bytes', -1);
        $status      = $request->input('status', 1);

        // 这里的开始和结束指 byte,从 0 开始算
        $bitpos = Redis::bitpos($key, $status, $start_bytes, $end_bytes);

        $data = [
            'redis_key'   => $key,
            'start_bytes' => $start_bytes,
            'end_bytes'   => $end_bytes,
            'bitcount'    => Redis::bitcount($key),
            'bitpos'      => $bitpos,
        ];

        return Tools::outSuccessInfo($data);
    }

    /**
     * Description: 设置位,简单字符串演示
     * Author: Shuxiaoyuan
     * Email: sxy@shuxiaoyuan.com
     * DateTime: 2022/1/7 16:07
     *
     * @param Request $request
     *
     * @return array
     */
    public function setBitExample(Request $request): array
    {
        $key = $this->key;

        $string = $request->input('string', 'shuxiao');

        $ascii  = [];
        $bin    = [];
        $offset = [];
        for ($i = 0; $i < strlen($string); $i++) {
            // $o = $string[$i] . $o; // 反转字符串

            // 将字符串转 ASCII 码: "s":115,"h":104,"u":117,"x":120,"i":105,"a":97,"o":111
            $ascii[$string[$i]] = ord($string[$i]);

            // 转成二进制值: "s":"1110011","h":"1101000","u":"1110101","x":"1111000","i":"1101001","a":"1100001","o":"1101111"
            $bin[] = decbin(ord($string[$i]));
        }

        /**
         * 字符 s=>1110011 和字符 h=>1101000 表示如下
         *  高位  字符s  低位   高位  字符h  低位
         * |0|1|1|1|0|0|1|1|  |0|1|1 |0 |1 |0 |0 |0 |   用8位字节表示
         * |7|6|5|4|3|2|1|0|  |7|6|5 |4 |3 |2 |1 |0 |   8个字节中,左边为高位,右边为低位
         * |0|1|2|3|4|5|6|7|……|8|9|10|11|12|13|14|15|   这个是位顺序
         *
         * 注意: 位数组的顺序和字符的位顺序是相反的
         * 所以 s 需要设置的位是: 1、2、3、6、7
         * Redis::setbit($key, 1, 1);
         * Redis::setbit($key, 2, 1);
         * Redis::setbit($key, 3, 1);
         * Redis::setbit($key, 6, 1);
         * Redis::setbit($key, 7, 1);
         * // 应该输出一个 s
         * dd(Redis::get($key));
         *
         * h 需要设置的位是: 9、10、12
         * Redis::setbit($key, 9, 1);
         * Redis::setbit($key, 10, 1);
         * Redis::setbit($key, 12, 1);
         * // 应该输出一个 sh
         * dd(Redis::get($key));
         *
         */

        // 设置偏移量:主要是计算偏移量在多少位
        foreach ($bin as $k => $v) {
            for ($i = 0; $i < strlen($v); $i++) {
                if ($v[$i]) {
                    // 计算偏移量
                    $bit = (int)bcmul($k, 8) + ($i + 1);

                    $offset[] = $bit;
                    Redis::setbit($key, $bit, 1);
                }
            }
        }

        $data = [
            '输入的字符串'   => $string,
            '取出键值'     => Redis::get($key),
            'ascii码'   => $ascii,
            '二进制值'     => $bin,
            '需要设置的偏移量' => $offset,
        ];

        Redis::del($key);

        return Tools::outSuccessInfo($data);
    }

    /**
     * Description: 用户签到
     * Author: Shuxiaoyuan
     * Email: sxy@shuxiaoyuan.com
     * DateTime: 2022/1/10 13:21
     *
     * @param Request   $request
     * @param ErrorCode $errorCode
     *
     * @return array
     */
    public function setUserTask(Request $request, ErrorCode $errorCode): array
    {
        $validator = Validator::make($request->all(), [
            'member_id' => 'required|integer|min:1',
            'date'      => 'required|date_format:Y-m-d',
        ], $message = [
            'member_id.required' => '用户ID未设置',
            'member_id.integer'  => '用户ID必须为整数',
            'member_id.min'      => '用户ID最小为1',
            'date.required'      => '签到日期未设置',
            'date.date_format'   => '签到日期格式不正确',
        ]);

        if ($validator->fails()) {
            return $errorCode->getValidatorErrorMessage($validator);
        }

        $member_id = $request->input('member_id');
        $date      = $request->input('date');
        $year      = date('Y', strtotime($date));

        // 将每一个用户的签到按照年份进行划分,Redis键按照会员和年来划分
        $key = $this->key . ':' . $year . ':' . $member_id;

        // 测试一年的每一天都签到
//        $days = ToolsFacade::getEverydayDate($year . '0101', $year . '1231', 'nd');

        /**
         * 将该年中的月日作为位来设置,这样每一个键最大的存储空间为 1231(bit) / 8 = 154(Bytes)
         * 因为月日不连续,并且第一天是101,所以这样会浪费一部分空间,可以进一步缩减到 365/366 天
         */
//        // 缩短到 366 天方案
//        // 今年的开始天到现在的天数
//        $formatted_dt1  = Carbon::parse(Carbon::make($date)->firstOfYear()->toDateString());
//        $formatted_dt2  = Carbon::parse($date);
//        $date_diff_days = $formatted_dt1->diffInDays($formatted_dt2); // 这个就当做相应位
//        dd($date_diff_days);

        $day = date('md', strtotime($date));

        // 返回值:0或1,存储在偏移量的原始位值
        $status = Redis::setbit($key, (int)$day, 1);

        $bitcount = Redis::bitcount($key);

        // 入库持久化过程略
        $data = [
            'redis_key'    => $key,
            'status'       => $status,
            'count_number' => $bitcount,
        ];

        Redis::expire($key, 8 * 3600);

        return Tools::outSuccessInfo($data);
    }

    /**
     * Description: 统计用户某段时间内的签到次数
     * Author: Shuxiaoyuan
     * Email: sxy@shuxiaoyuan.com
     * Website: https://www.shuxiaoyuan.com
     * DateTime: 2023/4/14 14:51
     *
     * @param Request $request
     *
     * @return array
     */
    public function getUserTaskByStartToEnd(Request $request): array
    {
        $member_id = $request->input('member_id');

        // 开始时间和结束时间-这里假设不考虑跨年的情况
        $start_date = $request->input('start_date', '2023-01-01');
        $end_date   = $request->input('end_date', '2023-01-31');

        $year = date('Y', strtotime($start_date));
        $key  = $this->key . ':' . $year . ':' . $member_id;

        /**
         * 方案一:根据开始时间和结束时间循环去取相应位,然后统计
         */
        $method1 = Redis::pipeline(function ($pipeline) use ($key, $start_date, $end_date) {
            $start_bytes = (int)date('md', strtotime($start_date));
            $end_bytes   = (int)date('md', strtotime($end_date));
            for ($i = $start_bytes; $i < $end_bytes; $i++) {
                $pipeline->getbit($key, $i);
            }
        });

        // 后续研究下效率问题
        $count1 = array_sum($method1);

        // 如果是采用的缩短到 366 天方案
//        $year_first_day = Carbon::parse(Carbon::make($start_date)->firstOfYear()->toDateString()); // 今年的第一天
//        $start_bytes    = $year_first_day->diffInDays($start_date);
//        $end_bytes      = $year_first_day->diffInDays($end_date);
//        dd($start_bytes, $end_bytes);

        /**
         * 方案二:使用 bitcount 命令,将包含开始时间和结束时间内的统计出来,然后过滤掉不合法数据
         * 采用默认即: 12B - 17B
         */
        $start_bytes = floor(bcdiv((int)date('md', strtotime($start_date)), 8, 2));
        $end_bytes   = ceil(bcdiv((int)date('md', strtotime($end_date)), 8, 2));

        // 这个范围内不仅包含了指定的日期,还包含了未指定的日期,所以要处理
        $method2 = Redis::bitcount($key, $start_bytes, $end_bytes);

        $tmp_start_bytes = bcmul($start_bytes, 8);  // 96b
        $tmp_end_bytes   = bcmul($end_bytes, 8);    // 136b

        // 需要剔除的位
        $exclude = Redis::pipeline(function ($pipeline) use ($key, $start_date, $end_date, $tmp_start_bytes, $tmp_end_bytes) {
            $start_bytes = (int)date('md', strtotime($start_date)); // 101b
            $end_bytes   = (int)date('md', strtotime($end_date));   // 131b

            // 96b - 101b
            for ($i = $tmp_start_bytes; $i < $start_bytes; $i++) {
                $pipeline->getbit($key, $i);
            }

            // 131b - 136b
            for ($i = $end_bytes; $i < $tmp_end_bytes; $i++) {
                $pipeline->getbit($key, $i);
            }
        });

        // 因为只有0和1两个值,所以可以直接求和
        $exclude_count = array_sum($exclude);

        $count2 = $method2 - $exclude_count;

        $data = [
            'redis_key'  => $key,
            'start_date' => $start_date,
            'end_date'   => $end_date,
            'count1'     => $count1,
            'method2'    => $method2,
            'count2'     => $count2,
        ];

        return Tools::outSuccessInfo($data);
    }

    /**
     * Description: 查看用户某一天是否签到
     * Author: Shuxiaoyuan
     * Email: sxy@shuxiaoyuan.com
     * DateTime: 2022/1/7 14:52
     *
     * @param Request $request
     *
     * @return array
     */
    public function getUserIsTaskByDay(Request $request): array
    {
        $member_id = $request->input('member_id');
        $date      = $request->input('date');
        $year      = date('Y', strtotime($date));

        // 将每一个用户的签到按照年份进行划分,Redis键按照会员和年来划分
        $key = $this->key . ':' . $year . ':' . $member_id;

        $day = (int)date('md', strtotime($date));

        // 查找某一位上面的值
        $status = Redis::getbit($key, $day);

        $data = [
            'redis_key' => $key,
            'day'       => $day,
            'status'    => $status,
        ];

        return Tools::outSuccessInfo($data);
    }
}