<?php
namespace Api\PhpServices\Doc;

use Api\PhpUtils\Common\Docid;
use Api\PhpUtils\Redis\RedisUtil;
use Api\PhpUtils\Log\FileLog;
use Redis;

/**
 *  使用redis存储文章的动态计数,目前包括'like','down','comment_count','up','comment_like','login_up','play_times'
 *  以上字段在redis中存储时分别缩写为:'l','d','cc','u','cl','lu','pt'
 *  为节约redis内存,使用hash结构进行存储,对于news_开头文章与其他文章区别对待
 *
 *  对于news_开头文章hash的key即为docid,具体存储格式如下:
 *  'news_a5205f59257f4d4d749ef933b4304ad7'=>array('l'=>1,'d'=>2,'cc'=>3232)
 *
 *  对于非news_开头文章hash的key为docid去掉后三位(1位有效位+2位校验位),hash内部的key为(docid倒数第三位+('l','d','cc','u','cl','lu','pt','t')等):
 *  例如对于docid 06No4hkp,具体存储格式如下:
 *  '06No4'=>array('hl'=>1,'hd'=>2,'hcc'=>3232)
 *  因为docid为62进制的数,因此每个hash结构下最多62*8=496个key
 */

class DocDynamic
{
    private $key_map = array('like'=>'l','down'=>'d','comment_count'=>'cc','up'=>'u','comment_like'=>'cl','login_up'=>'lu','play_times'=>'pt','top'=>'t');
    private $redis   = null;
    private $fields  = [];
    private $reverse_map = [];

    //主库开启了rdb及aof,主库只写,从库只读
    public function __construct($useMaster = false)
    {
        $this->fields      = array_keys($this->key_map);
        $this->reverse_map = array_flip($this->key_map);
        // //文章动态计数哨兵集群 Sentinel apiConnect
        $this->redis = RedisUtil::getInstance('api', ['serializer' => 'none', 'master'=> $useMaster]);
    }

    public function get($docid, array $fields = array())
    {
        $ret = array();
        try {
            $fields = empty($fields)?$this->fields:array_intersect( $fields, $this->fields );
            if( empty($fields) )
            {
                return $ret;
            }
            $fields = $this->fieldsMap($fields);

            if(strlen($docid)<8)
            {
                FileLog::waring('get dynamic doc redis failed', 'bad docid : '.$docid);
            }
            if(substr($docid , 0, 5) === 'news_')
            {
                $redis_ret  = $this->redis->hmget($docid, $fields);
                // var_dump($this->reverse_map);
                // var_dump($redis_ret);exit;
            } else {
                $temp       = array();
                $real_docid = substr( trim($docid), 0, -2);
                $hkey       = substr($real_docid, 0, -1);
                foreach ($fields as $field)
                {
                    $temp[] = substr($real_docid, -1).$field;
                }
                $redis_ret  = $this->redis->hmget($hkey,$temp);
            }
            foreach ($redis_ret as $redis_ret_key=>$redis_ret_value)
            {
                if( $redis_ret_value !== false && $redis_ret_value >=0 )
                {
                    if(substr($docid , 0, 5) === 'news_') {
                        $ret[$this->reverse_map[$redis_ret_key]] = $redis_ret_value;
                    } else {
                        $ret[$this->reverse_map[substr($redis_ret_key, 1)]] = $redis_ret_value;
                    }
                }
            }
            return $ret;
        } catch (\Exception $e)
        {
            FileLog::waring('get dynamic doc redis failed', $docid.' '.$e->getCode().' '.$e->getMessage());
            return $ret;
        }
    }

    /**
     * 可用倒数第三位不同的docid拼接同一条pipe减少请求次数,但实际情况中docid较为分散,每一条对应一个pipe比较清晰
     * @param    array             $docids
     * @param    array             $fields(可选值'like','down','comment_count','up','comment_like','login_up','play_times','top')
     * @return   array             eg:
     * array('06No4hkp'=>array('like'=>1,'up=>2'),'06No4hkm'=>array('like'=>1,'up=>2'))
     */
    public function gets(array $docids, array $fields = array())
    {
        $ret = array();
        try {
            $fields = empty($fields)?$this->fields:array_intersect( $fields ,$this->fields );
            if( empty($fields) || empty($docids) )
            {
                return $ret;
            }
            $fields = $this->fieldsMap($fields);

            $gets   = array();
            //根据docid的两种情况拼接hash的hkey和mkey
            foreach ($docids as $docid)
            {
                if(strlen($docid)<8)
                {
                    continue;
                }
                if(substr($docid , 0, 5) === 'news_')
                {
                    $gets[] = array('docid'=>$docid, 'hkey'=>$docid, 'mkey'=>$fields);
                }else
                {
                    $temp       = array();
                    $real_docid = substr( trim($docid), 0, -2);
                    $hkey       = substr($real_docid, 0, -1);
                    foreach ($fields as $field)
                    {
                        $temp[] = substr($real_docid, -1).$field;
                    }
                    $gets[] = array('docid'=>$docid, 'hkey'=>$hkey, 'mkey'=>$temp);
                }

            }
            if( empty($gets) )
            {
                return $ret;
            }
            //redis pipeline获取数据
            $this->redis->multi(Redis::PIPELINE);
            foreach ($gets as $get)
            {
                $this->redis->hmget($get['hkey'], $get['mkey']);
            }
            $redis_rets = $this->redis->exec();
            return $redis_rets == false?$ret:$this->formatRedisRet($redis_rets,$gets,'hmget');
        } catch (\Exception $e)
        {
            FileLog::waring('gets dynamic doc redis failed', implode(',',$docids).' '.$e->getCode().' '.$e->getMessage());
            return $ret;
        }
    }

