package bot

import (
	"bytes"
	"fmt"
	"net"
	"net/http"
	"os"
	"time"

	"github.com/google/uuid"

	"github.com/Tnze/go-mc/bot/world"
	"github.com/Tnze/go-mc/bot/world/entity/player"
	"github.com/Tnze/go-mc/chat"
	"github.com/Tnze/go-mc/data"
	pk "github.com/Tnze/go-mc/net/packet"
	"github.com/Tnze/go-mc/net/ptypes"
)

func (c *Client) updateServerPos(pos player.Pos) error {
	prev := c.Player.Pos
	c.Player.Pos = pos

	switch {
	case !prev.LookEqual(pos) && !prev.PosEqual(pos):
		sendPlayerPositionAndLookPacket(c)
	case !prev.PosEqual(pos):
		sendPlayerPositionPacket(c)
	case !prev.LookEqual(pos):
		sendPlayerLookPacket(c)
	case prev.OnGround != pos.OnGround:
		c.conn.WritePacket(
			//ClientSettings packet (serverbound)
			pk.Marshal(
				data.Flying,
				pk.Boolean(pos.OnGround),
			),
		)
	case time.Now().Add(-time.Second).After(c.lastPosTx):
		c.lastPosTx = time.Now()
		sendPlayerPositionPacket(c)
	}

	if c.Events.PositionChange != nil && !prev.Equal(pos) {
		if err := c.Events.PositionChange(pos); err != nil {
			return err
		}
	}
	return nil
}

// HandleGame receive server packet and response them correctly.
// Note that HandleGame will block if you don't receive from Events.
func (c *Client) HandleGame() error {
	c.wg.Add(1)
	go func() {
		defer c.wg.Done()
		for {
			select {
			case <-c.closing:
				return

			default:
				//Read packets
				p, err := c.conn.ReadPacket()
				if err != nil {
					if e, ok := err.(*net.OpError); ok && e.Err.Error() != "use of closed network connection" {
						fmt.Fprintf(os.Stderr, "ReadPacket error: %v\n", err)
					}
					return
				}
				c.inbound <- p
			}
		}
	}()

	cTick := time.NewTicker(time.Second / 10 / 2)
	defer cTick.Stop()
	for {
		select {
		case <-c.closing:
			return http.ErrServerClosed
		case <-cTick.C:
			if err := c.Physics.Tick(&c.Wd); err != nil {
				c.disconnect()
				return err
			}
			c.updateServerPos(c.Physics.Position())

		case task := <-c.Delegate:
			if err := task(); err != nil {
				c.disconnect()
				return err
			}
		case p := <-c.inbound:
			//handle packets
			disconnect, err := c.handlePacket(p)
			if err != nil {
				c.disconnect()
				return fmt.Errorf("handle packet 0x%X error: %w", p.ID, err)
			}
			if disconnect {
				return nil
			}
		}
	}
}

func (c *Client) handlePacket(p pk.Packet) (disconnect bool, err error) {
	if c.Events.ReceivePacket != nil {
		pass, err := c.Events.ReceivePacket(p)
		if err != nil {
			return false, err
		}
		if pass {
			return false, nil
		}
	}

	switch data.PktID(p.ID) {
	case data.Login:
		err = handleJoinGamePacket(c, p)
	case data.CustomPayloadClientbound:
		err = handlePluginPacket(c, p)
	case data.Difficulty:
		err = handleServerDifficultyPacket(c, p)
	case data.SpawnPosition:
		err = handleSpawnPositionPacket(c, p)
		if err2 := c.Events.updateSeenPackets(seenSpawnPos); err == nil {
			err = err2
		}
	case data.AbilitiesClientbound:
		err = handlePlayerAbilitiesPacket(c, p)
	case data.HeldItemSlotClientbound:
		err = handleHeldItemPacket(c, p)
	case data.UpdateLight:
		err = c.Events.updateSeenPackets(seenUpdateLight)
	case data.MapChunk:
		err = handleChunkDataPacket(c, p)
	case data.PositionClientbound:
		err = handlePlayerPositionAndLookPacket(c, p)
		sendPlayerPositionAndLookPacket(c) // to confirm the position
		if err2 := c.Events.updateSeenPackets(seenPlayerPositionAndLook); err == nil {
			err = err2
		}
	case data.DeclareRecipes:
		// handleDeclareRecipesPacket(g, reader)
	case data.KeepAliveClientbound:
		err = handleKeepAlivePacket(c, p)
	case data.Entity:
		//handleEntityPacket(g, reader)
	case data.NamedEntitySpawn:
		// err = handleSpawnPlayerPacket(g, reader)
	case data.WindowItems:
		err = handleWindowItemsPacket(c, p)
	case data.UpdateHealth:
		err = handleUpdateHealthPacket(c, p)
	case data.ChatClientbound:
		err = handleChatMessagePacket(c, p)
	case data.BlockChange:
		err = handleBlockChangePacket(c, p)
	case data.MultiBlockChange:
		err = handleMultiBlockChangePacket(c, p)
	case data.KickDisconnect:
		err = handleDisconnectPacket(c, p)
		disconnect = true
	case data.SetSlot:
		err = handleSetSlotPacket(c, p)
	case data.SoundEffect:
		err = handleSoundEffect(c, p)
	case data.NamedSoundEffect:
		err = handleNamedSoundEffect(c, p)
	case data.Experience:
		err = handleSetExperience(c, p)
	default:
		// fmt.Printf("ignore pack id %X\n", p.ID)
	}
	return
}

