/// <summary>
        /// Assign partition and replicas to kafka brokers.
        /// the basic idea here is to evenly assign the active brokers to a partition, and assign its replicas to other active brokers.
        /// </summary>
        /// <param name="numOfPartitions">the number of partitions.</param>
        /// <param name="numOfReplicas">the number of replications.</param>
        /// <param name="brokerInfos">the active kafka brokers.</param>
        /// <param name="startBrokerId">the perfered broker id to start for assignment.</param>
        /// <param name="startPartitionId">the perfered partition id to start for assignment.</param>
        /// <returns>The kafka partition assignment info.</returns>
        public IEnumerable <KafkaPartitionInfo> AssignPartitionsAndReplicasToBrokers(int numOfPartitions, int numOfReplicas, IEnumerable <KafkaBrokerInfo> brokerInfos, int startBrokerId = -1, int startPartitionId = -1)
        {
            if (numOfPartitions <= 0)
            {
                return(null);
            }

            if (numOfReplicas <= 0)
            {
                return(null);
            }

            if (brokerInfos == null)
            {
                return(null);
            }

            var brokersArray = brokerInfos.ToArray();
            int numOfBrokers = brokersArray.Count();

            if (numOfBrokers == 0)
            {
                return(null);
            }

            if (numOfReplicas > numOfBrokers)
            {
                return(null);
            }

            var result = new List <KafkaPartitionInfo>();

            Random random = new Random();

            // if caller did not specific a broker for the starting partition 0, then random choose a broker for the partition 0
            var brokerIds = brokersArray.Select(e => e.Id);
            var minId     = brokerIds.Min();
            var maxId     = brokerIds.Max();

            if (startBrokerId > maxId)
            {
                startBrokerId = minId;
            }

            if (startPartitionId > numOfPartitions)
            {
                startPartitionId = 0;
            }

            var startIndex = startBrokerId >= 0 ? startBrokerId : random.Next(numOfBrokers);

            // partition always start from 0, unless caller specific a number to start with.
            var currentPartitionId = Math.Max(0, startPartitionId);
            var nextReplicaShift   = startBrokerId >= 0 ? startBrokerId : random.Next(numOfBrokers);

            // for each partition , find a broker id for this partition, and also find broker ids for all replicas
            for (int i = 0; i < numOfPartitions; i++)
            {
                if (currentPartitionId > 0 && (currentPartitionId % numOfBrokers) == 0)
                {
                    nextReplicaShift += 1;
                }

                var replicaIndex = (currentPartitionId + startIndex) % numOfBrokers;

                var assignmentInfo = new KafkaPartitionInfo();

                assignmentInfo.PartitionId   = currentPartitionId.ToString(CultureInfo.InvariantCulture);
                assignmentInfo.ReplicaOwners = new int[numOfReplicas];

                // assign the broker id to first replica.
                assignmentInfo.ReplicaOwners[0] = brokersArray[replicaIndex].Id;

                // find borker ids for other replicas
                for (int j = 1; j < numOfReplicas; j++)
                {
                    var factor = j % numOfBrokers;
                    replicaIndex = (replicaIndex + factor) % numOfBrokers;

                    assignmentInfo.ReplicaOwners[j] = brokersArray[replicaIndex].Id;
                }

                result.Add(assignmentInfo);

                currentPartitionId += 1;
            }

            return(result);
        }
        /// <summary>
        /// Update partitions and others settings for a topic.
        /// </summary>
        /// <param name="topicConfiguration">The topic update information.</param>
        /// <returns>Task holds the topic info if update complete. null is return when topic does not exist or unable to update it.</returns>
        /// <remarks>this method is used to increase the number of partitions for a topic. the increasement of replica is not handled here.</remarks>
        public async Task <KafkaTopicInfo> UpdateKafkaTopicAsync(KafkaTopicConfiguration topicConfiguration)
        {
            if (topicConfiguration == null)
            {
                throw new ArgumentException("topicConfiguration is null or empty.");
            }

            // check arguments
            if (topicConfiguration == null || string.IsNullOrWhiteSpace(topicConfiguration.TopicName) ||
                topicConfiguration.NumOfPartitions <= 0 || topicConfiguration.NumOfReplicas <= 0 ||
                topicConfiguration.TopicName == "__consumer_offsets")
            {
                return(null);
            }

            // 1 get topic info from zookeeper
            var topicInfo = await this.GetKafkaTopicAsync(topicConfiguration.TopicName).ConfigureAwait(false);

            if (topicInfo == null)
            {
                return(null);
            }

            // 2 check partitions and replicas
            // only support add more partitions or replicas, not support reduce them
            int numOfExistingPartitions = 0;
            int numOfExistingReplicas   = 0;
            var partitions = topicInfo.Partitions;
            KafkaPartitionInfo partitionInfo = null;

            if (partitions != null)
            {
                numOfExistingPartitions = partitions.Length;

                partitionInfo = partitions.FirstOrDefault(p => p.PartitionId == "0");
                if (partitionInfo != null)
                {
                    var replicas = partitionInfo.ReplicaOwners;
                    if (replicas != null)
                    {
                        numOfExistingReplicas = replicas.Length;
                    }
                }
            }

            if (numOfExistingPartitions == 0 || numOfExistingReplicas == 0)
            {
                return(null);
            }

            if (numOfExistingPartitions >= topicConfiguration.NumOfPartitions)
            {
                return(topicInfo);
            }

            // NOTE : if want to update replicas, need to use another call
            if (numOfExistingReplicas != topicConfiguration.NumOfReplicas)
            {
                return(topicInfo);
            }


            // 3 update zookeeper node content
            var partitionsToAdd = topicConfiguration.NumOfPartitions - numOfExistingPartitions;

            // assign the brokers to brokers
            var brokers = await this.ListKakfaBrokersAsync().ConfigureAwait(false);

            var newPartitionAssignmentInfo = this.AssignPartitionsAndReplicasToBrokers(partitionsToAdd, numOfExistingReplicas, brokers, Math.Max(0, partitionInfo.ReplicaOwners[0]), numOfExistingPartitions);

            // check the newPartitios to see whether some partitions get more replicats
            if (newPartitionAssignmentInfo.Any(p => p.ReplicaOwners.Length != partitionInfo.ReplicaOwners.Length))
            {
                return(null);
            }

            // merge existing and new partition info
            var finalAssignmentInfo = new List <KafkaPartitionInfo>();

            finalAssignmentInfo.AddRange(topicInfo.Partitions);
            finalAssignmentInfo.AddRange(newPartitionAssignmentInfo);
            topicInfo.Partitions = finalAssignmentInfo.ToArray();

            // 4 update the node in zookeeper
            // update node at path 'brokers/topics/{name}', with new content
            var serializer = new ZookeeperDataSerializer();
            var content    = serializer.SerializeKafkaTopicInfo(topicInfo);

            await this.ZookeeperClient.SetDataAsync("/brokers/topics/" + topicConfiguration.TopicName, content, -1).ConfigureAwait(false);

            return(topicInfo);
        }