package d2asset

import (
	"errors"
	"fmt"
	"strings"
	"sync"

	"github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
	"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2cof"
	"github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface"
)

const (
	hardcodedFPS     = 25.0
	hardcodedDivisor = 1.0 / 256.0
	speedUnit        = hardcodedFPS * hardcodedDivisor
)

// Composite is a composite entity animation
type Composite struct {
	*AssetManager
	baseType    d2enum.ObjectType
	basePath    string
	token       string
	palettePath string
	direction   int
	equipment   [d2enum.CompositeTypeMax]string
	mode        *compositeMode
	size        *size
}

type size struct {
	Width  int
	Height int
}

// Advance moves the composite animation forward for a given elapsed time in nanoseconds.
func (c *Composite) Advance(elapsed float64) error {
	if c.mode == nil {
		return nil
	}

	c.mode.lastFrameTime += elapsed
	framesToAdd := int(c.mode.lastFrameTime / c.mode.animationSpeed)
	c.mode.lastFrameTime -= float64(framesToAdd) * c.mode.animationSpeed
	c.mode.frameIndex += framesToAdd
	c.mode.playedCount += c.mode.frameIndex / c.mode.frameCount
	c.mode.frameIndex %= c.mode.frameCount

	for _, layer := range c.mode.layers {
		if layer != nil {
			if err := layer.Advance(elapsed); err != nil {
				return err
			}
		}
	}

	return nil
}

// Render performs drawing of the Composite on the rendered d2interface.Surface.
func (c *Composite) Render(target d2interface.Surface) error {
	if c.mode == nil {
		return nil
	}

	direction := d2cof.Dir64ToCof(c.direction, c.mode.cof.NumberOfDirections)

	for _, layerIndex := range c.mode.cof.Priority[direction][c.mode.frameIndex] {
		layer := c.mode.layers[layerIndex]

		if layer != nil {
			layer.RenderFromOrigin(target, true)
		}
	}

	for _, layerIndex := range c.mode.cof.Priority[direction][c.mode.frameIndex] {
		layer := c.mode.layers[layerIndex]
		if layer != nil {
			layer.RenderFromOrigin(target, false)
		}
	}

	return nil
}

// ObjectAnimationMode returns the object animation mode
func (c *Composite) ObjectAnimationMode() d2enum.ObjectAnimationMode {
	return c.mode.animationMode.(d2enum.ObjectAnimationMode)
}

// GetAnimationMode returns the animation mode the Composite should render with.
func (c *Composite) GetAnimationMode() string {
	return c.mode.animationMode.String()
}

// GetCurrentFrame returns the frame index in the current animation mode.
func (c *Composite) GetCurrentFrame() int {
	return c.mode.frameIndex
}

// GetFrameCount returns the number of frames in the current animation mode.
func (c *Composite) GetFrameCount() int {
	return c.mode.frameCount
}

// GetWeaponClass returns the currently loaded weapon class
func (c *Composite) GetWeaponClass() string {
	return c.mode.weaponClass
}

// SetMode sets the Composite's animation mode weapon class and direction
func (c *Composite) SetMode(animationMode animationMode, weaponClass string) error {
	if c.mode != nil && c.mode.animationMode.String() == animationMode.String() && c.mode.weaponClass == weaponClass {
		return nil
	}

	mode, err := c.createMode(animationMode, weaponClass)
	if err != nil {
		return err
	}

	c.resetPlayedCount()
	c.mode = mode

	return nil
}

// Equip changes the current layer configuration
func (c *Composite) Equip(equipment *[d2enum.CompositeTypeMax]string) error {
	c.equipment = *equipment
	if c.mode == nil {
		return nil
	}

	mode, err := c.createMode(c.mode.animationMode, c.mode.weaponClass)

	if err != nil {
		return err
	}

	c.mode = mode

	return nil
}

// SetAnimSpeed sets the speed at which the Composite's animation should advance through its frames
func (c *Composite) SetAnimSpeed(speed int) {
	c.mode.animationSpeed = 1.0 / (float64(speed) * speedUnit) //nolint:gomnd // taking inverse
	for layerIdx := range c.mode.layers {
		layer := c.mode.layers[layerIdx]
		if layer != nil {
			layer.SetPlaySpeed(c.mode.animationSpeed)
		}
	}
}

// SetDirection sets the direction of the composite and its layers
func (c *Composite) SetDirection(direction int) {
	if c.mode == nil {
		return
	}

	wg := sync.WaitGroup{}

	c.direction = direction
	wg.Add(len(c.mode.layers))

	for layerIdx := range c.mode.layers {
		go func(idx int) {
			defer wg.Done()

			layer := c.mode.layers[idx]

			if layer != nil {
				if err := layer.SetDirection(c.direction); err != nil {
					fmt.Printf("failed to set direction of layer: %d, err: %v\n", idx, err)
				}
			}
		}(layerIdx)
	}

	wg.Wait()
}

// GetDirection returns the current direction the composite is facing
func (c *Composite) GetDirection() int {
	return c.direction
}

// GetPlayedCount returns the number of times the current animation mode has completed all its distinct frames
func (c *Composite) GetPlayedCount() int {
	if c.mode == nil {
		return 0
	}

	return c.mode.playedCount
}

// SetPlayLoop turns on or off animation looping
func (c *Composite) SetPlayLoop(loop bool) {
	for layerIdx := range c.mode.layers {
		layer := c.mode.layers[layerIdx]
		if layer != nil {
			layer.SetPlayLoop(loop)
		}
	}
}

