package slackscot

import (
	"context"
	"fmt"
	"github.com/slack-go/slack"
	"hash"
	"hash/crc32"
	"math"
)

type partitionRouter struct {
	// Logger
	log *sLogger

	// messageQueues with partition keyed by the hash of the incoming message id
	// so that processing of messages (new, updates and deletes) are handled by
	// the same work queue therefore ensuring correct ordered processing
	// of those events
	messageQueues []chan slack.MessageEvent

	// workerTerminationSignals are channels receiving a termination signal for each
	// workerQueue
	workerTerminationSignals []chan bool

	// hash function to direct message processing to partitions
	hasher   hash.Hash32
	hashMask int

	*instrumenter
}

func newPartitionRouter(partitionCount int, queueBufferSize int, log *sLogger, instrumenter *instrumenter) (pr *partitionRouter, err error) {
	if !isPowerOfTwo(partitionCount) {
		return nil, fmt.Errorf("A partition router can only work with a partitionCount that is a power of two but was [%d]", partitionCount)
	}

	pr = new(partitionRouter)
	pr.messageQueues = make([]chan slack.MessageEvent, partitionCount)
	for i := range pr.messageQueues {
		pr.messageQueues[i] = make(chan slack.MessageEvent, queueBufferSize)
	}
	pr.workerTerminationSignals = make([]chan bool, partitionCount)
	for i := range pr.workerTerminationSignals {
		pr.workerTerminationSignals[i] = make(chan bool)
	}
	pr.hasher = crc32.NewIEEE()
	pr.hashMask = hashMask(partitionCount)
	pr.log = log
	pr.instrumenter = instrumenter

	return pr, nil
}

// routeMessageEvent routes the message processing to the correct partition based on its original message id to ensure
// that all message and its updates are processed in order
func (pr *partitionRouter) routeMessageEvent(msgEvent slack.MessageEvent) {
	msgID := getOriginalMessageID(msgEvent)

	partition := pr.partitionForMsgID(msgID)

	pr.log.Debugf("Dispatching message [%s] to partition [%d]", msgID, partition)
	d := measure(func() {
		pr.messageQueues[partition] <- msgEvent
	})

	pr.coreMetrics.msgDispatchLatencyMillis.Record(context.Background(), d.Milliseconds())
}

// partitionForMsgID returns the partition index for a given message ID
func (pr *partitionRouter) partitionForMsgID(msgID SlackMessageID) (partition int) {
	pr.hasher.Reset()
	pr.hasher.Write([]byte(msgID.channelID))
	pr.hasher.Write([]byte(msgID.timestamp))
	res := pr.hasher.Sum32()

	pr.log.Debugf("Hash [%d] calculated for [%s]", res, msgID)

	// Keep only the rightmost bits so we have a max equal to the partition count
	return int(res) & pr.hashMask
}

// isPowerOfTwo returns true if val is a power of two or false if not
func isPowerOfTwo(val int) bool {
	return (val != 0) && (val&(val-1)) == 0
}

// hashMask builds a mask for a partitionCount (which should be a power of two) to get a hash value
// that is in the range of the number of partitions we have
func hashMask(partitionCount int) int {
	maskSize := int(math.Log2(float64(partitionCount)))
	mask := 0
	for i := 0; i < maskSize; i++ {
		mask = mask<<1 | 1
	}

	return mask
}
