package splashmenu

import (
	"bytes"
	"cosmos/diego/groovy"
	splashmenu "cosmos/diego/groovy/examples/splashmenu/fonts"
	"fmt"
	"image"
	"image/color"
	"log"
	"math"

	"github.com/hajimehoshi/ebiten/v2"
	"github.com/hajimehoshi/ebiten/v2/inpututil"
	"github.com/hajimehoshi/ebiten/v2/text"
	"github.com/hajimehoshi/ebiten/v2/vector"

	_ "embed"
)

const (
	FORCE_GRAVITY     = 0
	FORCE_ATTACK      = 1
	FORCE_ENVIRONMENT = 2
	MAX_FLOOR_POS     = 720 - 50
	HERO_WIDTH        = 20
	HERO_HEIGHT       = HERO_WIDTH

	//initial thrust
	MAX_JUMP_HEIGHT       = -150 //pixels
	TIME_TO_APEX          = 20.0 // in frame counts (60fps means this value is 1/60 seconds)
	JUMP_DURATION         = TIME_TO_APEX * 2.0
	GRAVITY_VECTOR        = -2.0 * MAX_JUMP_HEIGHT / (TIME_TO_APEX * TIME_TO_APEX)
	INITIAL_JUMP_VELOCITY = -GRAVITY_VECTOR * TIME_TO_APEX

	//weighted drop
	TIME_TO_APEX2         = 17.0 // in frame counts (60fps means this value is 1/60 seconds)
	GRAVITY_VECTOR2       = -2.0 * MAX_JUMP_HEIGHT / (TIME_TO_APEX2 * TIME_TO_APEX2)
	INITIAL_DROP_VELOCITY = -GRAVITY_VECTOR2 * TIME_TO_APEX2

	//run animation ramp up
	TOP_SPEED         = 20
	TIME_TO_TOP_SPEED = 6 //in frames
	VELOCITY_VECTOR   = 2 * TOP_SPEED / (TIME_TO_TOP_SPEED * TIME_TO_TOP_SPEED)

	//decel animation ramp down
	TIME_TO_STOP = 5 //in frames

	//attacking stats
	MAX_HERO_ATTACK_DURATION      = 22 //in frames
	TOTAL_ATTACK_ANIMATION_IMAGES = 8
	FRAMES_PER_ATTACK_SLIDE       = MAX_HERO_ATTACK_DURATION / TOTAL_ATTACK_ANIMATION_IMAGES

	SENSITIVITY = 0.15
)

var (
	whiteImage    = ebiten.NewImage(3, 3)
	whiteSubImage = whiteImage.SubImage(image.Rect(1, 1, 2, 2)).(*ebiten.Image)

	//go:embed assets/slash.png
	slashImg_png []byte
	//go:embed assets/slashright.png
	slashRightImg_png []byte

	//go:embed assets/forest.png
	forestImg_png []byte

	slashImgHeight     = 60
	slashImgCellsWidth = 72
	//slashImgCells      = 8
	//slashImgDimensions = groovy.Area{Width: 576, Height: slashImgHeight}
	slashImage      = ebiten.NewImage(576, slashImgHeight)
	slashRightImage = ebiten.NewImage(576, slashImgHeight)
	forestImage     = ebiten.NewImage(1280, 720)
	//slashRenderTarget  = ebiten.NewImage(slashImgCellsWidth, slashImgHeight)

	slowmo = false
)

func init() {
	// Decode an image from the image file's byte slice.
	img, _, err := image.Decode(bytes.NewReader(slashImg_png))
	if err != nil {
		log.Fatal(err)
	}
	slashImage = ebiten.NewImageFromImage(img)

	img, _, err = image.Decode(bytes.NewReader(slashRightImg_png))
	if err != nil {
		log.Fatal(err)
	}
	slashRightImage = ebiten.NewImageFromImage(img)

	img, _, err = image.Decode(bytes.NewReader(forestImg_png))
	if err != nil {
		log.Fatal(err)
	}
	forestImage = ebiten.NewImageFromImage(img)
}

type vec struct {
	x float32
	y float32
}

type impulse struct {
	f        vec
	timeleft int
}

