package eu.geyskens.pdfscan.scan

import android.graphics.Bitmap
import android.graphics.PointF
import org.opencv.android.Utils
import org.opencv.core.Mat
import org.opencv.core.MatOfPoint
import org.opencv.core.MatOfPoint2f
import org.opencv.core.Size
import org.opencv.imgproc.Imgproc

/**
 * Finds the largest document-like quadrilateral in an image using OpenCV.
 * All heavy work runs on a downscaled copy; corners are returned normalized
 * to 0..1 so callers can map them onto any resolution.
 */
object EdgeDetector {

    private const val PROCESS_WIDTH = 500.0

    /** @return four normalized corners (TL, TR, BR, BL), or null if nothing convincing was found. */
    fun detect(bitmap: Bitmap): List<PointF>? {
        val src = Mat()
        Utils.bitmapToMat(bitmap, src)
        val gray = Mat()
        Imgproc.cvtColor(src, gray, Imgproc.COLOR_RGBA2GRAY)
        src.release()
        return detectFromGray(gray)
    }

    /**
     * Detects on a full-resolution single-channel grayscale [gray] Mat (e.g. a camera
     * frame's Y plane). Corners are normalized to [gray]'s dimensions. Releases [gray].
     */
    fun detectFromGray(gray: Mat): List<PointF>? {
        try {
            val scale = PROCESS_WIDTH / gray.cols()
            val work = Mat()
            Imgproc.resize(gray, work, Size(PROCESS_WIDTH, gray.rows() * scale))
            Imgproc.GaussianBlur(work, work, Size(5.0, 5.0), 0.0)

            val edges = Mat()
            Imgproc.Canny(work, edges, 50.0, 150.0)
            // Close small gaps so contours form a continuous loop.
            val kernel = Imgproc.getStructuringElement(Imgproc.MORPH_RECT, Size(5.0, 5.0))
            Imgproc.dilate(edges, edges, kernel)

            val contours = ArrayList<MatOfPoint>()
            Imgproc.findContours(
                edges, contours, Mat(),
                Imgproc.RETR_EXTERNAL, Imgproc.CHAIN_APPROX_SIMPLE
            )

            val frameArea = work.rows() * work.cols()
            var best: List<PointF>? = null
            var bestArea = frameArea * 0.15 // ignore quads smaller than 15% of the frame

            for (contour in contours) {
                val c2f = MatOfPoint2f(*contour.toArray())
                val peri = Imgproc.arcLength(c2f, true)
                val approx = MatOfPoint2f()
                Imgproc.approxPolyDP(c2f, approx, 0.02 * peri, true)

                if (approx.total() == 4L && Imgproc.isContourConvex(MatOfPoint(*approx.toArray()))) {
                    val area = Imgproc.contourArea(approx)
                    if (area > bestArea) {
                        bestArea = area
                        best = orderCorners(approx.toArray().map {
                            PointF((it.x / work.cols()).toFloat(), (it.y / work.rows()).toFloat())
                        })
                    }
                }
            }
            return best
        } finally {
            gray.release()
        }
    }

    /** Sort four points into TL, TR, BR, BL by their x+y / x-y sums. */
    fun orderCorners(points: List<PointF>): List<PointF> {
        require(points.size == 4)
        val tl = points.minByOrNull { it.x + it.y }!!
        val br = points.maxByOrNull { it.x + it.y }!!
        val tr = points.maxByOrNull { it.x - it.y }!!
        val bl = points.minByOrNull { it.x - it.y }!!
        return listOf(tl, tr, br, bl)
    }
}
