package planar

import (
	"fmt"
	"math"

	"github.com/paulmach/orb"
)

// DistanceFromSegment returns the point's distance from the segment [a, b].
func DistanceFromSegment(a, b, point orb.Point) float64 {
	return math.Sqrt(DistanceFromSegmentSquared(a, b, point))
}

// DistanceFromSegmentSquared returns point's squared distance from the segment [a, b].
func DistanceFromSegmentSquared(a, b, point orb.Point) float64 {
	x := a[0]
	y := a[1]
	dx := b[0] - x
	dy := b[1] - y

	if dx != 0 || dy != 0 {
		t := ((point[0]-x)*dx + (point[1]-y)*dy) / (dx*dx + dy*dy)

		if t > 1 {
			x = b[0]
			y = b[1]
		} else if t > 0 {
			x += dx * t
			y += dy * t
		}
	}

	dx = point[0] - x
	dy = point[1] - y

	return dx*dx + dy*dy
}

// DistanceFrom returns the distance from the boundary of the geometry in
// the units of the geometry.
func DistanceFrom(g orb.Geometry, p orb.Point) float64 {
	d, _ := DistanceFromWithIndex(g, p)
	return d
}

// DistanceFromWithIndex returns the minimum euclidean distance
// from the boundary of the geometry plus the index of the sub-geometry
// that was the match.
func DistanceFromWithIndex(g orb.Geometry, p orb.Point) (float64, int) {
	if g == nil {
		return math.Inf(1), -1
	}

	switch g := g.(type) {
	case orb.Point:
		return Distance(g, p), 0
	case orb.MultiPoint:
		return multiPointDistanceFrom(g, p)
	case orb.LineString:
		return lineStringDistanceFrom(g, p)
	case orb.MultiLineString:
		dist := math.Inf(1)
		index := -1
		for i, ls := range g {
			if d, _ := lineStringDistanceFrom(ls, p); d < dist {
				dist = d
				index = i
			}
		}

		return dist, index
	case orb.Ring:
		return lineStringDistanceFrom(orb.LineString(g), p)
	case orb.Polygon:
		return polygonDistanceFrom(g, p)
	case orb.MultiPolygon:
		dist := math.Inf(1)
		index := -1
		for i, poly := range g {
			if d, _ := polygonDistanceFrom(poly, p); d < dist {
				dist = d
				index = i
			}
		}

		return dist, index
	case orb.Collection:
		dist := math.Inf(1)
		index := -1
		for i, ge := range g {
			if d, _ := DistanceFromWithIndex(ge, p); d < dist {
				dist = d
				index = i
			}
		}

		return dist, index
	case orb.Bound:
		return DistanceFromWithIndex(g.ToRing(), p)
	}

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

func multiPointDistanceFrom(mp orb.MultiPoint, p orb.Point) (float64, int) {
	dist := math.Inf(1)
	index := -1

	for i := range mp {
		if d := DistanceSquared(mp[i], p); d < dist {
			dist = d
			index = i
		}
	}

	return math.Sqrt(dist), index
}

func lineStringDistanceFrom(ls orb.LineString, p orb.Point) (float64, int) {
	dist := math.Inf(1)
	index := -1

	for i := 0; i < len(ls)-1; i++ {
		if d := segmentDistanceFromSquared(ls[i], ls[i+1], p); d < dist {
			dist = d
			index = i
		}
	}

	return math.Sqrt(dist), index
}

func polygonDistanceFrom(p orb.Polygon, point orb.Point) (float64, int) {
	if len(p) == 0 {
		return math.Inf(1), -1
	}

	dist, index := lineStringDistanceFrom(orb.LineString(p[0]), point)
	for i := 1; i < len(p); i++ {
		d, i := lineStringDistanceFrom(orb.LineString(p[i]), point)
		if d < dist {
			dist = d
			index = i
		}
	}

	return dist, index
}

func segmentDistanceFromSquared(p1, p2, point orb.Point) float64 {
	x := p1[0]
	y := p1[1]
	dx := p2[0] - x
	dy := p2[1] - y

	if dx != 0 || dy != 0 {
		t := ((point[0]-x)*dx + (point[1]-y)*dy) / (dx*dx + dy*dy)

		if t > 1 {
			x = p2[0]
			y = p2[1]
		} else if t > 0 {
			x += dx * t
			y += dy * t
		}
	}

	dx = point[0] - x
	dy = point[1] - y

	return dx*dx + dy*dy
}
