package part

import (
	"github.com/cyrilix/robocar-protobuf/go/events"
	log "github.com/sirupsen/logrus"
	"gocv.io/x/gocv"
	"image"
	"image/color"
)

const FILLED = -1

type RoadDetector struct {
	kernelSize                               int
	morphoIterations                         int
	approxPolyEpsilonFactor                  float64
	previousBoundingBox                      *image.Rectangle
	previousRoad                             *[]image.Point
	thresholdLowerBound, thresholdUpperBound gocv.Mat
}

func (rd *RoadDetector) Close() error {
	var err error
	err = nil
	if err1 := rd.thresholdLowerBound.Close(); err1 != nil {
		log.Errorf("unable to close thresholdLowerBound resource: %v", err1)
		err = err1
	}
	if err2 := rd.thresholdUpperBound.Close(); err2 != nil {
		log.Errorf("unable to close thresholdUpperBound resource: %v", err2)
		err = err2
	}
	return err
}

func NewRoadDetector() *RoadDetector {

	return &RoadDetector{
		kernelSize:              4,
		morphoIterations:        3,
		approxPolyEpsilonFactor: 0.01,
		thresholdLowerBound:     gocv.NewMatFromScalar(gocv.NewScalar(120.0, 120.0, 120.0, 120.0), gocv.MatTypeCV8U),
		thresholdUpperBound:     gocv.NewMatFromScalar(gocv.NewScalar(250.0, 250.0, 250.0, 250.0), gocv.MatTypeCV8U),
	}
}

func (rd *RoadDetector) DetectRoadContour(imgGray *gocv.Mat, horizonRow int) *[]image.Point {

	kernel := gocv.NewMatWithSizeFromScalar(gocv.NewScalar(1, 1, 1, 1), rd.kernelSize, rd.kernelSize, gocv.MatTypeCV8U)

	img := imgGray.Clone()
	defer func() {
		if err := img.Close(); err != nil {
			log.Warnf("unable to close mat resource: %v", err)
		}
	}()

	for i := rd.morphoIterations; i > 0; i-- {
		gocv.Dilate(img, &img, kernel)
	}
	for i := rd.morphoIterations; i > 0; i-- {
		gocv.Erode(img, &img, kernel)
	}
	gocv.Dilate(img, &img, kernel)

	gocv.Threshold(img, &img, 180, 255, gocv.ThresholdBinaryInv)

	// Draw black rectangle above horizon
	horizon := gocv.NewMatWithSize(1, 4, gocv.MatTypeCV32S)
	horizon.SetIntAt(0, 0, 0)                     // X1
	horizon.SetIntAt(0, 1, int32(horizonRow))     // Y1
	horizon.SetIntAt(0, 2, int32(imgGray.Cols())) // X2
	horizon.SetIntAt(0, 3, int32(horizonRow))     // Y2
	rectangle := image.Rect(0, 0, int(horizon.GetIntAt(0, 2)), int(horizon.GetIntAt(0, 3)))
	gocv.Rectangle(&img, rectangle, color.RGBA{0, 0, 0, 0}, FILLED)

	return rd.detectRoadContour(&img)
}

func (rd *RoadDetector) detectRoadContour(imgInversed *gocv.Mat) *[]image.Point {

	var (
		epsilon float64
		cntr    []image.Point
	)

	cntrs := gocv.FindContours(*imgInversed, gocv.RetrievalExternal, gocv.ChainApproxSimple)

	if len(cntrs) == 0 {
		emptyContours := make([]image.Point, 0)
		return &emptyContours
	} else if len(cntrs) == 1 {
		epsilon = rd.approxPolyEpsilonFactor * gocv.ArcLength(cntrs[0], true)
		cntr = cntrs[0]
	} else {
		// Search biggest contour
		peris := make([]float64, len(cntrs))
		maxArcIdx := 0
		maxArcValue := 0.
		for i, c := range cntrs {
			peri := gocv.ArcLength(c, true)
			peris[i] = peri
			if peri > maxArcValue {
				maxArcValue = peri
				maxArcIdx = i
			}
			cntr = cntrs[maxArcIdx]
		}
		epsilon = rd.approxPolyEpsilonFactor * peris[maxArcIdx]
	}
	approx := gocv.ApproxPolyDP(cntr, epsilon, true)
	return &approx
}

var EllipseNotFound = events.Ellipse{Confidence: 0.}

func (rd *RoadDetector) ComputeEllipsis(road *[]image.Point) *events.Ellipse {
	if len(*road) < 5 {
		return &EllipseNotFound
	}

	rotatedRect := gocv.FitEllipse(*road)

	trust := rd.computeTrustFromCenter(&rotatedRect.Center)
	log.Debugf("Trust: %v", trust)

	return &events.Ellipse{
		Center: &events.Point{
			X: int32(rotatedRect.Center.X),
			Y: int32(rotatedRect.Center.Y),
		},
		Width:      int32(rotatedRect.Width),
		Height:     int32(rotatedRect.Height),
		Angle:      float32(rotatedRect.Angle),
		Confidence: rd.computeTrustFromCenter(&rotatedRect.Center),
	}
}

func (rd *RoadDetector) computeTrustFromCenter(ellipsisCenter *image.Point) float32 {
	safeMinX := 48
	safeMaxX := 115
	safeMinY := 69
	safeMaxY := 119

	if safeMinX <= ellipsisCenter.X && ellipsisCenter.X <= safeMaxX && safeMinY <= ellipsisCenter.Y && ellipsisCenter.Y <= safeMaxY {
		return 1.0
	}

	if safeMinX <= ellipsisCenter.X && ellipsisCenter.X <= safeMaxX {
		return rd.computeTrustOnAxis(safeMaxY, safeMinY, ellipsisCenter.Y)
	}

	if safeMinY <= ellipsisCenter.Y && ellipsisCenter.Y <= safeMaxY {
		return rd.computeTrustOnAxis(safeMaxX, safeMinX, ellipsisCenter.X)
	}

	return rd.computeTrustOnAxis(safeMaxY, safeMinY, ellipsisCenter.Y) * rd.computeTrustOnAxis(safeMaxX, safeMinX, ellipsisCenter.X)
}

func (rd *RoadDetector) computeTrustOnAxis(safeMax, safeMin, value int) float32 {
	trust := 1.
	if value > safeMax {
		trust = 1. / float64(value-safeMax)
	} else if value < safeMin {
		trust = 1. / float64(safeMin-value)
	}
	trust = trust * 10.
	if trust > 0.9 {
		trust = 0.9
	}
	if trust < 0. {
		trust = 0.
	}
	return float32(trust)

}
