package state

import (
	"github.com/pkg/errors"

	"github.com/diamondburned/arikawa/v3/discord"
	"github.com/diamondburned/arikawa/v3/gateway"
	"github.com/diamondburned/arikawa/v3/state/store"
)

func (s *State) hookSession() {
	s.Session.AddSyncHandler(func(event interface{}) {
		// Call the pre-handler before the state handler.
		if s.PreHandler != nil {
			s.PreHandler.Call(event)
		}

		// Run the state handler.
		s.onEvent(event)

		switch event := event.(type) {
		case *gateway.ReadyEvent:
			s.Handler.Call(event)
			s.handleReady(event)
		case *gateway.GuildCreateEvent:
			s.Handler.Call(event)
			s.handleGuildCreate(event)
		case *gateway.GuildDeleteEvent:
			s.Handler.Call(event)
			s.handleGuildDelete(event)

		// https://github.com/discord/discord-api-docs/commit/01665c4
		case *gateway.MessageCreateEvent:
			if event.Member != nil {
				event.Member.User = event.Author
			}
			s.Handler.Call(event)

		case *gateway.MessageUpdateEvent:
			if event.Member != nil {
				event.Member.User = event.Author
			}
			s.Handler.Call(event)

		default:
			s.Handler.Call(event)
		}
	})
}

