<?php
namespace Api\PhpUtils\Kafka;
use Api\PhpUtils\Log\FileLog;

/**
 * Kafka工具类
 * 
 * rdkafka.so verion  3.0.5
 */
class KafkaUtil
{
    private static $instances = [];
    private $serverName = ''; // yaconf中kafka.ini配置的业务名，比如viewlog
    /**
     * @var \RdKafka\Producer
     */
    private $producer = null;
    /**
     * @var \RdKafka\Conf
     */
    private $conf = null;
    private $confSet = [];
    private $confFunction = [];
    /**
     * @var \RdKafka\TopicConf
     */
    private $topicConf;
    private $topicConfSet = [];
    private $topicConfFunction = [];
    
    private $brokers = '';
    private $topicName = '';
    private $context = [];
    private $config = [];

    /**
     * 获取实例
     *
     * @param string $serverName kafka.ini中的服务名
     * @param array  $options 其他参数数组，优先级高于yaconf中的相同配置
     * @param bool   $options['context']  上下文信息
     * @return mixed
     */
    public static function getInstance($serverName = '', array $options = []) 
    {
        $preKey = 'kakfa:';
        if (empty($options)) {
            $instancesKey = $preKey . $serverName;
        } else {
            $instancesKey = $preKey . $serverName . ':' . json_encode($options);
        }
        if (isset(self::$instances[$instancesKey])) {
            return self::$instances[$instancesKey];
        }
        return self::$instances[$instancesKey] = new Self($serverName, $options);
    }

    private function __construct($serverName = '', array $options = [])
    {
        $this->serverName = $serverName;
        $this->config = config('kafka', $this->serverName);
        if (!empty($this->config)) {
            isset($this->config['topic']) && $this->setTopicName($this->config['topic']);
            isset($this->config['broker_list']) && $this->setBrokers($this->config['broker_list']);
        }
        $this->context = $options['context'] ?? [];
        try{
            $this->conf = new \RdKafka\Conf();
            $this->topicConf = new \RdKafka\TopicConf();
            $this->defaultConfProducer();
            $this->defaultTopicConfProducer();
        } catch(\Exception $exception){
            FileLog::error('kafkaUtil __construct-Exception:' . $exception->getMessage());
        }
    }

    /**
     * Produce and send a single message
     *
     * @param string $payload
     * @param string $key
     * @param integer $partition
     * @return void
     */
    public function produce($payload, $key = NULL, $partition = RD_KAFKA_PARTITION_UA)
    {
        try{
            is_null($this->producer) && $this->initProducer();
            $topicConf = $this->runTopicConf()->getTopicConf();
            $topicName = $this->getTopicName();
            $topic = $this->producer->newTopic($topicName, $topicConf);
            $topic->produce($partition, 0, $payload, $key);
            while(($len = $this->producer->getOutQLen()) > 0){
                // 1.Kafka轮询一次就相当于拉取（poll）一定时间段broker中可消费的数据， 在这个指定时间段里拉取，时间到了就立刻返回数据。
                // 2.例如poll（5000）： 即在5s中内拉去的数据返回到消费者端。
                $this->producer->poll(5);
            }
            return true;
        } catch(\Exception $exception){
            FileLog::error('kafkaUtil producer run-Exception:' . $exception->getMessage());
            return false;
        }
    }

    /**
     * 初始化producer
     *
     * @return $this
     */
    public function initProducer()
    {
        $conf = $this->runConf()->getConf();
        $this->producer = new \RdKafka\Producer($conf);
        $brokers = $this->getBrokers();
        $this->producer->addBrokers($brokers);
        return $this;
    }

    public function getConf()
    {
        return $this->conf;
    }

    public function getTopicConf()
    {
        return $this->topicConf;
    }

    public function getBrokers()
    {
        return $this->brokers;
    }

    public function setBrokers($brokers)
    {
        if(!is_array($brokers) && !is_object($brokers)){
            $brokers = explode(',', $brokers);
        }
        $brokers = implode(',', $brokers);
        $this->brokers = $brokers;
        return $this;
    }

    public function getTopicName()
    {
        return $this->topicName;
    }

    public function setTopicName($topicName)
    {
        $this->topicName = $topicName;
        return $this;
    }

    public function setConfStatsCb($callback)
    {
        $this->addConfFunction('setStatsCb', $callback);
        return $this;
    }

    public function setConfDrMsgCb($callback)
    {
        $this->addConfFunction('setDrMsgCb', $callback);
        return $this;

    }

    public function setConfErrorCb($callback)
    {
        $this->addConfFunction('setErrorCb', $callback);
        return $this;
    }

    public function setConf($name, $value)
    {
        $this->addConfSet($name, $value);
        return $this;
    }

