package main

import (
	"math"
	"sync"

	gg "github.com/faiface/pixel-examples/community/amidakuji/glossary"

	"github.com/faiface/pixel"
	"github.com/faiface/pixel/imdraw"
)

// Path is for animating a path to the prize in a ladder.
type Path struct {
	imd   *imdraw.IMDraw // shared variable
	mutex sync.Mutex     // synchronize
	//
	roads  []pixel.Vec // A list of vectors - each vector for a position where a road starts.
	prize  *int
	tip    *pixel.Vec
	tipDir pixel.Vec
	iroad  int
	//
	watchAnim          gg.DtWatch // When it started to animate.
	timeLimitAnimInSec float64
	animateInTime      bool
	isAnimating        bool

	// -----------------------------------------------------------
	// exported callbacks(listeners) regarding animation

	// callback on reaching the prize level of a ladder.
	OnFinishedAnimation func()

	// callback when the animating 'tip' passes a point of a road.
	// pt: a point(road) just passed.
	// dir: ...
	// dir is a normalized vector. (pixel.ZV) is passed if the direction can't be found.
	// dir can be different depending on how fast this Path is updated.
	OnPassedEachPoint func(pt pixel.Vec, dir pixel.Vec)
}

// NewPath is a contructor.
func NewPath(_roads []pixel.Vec, _prize *int) *Path {
	newTip := func() *pixel.Vec {
		if _roads != nil {
			if len(_roads) > 0 {
				v := _roads[0]
				return &v
			}
		}
		return nil
	}
	return &Path{
		roads:  _roads,
		prize:  _prize,
		tip:    newTip(),
		tipDir: pixel.ZV,
	}
}

// NewPathEmpty is a contructor.
func NewPathEmpty() *Path {
	return &Path{}
}

// -------------------------------------------------------------------------
// Important methods

// Draw guarantees the thread safety, though it's not a necessary condition.
// It is quite dangerous to access this struct's member (imdraw) directly from outside these methods.
func (path *Path) Draw(t pixel.Target) {
	path.mutex.Lock()
	defer path.mutex.Unlock()

	if path.imd == nil { // isInvisible set to true.
		return // An empty image is drawn.
	}

	path.imd.Draw(t)
}

// Update animates a path. A path is drawn on an imdraw.
func (path *Path) Update(color pixel.RGBA) {
	var (
		iroad = len(path.roads) - 1
		from  = path.roads[len(path.roads)-1]
		to    = path.roads[len(path.roads)-1]
		dir   = pixel.ZV
	)

	if path.isAnimating {
		// get where it is abstract // get a scalar
		dt := path.watchAnim.DtSinceStart()
		const distPerSec = 500
		scalarProgress := dt * distPerSec
		// log.Println(dt, path.Len(), scalarProgress) //

		if path.animateInTime { // overwrite scalarProgress
			fromLot := pixel.V(0, 0)
			toPrize := pixel.V(path.Len(), 0)
			percentagePointPerSec := 1 / path.timeLimitAnimInSec
			scalarProgress = pixel.Lerp(fromLot, toPrize, dt*percentagePointPerSec).X
			// log.Println(dt, path.Len(), scalarProgress, dt*percentagePointPerSec) //
		}

		// get where it is concrete // turn a scalar into a set of vectors
		iroad, from, to, dir = path.FindRoadByDist(scalarProgress)
		if iroad > path.iroad {
			if path.OnPassedEachPoint != nil {
				go path.OnPassedEachPoint(from, dir)
			}
		}
		path.iroad = iroad
		// log.Println(iroad, len(path.roads), iroad == len(path.roads)-1, path.isAnimating) //
		if iroad >= len(path.roads)-1 { // the end
			path.isAnimating = false
			// log.Println(path.Len(), dt) //
			if path.OnFinishedAnimation != nil {
				go path.OnFinishedAnimation()
			} // callback
		}
	}

	// lock before imdraw update
	path.mutex.Lock()
	defer path.mutex.Unlock()

	// imdraw (a state machine)
	if path.imd == nil { // lazy creation
		path.imd = imdraw.New(nil)
	}
	imd := path.imd
	imd.Clear()

	// draw path
	imd.Color = color
	imd.EndShape = imdraw.RoundEndShape
	for i := 0; i < iroad; i++ {
		imd.Push(path.roads[i], path.roads[i+1])
		imd.Line(9)
	}
	imd.Push(from, to)
	imd.Line(9)

	// save where the tip is
	path.tip = &to
}

// -------------------------------------------------------------------------
// Read only methods

// IsAnimating determines whether this Path is about to be updated or not.
// Pass lock by value warning from (path Path) should be ignored,
// because a Path here is just passed as a read only argument.
func (path Path) IsAnimating() bool {
	return path.isAnimating
}