    /**
     * 批量写入数据
     * @param    [array]           $data,格式如下:
     * array(
     *     '06No4hkp'=>array('like'=>1,'down'=>2),
     *     '06No4hk1'=>array('comment_count'=>3,'up'=>4,'comment_like'=>'7'),
     * )
     * @return
     */
    private function sets(array $data)
    {
        try {
            if( empty($data) )
            {
                return false;
            }
            foreach ($data as $docid => &$value)
            {
                if( !Docid::validDocid($docid) )
                {
                    throw new \Exception("bad docid ".$docid, -1);
                }
            }
            $inserts = $this->getUpdateData($data ,'hmset');
            if( empty($inserts) )
            {
                return false;
            }
            $this->redis->multi(Redis::PIPELINE);
            foreach ($inserts as $insert)
            {
                if( isset($insert['hkey']) && isset($insert['value']) )
                {
                    $this->redis->hmset($insert['hkey'], $insert['value']);
                }
            }
            $redis_ret = $this->redis->exec();
            if(!empty($redis_ret))
            {
                return true;
            }
            return false;
        } catch (\Exception $e)
        {
            FileLog::waring('set dynamic doc redis failed', json_encode($data).' '.$e->getCode().' '.$e->getMessage());
            return false;
        }
    }

    /**
     * 批量inc deinc
     * @param    [array]           $data,格式如下:
     * array(
     *     '06No4hkp'=>array('like'=>1,'down'=>2),
     *     '06No4hk1'=>array('comment_count'=>-3,'up'=>-4,'comment_like'=>'-7'),
     * )
     * @return   返回更新后的值
     * array(
     *     '06No4hkp'=>array('like'=>12,'down'=>2),
     *     '06No4hk1'=>array('comment_count'=>3,'up'=>4,'comment_like'=>'7'),
     * )
     */
    private function hincrs(array $data)
    {
        $ret = array();
        try {
            if( empty($data) )
            {
                return $ret;
            }
            foreach ($data as $docid => &$value)
            {
                if( !Docid::validDocid($docid) )
                {
                    throw new \Exception("bad docid ".$docid, -1);
                }
            }
            $updates = $this->getUpdateData($data ,'hincby');
            if( empty($updates) )
            {
                return $ret;
            }
            $this->redis->multi(Redis::PIPELINE);
            foreach ($updates as $update)
            {
                $this->redis->hincrby($update['hkey'],$update['mkey'],$update['value']);
            }
            $redis_rets =  $this->redis->exec();
            return $redis_rets == false?$ret:$this->formatRedisRet($redis_rets,$updates,'hincby');
        } catch (\Exception $e)
        {
            FileLog::waring('update dynamic doc redis failed', json_encode($data).' '.$e->getCode().' '.$e->getMessage());
            return $ret;
        }
    }

    /**
     * 映射'like','down','comment_count','up','comment_like'到缩写
     * @param    array                    $fields
     * @return   [type]
     */
    private function fieldsMap( array $fields )
    {
        $ret = array();
        foreach ($fields as $field)
        {
            if( isset($this->key_map[$field]) )
            {
                $ret[] = $this->key_map[$field];
            }
        }
        return $ret;
    }

    private function fieldsKeyMap( array $doc )
    {
        $ret = array();
        $key_map = $this->key_map;
        foreach ($key_map as $key => $value)
        {
            if( isset($doc[$key]) )
            {
                $ret[$value] = $doc[$key];
            }
        }
        return $ret;
    }