func handleSoundEffect(c *Client, p pk.Packet) error {
	var s ptypes.SoundEffect
	if err := s.Decode(p); err != nil {
		return err
	}

	if c.Events.SoundPlay != nil {
		return c.Events.SoundPlay(
			data.SoundNames[s.Sound], int(s.Category),
			float64(s.X)/8, float64(s.Y)/8, float64(s.Z)/8,
			float32(s.Volume), float32(s.Pitch))
	}

	return nil
}

func handleNamedSoundEffect(c *Client, p pk.Packet) error {
	var s ptypes.NamedSoundEffect
	if err := s.Decode(p); err != nil {
		return err
	}

	if c.Events.SoundPlay != nil {
		return c.Events.SoundPlay(
			string(s.Sound), int(s.Category),
			float64(s.X)/8, float64(s.Y)/8, float64(s.Z)/8,
			float32(s.Volume), float32(s.Pitch))
	}

	return nil
}

func handleDisconnectPacket(c *Client, p pk.Packet) error {
	var reason chat.Message

	err := p.Scan(&reason)
	if err != nil {
		return err
	}

	if c.Events.Disconnect != nil {
		return c.Events.Disconnect(reason)
	}
	return nil
}

func handleSetSlotPacket(c *Client, p pk.Packet) error {
	if c.Events.WindowsItemChange == nil {
		return nil
	}
	var pkt ptypes.SetSlot
	if err := pkt.Decode(p); err != nil {
		return err
	}

	return c.Events.WindowsItemChange(byte(pkt.WindowID), int(pkt.Slot), pkt.SlotData)
}

func handleMultiBlockChangePacket(c *Client, p pk.Packet) error {
	if !c.settings.ReceiveMap {
		return nil
	}
	r := bytes.NewReader(p.Data)

	var (
		loc            pk.Long
		dontTrustEdges pk.Boolean
		sz             pk.VarInt
	)

	if err := loc.Decode(r); err != nil {
		return fmt.Errorf("packed location: %v", err)
	}
	if err := dontTrustEdges.Decode(r); err != nil {
		return fmt.Errorf("unknown 1: %v", err)
	}
	if err := sz.Decode(r); err != nil {
		return fmt.Errorf("array size: %v", err)
	}

	packedBlocks := make([]pk.VarLong, int(sz))
	for i := 0; i < int(sz); i++ {
		if err := packedBlocks[i].Decode(r); err != nil {
			return fmt.Errorf("block[%d]: %v", i, err)
		}
	}

	x, z, y := int((loc>>42)&((1<<22)-1)),
		int((loc>>20)&((1<<22)-1)),
		int(loc&((1<<20)-1))

	// Apply transform into negative (these numbers are signed)
	if x >= 1<<21 {
		x -= 1 << 22
	}
	if z >= 1<<21 {
		z -= 1 << 22
	}

	c.Wd.MultiBlockUpdate(world.ChunkLoc{X: x, Z: z}, y, packedBlocks)
	return nil
}

func handleBlockChangePacket(c *Client, p pk.Packet) error {
	if !c.settings.ReceiveMap {
		return nil
	}
	var (
		pos pk.Position
		bID pk.VarInt
	)

	if err := p.Scan(&pos, &bID); err != nil {
		return err
	}

	c.Wd.UnaryBlockUpdate(pos, world.BlockStatus(bID))
	return nil
}

func handleChatMessagePacket(c *Client, p pk.Packet) (err error) {
	var msg ptypes.ChatMessageClientbound
	if err := msg.Decode(p); err != nil {
		return err
	}

	if c.Events.ChatMsg != nil {
		return c.Events.ChatMsg(msg.S, byte(msg.Pos), uuid.UUID(msg.Sender))
	}
	return nil
}