    public function addConfSet($name, $value, $default = FALSE)
    {
        if($default && isset($this->confSet[$name])){
            return $this;
        }
        $this->confSet[$name] = $value;
        return $this;
    }

    public function addConfFunction($function, $callback, $default = FALSE)
    {
        if($default && isset($this->confFunction[$function])){
            return $this;
        }
        $this->confFunction[$function] = $callback;
        return $this;
    }

    public function setTopicConf($name, $value)
    {
        $this->addTopicConfSet($name, $value);
        return $this;
    }

    public function addTopicConfSet($name, $value, $default = FALSE)
    {
        if($default && isset($this->topicConfSet[$name])){
            return $this;
        }
        $this->topicConfSet[$name] = $value;
        return $this;
    }

    public function addTopicConfFunction($function, $callback, $default = FALSE)
    {
        if($default && isset($this->topicConfFunction[$function])){
            return $this;
        }
        $this->topicConfFunction[$function] = $callback;
        return $this;
    }

    public function runConf()
    {
        if($this->confSet){
            foreach($this->confSet as $key => $value){
                $this->conf->set($key, $value);
            }
        }
        if($this->confFunction){
            foreach($this->confFunction as $key => $value){
                if(method_exists($this->conf, $key)){
                    call_user_func([$this->conf, $key], $value);
                }
            }
        }
        return $this;
    }

    public function runTopicConf()
    {
        if($this->topicConfSet){
            foreach($this->topicConfSet as $key => $value){
                $this->topicConf->set($key, $value);
            }
        }
        if($this->topicConfFunction){
            foreach($this->topicConfFunction as $key => $value){
                if(method_exists($this->topicConf, $key)){
                    call_user_func([$this->topicConf, $key], $value);
                }
            }
        }
        return $this;
    }

    /**
     * 初始化通用配置,暂不使用，setStatsCb（rdkafka > 3.0.5）
     *
     * @return void
     */
    public function defaultInit()
    {
        $this->addConfFunction('setStatsCb', function ($kafka, $json, $json_len) {
            // FileLog::error('setStatsCb $json＝' . $json);
        }, TRUE);
        $this->addConfFunction('setErrorCb', function ($kafka, $err, $reason) {
            // FileLog::error(sprintf("setErrorCb (error_ori:%s)(error: %s) (reason: %s)", $err, rd_kafka_err2str($err), $reason));
        }, TRUE);
        return $this;
    }

    /**
     * 初始化生产者producer配置
     *
     * @return $this
     */
    public function defaultConfProducer()
    {
        $context = $this->context;
        $this->addConfFunction('setDrMsgCb', function ($kafka, \RdKafka\Message $message) use ($context) {
            // var_dump('111----');
            if($message->err){
                $msg = $message->errstr() . ' topic : ' . $message->topic_name . (empty($context) ? (' payload : ' . $message->payload) : (' context: ' . json_encode($context)));
                FileLog::error('kafka produce callback error', $msg);
            } else{
                $msg = ' topic : ' . $message->topic_name . (empty($context) ? (' payload : ' . $message->payload) : (' context: ' . json_encode($context)));
                FileLog::info('kafka produce callback success', $msg);
            }
        }, TRUE);

        $this->addConfFunction('setErrorCb', function ($kafka, $err, $reason) {
            // FileLog::error('kafka error callback', rd_kafka_err2str($err).' reason: '.$reason);
            if ($err == RD_KAFKA_RESP_ERR__ALL_BROKERS_DOWN) {
                return false;
            }
        }, TRUE);

        $this->addConfSet('socket.timeout.ms', 50, TRUE);
        $this->addConfSet('socket.blocking.max.ms', 50, TRUE);
        $this->addConfSet('topic.metadata.refresh.sparse', TRUE, TRUE);
        $this->addConfSet('topic.metadata.refresh.interval.ms', 600, TRUE);
        $this->addConfSet('socket.nagle.disable', TRUE, TRUE);
        $this->addConfSet('message.timeout.ms', 50, TRUE);
        $this->addConfSet('log_level', LOG_WARNING, TRUE);
        if(function_exists('pcntl_sigprocmask')){
            pcntl_sigprocmask(SIG_BLOCK, array(SIGIO));
            $this->addConfSet('internal.termination.signal', SIGIO, TRUE);//设置kafka客户端线程在其完成后立即终止
        } else{
            $this->addConfSet('queue.buffering.max.ms', 1, TRUE);//确保消息尽快发送
        }
        return $this;
    }

    /**
     * 初始化生产者topic配置
     *
     * @return $this
     */
    public function defaultTopicConfProducer()
    {
        $this->addTopicConfSet('request.required.acks', 1, TRUE);
        return $this;
    }
}
