package play

import (
	"opslag.de/schobers/geom"
)

// IsometricProjection represents an 2D area (view) that contains isometric tiles.
type IsometricProjection struct {
	center      geom.PointF32 // tile coordinate
	zoom        float32       // factor a tile is blown up (negative is smaller, possitive is larger)
	zoomInverse float32       // 1/zoom; calculated

	tileSize   geom.PointF32     // size of a single tile (maximum width & height difference of its corners)
	viewBounds geom.RectangleF32 // bounds of the view (screen coordinates)
	viewCenter geom.PointF32     // center of view; calculated

	tileSizeTransformed      geom.PointF32 // calculated
	tileToViewTransformation geom.PointF32 // calculated
	viewToTileTransformation geom.PointF32 // calculated
}

// NewIsometricProjection creates a new isometric projection. By default the tile with the coordinate (0, 0) will be centered in the viewBounds. The tile size is represented with maximum width & height difference of its corners.
func NewIsometricProjection(tileSize geom.PointF32, viewBounds geom.RectangleF32) *IsometricProjection {
	p := &IsometricProjection{zoom: 1, tileSize: tileSize, viewBounds: viewBounds}
	p.update()
	return p
}

func (p *IsometricProjection) update() {
	if p.zoom == 0 {
		p.zoom = 1
	}
	p.zoomInverse = 1 / p.zoom

	p.viewCenter = p.viewBounds.Center()

	p.tileSizeTransformed = p.tileSize.Mul(p.zoom)
	p.tileToViewTransformation = p.tileSize.Mul(.5 * p.zoom)
	p.viewToTileTransformation = geom.PtF32(1/p.tileSizeTransformed.X, 1/p.tileSizeTransformed.Y)
}

// Center gives back the coordinate of the center tile
func (p *IsometricProjection) Center() geom.PointF32 { return p.center }

// Enumerate enumerates all tiles in the set view bounds and calls action for every tile.
func (p *IsometricProjection) Enumerate(action func(tile geom.PointF32, view geom.PointF32)) {
	p.EnumerateInt(func(tile geom.Point, view geom.PointF32) {
		action(tile.ToF32(), view)
	})
}

// EnumerateInt enumerates all tiles in the set view bounds and calls action for every tile.
func (p *IsometricProjection) EnumerateInt(action func(tile geom.Point, view geom.PointF32)) {
	visible := p.viewBounds
	visible.Max.Y += p.tileSize.Y * p.zoom
	topLeft := p.ViewToTile(geom.PtF32(visible.Min.X, visible.Min.Y))
	topRight := p.ViewToTile(geom.PtF32(visible.Max.X, visible.Min.Y))
	bottomLeft := p.ViewToTile(geom.PtF32(visible.Min.X, visible.Max.Y))
	bottomRight := p.ViewToTile(geom.PtF32(visible.Max.X, visible.Max.Y))
	minY, maxY := int(geom.Floor32(topRight.Y)), int(geom.Ceil32(bottomLeft.Y))
	minX, maxX := int(geom.Floor32(topLeft.X)), int(geom.Ceil32(bottomRight.X))
	tileOffset := p.tileSizeTransformed.Mul(.5)
	for y := minY; y <= maxY; y++ {
		for x := minX; x <= maxX; x++ {
			tile := geom.Pt(x, y)
			view := p.TileToView(tile.ToF32())
			if view.X+tileOffset.X < visible.Min.X || view.Y+tileOffset.Y < visible.Min.Y {
				continue
			}
			if view.X-tileOffset.X > visible.Max.X || view.Y-tileOffset.Y > visible.Max.Y {
				break
			}
			action(tile, view)
		}
	}
}

// MoveCenterTo moves the center of the projection to the given tile.
func (p *IsometricProjection) MoveCenterTo(tile geom.PointF32) {
	p.center = tile
	p.update()
}

// Pan translates the center of the projection with the given delta in view coordinates.
func (p *IsometricProjection) Pan(delta geom.PointF32) {
	p.PanTile(p.ViewToTileRelative(delta))
}

// PanTile translates the center of the projection with the given delta in tile coordinates.
func (p *IsometricProjection) PanTile(delta geom.PointF32) {
	p.MoveCenterTo(p.center.Add(delta))
}

// SetTileSize sets the size of a single tile (maximum width & height difference of its corners).
func (p *IsometricProjection) SetTileSize(size geom.PointF32) {
	p.tileSize = size
	p.update()
}

// SetViewBounds sets the bounds of the view coordinates. Used to calculate the center with & for calculating the visible tiles.
func (p *IsometricProjection) SetViewBounds(bounds geom.RectangleF32) {
	p.viewBounds = bounds
	p.update()
}

// SetZoom changes the zoom to and keeps the around (tile) coordinate on the same position.
func (p *IsometricProjection) SetZoom(around geom.PointF32, zoom float32) {
	if p.zoom == zoom {
		return
	}
	p.center = around.Sub(around.Sub(p.center).Mul(p.zoom / zoom))
	p.zoom = zoom
	p.update()
}

// TileInt gives the integer tile coordinate.
func (p *IsometricProjection) TileInt(tile geom.PointF32) geom.Point {
	return geom.Pt(int(geom.Round32(tile.X)), int(geom.Round32(tile.Y)))
}

// TileToView transforms the tile coordinate to the corresponding view coordinate.
func (p *IsometricProjection) TileToView(tile geom.PointF32) geom.PointF32 {
	translated := tile.Sub(p.center)
	return p.viewCenter.Add2D((translated.X-translated.Y)*p.tileToViewTransformation.X, (translated.X+translated.Y)*p.tileToViewTransformation.Y)
}

// ViewCenter returns the center of the view (calculated from the set view bounds).
func (p *IsometricProjection) ViewCenter() geom.PointF32 { return p.viewCenter }

// ViewToTile transforms the view coordinate to the corresponding tile coordinate.
func (p *IsometricProjection) ViewToTile(view geom.PointF32) geom.PointF32 {
	return p.ViewToTileRelative(view.Sub(p.viewCenter)).Add(p.center)
}

// ViewToTileInt transforms the view coordinate to the corresponding integer tile coordinate.
func (p *IsometricProjection) ViewToTileInt(view geom.PointF32) geom.Point {
	tile := p.ViewToTile(view)
	return p.TileInt(tile)
}

// ViewToTileRelative transforms the relative (to 0,0) view coordinate to the corresponding tile coordinate
func (p *IsometricProjection) ViewToTileRelative(view geom.PointF32) geom.PointF32 {
	return geom.PtF32(view.X*p.viewToTileTransformation.X+view.Y*p.viewToTileTransformation.Y, -view.X*p.viewToTileTransformation.X+view.Y*p.viewToTileTransformation.Y)
}

// Zoom returns the current zoom.
func (p *IsometricProjection) Zoom() float32 { return p.zoom }

// ZoomIn zooms in around the given tile coordinate.
func (p *IsometricProjection) ZoomIn(around geom.PointF32) {
	if p.zoom >= 2 {
		return
	}
	p.SetZoom(around, 2*p.zoom)
}

// ZoomOut zooms in around the given tile coordinate.
func (p *IsometricProjection) ZoomOut(around geom.PointF32) {
	if p.zoom <= .25 {
		return
	}
	p.SetZoom(around, .5*p.zoom)
}