    /**
     * 获取特定格式化数据,用于redis pipeline
     * @param    array                    $records
     * @param    string                   $type    hmset or hincby
     * @return   array
     * eg:hmset
     * array(
     *     array('hkey'=>'06No4','value'=>array('6d'=>123,'6cl'=>'123')),
     *     array('hkey'=>'06No6','value'=>array('4d'=>123,'4cl'=>'123')),
     * )
     * eg:hincby
     * array(
     *     array('docid'=>'06No42qP','hkey'=>'06No4','mkey'=>'2cl','value'=>123),
     *     array('docid'=>'news_a5205f59257f4d4d749ef933b4304ad7','hkey'=>'news_a5205f59257f4d4d749ef933b4304ad7','mkey'=>'cl','value'=>123),
     * )
     */
    private function getUpdateData(array $records, $type)
    {
        $ret  = array();
        foreach ($records as $docid=>$record)
        {
            if(substr($docid , 0, 5) === 'news_')
            {
                if( $type == 'hmset' )
                {
                    $ret[] = array('hkey'=>$docid,'value'=>$this->fieldsKeyMap($record));
                }elseif ( $type == 'hincby' )
                {
                    $value = $this->fieldsKeyMap($record);
                    foreach ($value as $key1 => $value1)
                    {
                        $ret[] = array('docid'=>$docid,'hkey'=>$docid, 'mkey'=>$key1, 'value'=>$value1);
                    }
                }
            }else
            {
                $values = $this->fieldsKeyMap($record);
                $real_docid  = substr( trim($docid), 0, -2);
                $hkey   = substr($real_docid, 0, -1);
                $mkey   = substr($real_docid, -1);
                $temp   = array();
                foreach ($values as $key => $value)
                {
                    $temp[$mkey.$key] = $value;
                }
                if( $type == 'hmset' )
                {
                    $ret[] = array('hkey'=>$hkey,'value'=>$temp);
                }elseif ( $type == 'hincby' )
                {
                    foreach ($temp as $key2 => $value2)
                    {
                        $ret[] = array('docid'=>$docid, 'hkey'=>$hkey, 'mkey'=>$key2, 'value'=>$value2);
                    }
                }
            }
        }
        return $ret;
    }

    /**
     * //格式化redis返回值
     * @param    array                    $redis_rets redis pipeline返回值
     * @param    array                    $pipe_datas pipe迭代前原始值
     * @param    string                   $type       hmset or hincby
     * @return   array
     */
    private function formatRedisRet(array $redis_rets, array $pipe_datas ,$type)
    {
        $ret_length = is_array($redis_rets)?count($redis_rets):0;
        $ret = array();
        for ($i=0; $i <$ret_length; $i++)
        {
            if( $type == 'hmget' )
            {
                foreach ($redis_rets[$i] as $redis_key=>$redis_value)
                {
                    if( $redis_value !== false && is_numeric($redis_value) )
                    {
                        if(substr($pipe_datas[$i]['docid'] , 0, 5) === 'news_')
                        {
                            $ret_key =  $this->reverse_map[$redis_key];
                            $ret[$pipe_datas[$i]['docid']][$ret_key] = intval( $redis_value );
                        }else
                        {
                            $ret_key =  $this->reverse_map[substr($redis_key, 1)];
                            $ret[$pipe_datas[$i]['docid']][$ret_key] = intval( $redis_value );
                        }

                    }
                }
            }elseif( $type == 'hincby' )
            {
                if( $redis_rets[$i] !== false && is_numeric($redis_rets[$i]) )
                {
                    if(substr($pipe_datas[$i]['docid'] , 0, 5) === 'news_')
                    {
                        $ret_key =  $this->reverse_map[$pipe_datas[$i]['mkey']];
                    }else
                    {
                        $ret_key =  $this->reverse_map[substr($pipe_datas[$i]['mkey'], 1)];
                    }
                    $int_value = intval( $redis_rets[$i] );
                    $ret[$pipe_datas[$i]['docid']][$ret_key] = $int_value>=0?$int_value:0;
                }
            }

        }
        return $ret;
    }

    private function incrCount($docid, $field, $value)
    {
        $result = $this->hincrs(array($docid => array($field => $value)));
        if (isset($result[$docid][$field]))
        {
            if ($result[$docid][$field] > 0)
            {
                return $result[$docid][$field];
            }
            else
            {
                $this->sets(array($docid => array($field => 0)));
                return 0;
            }
        }
        else
        {
            return 0;
        }
    }

    private function setCount($docid, $field, $value)
    {
        return $this->sets(array($docid => array($field => $value)));
    }

    public function incrCommentCount($docid, $value = 1)
    {
        $this->incrCount($docid, 'comment_count', $value);
    }

    public function incrLike($docid, $value = 1)
    {
        $this->incrCount($docid, 'like', $value);
    }

    public function incrLoginThumbsup($docid, $value = 1)
    {
        $this->incrCount($docid, 'login_up', $value);
    }

    public function incrThumbsup($docid, $value = 1)
    {
        $this->incrCount($docid, 'up', $value);
    }

    public function incrThumbsdown($docid, $value = 1)
    {
        $this->incrCount($docid, 'down', $value);
    }

    public function incrCommentLike($docid, $value = 1)
    {
        $this->incrCount($docid, 'comment_like', $value);
    }

    public function incrTop($docid, $value = 1)
    {
        $this->incrCount($docid, 'top', $value);
    }

    public function setCommentCount($docid, $value)
    {
        $this->setCount($docid, 'comment_count', $value);
    }

    public function setThumbsdown($docid, $value)
    {
        $this->setCount($docid, 'down', $value);
    }

    public function incrPlayTimes($docid, $value=1)
    {
        $this->incrCount($docid, 'play_times', $value);
    }

    public function setPlayTimes($docid, $value)
    {
        return $this->setCount($docid, 'play_times', $value);
    }
}
