package user import ( "context" "database/sql" "errors" "fmt" "math" "sync" "time" "git.anhgelus.world/anhgelus/les-copaings-bot/common" "github.com/nyttikord/avl" "github.com/nyttikord/gokord/state" ) var ErrSyncingUnsavedData = errors.New("trying to sync unsaved data") type XPCached struct { XP uint Time time.Duration } type CopaingCached struct { ID uint64 GuildID uint64 XP uint XPs []XPCached XPToAdd uint } // copaing turns a CopaingCached into a Copaing. // This operation get the copaing synced with the database. // It doesn't: // - save the copaing in the database, use CopaingCached.SaveInDB for that; // - sync the copaing cached, use CopaingCached.Sync for that. // TL;DR: don't use this method, unless you know what are you doing. func (cc *CopaingCached) copaing(ctx context.Context) *Copaing { c := &Copaing{ID: cc.ID, GuildID: cc.GuildID} if err := c.load(ctx); err != nil { panic(err) } return c } func (cc *CopaingCached) Sync(ctx context.Context) error { if cc.mustSave() { return ErrSyncingUnsavedData } synced := FromCopaing(cc.copaing(ctx)) synced.XP += cc.XPToAdd synced.XPToAdd = cc.XPToAdd err := synced.Save(ctx) if err != nil { return err } *cc = *synced return nil } func (cc *CopaingCached) Save(ctx context.Context) error { state := GetState(ctx) state.mu.Lock() defer state.mu.Unlock() return state.storage.Write(KeyCopaingCachedRaw(cc.GuildID, cc.ID), *cc) } func (cc *CopaingCached) SaveInDB(ctx context.Context) error { c := cc.copaing(ctx) c.CopaingXPs = append(c.CopaingXPs, CopaingXP{CopaingID: c.ID, XP: cc.XPToAdd, GuildID: c.GuildID}) err := c.Save(ctx) if err != nil { return err } cc.XPToAdd = 0 return cc.Save(ctx) } func (cc *CopaingCached) Delete(ctx context.Context) error { c := cc.copaing(ctx) err := c.Delete(ctx) if err != nil { return err } state := GetState(ctx) state.mu.Lock() defer state.mu.Unlock() return state.storage.Delete(KeyCopaingCached(c)) } func (cc *CopaingCached) mustSave() bool { return cc.XPToAdd > 0 } func saveStateInDB(ctx context.Context) error { state := GetState(ctx) state.saveInDB.Lock() defer state.saveInDB.Unlock() for _, v := range state.storage.All() { if v.mustSave() { err := v.SaveInDB(ctx) if err != nil { return err } } } return nil } func FromCopaing(c *Copaing) *CopaingCached { return &CopaingCached{ ID: c.ID, GuildID: c.GuildID, XP: calcXP(c), XPs: generateXPs(c), XPToAdd: 0, } } const KeyCopaingCachedPrefix = "cc:" func KeyCopaingCached(c *Copaing) string { return KeyCopaingCachedRaw(c.GuildID, c.ID) } func KeyCopaingCachedRaw(guildID, copaingID uint64) string { return fmt.Sprintf("%d:%d", guildID, copaingID) } type State struct { mu sync.RWMutex saveInDB sync.Mutex storage *state.AVLStorage[string, CopaingCached] } func NewState(db *sql.DB) *State { state := &State{ storage: state.WrapAVLAsStorage(avl.NewKeyString[CopaingCached]()), } var cs []*Copaing /*err := db.Find(&cs).Error if err != nil { panic(err) }*/ for _, v := range cs { FromCopaing(v).Save(SetState(context.Background(), state)) } return state } func GetState(ctx context.Context) *State { return ctx.Value(common.KeyCopaingState).(*State) } func SetState(ctx context.Context, state *State) context.Context { return context.WithValue(ctx, common.KeyCopaingState, state) } func deepCopy(src CopaingCached) CopaingCached { res := CopaingCached{ ID: src.ID, GuildID: src.GuildID, XP: src.XP, XPToAdd: src.XPToAdd, XPs: make([]XPCached, len(src.XPs)), } copy(res.XPs, src.XPs) return res } func (s *State) Copaing(guildID, copaingID uint64) (*CopaingCached, error) { s.mu.RLock() defer s.mu.RUnlock() c, err := s.storage.Get(KeyCopaingCachedRaw(guildID, copaingID)) if err != nil { return nil, err } return &c, nil } func (s *State) Copaings(guild uint64) []CopaingCached { s.mu.RLock() defer s.mu.RUnlock() var ccs []CopaingCached for _, cc := range s.storage.All() { if cc.GuildID == guild { ccs = append(ccs, deepCopy(cc)) } } return ccs } func calcXP(c *Copaing) uint { var sum uint for _, entry := range c.CopaingXPs { sum += entry.XP } return sum } func generateXPs(c *Copaing) []XPCached { data := map[time.Duration]XPCached{} var sumEach time.Duration = 6 for _, xp := range c.CopaingXPs { // we add sumEach at the end because we want it to be rounded to ceil tmp := time.Since(xp.CreatedAt)/(sumEach*time.Hour) + sumEach since := time.Duration(math.Floor(float64(tmp))) if v, ok := data[since]; ok { v.XP += xp.XP } else { data[since] = XPCached{ Time: since * time.Hour, XP: xp.XP, } } } ccs := make([]XPCached, len(data)) i := 0 for _, v := range data { ccs[i] = v i++ } return ccs }