<?php

/**
 * 游戏表
 * @author solu
 */
class Game extends Model {
    protected $tableName = 'game';
    protected $dbKey = 'dw_baccarat';

    const REDIS_PRE_BLOCK_ID = 'globals:reveal_block_id_cache:';
    const REDIS_PLAYER_ONLINE_SET = 'globals:baccarat:player_online_set';

    const MAX_PLAYER_ONLINE_COUNT = 100000;
    const PLAYER_TIME_OUT = 1800; // 玩家在线超时时间


    const STATUS_PLAYING = 0; // 进行中
    const STATUS_OPENING = 1; // 开奖中
    const STATUS_FINISH = 2; //  已开奖

    const WIN_PLAYER = 0;
    const WIN_BANKER = 1;
    const WIN_TIE = 2;

    /**
     * 计算游戏结果
     * @param $createBlockNum
     * @param $ttl
     * @throws Exception
     * @return array
     */
    public static function revealGameData($createBlockNum, $ttl) {
        $objRedis = dwRedis::init(Eos::REDIS_SERV);

        $revealBlockNum = $createBlockNum + $ttl * 2; // 每秒造两个块
        $blockNum = $revealBlockNum;
        $blockInfo = [];

        $banker = []; // 庄
        $player = []; // 闲
        $bankerNum = []; // 庄
        $playerNum = []; // 闲

        // 最多六张牌
        // 闲
        list($num, $color, $blockId) = self::blockNum2Poker($blockNum++, $objRedis);
        $poker = self::toPoker($num, $color);
        $player[] = $poker;
        $playerNum[] = $num;
        $blockInfo['player'][] = compact('blockId', 'poker');
        $eventData = [
            'type' => 'player',
            'poker' => $poker,
            'num' => $num,
            'color' => $color,
            'block_num' => ($blockNum - 1),
            'block_id' => $blockId,
        ];
        Eos::pubEvent('baccarat:new_poker', $eventData);

        // 庄
        list($num, $color, $blockId) = self::blockNum2Poker($blockNum++, $objRedis);
        $poker = self::toPoker($num, $color);
        $banker[] = $poker;
        $bankerNum[] = $num;
        $blockInfo['banker'][] = compact('blockId', 'poker');
        $eventData = [
            'type' => 'banker',
            'poker' => $poker,
            'num' => $num,
            'color' => $color,
            'block_num' => ($blockNum - 1),
            'block_id' => $blockId,
        ];
        Eos::pubEvent('baccarat:new_poker', $eventData);

        // 闲
        list($num, $color, $blockId) = self::blockNum2Poker($blockNum++, $objRedis);
        $poker = self::toPoker($num, $color);
        $player[] = $poker;
        $playerNum[] = $num;
        $blockInfo['player'][]  = compact('blockId', 'poker');
        $eventData = [
            'type' => 'player',
            'poker' => $poker,
            'num' => $num,
            'color' => $color,
            'block_num' => ($blockNum - 1),
            'block_id' => $blockId,
        ];
        Eos::pubEvent('baccarat:new_poker', $eventData);

        // 庄
        list($num, $color, $blockId) = self::blockNum2Poker($blockNum++, $objRedis);
        $poker = self::toPoker($num, $color);
        $banker[] = $poker;
        $bankerNum[] = $num;
        $blockInfo['banker'][] = compact('blockId', 'poker');
        $eventData = [
            'type' => 'banker',
            'poker' => $poker,
            'num' => $num,
            'color' => $color,
            'block_num' => ($blockNum - 1),
            'block_id' => $blockId,
        ];
        Eos::pubEvent('baccarat:new_poker', $eventData);

        // 算对子
        $playerPair = self::isPair($playerNum); // 闲对
        $bankerPair = self::isPair($bankerNum); // 庄对

        // 算点数
        $playerPoint = self::pokerPoint($playerNum);
        $playerPointSource = $playerPoint;
        $bankerPoint = self::pokerPoint($bankerNum);

        // 闲判断加牌
        $playerExtNum = 0;
        if ($playerPoint < 6 && $bankerPoint < 8) {
            list($playerExtNum, $color, $blockId) = self::blockNum2Poker($blockNum++, $objRedis);
            $poker = self::toPoker($playerExtNum, $color);
            $player[] = $poker;
            $playerNum[] = $playerExtNum;
            $blockInfo['player'][]  = compact('blockId', 'poker');
            $playerPoint = self::pokerPoint($playerNum);
            $eventData = [
                'type' => 'player',
                'poker' => $poker,
                'num' => $playerExtNum,
                'color' => $color,
                'block_num' => ($blockNum - 1),
                'block_id' => $blockId,
            ];
            Eos::pubEvent('baccarat:new_poker', $eventData);
        }

        // 庄判断加牌
        if (self::addPokerBanker($playerPointSource, $bankerPoint, $playerExtNum)) {
            list($num, $color, $blockId) = self::blockNum2Poker($blockNum, $objRedis);
            $poker = self::toPoker($num, $color);
            $banker[] = $poker;
            $bankerNum[] = $num;
            $blockInfo['banker'][] = compact('blockId', 'poker');
            $bankerPoint = self::pokerPoint($bankerNum);
            $eventData = [
                'type' => 'banker',
                'poker' => $poker,
                'num' => $num,
                'color' => $color,
                'block_num' => $blockNum,
                'block_id' => $blockId,
            ];
            Eos::pubEvent('baccarat:new_poker', $eventData);
        }

        if ($bankerPoint > $playerPoint) {
            $win = self::WIN_BANKER; // 庄赢
        } elseif ($playerPoint > $bankerPoint) {
            $win = self::WIN_PLAYER; // 闲赢
        } else {
            $win = self::WIN_TIE;
        }

        $revealInfo = compact('win', 'player', 'banker', 'playerPair', 'bankerPair', 'playerNum', 'bankerNum', 'playerPoint', 'bankerPoint', 'revealBlockNum', 'blockInfo');
        return [$win, $player, $banker, $playerPair, $bankerPair, $revealBlockNum, $revealInfo];
    }

