// Package clip provides everything for calculating and altering polygons.
// The external clipper lib should only be used inside of this package.
package clip

import (
	"github.com/aligator/goslice/data"

	clipper "github.com/aligator/go.clipper"
	goconvexhull2d "github.com/furstenheim/go-convex-hull-2d"
)

// Pattern is an interface for all infill types which can be used to fill layer parts.
type Pattern interface {
	// Fill fills the given part.
	// It returns the final infill pattern.
	Fill(layerNr int, part data.LayerPart) (data.Paths, error)
}

// OffsetResult is built the following way: [partNr][insetNr][insetPartsNr]data.LayerPart
//
//  * partNr is the part number from the input-layer.
//  * insetNr is the number of the inset (so if the inset count is 5 this contains 5 insets with 0 the ounter one)
//  * insetPartsNr: If the insetting of one line results in several polygons and not only one this is filled with them.
//  For example if this polygon is inset (by one inset):
//  ---------------------|    |-----------|
//  |                    |    |           |
//  |                    |----|           |
//  |                                     |
//  |                                     |
//  ---------------------------------------
//  It results in this: (the line is partNr 1, insetNr 1 and insetPartsNr 1)
//  ---------------------|    |-----------|
//  |11111111111111111111|    |11111111111|
//  |1                  1|----|1         1|
//  |1                  11111111         1|
//  |1111111111111111111111111111111111111|
//  ---------------------------------------
//  If it is inset by another line the new resulting 2 lines are
//  partNr 1, insetNr 2 and insetPartsNr 1 (left circle of 2ers)
//  and
//  partNr 1, insetNr 2 and insetPartsNr 2 (right circle of 2ers)
//  ---------------------|    |-----------|
//  |11111111111111111111|    |11111111111|
//  |12222222222222222221|----|12222222221|
//  |1222222222222222222111111112222222221|
//  |1111111111111111111111111111111111111|
//  ---------------------------------------
type OffsetResult [][][]data.LayerPart

func (or OffsetResult) ToOneDimension() []data.LayerPart {
	var result []data.LayerPart

	for _, part := range or {
		for _, inset := range part {
			result = append(result, inset...)
		}
	}

	return result
}

// ForEach just runs through everything and calls the callback cb for each element.
// If the callback returns true, the whole looping just stops and ForEach returns immediately.
func (or OffsetResult) ForEach(cb func(part data.LayerPart, partNr, insetNr, insetPartsNr int) (doBreak bool)) {
	for partNr, part := range or {
		for insetNr, inset := range part {
			for insetPartsNr, insetParts := range inset {
				if cb(insetParts, partNr, insetNr, insetPartsNr) {
					return
				}
			}
		}
	}
}

// Clipper is an interface that provides methods needed by GoSlice to clip and alter polygons.
type Clipper interface {
	// GenerateLayerParts partitions the whole layer into several partition parts.
	// Each of them describes a polygon with holes.
	GenerateLayerParts(l data.Layer) (data.PartitionedLayer, bool)

	// InsetLayer returns all new paths generated by insetting all parts of the layer.
	// If you need to ex-set a part, just provide a negative offset.
	// The initialOffset is used for the first inset, so that the first inset can be a bit more or less offset.
	InsetLayer(layer []data.LayerPart, offset data.Micrometer, insetCount int, initialOffset data.Micrometer) OffsetResult

	// Inset insets the given layer part.
	// The result is built the following way: [insetNr][insetParts]data.LayerPart
	// See also OffsetResult for a more specific description.
	//
	// If you need to ex-set a part, just provide a negative offset.
	// The initialOffset is used for the first inset, so that the first inset can be a bit more or less offset.
	Inset(part data.LayerPart, offset data.Micrometer, insetCount int, initialOffset data.Micrometer) [][]data.LayerPart

	// Difference calculates the difference between the parts and the toRemove parts.
	// It returns the result as a new slice of layer parts.
	Difference(parts []data.LayerPart, toRemove []data.LayerPart) (clippedParts []data.LayerPart, ok bool)

	// Intersection calculates the intersection between the parts and the toIntersect parts.
	// It returns the result as a new slice of layer parts.
	Intersection(parts []data.LayerPart, toIntersect []data.LayerPart) (clippedParts []data.LayerPart, ok bool)

	// Union calculates the union of the parts and the toMerge parts.
	// It returns the result as a new slice of layer parts.
	Union(parts []data.LayerPart, toIntersect []data.LayerPart) (clippedParts []data.LayerPart, ok bool)

	// IsCrossingPerimeter checks if the given line crosses any perimeter of the given parts. If yes, the result is true.
	IsCrossingPerimeter(parts []data.LayerPart, line data.Path) (result, ok bool)

	// Hull generates an outline around all LayerParts.
	Hull(parts []data.LayerPart) (hull data.Path, ok bool)

	// TopLevelPolygons only returns polygons which are at the top level.
	// this means if there are 3 (1, 2, 3) polygons and one of them (3) is inside of another (2):
	// #########   ##############
	// #   1   #   #      2     #
	// #       #   #    +++++   #
	// #       #   #    + 3 +   #
	// #       #   #    +++++   #
	// #       #   #            #
	// #########   ##############
	//
	// Then the top level polygons are only 1 and 2.
	// This is also true if the polygons are stacked even deeper.
	//
	// Only the outlines are checked and returned, the holes of the LayerParts are ignored!
	TopLevelPolygons(parts []data.LayerPart) (topLevel data.Paths, ok bool)
}

