memberstore.gno

package memberstore

import (
	"chain/runtime"
	"strings"

	"gno.land/p/demo/svg"
	"gno.land/p/moul/md"
	"gno.land/p/nt/avl/v0"
	"gno.land/p/nt/mux/v0"
	"gno.land/p/nt/ufmt/v0"
	"gno.land/r/gov/dao"
)

var (
	members MembersByTier
	tiers   TiersByName // private to prevent external modification
	router  *mux.Router
)

const (
	T1 = "T1"
	T2 = "T2"
	T3 = "T3"
)

func init() {
	members = NewMembersByTier()

	tiers = TiersByName{avl.NewTree()}
	tiers.Set(T1, Tier{
		InvitationPoints: 3,
		MinSize: func(membersByTier MembersByTier, tiersByName TiersByName) int {
			return 70
		},
		MaxSize: func(membersByTier MembersByTier, tiersByName TiersByName) int {
			return 0
		},
		BasePower: 3,
		PowerHandler: func(membersByTier MembersByTier, tiersByName TiersByName) float64 {
			return 3
		},
	})

	tiers.Set(T2, Tier{
		InvitationPoints: 2,
		MaxSize: func(membersByTier MembersByTier, tiersByName TiersByName) int {
			return membersByTier.GetTierSize(T1) * 2
		},
		MinSize: func(membersByTier MembersByTier, tiersByName TiersByName) int {
			return membersByTier.GetTierSize(T1) / 4
		},
		BasePower: 2,
		PowerHandler: func(membersByTier MembersByTier, tiersByName TiersByName) float64 {
			t1ms := float64(membersByTier.GetTierSize(T1))
			t1, _ := tiersByName.GetTier(T1)
			t2ms := float64(membersByTier.GetTierSize(T2))
			t2, _ := tiersByName.GetTier(T2)

			t1p := t1.BasePower * t1ms
			t2p := t2.BasePower * t2ms

			// capped to 2/3 of tier 1
			t1ptreshold := t1p * (2.0 / 3.0)
			if t2p > t1ptreshold {
				return t1ptreshold / t2ms
			}

			return t2.BasePower
		},
	})

	tiers.Set(T3, Tier{
		InvitationPoints: 1,
		MaxSize: func(membersByTier MembersByTier, tiersByName TiersByName) int {
			return 0
		},
		MinSize: func(membersByTier MembersByTier, tiersByName TiersByName) int {
			return 0
		},
		BasePower: 1,
		PowerHandler: func(membersByTier MembersByTier, tiersByName TiersByName) float64 {
			t1ms := float64(membersByTier.GetTierSize(T1))
			t1, _ := tiersByName.GetTier(T1)
			t3ms := float64(membersByTier.GetTierSize(T3))
			t3, _ := tiersByName.GetTier(T3)

			t1p := t1.BasePower * t1ms
			t3p := t3.BasePower * t3ms

			// capped to 1/3 of tier 1
			t1ptreshold := t1p * (1.0 / 3.0)
			if t3p > t1ptreshold {
				return t1ptreshold / t3ms
			}

			return t3.BasePower
		},
	})

	initRouter()
}

// initRouter initializes the router for the memberstore.
func initRouter() {
	router = mux.NewRouter()
	router.HandleFunc("", renderHome)
	router.HandleFunc("members", renderMembers)
	router.NotFoundHandler = renderNotFound
}

// renderHome displays the tiers data (Number of members and powers) and tiers charts.
func renderHome(res *mux.ResponseWriter, req *mux.Request) {
	var sb strings.Builder
	sb.WriteString(md.Link("> Go to Members list <", "/r/gov/dao/v3/memberstore:members") + "\n")

	members.Iterate("", "", func(tn string, ti interface{}) bool {
		tree, ok := ti.(*avl.Tree)
		if !ok {
			return false
		}

		tier, ok := tiers.GetTier(tn)
		if !ok {
			return false
		}

		tp := (tier.PowerHandler(members, tiers) * float64(members.GetTierSize(tn)))

		sb.WriteString(ufmt.Sprintf("- %v Tier %v contains %v members with power: %v\n", tierColoredChip(tn), tn, tree.Size(), tp))

		return false
	})

	sb.WriteString("\n" + RenderCharts(members))
	res.Write(sb.String())
}

// renderMembers displays the members list.
func renderMembers(res *mux.ResponseWriter, req *mux.Request) {
	path := strings.Replace(req.RawPath, "members", "", 1) // We have to clean the path
	res.Write(RenderMembers(path, members))
}

func renderNotFound(res *mux.ResponseWriter, req *mux.Request) {
	res.Write("# 404\n\nThat page was not found. Would you like to [**go home**?](/r/gov/dao/v3/memberstore)")
}

func tierColor(tn string) string {
	switch tn {
	case T1:
		return "#329175"
	case T2:
		return "#21577A"
	case T3:
		return "#F3D3BC"
	default:
		return "#FFF"
	}
}

// tierColoredChip returns a colored chip svg for the given tier name.
func tierColoredChip(tn string) string {
	canvas := svg.NewCanvas(16, 16)
	canvas.Append(svg.NewRectangle(0, 0, 16, 16, tierColor(tn)))
	return canvas.Render(tn + " colored chip")
}

func Render(path string) string {
	var sb strings.Builder
	sb.WriteString(md.H1("Memberstore Govdao v3"))
	sb.WriteString(router.Render(path))
	return sb.String()
}

// Get gets the Members store
func Get() MembersByTier {
	currealm := runtime.CurrentRealm().PkgPath()
	if !dao.InAllowedDAOs(currealm) {
		panic("this Realm is not allowed to get the Members data: " + currealm)
	}

	return members
}

// GetTier returns a tier by name. This is a read-only accessor.
func GetTier(name string) (Tier, bool) {
	return tiers.GetTier(name)
}

// IterateTiers iterates over all tiers in order. This is a read-only accessor.
// The callback receives the tier name and tier data.
// Return true from the callback to stop iteration.
func IterateTiers(fn func(name string, tier Tier) bool) {
	tiers.Iterate("", "", func(name string, value interface{}) bool {
		tier, ok := value.(Tier)
		if !ok {
			return false
		}
		return fn(name, tier)
	})
}

// setTiers replaces the tiers configuration.
// This is internal and should only be called via governance proposal execution.
func setTiers(newTiers TiersByName) {
	tiers = newTiers
}

// GetTierPower calculates the effective voting power for a tier given the current members.
// This is a safe accessor that uses the internal tiers configuration.
func GetTierPower(tierName string, members MembersByTier) float64 {
	tier, ok := tiers.GetTier(tierName)
	if !ok {
		return 0
	}
	return tier.PowerHandler(members, tiers)
}