    /**
     * 判断庄是否需要加牌
     * @param $playerPoint
     * @param $bankerPoint
     * @param $playerExtNum
     * @wiki https://zh.wikipedia.org/wiki/百家樂#博牌規則
     * @return bool
     */
    private static function addPokerBanker($playerPoint, $bankerPoint, $playerExtNum) {
        $playerExtNum > 9 && $playerExtNum = 0;
        $add = false;

        if ($playerPoint >= 8) {
            return false;
        }

        if ($bankerPoint < 3) {
            $add = true;
        } elseif ($bankerPoint == 3 && $playerExtNum != 8) {
            $add = true;
        } elseif ($bankerPoint == 4 && !in_array($playerExtNum, [0, 1, 8, 9])) {
            $add = true;
        } elseif ($bankerPoint == 5 && !in_array($playerExtNum, [0, 1, 2, 3, 8, 9])) {
            $add = true;
        } elseif ($bankerPoint == 6 && $playerPoint > 0 && $playerPoint < 6 && in_array($playerExtNum, [6, 7])) {
            $add = true;
        }

        return $add;
    }

    /**
     * 算点数
     * @param array $pokerNums
     * @return int
     */
    private static function pokerPoint(array $pokerNums) {
        $total = 0;
        // 10,J,Q,K 当 0 处理
        foreach ($pokerNums as $num) {
            $num > 9 && $num = 0;
            $total += $num;
        }

        return $total % 10; // 超过10取各位
    }

    /**
     * 对子
     * @param array $nums
     * @return bool
     */
    private static function isPair(array $nums) {
        return count(array_unique($nums)) === 1;
    }

    /**
     * @param $blockNum
     * @param null $objRedis
     * @return array
     * @throws Exception
     */
    private static function blockNum2Poker($blockNum, $objRedis = null) {
        $blockId = self::getBlockIdWithCache($blockNum, $objRedis);
        if (!$blockId) {
            throw new Exception("num:{$blockNum}， 获取blockId失败");
        }
        list($num, $color) = self::code2Poker($blockId);

        return [$num, $color, $blockId];
    }

    public static function code2Poker($code) {
        $code2 = preg_replace('/[^0-9a-d]/i', '', $code);
        $len = strlen($code2);

        // 取num
        $num = 0;
        $i = $len - 1;
        for (; $i > 0; $i--) {
            $c = $code2[$i];
            $num = hexdec($c);
            if ($num > 0) {
                break;
            }
        }

        // 取花色
        $color = 0;
        $i--;
        for (; $i > 0; $i--) {
            $c = $code2[$i];
            $color = hexdec($c);
            if ($color >= 0 && $color <= 3) {
                break;
            }
        }

        return [$num, $color];
    }

    /**
     * 牌序号值
     * @param $num
     * @param $color
     * @return int
     */
    public static function toPoker($num, $color) {
        if ($num < 1 || $num > 13) {
            return 0;
        }
        if ($color < 0 || $color > 3) {
            return 0;
        }

        return intval($color*13 + $num);
    }

    /**
     * 牌面值
     * @param $poker
     * @return int
     */
    public static function toPokerNum($poker) {
        return (($poker - 1) % 13) + 1;
    }

    /**
     * 牌花色
     * @param $poker
     * @return int
     */
    public static function toPockColor($poker) {
        return intval(($poker - 1) / 13);
    }