// clipperClipper implements Clipper using the external clipper library.
type clipperClipper struct{}

// NewClipper returns a new instance of a polygon Clipper.
func NewClipper() Clipper {
	return &clipperClipper{}
}

// clipperPoint converts the GoSlice point representation to the
// representation which is used by the external clipper lib.
func clipperPoint(p data.MicroPoint) *clipper.IntPoint {
	return &clipper.IntPoint{
		X: clipper.CInt(p.X()),
		Y: clipper.CInt(p.Y()),
	}
}

// clipperPaths converts the GoSlice Paths representation
// to the representation which is used by the external clipper lib.
func clipperPaths(p data.Paths) clipper.Paths {
	result := make(clipper.Paths, len(p))
	for i, path := range p {
		result[i] = clipperPath(path)
	}

	return result
}

// clipperPath converts the GoSlice Path representation
// to the representation which is used by the external clipper lib.
func clipperPath(p data.Path) clipper.Path {
	result := make(clipper.Path, len(p))
	for i, point := range p {
		result[i] = clipperPoint(point)
	}

	return result
}

// microPoint converts the external clipper lib representation of a point
// to the representation which is used by GoSlice.
func microPoint(p *clipper.IntPoint) data.MicroPoint {
	return data.NewMicroPoint(data.Micrometer(p.X), data.Micrometer(p.Y))
}

// microPath converts the external clipper lib representation of a path
// to the representation which is used by GoSlice.
// The parameter simplify enables simplifying of the path using
// the default simplification settings.
func microPath(p clipper.Path, simplify bool) data.Path {
	result := make(data.Path, len(p))
	for i, point := range p {
		result[i] = microPoint(point)
	}

	if simplify {
		return result.Simplify(-1, -1)
	}
	return result
}

// microPaths converts the external clipper lib representation of paths
// to the representation which is used by GoSlice.
// The parameter simplify enables simplifying of the paths using
// the default simplification settings.
func microPaths(p clipper.Paths, simplify bool) data.Paths {
	result := make(data.Paths, len(p))
	for i, path := range p {
		result[i] = microPath(path, simplify)
	}
	return result
}

func (c clipperClipper) GenerateLayerParts(l data.Layer) (data.PartitionedLayer, bool) {
	polyList := clipper.Paths{}
	// convert all polygons to clipper polygons
	for _, layerPolygon := range l.Polygons() {
		polyList = append(polyList, clipperPath(layerPolygon.Simplify(-1, -1)))
	}

	if len(polyList) == 0 {
		return data.NewPartitionedLayer([]data.LayerPart{}), true
	}

	cl := clipper.NewClipper(clipper.IoNone)
	cl.AddPaths(polyList, clipper.PtSubject, true)
	resultPolys, ok := cl.Execute2(clipper.CtUnion, clipper.PftEvenOdd, clipper.PftEvenOdd)
	if !ok {
		return nil, false
	}

	return data.NewPartitionedLayer(polyTreeToLayerParts(resultPolys)), true
}

// polyTreeToLayerParts creates layer parts out of a poly tree (which is the result of clipper's Execute2).
func polyTreeToLayerParts(tree *clipper.PolyTree) []data.LayerPart {
	var layerParts []data.LayerPart

	var polysForNextRound []*clipper.PolyNode

	for _, c := range tree.Childs() {
		polysForNextRound = append(polysForNextRound, c)
	}
	for {
		if polysForNextRound == nil {
			break
		}
		thisRound := polysForNextRound
		polysForNextRound = nil

		for _, p := range thisRound {
			var holes data.Paths

			for _, child := range p.Childs() {
				// TODO: simplify, yes / no ??
				holes = append(holes, microPath(child.Contour(), false))
				for _, c := range child.Childs() {
					polysForNextRound = append(polysForNextRound, c)
				}
			}

			// TODO: simplify, yes / no ??
			layerParts = append(layerParts, data.NewBasicLayerPart(microPath(p.Contour(), false), holes))
		}
	}

	return layerParts
}