func handleUpdateHealthPacket(c *Client, p pk.Packet) error {
	var pkt ptypes.UpdateHealth
	if err := pkt.Decode(p); err != nil {
		return err
	}

	c.Health = float32(pkt.Health)
	c.Food = int32(pkt.Food)
	c.FoodSaturation = float32(pkt.FoodSaturation)

	if c.Events.HealthChange != nil {
		if err := c.Events.HealthChange(); err != nil {
			return err
		}
	}
	if c.Health < 1 { //player is dead
		c.Physics.Run = false
		sendPlayerPositionAndLookPacket(c)
		if c.Events.Die != nil {
			if err := c.Events.Die(); err != nil {
				return err
			}
		}
	}
	return nil
}

func handleJoinGamePacket(c *Client, p pk.Packet) error {
	var pkt ptypes.JoinGame
	if err := pkt.Decode(p); err != nil {
		return err
	}

	c.EntityID = int(pkt.PlayerEntity)
	c.Gamemode = int(pkt.Gamemode & 0x7)
	c.Hardcore = pkt.Gamemode&0x8 != 0
	c.Dimension = int(pkt.Dimension)
	c.WorldName = string(pkt.WorldName)
	c.ViewDistance = int(pkt.ViewDistance)
	c.ReducedDebugInfo = bool(pkt.RDI)
	c.IsDebug = bool(pkt.IsDebug)
	c.IsFlat = bool(pkt.IsFlat)

	if c.Events.GameStart != nil {
		if err := c.Events.GameStart(); err != nil {
			return err
		}
	}

	c.conn.WritePacket(
		//PluginMessage packet (serverbound) - sending minecraft brand.
		pk.Marshal(
			data.CustomPayloadServerbound,
			pk.Identifier("minecraft:brand"),
			pk.String(c.settings.Brand),
		),
	)
	if err := c.Events.updateSeenPackets(seenJoinGame); err != nil {
		return err
	}
	return nil
}

func handlePluginPacket(c *Client, p pk.Packet) error {
	var msg ptypes.PluginMessage
	if err := msg.Decode(p); err != nil {
		return err
	}

	switch msg.Channel {
	case "minecraft:brand":
		var brandRaw pk.String
		if err := brandRaw.Decode(bytes.NewReader(msg.Data)); err != nil {
			return err
		}
		c.ServInfo.Brand = string(brandRaw)
	}

	if c.Events.PluginMessage != nil {
		return c.Events.PluginMessage(string(msg.Channel), []byte(msg.Data))
	}
	return nil
}

func handleServerDifficultyPacket(c *Client, p pk.Packet) error {
	var difficulty pk.Byte
	if err := p.Scan(&difficulty); err != nil {
		return err
	}
	c.Difficulty = int(difficulty)

	if c.Events.ServerDifficultyChange != nil {
		if err := c.Events.ServerDifficultyChange(c.Difficulty); err != nil {
			return err
		}
	}
	return c.Events.updateSeenPackets(seenServerDifficulty)
}

func handleSpawnPositionPacket(c *Client, p pk.Packet) error {
	var pos pk.Position
	err := p.Scan(&pos)
	if err != nil {
		return err
	}
	c.SpawnPosition.X, c.SpawnPosition.Y, c.SpawnPosition.Z =
		pos.X, pos.Y, pos.Z
	return nil
}

func handlePlayerAbilitiesPacket(c *Client, p pk.Packet) error {
	var (
		flags    pk.Byte
		flySpeed pk.Float
		viewMod  pk.Float
	)
	err := p.Scan(&flags, &flySpeed, &viewMod)
	if err != nil {
		return err
	}
	c.abilities.Flags = int8(flags)
	c.abilities.FlyingSpeed = float32(flySpeed)
	c.abilities.FieldofViewModifier = float32(viewMod)

	c.conn.WritePacket(
		//ClientSettings packet (serverbound)
		pk.Marshal(
			data.Settings,
			pk.String(c.settings.Locale),
			pk.Byte(c.settings.ViewDistance),
			pk.VarInt(c.settings.ChatMode),
			pk.Boolean(c.settings.ChatColors),
			pk.UnsignedByte(c.settings.DisplayedSkinParts),
			pk.VarInt(c.settings.MainHand),
		),
	)
	return c.Events.updateSeenPackets(seenPlayerAbilities)
}

func handleHeldItemPacket(c *Client, p pk.Packet) error {
	var hi pk.Byte
	if err := p.Scan(&hi); err != nil {
		return err
	}
	c.HeldItem = int(hi)

	if c.Events.HeldItemChange != nil {
		return c.Events.HeldItemChange(c.HeldItem)
	}
	return nil
}

