package pomelo

import (
	"net"
	"time"

	ccode "github.com/cherry-game/cherry/code"
	cfacade "github.com/cherry-game/cherry/facade"
	clog "github.com/cherry-game/cherry/logger"
	cactor "github.com/cherry-game/cherry/net/actor"
	pomeloMessage "github.com/cherry-game/cherry/net/parser/pomelo/message"
	ppacket "github.com/cherry-game/cherry/net/parser/pomelo/packet"
	cproto "github.com/cherry-game/cherry/net/proto"
	"github.com/nats-io/nuid"
	"go.uber.org/zap/zapcore"
)

type (
	Actor struct {
		cactor.Base
		agentActorID   string
		connectors     []cfacade.IConnector
		onNewAgentFunc OnNewAgentFunc
		onInitFunc     func()
	}

	OnNewAgentFunc func(newAgent *Agent)
)

func NewActor(agentActorID string) *Actor {
	if agentActorID == "" {
		panic("agentActorID is empty.")
	}

	parser := &Actor{
		agentActorID: agentActorID,
		connectors:   make([]cfacade.IConnector, 0),
		onInitFunc:   nil,
	}

	return parser
}

// OnInit Actor初始化前触发该函数
func (p *Actor) OnInit() {
	p.Remote().Register(ResponseFuncName, p.response)
	p.Remote().Register(PushFuncName, p.push)
	p.Remote().Register(KickFuncName, p.kick)
	p.Remote().Register(BroadcastName, p.broadcast)

	if p.onInitFunc != nil {
		p.onInitFunc()
	}
}

func (p *Actor) SetOnInitFunc(fn func()) {
	p.onInitFunc = fn
}

func (p *Actor) Load(app cfacade.IApplication) {
	if len(p.connectors) < 1 {
		panic("connectors is nil. Please call the AddConnector(...) method add IConnector.")
	}

	cmd.init(app)

	//  Create agent actor
	if _, err := app.ActorSystem().CreateActor(p.agentActorID, p); err != nil {
		clog.Panicf("Create agent actor fail. err = %+v", err)
	}

	for _, connector := range p.connectors {
		connector.OnConnect(p.defaultOnConnectFunc)
		go connector.Start() // start connector!
	}
}

func (p *Actor) AddConnector(connector cfacade.IConnector) {
	p.connectors = append(p.connectors, connector)
}

func (p *Actor) Connectors() []cfacade.IConnector {
	return p.connectors
}

// defaultOnConnectFunc 创建新连接时，通过当前agentActor创建child agent actor
func (p *Actor) defaultOnConnectFunc(conn net.Conn) {
	session := &cproto.Session{
		Sid:       nuid.Next(),
		AgentPath: p.Path().String(),
		Data:      map[string]string{},
	}

	agent := NewAgent(p.App(), conn, session)

	if p.onNewAgentFunc != nil {
		p.onNewAgentFunc(&agent)
	}

	BindSID(&agent)
	agent.Run()
}

func (*Actor) SetDictionary(dict map[string]uint16) {
	pomeloMessage.SetDictionary(dict)
}

func (*Actor) SetDataCompression(compression bool) {
	pomeloMessage.SetDataCompression(compression)
}

func (*Actor) SetWriteBacklog(size int) {
	cmd.writeBacklog = size
}

func (*Actor) SetHeartbeat(t time.Duration) {
	if t.Seconds() < 1 {
		t = 60 * time.Second
	}
	cmd.heartbeatTime = t
}

func (*Actor) SetSysData(key string, value interface{}) {
	cmd.sysData[key] = value
}

func (p *Actor) SetOnNewAgent(fn OnNewAgentFunc) {
	p.onNewAgentFunc = fn
}

func (*Actor) SetOnDataRoute(fn DataRouteFunc) {
	if fn != nil {
		cmd.onDataRouteFunc = fn
	}
}

func (*Actor) SetOnPacket(typ ppacket.Type, fn PacketFunc) {
	cmd.onPacketFuncMap[typ] = fn
}

func (p *Actor) response(rsp *cproto.PomeloResponse) {
	agent, found := GetAgentWithSID(rsp.Sid)
	if !found {
		if clog.PrintLevel(zapcore.DebugLevel) {
			clog.Debugf("[response] Not found agent. [rsp = %+v]", rsp)
		}
		return
	}

	if ccode.IsOK(rsp.Code) {
		agent.ResponseMID(rsp.Mid, rsp.Data, false)
	} else {
		errRsp := &cproto.Response{
			Code: rsp.Code,
		}
		agent.ResponseMID(rsp.Mid, errRsp, true)
	}
}

func (p *Actor) push(rsp *cproto.PomeloPush) {
	if rsp.Sid != "" || rsp.Uid > 0 {
		if agent, found := GetAgent(rsp.Sid, rsp.Uid); found {
			agent.Push(rsp.Route, rsp.Data)
		}

		return
	}
}

func (p *Actor) kick(rsp *cproto.PomeloKick) {
	if rsp.Sid != "" || rsp.Uid > 0 {
		if agent, found := GetAgent(rsp.Sid, rsp.Uid); found {
			agent.Kick(rsp.Reason, rsp.Close)
		}

		return
	}
}

func (p *Actor) broadcast(rsp *cproto.PomeloBroadcast) {
	switch rsp.PushType {
	case cproto.PomeloBroadcast_AllUID:
		{
			ForeachAgent(func(agent *Agent) {
				if agent.IsBind() {
					agent.Push(rsp.Route, rsp.Data)
				}
			})

			return
		}
	case cproto.PomeloBroadcast_UID:
		{
			for _, uid := range rsp.UidList {
				if agent, found := GetAgentWithUID(uid); found {
					agent.Push(rsp.Route, rsp.Data)
				}
			}

			return
		}
	}
}