// SetSubLoop sets a loop to be between the specified frame indices
func (c *Composite) SetSubLoop(startFrame, endFrame int) {
	for layerIdx := range c.mode.layers {
		layer := c.mode.layers[layerIdx]
		if layer != nil {
			layer.SetSubLoop(startFrame, endFrame)
		}
	}
}

// SetCurrentFrame sets the current frame index of the animation
func (c *Composite) SetCurrentFrame(frame int) {
	for layerIdx := range c.mode.layers {
		layer := c.mode.layers[layerIdx]
		if layer != nil {
			if err := layer.SetCurrentFrame(frame); err != nil {
				fmt.Printf("failed to set current frame of layer: %d, err: %v\n", layerIdx, err)
			}
		}
	}
}

func (c *Composite) resetPlayedCount() {
	if c.mode != nil {
		c.mode.playedCount = 0
	}
}

type animationMode interface {
	String() string
}

type compositeMode struct {
	cof           *d2cof.COF
	animationMode animationMode
	weaponClass   string
	playedCount   int

	layers []d2interface.Animation

	frameCount     int
	frameIndex     int
	animationSpeed float64
	lastFrameTime  float64
}

func (c *Composite) createMode(animationMode animationMode, weaponClass string) (*compositeMode, error) {
	cofPath := fmt.Sprintf("%s/%s/COF/%s%s%s.COF", c.basePath, c.token, c.token, animationMode, weaponClass)
	if exists, err := c.FileExists(cofPath); !exists {
		return nil, fmt.Errorf("composite not found at path '%s': %v", cofPath, err)
	}

	cof, err := c.LoadCOF(cofPath)
	if err != nil {
		return nil, err
	}

	animationKey := strings.ToUpper(c.token + animationMode.String() + weaponClass)

	animationData := c.Records.Animation.Data.GetRecords(animationKey)
	if len(animationData) == 0 {
		return nil, errors.New("could not find Animation data")
	}

	mode := &compositeMode{
		cof:            cof,
		animationMode:  animationMode,
		weaponClass:    weaponClass,
		layers:         make([]d2interface.Animation, d2enum.CompositeTypeMax),
		frameCount:     animationData[0].FramesPerDirection(),
		animationSpeed: 1.0 / (float64(animationData[0].Speed()) * speedUnit), //nolint:gomnd // taking inverse
	}

	for _, cofLayer := range cof.CofLayers {
		layerValue := c.equipment[cofLayer.Type]
		if layerValue == "" {
			layerValue = "lit"
		}

		drawEffect := d2enum.DrawEffectNone

		if cofLayer.Transparent {
			drawEffect = cofLayer.DrawEffect
		}

		layer, err := c.loadCompositeLayer(cofLayer.Type.String(), layerValue, animationMode.String(),
			cofLayer.WeaponClass.String(), c.palettePath, drawEffect)
		if err == nil {
			layer.SetPlaySpeed(mode.animationSpeed)
			layer.PlayForward()
			layer.SetShadow(cofLayer.Shadow != 0)

			if err := layer.SetDirection(c.direction); err != nil {
				return nil, err
			}

			mode.layers[cofLayer.Type] = layer
		}
	}

	return mode, nil
}

func (c *Composite) loadCompositeLayer(layerKey, layerValue, animationMode, weaponClass,
	palettePath string, drawEffect d2enum.DrawEffect) (d2interface.Animation, error) {
	animationPaths := []string{
		fmt.Sprintf("%s/%s/%s/%s%s%s%s%s.dcc", c.basePath, c.token, layerKey, c.token, layerKey, layerValue, animationMode, weaponClass),
		fmt.Sprintf("%s/%s/%s/%s%s%s%s%s.dc6", c.basePath, c.token, layerKey, c.token, layerKey, layerValue, animationMode, weaponClass),
	}

	for idx := range animationPaths {
		exists, err := c.FileExists(animationPaths[idx])
		if !exists || err != nil {
			return nil, fmt.Errorf("animation path '%s' not found: %v", animationPaths[idx], err)
		}

		animation, err := c.LoadAnimationWithEffect(animationPaths[idx], palettePath, drawEffect)
		if err == nil {
			return animation, nil
		}
	}

	return nil, errors.New("animation not found")
}

// GetSize returns the size of the composite
func (c *Composite) GetSize() (w, h int) {
	c.updateSize()
	return c.size.Width, c.size.Height
}

func (c *Composite) updateSize() {
	if c.mode == nil {
		return
	}

	direction := d2cof.Dir64ToCof(c.direction, c.mode.cof.NumberOfDirections)

	biggestW, biggestH := 0, 0

	for _, layerIndex := range c.mode.cof.Priority[direction][c.mode.frameIndex] {
		layer := c.mode.layers[layerIndex]
		if layer != nil {
			w, h := layer.GetCurrentFrameSize()

			if biggestW < w {
				biggestW = w
			}

			if biggestH < h {
				biggestH = h
			}
		}
	}

	if c.size == nil {
		c.size = &size{}
	}

	c.size.Width = biggestW
	c.size.Height = biggestH
}

func baseString(baseType d2enum.ObjectType) string {
	switch baseType {
	case d2enum.ObjectTypePlayer:
		return "/data/global/chars"
	case d2enum.ObjectTypeCharacter:
		return "/data/global/monsters"
	case d2enum.ObjectTypeItem:
		return "/data/global/objects"
	default:
		return ""
	}
}