func (s *State) onEvent(iface interface{}) {
	switch ev := iface.(type) {
	case *gateway.ReadyEvent:
		// Acquire the ready mutex, but since we're only writing the value and
		// not anything in it, we should be fine.
		s.readyMu.Lock()
		s.ready = *ev
		s.readyMu.Unlock()

		// Reset the store before proceeding.
		if err := s.Cabinet.Reset(); err != nil {
			s.stateErr(err, "failed to reset state in Ready")
		}

		// Handle guilds
		for i := range ev.Guilds {
			s.batchLog(storeGuildCreate(s.Cabinet, &ev.Guilds[i]))
		}

		// Handle guild presences
		for i, presence := range ev.Presences {
			if err := s.Cabinet.PresenceSet(presence.GuildID, &ev.Presences[i], false); err != nil {
				s.stateErr(err, "failed to set presence in Ready")
			}
		}

		// Handle private channels
		for i := range ev.PrivateChannels {
			if err := s.Cabinet.ChannelSet(&ev.PrivateChannels[i], false); err != nil {
				s.stateErr(err, "failed to set channel in Ready")
			}
		}

		// Handle user
		if err := s.Cabinet.MyselfSet(ev.User, false); err != nil {
			s.stateErr(err, "failed to set self in Ready")
		}

	case *gateway.ReadySupplementalEvent:
		// Handle guilds
		for _, guild := range ev.Guilds {
			// Handle guild voice states
			for i := range guild.VoiceStates {
				v := &guild.VoiceStates[i]
				if err := s.Cabinet.VoiceStateSet(guild.ID, v, false); err != nil {
					s.stateErr(err, "failed to set guild voice state in Ready Supplemental")
				}
			}
		}

		friendPresences := gateway.ConvertSupplementalPresences(ev.MergedPresences.Friends)
		for i := range friendPresences {
			if err := s.Cabinet.PresenceSet(0, &friendPresences[i], false); err != nil {
				s.stateErr(err, "failed to set friend presence in Ready Supplemental")
			}
		}

		// Discord uses weird indexing, so we'll need the Guilds slice.
		ready := s.Ready()

		for i := 0; i < len(ready.Guilds) && i < len(ev.MergedMembers); i++ {
			guild := ready.Guilds[i]

			members := gateway.ConvertSupplementalMembers(ev.MergedMembers[i])
			for i := range members {
				if err := s.Cabinet.MemberSet(guild.ID, &members[i], false); err != nil {
					s.stateErr(err, "failed to set friend presence in Ready Supplemental")
				}
			}

			presences := gateway.ConvertSupplementalPresences(ev.MergedPresences.Guilds[i])
			for i := range presences {
				if err := s.Cabinet.PresenceSet(guild.ID, &presences[i], false); err != nil {
					s.stateErr(err, "failed to set member presence in Ready Supplemental")
				}
			}
		}

	case *gateway.GuildCreateEvent:
		s.batchLog(storeGuildCreate(s.Cabinet, ev))

	case *gateway.GuildUpdateEvent:
		if err := s.Cabinet.GuildSet(&ev.Guild, true); err != nil {
			s.stateErr(err, "failed to update guild in state")
		}

	case *gateway.GuildDeleteEvent:
		if err := s.Cabinet.GuildRemove(ev.ID); err != nil && !ev.Unavailable {
			s.stateErr(err, "failed to delete guild in state")
		}

	case *gateway.GuildMemberAddEvent:
		if err := s.Cabinet.MemberSet(ev.GuildID, &ev.Member, false); err != nil {
			s.stateErr(err, "failed to add a member in state")
		}

	case *gateway.GuildMemberUpdateEvent:
		m, err := s.Cabinet.Member(ev.GuildID, ev.User.ID)
		if err != nil {
			// We can't do much here.
			m = &discord.Member{}
		}

		// Update available fields from ev into m
		ev.UpdateMember(m)

		if err := s.Cabinet.MemberSet(ev.GuildID, m, true); err != nil {
			s.stateErr(err, "failed to update a member in state")
		}

	case *gateway.GuildMemberRemoveEvent:
		if err := s.Cabinet.MemberRemove(ev.GuildID, ev.User.ID); err != nil {
			s.stateErr(err, "failed to remove a member in state")
		}

	case *gateway.GuildMembersChunkEvent:
		for i := range ev.Members {
			if err := s.Cabinet.MemberSet(ev.GuildID, &ev.Members[i], false); err != nil {
				s.stateErr(err, "failed to add a member from chunk in state")
			}
		}

		for i := range ev.Presences {
			if err := s.Cabinet.PresenceSet(ev.GuildID, &ev.Presences[i], false); err != nil {
				s.stateErr(err, "failed to add a presence from chunk in state")
			}
		}

	case *gateway.GuildRoleCreateEvent:
		if err := s.Cabinet.RoleSet(ev.GuildID, &ev.Role, false); err != nil {
			s.stateErr(err, "failed to add a role in state")
		}

	case *gateway.GuildRoleUpdateEvent:
		if err := s.Cabinet.RoleSet(ev.GuildID, &ev.Role, true); err != nil {
			s.stateErr(err, "failed to update a role in state")
		}

	case *gateway.GuildRoleDeleteEvent:
		if err := s.Cabinet.RoleRemove(ev.GuildID, ev.RoleID); err != nil {
			s.stateErr(err, "failed to remove a role in state")
		}

	case *gateway.GuildEmojisUpdateEvent:
		if err := s.Cabinet.EmojiSet(ev.GuildID, ev.Emojis, true); err != nil {
			s.stateErr(err, "failed to update emojis in state")
		}

	case *gateway.ChannelCreateEvent:
		if err := s.Cabinet.ChannelSet(&ev.Channel, false); err != nil {
			s.stateErr(err, "failed to create a channel in state")
		}

	case *gateway.ChannelUpdateEvent:
		if err := s.Cabinet.ChannelSet(&ev.Channel, true); err != nil {
			s.stateErr(err, "failed to update a channel in state")
		}

	case *gateway.ChannelDeleteEvent:
		if err := s.Cabinet.ChannelRemove(&ev.Channel); err != nil {
			s.stateErr(err, "failed to remove a channel in state")
		}

	case *gateway.ChannelPinsUpdateEvent:
		// not tracked.

	case *gateway.ThreadListSyncEvent:
		for i := range ev.Threads {
			if err := s.Cabinet.ChannelSet(&ev.Threads[i], true); err != nil {
				s.stateErr(err, "failed to set a thread in state sync")
			}
		}

	case *gateway.ThreadCreateEvent:
		if err := s.Cabinet.ChannelSet(&ev.Channel, false); err != nil {
			s.stateErr(err, "failed to create a thread in state")
		}

	case *gateway.ThreadUpdateEvent:
		if err := s.Cabinet.ChannelSet(&ev.Channel, true); err != nil {
			s.stateErr(err, "failed to update a thread in state")
		}

	case *gateway.ThreadDeleteEvent:
		if ch, err := s.Cabinet.Channel(ev.ID); err == nil {
			if err := s.Cabinet.ChannelRemove(ch); err != nil {
				s.stateErr(err, "failed to delete a thread in state")
			}
		}

	case *gateway.MessageCreateEvent:
		if err := s.Cabinet.MessageSet(&ev.Message, false); err != nil {
			s.stateErr(err, "failed to add a message in state")
		}

	case *gateway.MessageUpdateEvent:
		if err := s.Cabinet.MessageSet(&ev.Message, true); err != nil {
			s.stateErr(err, "failed to update a message in state")
		}

	case *gateway.MessageDeleteEvent:
		if err := s.Cabinet.MessageRemove(ev.ChannelID, ev.ID); err != nil {
			s.stateErr(err, "failed to delete a message in state")
		}

	case *gateway.MessageDeleteBulkEvent:
		for _, id := range ev.IDs {
			if err := s.Cabinet.MessageRemove(ev.ChannelID, id); err != nil {
				s.stateErr(err, "failed to delete bulk messages in state")
			}
		}

	case *gateway.MessageReactionAddEvent:
		s.editMessage(ev.ChannelID, ev.MessageID, func(m *discord.Message) bool {
			if i := findReaction(m.Reactions, ev.Emoji); i > -1 {
				// Copy the reactions slice so it's not racy.
				m.Reactions = append([]discord.Reaction(nil), m.Reactions...)
				m.Reactions[i].Count++
			} else {
				var me bool
				if u, _ := s.Cabinet.Me(); u != nil {
					me = ev.UserID == u.ID
				}
				old := m.Reactions
				m.Reactions = make([]discord.Reaction, 0, len(old)+1)
				m.Reactions = append(m.Reactions, old...)
				m.Reactions = append(m.Reactions, discord.Reaction{
					Count: 1,
					Me:    me,
					Emoji: ev.Emoji,
				})
			}
			return true
		})

	case *gateway.MessageReactionRemoveEvent:
		s.editMessage(ev.ChannelID, ev.MessageID, func(m *discord.Message) bool {
			var i = findReaction(m.Reactions, ev.Emoji)
			if i < 0 {
				return false
			}

			r := &m.Reactions[i]
			newCount := r.Count - 1

			switch {
			case newCount < 1: // If the count is 0:
				old := m.Reactions
				m.Reactions = make([]discord.Reaction, len(m.Reactions)-1)
				copy(m.Reactions[0:], old[:i])
				copy(m.Reactions[i:], old[i+1:])

			default:
				r.Count--
				if r.Me { // If reaction removal is the user's
					u, err := s.Cabinet.Me()
					if err == nil && ev.UserID == u.ID {
						r.Me = false
					}
				}
			}

			return true
		})

	case *gateway.MessageReactionRemoveAllEvent:
		s.editMessage(ev.ChannelID, ev.MessageID, func(m *discord.Message) bool {
			m.Reactions = nil
			return true
		})

	case *gateway.MessageReactionRemoveEmojiEvent:
		s.editMessage(ev.ChannelID, ev.MessageID, func(m *discord.Message) bool {
			var i = findReaction(m.Reactions, ev.Emoji)
			if i < 0 {
				return false
			}
			m.Reactions = append(m.Reactions[:i], m.Reactions[i+1:]...)
			return true
		})

	case *gateway.PresenceUpdateEvent:
		if err := s.Cabinet.PresenceSet(ev.GuildID, &ev.Presence, true); err != nil {
			s.stateErr(err, "failed to update presence in state")
		}

	case *gateway.PresencesReplaceEvent:
		for _, p := range *ev {
			if err := s.Cabinet.PresenceSet(p.GuildID, &p.Presence, true); err != nil {
				s.stateErr(err, "failed to update presence in state")
			}
		}

	case *gateway.SessionsReplaceEvent:
		// TODO

	case *gateway.UserGuildSettingsUpdateEvent:
		// TODO

	case *gateway.UserSettingsUpdateEvent:
		s.readyMu.Lock()
		s.ready.UserSettings = &ev.UserSettings
		s.readyMu.Unlock()

	case *gateway.UserNoteUpdateEvent:
		// TODO

	case *gateway.UserUpdateEvent:
		if err := s.Cabinet.MyselfSet(ev.User, true); err != nil {
			s.stateErr(err, "failed to update myself from USER_UPDATE")
		}

	case *gateway.VoiceStateUpdateEvent:
		vs := &ev.VoiceState
		if !vs.ChannelID.IsValid() {
			if err := s.Cabinet.VoiceStateRemove(vs.GuildID, vs.UserID); err != nil {
				s.stateErr(err, "failed to remove voice state from state")
			}
		} else {
			if err := s.Cabinet.VoiceStateSet(vs.GuildID, vs, true); err != nil {
				s.stateErr(err, "failed to update voice state in state")
			}
		}
	}
}