// GetPrize is just an average getter.
// It returns -1 if the receiver is not initialized with that member(prize).
// Pass lock by value warning from (path Path) should be ignored,
// because a Path here is just passed as a read only argument.
func (path Path) GetPrize() int {
	if path.prize == nil {
		return -1
	}
	return *path.prize
}

// PosTip returns a vector that tells you how far the animating path currently has reached.
// A non-ptr Path as a read only argument passes lock by value within itself but that seems totally fine.
func (path Path) PosTip() (v pixel.Vec) {
	return *path.tip
}

// Len returns the total length of all roads.
// A non-ptr Path as a read only argument passes lock by value within itself but that seems totally fine.
func (path Path) Len() (sum float64) {
	for i := 0; i < len(path.roads)-1; i++ {
		sum += math.Abs(path.roads[i].Sub(path.roads[i+1]).Len())
	}
	return
}

// FindRoadByDist converts a scalar into a set of vectors.
// A non-ptr Path as a read only argument passes lock by value within itself but that seems totally fine.
//
// Returns
// iroad: The index of a road found.
// road: The vector representation of a road found. A road is a line from pt A to B, and that vector points to where pt A is.
// pos: A position(point) found which is in the middle of that found road(line).
// dirVecNormalized: A direction as a normalized vector. This vector always has a length of 1.
func (path Path) FindRoadByDist(distProgress float64) (iroad int, road pixel.Vec, pos pixel.Vec, dirVecNormalized pixel.Vec) {
	lengthOfTraveledRoads := float64(0.0)
	for iroad = 0; iroad < len(path.roads)-1; iroad++ {
		var lengthOfThisRoad float64
		iroadNext := iroad + 1
		lengthOfThisRoad = math.Abs(path.roads[iroad].Sub(path.roads[iroadNext]).Len())
		lengthOfTraveledRoads += lengthOfThisRoad
		// For loop breaker: distProgress is somewhere between the total length of a path.
		if lengthOfTraveledRoads > distProgress {
			scalar := lengthOfThisRoad - (lengthOfTraveledRoads - distProgress)
			if path.roads[iroad].Y == path.roads[iroadNext].Y &&
				path.roads[iroad].X < path.roads[iroadNext].X { // to the bottom (east)
				pos = path.roads[iroad]
				pos.X += scalar
				dirVecNormalized = pixel.V(1, 0)
			} else if path.roads[iroad].X == path.roads[iroadNext].X &&
				path.roads[iroad].Y > path.roads[iroadNext].Y { // to the left (south)
				pos = path.roads[iroad]
				pos.Y -= scalar
				dirVecNormalized = pixel.V(0, -1)
			} else if path.roads[iroad].X == path.roads[iroadNext].X &&
				path.roads[iroad].Y < path.roads[iroadNext].Y { // to the right (north)
				pos = path.roads[iroad]
				pos.Y += scalar
				dirVecNormalized = pixel.V(0, 1)
			} else if path.roads[iroad].Y == path.roads[iroadNext].Y &&
				path.roads[iroad].X > path.roads[iroadNext].X { // to the top (west)
				// Placed at the end of an elif statement,
				// since this case is of no possibility unless the path finding is going reverse.
				pos = path.roads[iroad]
				pos.X -= scalar
				dirVecNormalized = pixel.V(-1, 0)
			} else {
				panic("unhandled exception: it may be a diagonal bridge")
			} // elif
			return iroad, path.roads[iroad], pos, dirVecNormalized
		} // if - for loop breaker
	} // for

	// coming down to here means that the case is (road == pos)
	from := iroad - 1
	to := iroad
	if iroad == 0 {
		from = iroad
		to = iroad + 1
	}
	if from < 0 || to >= len(path.roads) {
		dirVecNormalized = pixel.ZV
	} else {
		dirVecNormalized = gg.Direction(path.roads[from], path.roads[to])
	}
	return iroad, path.roads[iroad], path.roads[iroad], dirVecNormalized
}

// -------------------------------------------------------------------------
// Methods that write to itself

// Animate a path.
func (path *Path) Animate() {
	path.watchAnim.Start()
	path.animateInTime = false
	path.isAnimating = true
}

// AnimateInTime animates a path in given time.
func (path *Path) AnimateInTime(sec float64) {
	path.watchAnim.Start()
	path.timeLimitAnimInSec = sec
	path.animateInTime = true
	path.isAnimating = true
}

// Pause a path's clock.
func (path *Path) Pause() {
	if path.watchAnim.IsStarted() {
		path.watchAnim.Dt()
	}
}

// Resume after pause.
func (path *Path) Resume() {
	if path.watchAnim.IsStarted() {
		started := path.watchAnim.GetTimeStarted()
		dtPause := path.watchAnim.DtNano()
		path.watchAnim.SetTimeStarted(started.Add(dtPause))
		// log.Println(dtPause, started, path.watchAnim.GetTimeStarted()) //
	}
}
