package geom

import "math"

func similar(a, b, e float64) bool {
	return math.Abs(a-b) < e
}

func pointSimilar(p1, p2 Point, e float64) bool {
	return similar(p1.X, p2.X, e) && similar(p1.Y, p2.Y, e)
}

func pointsSimilar(p1s, p2s []Point, e float64) bool {
	if len(p1s) != len(p2s) {
		return false
	}
	for i, n := 0, len(p1s); i < n; i++ {
		if !pointSimilar(p1s[i], p2s[i], e) {
			return false
		}
	}
	return true
}

func pointssSimilar(p1ss, p2ss [][]Point, e float64) bool {
	if len(p1ss) != len(p2ss) {
		return false
	}
	for i, n := 0, len(p1ss); i < n; i++ {
		if !pointsSimilar(p1ss[i], p2ss[i], e) {
			return false
		}
	}
	return true
}

// Similar determines whether two geometries are similar within tolerance.
func (p Point) Similar(g Geom, tolerance float64) bool {
	switch g.(type) {
	case Point:
		return pointSimilar(p, g.(Point), tolerance)
	default:
		return false
	}
}

// Similar determines whether two geometries are similar within tolerance.
func (mp MultiPoint) Similar(g Geom, tolerance float64) bool {
	switch g.(type) {
	case MultiPoint:
		return pointsSimilar(mp, g.(MultiPoint), tolerance)
	default:
		return false
	}
}

// Similar determines whether two geometries are similar within tolerance.
// If two lines contain the same points but in different directions it will
// return false.
func (l LineString) Similar(g Geom, tolerance float64) bool {
	switch g.(type) {
	case LineString:
		return pointsSimilar(l, g.(LineString), tolerance)
	default:
		return false
	}
}

// Similar determines whether two geometries are similar within tolerance.
// If ml and g have the similar linestrings but in a different order, it
// will return true.
func (ml MultiLineString) Similar(g Geom, tolerance float64) bool {
	switch g.(type) {
	case MultiLineString:
		ml2 := g.(MultiLineString)
		indices := make([]int, len(ml2))
		for i := range ml2 {
			indices[i] = i
		}
		for _, l := range ml {
			matched := false
			for ii, i := range indices {
				if l.Similar(ml2[i], tolerance) { // we found a match
					matched = true
					// remove index i from futher consideration.
					if ii == len(indices)-1 {
						indices = indices[0:ii]
					} else {
						indices = append(indices[0:ii], indices[ii+1:len(indices)]...)
					}
					break
				}
			}
			if !matched {
				return false
			}
		}
		return true
	default:
		return false
	}
}

// Similar determines whether two geometries are similar within tolerance.
// If ml and g have the similar polygons but in a different order, it
// will return true.
func (mp MultiPolygon) Similar(g Geom, tolerance float64) bool {
	switch g.(type) {
	case MultiPolygon:
		mp2 := g.(MultiPolygon)
		indices := make([]int, len(mp2))
		for i := range mp2 {
			indices[i] = i
		}
		for _, l := range mp {
			matched := false
			for ii, i := range indices {
				if l.Similar(mp2[i], tolerance) { // we found a match
					matched = true
					// remove index i from futher consideration.
					if ii == len(indices)-1 {
						indices = indices[0:ii]
					} else {
						indices = append(indices[0:ii], indices[ii+1:len(indices)]...)
					}
					break
				}
			}
			if !matched {
				return false
			}
		}
		return true
	default:
		return false
	}
}

// Similar determines whether two geometries are similar within tolerance.
// If p and g have the same points with the same winding direction, but a
// different starting point, it will return true. If they have the same
// rings but in a different order, it will return true. If the rings have the same
// points but different winding directions, it will return false.
func (p Polygon) Similar(g Geom, tolerance float64) bool {
	switch g.(type) {
	case Polygon:
		p2 := g.(Polygon)
		indices := make([]int, len(p2))
		for i := range p2 {
			indices[i] = i
		}
		for _, r1 := range p {
			matched := false
			for ii, i := range indices {
				if ringSimilar(r1, p2[i], tolerance) { // we found a match
					matched = true
					// remove index i from futher consideration.
					if ii == len(indices)-1 {
						indices = indices[0:ii]
					} else {
						indices = append(indices[0:ii], indices[ii+1:len(indices)]...)
					}
					break
				}
			}
			if !matched {
				return false
			}
		}
		return true
	default:
		return false
	}
}

// Similar determines whether two bounds are similar within tolerance.
func (b *Bounds) Similar(g Geom, tolerance float64) bool {
	switch g.(type) {
	case *Bounds:
		b2 := g.(*Bounds)
		return pointSimilar(b.Min, b2.Min, tolerance) && pointSimilar(b.Max, b2.Max, tolerance)
	default:
		return false
	}
}

// Similar determines whether two geometries collections are similar within tolerance.
// If gc and g have the same geometries
// but in a different order, it will return true.
func (gc GeometryCollection) Similar(g Geom, tolerance float64) bool {
	switch g.(type) {
	case GeometryCollection:
		gc2 := g.(GeometryCollection)
		indices := make([]int, len(gc2))
		for i := range gc2 {
			indices[i] = i
		}
		for _, gc1 := range gc {
			matched := false
			for ii, i := range indices {
				if gc1.Similar(gc2[i], tolerance) { // we found a match
					matched = true
					// remove index i from futher consideration.
					if ii == len(indices)-1 {
						indices = indices[0:ii]
					} else {
						indices = append(indices[0:ii], indices[ii+1:len(indices)]...)
					}
					break
				}
			}
			if !matched {
				return false
			}
		}
		return true
	default:
		return false
	}
}

func ringSimilar(a, b []Point, e float64) bool {
	if len(a) != len(b) {
		return false
	}
	ia := minPt(a)
	ib := minPt(b)
	for i := 0; i < len(a); i++ {
		if !pointSimilar(a[ia], b[ib], e) {
			return false
		}
		ia = nextPt(ia, len(a))
		ib = nextPt(ib, len(b))
	}
	return true
}

// ring iterator function
func nextPt(i, l int) int {
	if i == l-2 { // Skip the last point that matches the first point.
		return 0
	}
	return i + 1
}

// find bottom-most of leftmost points, to have fixed anchor
func minPt(c []Point) int {
	min := 0
	for j, p := range c {
		if p.X < c[min].X || p.X == c[min].X && p.Y < c[min].Y {
			min = j
		}
	}
	return min
}