package svc

import (
	"errors"
	"math/rand"
	"sync"

	"github.com/globalsign/mgo"
	"github.com/globalsign/mgo/bson"
	"mongo.games.com/goserver/core/logger"

	"mongo.games.com/game/dbproxy/mongo"
	"mongo.games.com/game/model"
)

/*
 玩家id生成算法
*/

const (
	PlayerBucketIDMin      = 10000000 // 第一个玩家id
	BatchPlayerBucketIDCnt = 99999999 // 玩家id自增步长
)

var (
	PlayerBucketIdsDBName       = "user"
	PlayerBucketIdsCollName     = "user_bucketids"
	PlayerBucketIdsAutoId       = bson.ObjectIdHex("60f69b4a09dbe3323c632f25") // 记录id使用到多少了
	ErrPlayerBucketIdsDBNotOpen = model.NewDBError(PlayerBucketIdsDBName, PlayerBucketIdsCollName, model.NOT_OPEN)
	playerBucketId              = &model.PlayerBucketId{}
	playerBucketIdsCursor       = int32(0)
	playerBucketIdLock          = sync.Mutex{}
)

type PlayerBucketAutoId struct {
	Id     bson.ObjectId `bson:"_id"`
	AutoId int
}

func PlayerBucketIdCollection() *mongo.Collection {
	s := mongo.MgoSessionMgrSington.GetPltMgoSession(mongo.G_P, PlayerBucketIdsDBName)
	if s != nil {
		c, first := s.DB().C(PlayerBucketIdsCollName)
		if first {
			var id PlayerBucketAutoId
			err := c.Find(bson.M{"_id": PlayerBucketIdsAutoId}).One(&id)
			if err != nil && errors.Is(err, mgo.ErrNotFound) {
				id.Id = PlayerBucketIdsAutoId
				id.AutoId = PlayerBucketIDMin
				c.Insert(id)
			}
		}
		return c
	}
	return nil
}

// id 增加
func autoIncPlayerBucketId() (int, error) {
	c := PlayerBucketIdCollection()
	if c == nil {
		return 0, ErrPlayerBucketIdsDBNotOpen
	}
	change := mgo.Change{
		Update:    bson.M{"$inc": bson.M{"autoid": BatchPlayerBucketIDCnt}},
		ReturnNew: true,
	}
	doc := PlayerBucketAutoId{}
	_, err := c.Find(bson.M{"_id": PlayerBucketIdsAutoId}).Apply(change, &doc)
	if err == nil {
		return doc.AutoId, nil
	}
	return 0, err
}

type PlayerBucketId struct {
	Id       bson.ObjectId `bson:"_id"`
	StartPos int32
	EndPos   int32
	Used     bool
}

func (receiver *PlayerBucketId) IsValid(cursor int32) bool {
	return receiver.Id.Valid() &&
		receiver.StartPos != 0 &&
		receiver.EndPos != 0 &&
		receiver.StartPos != receiver.EndPos &&
		cursor >= receiver.StartPos &&
		cursor <= receiver.EndPos &&
		cursor != 0
}

// 生成一批玩家id
func genABatchPlayerBucketId() error {
	maxV, err := autoIncPlayerBucketId()
	if err != nil {
		return err
	}
	var (
		minV       = maxV - BatchPlayerBucketIDCnt
		bucketSize = 100 // 最长100个玩家id是连续的
		bucketArr  []*PlayerBucketId
	)
	cnt := ((maxV - minV) / bucketSize) + 1
	bucketArr = make([]*PlayerBucketId, 0, cnt)
	for i := minV; i < maxV; i = i + bucketSize {
		bucketArr = append(bucketArr, &PlayerBucketId{
			Id:       bson.NewObjectId(),
			StartPos: int32(i),
			EndPos:   int32(i + bucketSize - 1),
			Used:     false,
		})
	}
	for i, _ := range bucketArr {
		rnd := rand.Intn(len(bucketArr))
		bucketArr[i], bucketArr[rnd] = bucketArr[rnd], bucketArr[i]
	}
	c := PlayerBucketIdCollection()
	docs := make([]interface{}, 0, len(bucketArr))
	for _, log := range bucketArr {
		docs = append(docs, log)
	}
	if c != nil {
		for len(docs) > 0 {
			cnt := len(docs)
			if cnt > 1000 {
				cnt = 1000
			}
			err = c.Insert(docs[:cnt]...)
			docs = docs[cnt:]
		}
	}
	return err
}

// GetOnePlayerIdFromBucket 获取一个玩家id
func GetOnePlayerIdFromBucket() (pid int32, err error) {
	playerBucketIdLock.Lock()
	defer playerBucketIdLock.Unlock()

	if !playerBucketId.IsValid(playerBucketIdsCursor) {
		var flag bool
		c := PlayerBucketIdCollection()
		if c != nil {
			change := mgo.Change{
				Update:    bson.M{"$set": bson.M{"used": true}},
				ReturnNew: true,
			}
		redo:
			_, err := c.Find(bson.M{"used": false}).Apply(change, playerBucketId)
			if err != nil {
				logger.Logger.Warnf("GetOnePlayerIdFromBucket Find failed:%v", err)
				if !flag && errors.Is(err, mgo.ErrNotFound) {
					err = genABatchPlayerBucketId()
					flag = true
					if err == nil {
						goto redo
					}
				}
				return 0, err
			}
			err = c.RemoveId(playerBucketId.Id)
			if err != nil {
				logger.Logger.Warnf("GetOnePlayerIdFromBucket RemoveId(%v) failed:%v ", playerBucketId.Id, err)
			}
			playerBucketIdsCursor = playerBucketId.StartPos
		} else {
			return 0, ErrPlayerBucketIdsDBNotOpen
		}
	}
	pid = playerBucketIdsCursor
	playerBucketIdsCursor++
	return pid, nil
}