type forceType uint32

type MyChar struct {
	posX int16
	posY int16
	mass float32 //g

	//acceleration
	aX           float32
	aY           float32
	jumptime     int
	jumping      bool
	leftRamptime int
	runningLeft  bool

	rightRamptime    int
	runningRight     bool
	runningStopFrame int

	attacking  bool
	attacktime int

	facingLeft bool
}

type force struct {
	vector vec
	ftype  forceType
}

func (v *vec) Size() float32 {
	return float32(math.Sqrt(float64(v.x*v.x + v.y*v.y)))

}

type SplashPad struct {
	increment  int
	Dimensions groovy.Area
	events     map[groovy.SceneEvent]func()
	gamepadIDs map[ebiten.GamepadID]struct{}

	globalForces []force

	hero        MyChar
	heroImpulse impulse
	jumping     bool
	jumptimer   int

	controller bool
}

func NewSplashPad() SplashPad {
	h := MyChar{
		posX:             640,
		posY:             MAX_FLOOR_POS,
		mass:             80000,
		aX:               0,
		aY:               0,
		jumptime:         0,
		jumping:          false,
		leftRamptime:     0,
		runningLeft:      false,
		rightRamptime:    0,
		runningRight:     false,
		runningStopFrame: 0,
		attacking:        false,
		attacktime:       0,
		facingLeft:       false,
	}

	s := SplashPad{
		increment:  0,
		events:     make(map[groovy.SceneEvent]func()),
		gamepadIDs: make(map[ebiten.GamepadID]struct{}),
		hero:       h,
		jumping:    false,
		jumptimer:  0,
		controller: false,
	}

	gravity := &force{}
	gravity.vector = vec{x: 0, y: 9.8}
	gravity.ftype = FORCE_GRAVITY
	s.globalForces = append(s.globalForces, *gravity)

	s.heroImpulse = impulse{f: vec{x: 0, y: 0}, timeleft: 0}

	whiteImage.Fill(color.White)
	log.Println("hero.PosY, prev.impulse.Y, impulse.Y, inc, netFy, timer")

	return s
}

func (s *SplashPad) Draw(screen *ebiten.Image) {

	s.DrawBackground(screen)
	s.DrawController(screen)
	s.DrawHero(screen)

	if slowmo && s.increment/60%2 == 0 {
		text.Draw(screen, " slow mo ", splashmenu.SplashFont.BigTitle, s.Dimensions.Width/2-140, 80, color.White)
	}

}

func (s *SplashPad) Update() error {

	var keys []ebiten.Key
	keys = inpututil.AppendJustPressedKeys(keys[:0])
	for _, k := range keys {
		if k == ebiten.Key9 {
			slowmo = !slowmo
		}
	}

	s.increment++
	keys = inpututil.AppendJustPressedKeys(keys[:0])
	for _, k := range keys {
		if k == ebiten.KeyB {
			s.hero.posX = int16(s.Dimensions.Width) / 2
			s.hero.posY = MAX_FLOOR_POS
		}
	}

	s.doThePhysics()

	if s.hero.jumping && s.hero.jumptime < JUMP_DURATION {
		s.hero.jumptime++
	} else if s.hero.posY >= MAX_FLOOR_POS {
		s.hero.posY = MAX_FLOOR_POS
		s.heroImpulse.f.y = 0.0
		s.hero.jumptime = 0
		s.hero.jumping = false
	}

	//check for user input to transition scene
	if inpututil.IsKeyJustPressed(ebiten.KeyQ) {
		if s.events[groovy.COMPLETED] != nil {
			s.events[groovy.COMPLETED]()
		}
	}
	return nil
}

func (s *SplashPad) SetEventHandler(event groovy.SceneEvent, f func()) {
	s.events[event] = f
}

func (s *SplashPad) SetDimensions(a groovy.Area) {
	s.Dimensions = a
}