func (s *State) stateErr(err error, wrap string) {
	s.StateLog(errors.Wrap(err, wrap))
}

func (s *State) batchLog(errors []error) {
	for _, err := range errors {
		s.StateLog(err)
	}
}

// Helper functions

func (s *State) editMessage(ch discord.ChannelID, msg discord.MessageID, fn func(m *discord.Message) bool) {
	m, err := s.Cabinet.Message(ch, msg)
	if err != nil {
		return
	}

	// Copy the messages.
	cpy := *m
	m = &cpy

	if !fn(m) {
		return
	}

	if err := s.Cabinet.MessageSet(m, true); err != nil {
		s.stateErr(err, "failed to save message in reaction add")
	}
}

func findReaction(rs []discord.Reaction, emoji discord.Emoji) int {
	for i := range rs {
		if rs[i].Emoji.ID == emoji.ID && rs[i].Emoji.Name == emoji.Name {
			return i
		}
	}
	return -1
}

func storeGuildCreate(cab *store.Cabinet, guild *gateway.GuildCreateEvent) []error {
	if guild.Unavailable {
		return nil
	}

	stack, errs := newErrorStack()

	if err := cab.GuildSet(&guild.Guild, false); err != nil {
		errs(err, "failed to set guild in Ready")
	}

	// Handle guild emojis
	if len(guild.Emojis) > 0 {
		if err := cab.EmojiSet(guild.ID, guild.Emojis, false); err != nil {
			errs(err, "failed to set guild emojis")
		}
	}

	// Handle guild member
	for i := range guild.Members {
		if err := cab.MemberSet(guild.ID, &guild.Members[i], false); err != nil {
			errs(err, "failed to set guild member in Ready")
		}
	}

	// Handle guild channels
	for _, ch := range guild.Channels {
		// I HATE Discord.
		ch := ch
		ch.GuildID = guild.ID

		if err := cab.ChannelSet(&ch, false); err != nil {
			errs(err, "failed to set guild channel in Ready")
		}
	}

	// Handle threads.
	for _, ch := range guild.Threads {
		ch := ch
		ch.GuildID = guild.ID

		if err := cab.ChannelSet(&ch, false); err != nil {
			errs(err, "failed to set guild thread in Ready")
		}
	}

	// Handle guild presences
	for _, p := range guild.Presences {
		p := p
		p.GuildID = guild.ID

		if err := cab.PresenceSet(guild.ID, &p, false); err != nil {
			errs(err, "failed to set guild presence in Ready")
		}
	}

	// Handle guild voice states
	for _, v := range guild.VoiceStates {
		v := v
		v.GuildID = guild.ID

		if err := cab.VoiceStateSet(guild.ID, &v, false); err != nil {
			errs(err, "failed to set guild voice state in Ready")
		}
	}

	// Handle guild roles
	for _, r := range guild.Roles {
		r := r

		if err := cab.RoleSet(guild.ID, &r, false); err != nil {
			errs(err, "failed to set role in Ready")
		}
	}

	return *stack
}

func newErrorStack() (*[]error, func(error, string)) {
	var errs = new([]error)
	return errs, func(err error, wrap string) {
		*errs = append(*errs, errors.Wrap(err, wrap))
	}
}