// Package planar computes properties on geometries assuming they are
// in 2d euclidean space.
package planar

import (
	"fmt"
	"math"

	"github.com/paulmach/orb"
)

// Area returns the area of the geometry in the 2d plane.
func Area(g orb.Geometry) float64 {
	// TODO: make faster non-centroid version.
	_, a := CentroidArea(g)
	return a
}

// CentroidArea returns both the centroid and the area in the 2d plane.
// Since the area is need for the centroid, return both.
// Polygon area will always be >= zero. Ring area may be negative if it has
// a clockwise winding order.
func CentroidArea(g orb.Geometry) (orb.Point, float64) {
	if g == nil {
		return orb.Point{}, 0
	}

	switch g := g.(type) {
	case orb.Point:
		return multiPointCentroid(orb.MultiPoint{g}), 0
	case orb.MultiPoint:
		return multiPointCentroid(g), 0
	case orb.LineString:
		return multiLineStringCentroid(orb.MultiLineString{g}), 0
	case orb.MultiLineString:
		return multiLineStringCentroid(g), 0
	case orb.Ring:
		return ringCentroidArea(g)
	case orb.Polygon:
		return polygonCentroidArea(g)
	case orb.MultiPolygon:
		return multiPolygonCentroidArea(g)
	case orb.Collection:
		return collectionCentroidArea(g)
	case orb.Bound:
		return CentroidArea(g.ToRing())
	}

	panic(fmt.Sprintf("geometry type not supported: %T", g))
}

func multiPointCentroid(mp orb.MultiPoint) orb.Point {
	if len(mp) == 0 {
		return orb.Point{}
	}

	x, y := 0.0, 0.0
	for _, p := range mp {
		x += p[0]
		y += p[1]
	}

	num := float64(len(mp))
	return orb.Point{x / num, y / num}
}

func multiLineStringCentroid(mls orb.MultiLineString) orb.Point {
	point := orb.Point{}
	dist := 0.0

	if len(mls) == 0 {
		return orb.Point{}
	}

	validCount := 0
	for _, ls := range mls {
		c, d := lineStringCentroidDist(ls)
		if d == math.Inf(1) {
			continue
		}

		dist += d
		validCount++

		if d == 0 {
			d = 1.0
		}

		point[0] += c[0] * d
		point[1] += c[1] * d
	}

	if validCount == 0 {
		return orb.Point{}
	}

	if dist == math.Inf(1) || dist == 0.0 {
		point[0] /= float64(validCount)
		point[1] /= float64(validCount)
		return point
	}

	point[0] /= dist
	point[1] /= dist

	return point
}

func lineStringCentroidDist(ls orb.LineString) (orb.Point, float64) {
	dist := 0.0
	point := orb.Point{}

	if len(ls) == 0 {
		return orb.Point{}, math.Inf(1)
	}

	// implicitly move everything to near the origin to help with roundoff
	offset := ls[0]
	for i := 0; i < len(ls)-1; i++ {
		p1 := orb.Point{
			ls[i][0] - offset[0],
			ls[i][1] - offset[1],
		}

		p2 := orb.Point{
			ls[i+1][0] - offset[0],
			ls[i+1][1] - offset[1],
		}

		d := Distance(p1, p2)

		point[0] += (p1[0] + p2[0]) / 2.0 * d
		point[1] += (p1[1] + p2[1]) / 2.0 * d
		dist += d
	}

	if dist == 0 {
		return ls[0], 0
	}

	point[0] /= dist
	point[1] /= dist

	point[0] += ls[0][0]
	point[1] += ls[0][1]
	return point, dist
}

func ringCentroidArea(r orb.Ring) (orb.Point, float64) {
	centroid := orb.Point{}
	area := 0.0

	if len(r) == 0 {
		return orb.Point{}, 0
	}

	// implicitly move everything to near the origin to help with roundoff
	offsetX := r[0][0]
	offsetY := r[0][1]
	for i := 1; i < len(r)-1; i++ {
		a := (r[i][0]-offsetX)*(r[i+1][1]-offsetY) -
			(r[i+1][0]-offsetX)*(r[i][1]-offsetY)
		area += a

		centroid[0] += (r[i][0] + r[i+1][0] - 2*offsetX) * a
		centroid[1] += (r[i][1] + r[i+1][1] - 2*offsetY) * a
	}

	if area == 0 {
		return r[0], 0
	}

	// no need to deal with first and last vertex since we "moved"
	// that point the origin (multiply by 0 == 0)

	area /= 2
	centroid[0] /= 6 * area
	centroid[1] /= 6 * area

	centroid[0] += offsetX
	centroid[1] += offsetY

	return centroid, area
}

func polygonCentroidArea(p orb.Polygon) (orb.Point, float64) {
	if len(p) == 0 {
		return orb.Point{}, 0
	}

	centroid, area := ringCentroidArea(p[0])
	area = math.Abs(area)
	if len(p) == 1 {
		if area == 0 {
			c, _ := lineStringCentroidDist(orb.LineString(p[0]))
			return c, 0
		}
		return centroid, area
	}

	holeArea := 0.0
	weightedHoleCentroid := orb.Point{}
	for i := 1; i < len(p); i++ {
		hc, ha := ringCentroidArea(p[i])
		ha = math.Abs(ha)

		holeArea += ha
		weightedHoleCentroid[0] += hc[0] * ha
		weightedHoleCentroid[1] += hc[1] * ha
	}

	totalArea := area - holeArea
	if totalArea == 0 {
		c, _ := lineStringCentroidDist(orb.LineString(p[0]))
		return c, 0
	}

	centroid[0] = (area*centroid[0] - weightedHoleCentroid[0]) / totalArea
	centroid[1] = (area*centroid[1] - weightedHoleCentroid[1]) / totalArea

	return centroid, totalArea
}

func multiPolygonCentroidArea(mp orb.MultiPolygon) (orb.Point, float64) {
	point := orb.Point{}
	area := 0.0

	for _, p := range mp {
		c, a := polygonCentroidArea(p)

		point[0] += c[0] * a
		point[1] += c[1] * a

		area += a
	}

	if area == 0 {
		return orb.Point{}, 0
	}

	point[0] /= area
	point[1] /= area

	return point, area
}

func collectionCentroidArea(c orb.Collection) (orb.Point, float64) {
	point := orb.Point{}
	area := 0.0

	max := maxDim(c)
	for _, g := range c {
		if g.Dimensions() != max {
			continue
		}

		c, a := CentroidArea(g)

		point[0] += c[0] * a
		point[1] += c[1] * a

		area += a
	}

	if area == 0 {
		return orb.Point{}, 0
	}

	point[0] /= area
	point[1] /= area

	return point, area
}

func maxDim(c orb.Collection) int {
	max := 0
	for _, g := range c {
		if d := g.Dimensions(); d > max {
			max = d
		}
	}

	return max
}