func (s *SplashPad) DrawController(screen *ebiten.Image) {

	const radInner = 20
	const radOuter = 25
	const maxOffset = radOuter - radInner
	const padding = 20

	//leftOriginX := float32(s.Dimensions.Width/2.0) - 1.75*radOuter
	//leftOriginY := float32(s.Dimensions.Width / 2.0)

	leftOriginX := float32(s.Dimensions.Width - 3*(2*radOuter) - padding)
	leftOriginY := float32((2 * radOuter) - padding)

	leftXOffset := float32(ebiten.StandardGamepadAxisValue(0, ebiten.StandardGamepadAxisLeftStickHorizontal) * maxOffset)
	leftYOffset := float32(ebiten.StandardGamepadAxisValue(0, ebiten.StandardGamepadAxisLeftStickVertical) * maxOffset)

	vector.DrawFilledCircle(screen, leftOriginX, leftOriginY, radOuter, color.White, true)
	vector.DrawFilledCircle(screen, leftOriginX+leftXOffset, leftOriginY+leftYOffset, radInner, color.Black, true)
	vector.DrawFilledCircle(screen, leftOriginX+leftXOffset, leftOriginY+leftYOffset, radInner-5, color.White, true)
	vector.DrawFilledCircle(screen, leftOriginX+leftXOffset, leftOriginY+leftYOffset, radInner-8, color.RGBA{0xff, 0x5c, 0x5c, 0xff}, true)

	//rightOriginX := float32(s.Dimensions.Width/2.0) + 1.75*radOuter
	//rightOriginY := float32(s.Dimensions.Width / 2.0)

	rightOriginX := float32(s.Dimensions.Width - 2*(2*radOuter))
	rightOriginY := float32((2 * radOuter) - padding)

	rightXOffset := float32(ebiten.StandardGamepadAxisValue(0, ebiten.StandardGamepadAxisRightStickHorizontal) * maxOffset)
	rightYOffset := float32(ebiten.StandardGamepadAxisValue(0, ebiten.StandardGamepadAxisRightStickVertical) * maxOffset)

	vector.DrawFilledCircle(screen, rightOriginX, rightOriginY, radOuter, color.White, true)
	vector.DrawFilledCircle(screen, rightOriginX+rightXOffset, rightOriginY+rightYOffset, radInner, color.Black, true)
	vector.DrawFilledCircle(screen, rightOriginX+rightXOffset, rightOriginY+rightYOffset, radInner-5, color.White, true)
	vector.DrawFilledCircle(screen, rightOriginX+rightXOffset, rightOriginY+rightYOffset, radInner-8, color.RGBA{0xff, 0x5c, 0x5c, 0xff}, true)

	gpInput := ebiten.GamepadAxisValue(0, 0)
	text.Draw(screen, fmt.Sprintf("%1.02f", gpInput), splashmenu.SplashFont.Menu, 20, 20, color.White)
}

func (s *SplashPad) DrawBackground(screen *ebiten.Image) {
	screen.DrawImage(forestImage, nil)
}

func (s *SplashPad) DrawHero(screen *ebiten.Image) {

	var path vector.Path

	//set up origin and boundaries
	x0 := float32(s.hero.posX)
	y0 := float32(s.hero.posY)

	xd := float32(HERO_WIDTH / 2.0)
	yd := float32(HERO_WIDTH / 2.0)

	//connect the dots
	path.MoveTo(x0-xd, y0-yd)
	path.LineTo(x0+xd, y0-yd)
	path.LineTo(x0+xd, y0+yd)
	path.LineTo(x0-xd, y0+yd)
	path.LineTo(x0-xd, y0+yd)
	path.Close()

	//fill time
	var vs []ebiten.Vertex
	var is []uint16

	vs, is = path.AppendVerticesAndIndicesForFilling(nil, nil)

	for i := range vs {
		vs[i].SrcX = 1
		vs[i].SrcY = 1
		vs[i].ColorR = float32(0xff) / 0xFF
		vs[i].ColorG = float32(0x00) / 0xFF
		vs[i].ColorB = float32(0x00) / 0xFF
		vs[i].ColorA = 1
	}

	//op := &ebiten.DrawTrianglesOptions{}
	screen.DrawTriangles(vs, is, whiteSubImage, nil)
	op := &ebiten.DrawImageOptions{}
	//handle attacking animation
	if s.hero.attacking {

		i := (s.hero.attacktime / FRAMES_PER_ATTACK_SLIDE)

		if s.hero.facingLeft {
			op.GeoM.Translate(float64(s.hero.posX-int16(slashImgCellsWidth)/2-13), float64(s.hero.posY-HERO_HEIGHT/2-5))
			dx := i * slashImgCellsWidth
			screen.DrawImage(slashImage.SubImage(image.Rect(dx, 0, dx+slashImgCellsWidth, slashImgHeight)).(*ebiten.Image), op)
		} else {
			op.GeoM.Translate(float64(s.hero.posX-24), float64(s.hero.posY-HERO_HEIGHT/2-5))
			dx := slashImgCellsWidth*TOTAL_ATTACK_ANIMATION_IMAGES - i*slashImgCellsWidth
			screen.DrawImage(slashRightImage.SubImage(image.Rect(dx, 0, dx+slashImgCellsWidth, slashImgHeight)).(*ebiten.Image), op)
		}

	}

	if s.hero.jumping {
		text.Draw(screen, "hup!", splashmenu.SplashFont.Splash, int(s.hero.posX), int(s.hero.posY)-HERO_HEIGHT, color.White)
	}

}