func (c clipperClipper) InsetLayer(layer []data.LayerPart, offset data.Micrometer, insetCount int, initialOffset data.Micrometer) OffsetResult {
	var result OffsetResult
	for _, part := range layer {
		result = append(result, c.Inset(part, offset, insetCount, initialOffset))
	}

	return result
}

func (c clipperClipper) Inset(part data.LayerPart, offset data.Micrometer, insetCount int, initialOffset data.Micrometer) [][]data.LayerPart {
	var insets [][]data.LayerPart

	co := clipper.NewClipperOffset()

	currentOffset := float64(initialOffset)

	for insetNr := 0; insetNr < insetCount; insetNr++ {
		// insets for the outline
		co.Clear()
		co.AddPaths(clipperPaths(data.Paths{part.Outline()}), clipper.JtSquare, clipper.EtClosedPolygon)
		co.AddPaths(clipperPaths(part.Holes()), clipper.JtSquare, clipper.EtClosedPolygon)

		co.MiterLimit = 2
		allNewInsets := co.Execute2(currentOffset)
		insets = append(insets, polyTreeToLayerParts(allNewInsets))

		currentOffset += float64(-int(offset))
	}

	return insets
}

func (c clipperClipper) Difference(parts []data.LayerPart, toRemove []data.LayerPart) (clippedParts []data.LayerPart, ok bool) {
	return c.runClipper(clipper.CtDifference, parts, toRemove)
}

func (c clipperClipper) Intersection(parts []data.LayerPart, toIntersect []data.LayerPart) (clippedParts []data.LayerPart, ok bool) {
	return c.runClipper(clipper.CtIntersection, parts, toIntersect)
}

func (c clipperClipper) Union(parts []data.LayerPart, toMerge []data.LayerPart) (clippedParts []data.LayerPart, ok bool) {
	return c.runClipper(clipper.CtUnion, parts, toMerge)
}

func (c clipperClipper) runClipper(clipType clipper.ClipType, parts []data.LayerPart, toClip []data.LayerPart) (clippedParts []data.LayerPart, ok bool) {
	if parts == nil && toClip == nil {
		return nil, true
	}

	cl := clipper.NewClipper(clipper.IoNone)
	for _, part := range parts {
		cl.AddPath(clipperPath(part.Outline()), clipper.PtSubject, true)
		cl.AddPaths(clipperPaths(part.Holes()), clipper.PtSubject, true)
	}

	for _, intersect := range toClip {
		cl.AddPath(clipperPath(intersect.Outline()), clipper.PtClip, true)
		cl.AddPaths(clipperPaths(intersect.Holes()), clipper.PtClip, true)
	}

	tree, ok := cl.Execute2(clipType, clipper.PftEvenOdd, clipper.PftEvenOdd)

	if !ok {
		return nil, ok
	}
	return polyTreeToLayerParts(tree), ok
}

func (c clipperClipper) IsCrossingPerimeter(parts []data.LayerPart, line data.Path) (result, ok bool) {
	// TODO: Is there a more performant way to detect this?
	cl := clipper.NewClipper(clipper.IoReverseSolution) // inverse solution so that it is basically LINE - PARTS

	for _, part := range parts {
		cl.AddPaths(clipperPaths(part.Holes()), clipper.PtClip, true)
		cl.AddPath(clipperPath(part.Outline()), clipper.PtClip, true)
	}

	cl.AddPath(clipperPath(line), clipper.PtSubject, false)

	// calculate the difference of the parts and the line, then look if the (inverted) result contains any left path which would be a line not inside of the parts.
	// If any part is left, the line crossed a perimeter.
	tree, ok := cl.Execute2(clipper.CtDifference, clipper.PftEvenOdd, clipper.PftEvenOdd)

	if !ok {
		return false, ok
	}

	return tree.Total() > 0, true
}

func (c clipperClipper) Hull(parts []data.LayerPart) (hull data.Path, ok bool) {
	var allPoints data.Path
	for _, part := range parts {
		allPoints = append(allPoints, part.Outline()...)
	}

	convexHull := goconvexhull2d.New(allPoints)

	hullPath, ok := convexHull.(data.Path)
	if !ok {
		return nil, ok
	}
	return hullPath, true
}

func (c clipperClipper) TopLevelPolygons(parts []data.LayerPart) (topLevel data.Paths, ok bool) {
	cl := clipper.NewClipper(clipper.IoNone)

	for _, part := range parts {
		cl.AddPath(clipperPath(part.Outline()), clipper.PtSubject, true)
	}

	// this is just a dummy-call to Execute2 as I found no other way to get a tree from clipper...
	tree, ok := cl.Execute2(clipper.CtUnion, clipper.PftEvenOdd, clipper.PftEvenOdd)
	if !ok {
		return nil, false
	}

	for _, child := range tree.Childs() {
		topLevel = append(topLevel, microPath(child.Contour(), false))
	}

	return topLevel, true
}