    private static function getBlockIdWithCache($blockNum, $objRedis = null) {
        !$objRedis && $objRedis = dwRedis::init(Eos::REDIS_SERV);

        $key = self::REDIS_PRE_BLOCK_ID . $blockNum;
        $blockId = $objRedis->get($key);
        if (!$blockId) {
            $blockId = Eos::getBlockId($blockNum);
            if ($blockId) {
                $objRedis->setex($key, 300, $blockId);
            }
        }

        return $blockId;
    }

    /**
     * 近N期开奖信息
     * @param int $limit
     * @return array
     */
    public function winData($limit = 50) {
        $list = $this->objTable->getAll(['game_state' => self::STATUS_FINISH], [
            '_field' => 'id, winner, banker_pair, player_pair, reveal_trx_id, reveal_block_num, reveal_info',
            '_sortKey' => 'id DESC',
            '_limit' => $limit,
        ]);

        foreach ($list as $k => $v) {
            $v['id'] = intval($v['id']);
            $v['winner'] = intval($v['winner']);
            $v['banker_pair'] = intval($v['banker_pair']);
            $v['player_pair'] = intval($v['player_pair']);
            $v['reveal_block_num'] = intval($v['reveal_block_num']);

            $revealInfo = json_decode($v['reveal_info'], true);
            unset($v['reveal_info']);
            $v['block_info'] = $revealInfo['blockInfo'];

            $list[$k] = $v;
        }

        return $list ?: [];
    }

    /**
     * 游戏信息
     * @param $old
     * @param $new
     * @param $player
     * @return array
     */
    public function info($old, $new, $player) {
        $data = [];
        $keyword = [
            '_field' => 'id, ttl, game_state, create_time, update_time_int, reveal_info',
        ];
        $games = $this->objTable->getAll(['id' => [$old, $new]], $keyword);
        $games = arrayFormatKey($games, 'id');
        $data['old'] = $games[$old] ?: [];
        $data['old']['reveal_info'] = json_decode($data['old']['reveal_info'], true);


        $data['new'] = $games[$new] ?: [];
        unset($data['new']['reveal_info']);
        $data['new']['timeout'] = max(strtotime($data['new']['create_time'])  + $data['new']['ttl'] - time(), 0);

        $row = null;
        if ($player) {
            $objOffer = new Offer();
            $row = $objOffer->objTable->getRow(['game_id' => $old, 'player' => $player, 'state' => self::STATUS_FINISH], [
                '_field' => 'sum(offerall_int) offerall, sum(win_int) win',
            ]);
        }

        $data['old']['offer'] = $row ?: null;

        return $data;
    }

    /**
     * 在线玩家队列
     * @author solu
     * @param $player
     */
    public static function setPlayerOnline($player) {
        $objRedis = dwRedis::init(Eos::REDIS_SERV);
        $time = time();

        $objRedis->zAdd(self::REDIS_PLAYER_ONLINE_SET, $time, $player);
        $len = $objRedis->zCard(self::REDIS_PLAYER_ONLINE_SET);
        if ($len > self::MAX_PLAYER_ONLINE_COUNT) {
            $out = $len - self::MAX_PLAYER_ONLINE_COUNT;
            $objRedis->zRemRangeByRank(self::REDIS_PLAYER_ONLINE_SET, 0, $out-1);
        }
    }

    /**
     * 在线信息
     * @author solu
     * @param $limit
     * @return array
     */
    public static function getOnlineInfo($limit = 10) {
        $objRedis = dwRedis::init(Eos::REDIS_SERV);

        self::trimOnline();

        $total = $objRedis->zCard(self::REDIS_PLAYER_ONLINE_SET);
        $list = $objRedis->zRevRange(self::REDIS_PLAYER_ONLINE_SET, 0, $limit);

        return [$total, $list];
    }

    /**
     * 整理在线玩家
     * @author solu
     */
    public static function trimOnline() {
        $objRedis = dwRedis::init(Eos::REDIS_SERV);
        $timeout = time() - self::PLAYER_TIME_OUT;

        return $objRedis->zRemRangeByScore(self::REDIS_PLAYER_ONLINE_SET, '-inf', $timeout);
    }

    /**
     * 游戏信息
     * @author solu
     * @param array $gameIds
     * @return array
     */
    public function getGameRevealInfo(array $gameIds) {
        $items = $this->objTable->getAll(['id' => $gameIds], ['_field' => 'id, game_state, reveal_info']);
        $data = [];
        foreach ($items as $v) {
            $id = $v['id'];
            $v['reveal_info'] = $v['reveal_info'] ? json_decode($v['reveal_info'], true) : null;
            $data[$id] = $v;
        }

        return $data;
    }
}