func (s *SplashPad) doThePhysics() {
	var netFx float32 = 0
	var netFy float32 = 0

	for _, i := range s.globalForces {
		netFx = netFx + i.vector.x
		netFy = netFy + i.vector.y
	}

	//HANDLE JUMPING
	s.handleJumping()

	//HANDLE LEFT/RIGHT MOVEMENT
	s.handleLeftMovement()
	s.handleRightMovement()

	//HANDLE ATTACK
	s.handleAttack()

	//if s.hero.ramptime < TIME_TO_STOP {
	//	s.hero.posX = s.hero.posX - (VELOCITY_VECTOR * s.hero.ramptime)
	//} else if s.hero.ramptime >= TIME_TO_TOP_SPEED {
	//	s.hero.posX = s.hero.posX - TOP_SPEED
	//}
	//
	//HANDLE FLOOR COLLISION CHECK
	if s.hero.posY > MAX_FLOOR_POS {
		s.hero.posY = MAX_FLOOR_POS
	}
}

func (s *SplashPad) handleAttack() {
	maxButton := ebiten.GamepadButton(ebiten.GamepadButtonCount(0))
	for b := ebiten.GamepadButton(0); b < maxButton; b++ {
		if ebiten.IsGamepadButtonPressed(0, ebiten.GamepadButton0) {
			s.hero.attacking = true
		}
	}

	if ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft) {
		s.hero.attacking = true
	}

	if s.hero.attacking {
		s.hero.attacktime++

		if s.hero.attacktime < MAX_HERO_ATTACK_DURATION {

		} else {
			s.hero.attacking = false
			s.hero.attacktime = 0
		}
	}

}

func (s *SplashPad) handleJumping() {
	maxButton := ebiten.GamepadButton(ebiten.GamepadButtonCount(0))
	for b := ebiten.GamepadButton(0); b < maxButton; b++ {
		if ebiten.IsGamepadButtonPressed(0, ebiten.GamepadButton1) {
			if !s.hero.jumping {
				s.hero.jumptime = 0
				s.hero.jumping = true
			}
		}
	}

	if inpututil.IsKeyJustPressed(ebiten.KeySpace) {
		if !s.hero.jumping {
			s.hero.jumptime = 0
			s.hero.jumping = true
		}
	}

	if s.hero.jumping {
		//asymmetric jump to simulate additional character weight
		if s.hero.jumptime < TIME_TO_APEX {
			t2 := float32((s.hero.jumptime * s.hero.jumptime))
			s.hero.posY = int16(0.5*GRAVITY_VECTOR*t2 + INITIAL_JUMP_VELOCITY*float32(s.hero.jumptime) + MAX_FLOOR_POS)
		} else {
			t2 := float32((s.hero.jumptime * s.hero.jumptime))
			s.hero.posY = int16(0.5*GRAVITY_VECTOR2*t2 + INITIAL_DROP_VELOCITY*float32(s.hero.jumptime) + MAX_FLOOR_POS)

		}
	}
}