func handleChunkDataPacket(c *Client, p pk.Packet) error {
	if err := c.Events.updateSeenPackets(seenChunkData); err != nil {
		return err
	}
	if !c.settings.ReceiveMap {
		return nil
	}

	var pkt ptypes.ChunkData
	if err := pkt.Decode(p); err != nil {
		return err
	}

	chunk, err := world.DecodeChunkColumn(int32(pkt.PrimaryBitMask), pkt.Data)
	if err != nil {
		return fmt.Errorf("decode chunk column: %w", err)
	}

	c.Wd.LoadChunk(int(pkt.X), int(pkt.Z), chunk)
	return nil
}

func handlePlayerPositionAndLookPacket(c *Client, p pk.Packet) error {
	var pkt ptypes.PositionAndLookClientbound
	if err := pkt.Decode(p); err != nil {
		return err
	}

	pp := c.Player.Pos
	if pkt.RelativeX() {
		pp.X += float64(pkt.X)
	} else {
		pp.X = float64(pkt.X)
	}
	if pkt.RelativeY() {
		pp.Y += float64(pkt.Y)
	} else {
		pp.Y = float64(pkt.Y)
	}
	if pkt.RelativeZ() {
		pp.Z += float64(pkt.Z)
	} else {
		pp.Z = float64(pkt.Z)
	}
	if pkt.RelativeYaw() {
		pp.Yaw += float32(pkt.Yaw)
	} else {
		pp.Yaw = float32(pkt.Yaw)
	}
	if pkt.RelativePitch() {
		pp.Pitch += float32(pkt.Pitch)
	} else {
		pp.Pitch = float32(pkt.Pitch)
	}
	if err := c.Physics.ServerPositionUpdate(pp, &c.Wd); err != nil {
		return err
	}
	c.Player.Pos = pp

	if c.Events.PositionChange != nil {
		if err := c.Events.PositionChange(pp); err != nil {
			return err
		}
	}

	//Confirm
	return c.conn.WritePacket(pk.Marshal(
		data.TeleportConfirm,
		pk.VarInt(pkt.TeleportID),
	))
}

func handleKeepAlivePacket(c *Client, p pk.Packet) error {
	var KeepAliveID pk.Long
	if err := p.Scan(&KeepAliveID); err != nil {
		return err
	}
	//Response
	return c.conn.WritePacket(pk.Marshal(
		data.KeepAliveServerbound,
		KeepAliveID,
	))
}

func handleWindowItemsPacket(c *Client, p pk.Packet) error {
	var pkt ptypes.WindowItems
	if err := pkt.Decode(p); err != nil {
		return err
	}

	if pkt.WindowID == 0 { // Window ID 0 is the players' inventory.
		if err := c.Events.updateSeenPackets(seenPlayerInventory); err != nil {
			return err
		}
	}
	if c.Events.WindowsItem != nil {
		return c.Events.WindowsItem(byte(pkt.WindowID), pkt.Slots)
	}
	return nil
}

func handleSetExperience(c *Client, p pk.Packet) (err error) {
	var (
		bar   pk.Float
		level pk.VarInt
		total pk.VarInt
	)

	if err := p.Scan(&bar, &level, &total); err != nil {
		return err
	}

	c.Level = int32(level)

	if c.Events.ExperienceChange != nil {
		return c.Events.ExperienceChange(float32(bar), int32(level), int32(total))
	}

	return nil
}

func sendPlayerPositionAndLookPacket(c *Client) error {
	// fmt.Println("PPL")
	return c.conn.WritePacket(ptypes.PositionAndLookServerbound{
		X:        pk.Double(c.Pos.X),
		Y:        pk.Double(c.Pos.Y),
		Z:        pk.Double(c.Pos.Z),
		Yaw:      pk.Float(c.Pos.Yaw),
		Pitch:    pk.Float(c.Pos.Pitch),
		OnGround: pk.Boolean(c.Pos.OnGround),
	}.Encode())
}

func sendPlayerPositionPacket(c *Client) error {
	// fmt.Println("P")
	return c.conn.WritePacket(ptypes.Position{
		X:        pk.Double(c.Pos.X),
		Y:        pk.Double(c.Pos.Y),
		Z:        pk.Double(c.Pos.Z),
		OnGround: pk.Boolean(c.Pos.OnGround),
	}.Encode())
}

func sendPlayerLookPacket(c *Client) error {
	// fmt.Println("L")
	return c.conn.WritePacket(ptypes.Look{
		Yaw:      pk.Float(c.Pos.Yaw),
		Pitch:    pk.Float(c.Pos.Pitch),
		OnGround: pk.Boolean(c.Pos.OnGround),
	}.Encode())
}