func (s *SplashPad) handleLeftMovement() {

	var keys []ebiten.Key
	keys = inpututil.AppendPressedKeys(keys[:0])
	for _, pressed := range keys {
		if pressed == ebiten.KeyA {
			s.hero.leftRamptime++
			s.hero.runningLeft = true
			log.Println(" a engaged")
			s.hero.facingLeft = true
		}
	}

	keys = inpututil.AppendJustReleasedKeys(keys[:0])
	for _, pressed := range keys {
		if pressed == ebiten.KeyA {
			s.hero.runningLeft = false
			s.hero.runningStopFrame = s.increment
			log.Println(" a released")
		}
	}

	if ebiten.GamepadAxisValue(0, 0) <= -SENSITIVITY {
		s.hero.leftRamptime++
		s.hero.runningLeft = true
		log.Println(" left gamepad engaged")
		s.hero.facingLeft = true
		s.controller = true
	}

	if s.controller {
		if -SENSITIVITY < ebiten.GamepadAxisValue(0, 0) && ebiten.GamepadAxisValue(0, 0) < SENSITIVITY {
			s.hero.runningLeft = false
			if s.hero.runningStopFrame > 0 {
				s.hero.runningStopFrame = s.increment
				log.Println(" left gamepad released")
				s.controller = false
			}

		}
	}

	//if running, need to update their position
	var offset = 0
	if s.hero.runningLeft {

		if s.hero.leftRamptime < TIME_TO_TOP_SPEED {
			offset = s.hero.leftRamptime * s.hero.leftRamptime
		} else {
			offset = TOP_SPEED
		}
	}

	////we've let go, time to decel
	//if !s.hero.runningLeft && s.hero.leftRamptime <  {
	//	s.hero.posX = s.hero.posX
	//}

	if s.hero.runningStopFrame > 0 {
		if s.increment-s.hero.runningStopFrame < TIME_TO_STOP {
			scaler := float32(TOP_SPEED) / float32(TIME_TO_STOP*TIME_TO_STOP)
			deltaT := float32(s.increment - s.hero.runningStopFrame)

			offset = TOP_SPEED - int(scaler*deltaT)
			offset = int(math.Max(0, float64(offset))) //should not be less than zero, otherwise player would move wrong way ??
			log.Printf(" stopping (%d,%d)...", s.hero.posX, s.hero.posY)
		} else {
			s.hero.runningStopFrame = 0
			s.hero.leftRamptime = 0
		}
	}

	s.hero.posX = s.hero.posX - int16(offset)

}

func (s *SplashPad) handleRightMovement() {
	var keys []ebiten.Key
	keys = inpututil.AppendPressedKeys(keys[:0])
	for _, pressed := range keys {
		if pressed == ebiten.KeyD {
			s.hero.rightRamptime++
			s.hero.runningRight = true
			log.Println(" d engaged")
			s.hero.facingLeft = false
		}
	}

	keys = inpututil.AppendJustReleasedKeys(keys[:0])
	for _, pressed := range keys {
		if pressed == ebiten.KeyD {
			s.hero.runningRight = false
			log.Println(" d released")
		}
	}

	if ebiten.GamepadAxisValue(0, 0) >= SENSITIVITY {
		s.hero.rightRamptime++
		s.hero.runningRight = true
		log.Println(" right gamepad engaged")
		s.hero.facingLeft = false
		s.controller = true
	}
	if s.controller {
		if -SENSITIVITY < ebiten.GamepadAxisValue(0, 0) && ebiten.GamepadAxisValue(0, 0) < SENSITIVITY {
			s.hero.runningRight = false
			if s.hero.runningStopFrame > 0 {
				s.hero.runningStopFrame = s.increment
				log.Println(" right gamepad released")
				s.controller = false
			}

		}
	}

	//if running, need to update their position
	var offset = 0
	if s.hero.runningRight {

		if s.hero.rightRamptime < TIME_TO_TOP_SPEED {
			offset = s.hero.rightRamptime * s.hero.rightRamptime
		} else {
			offset = TOP_SPEED
		}
		s.hero.posX = s.hero.posX + int16(offset)
	}
}
