/*
 * ImageToolbox is an image editor for android
 * Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * You should have received a copy of the Apache License
 * along with this program.  If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
 */

@file:Suppress("DEPRECATION")

package com.websitebeaver.documentscanner

import android.content.Intent
import android.graphics.Bitmap
import android.graphics.Color
import android.net.Uri
import android.os.Bundle
import android.view.View
import android.view.WindowManager
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.FileProvider
import androidx.core.graphics.Insets
import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import com.t8rin.opencv_tools.document_detector.DocumentDetector
import com.websitebeaver.documentscanner.constants.DefaultSetting
import com.websitebeaver.documentscanner.constants.DocumentScannerExtra
import com.websitebeaver.documentscanner.extensions.move
import com.websitebeaver.documentscanner.extensions.onClick
import com.websitebeaver.documentscanner.extensions.saveToFile
import com.websitebeaver.documentscanner.extensions.screenHeight
import com.websitebeaver.documentscanner.extensions.screenWidth
import com.websitebeaver.documentscanner.models.Document
import com.websitebeaver.documentscanner.models.Quad
import com.websitebeaver.documentscanner.ui.CircleButton
import com.websitebeaver.documentscanner.ui.DoneButton
import com.websitebeaver.documentscanner.ui.ImageCropView
import com.websitebeaver.documentscanner.utils.FileUtil
import com.websitebeaver.documentscanner.utils.ImageUtil
import org.opencv.android.OpenCVLoader
import org.opencv.core.Point
import java.io.File

/**
 * This class contains the main document scanner code. It opens the camera, lets the user
 * take a photo of a document (homework paper, business card, etc.), detects document corners,
 * allows user to make adjustments to the detected corners, depending on options, and saves
 * the cropped document. It allows the user to do this for 1 or more documents.
 *
 * @constructor creates document scanner activity
 */
class DocumentScannerActivity : AppCompatActivity() {
    /**
     * @property maxNumDocuments maximum number of documents a user can scan at a time
     */
    private var maxNumDocuments = DefaultSetting.MAX_NUM_DOCUMENTS

    /**
     * @property letUserAdjustCrop whether or not to let user move automatically detected corners
     */
    private var letUserAdjustCrop = DefaultSetting.LET_USER_ADJUST_CROP

    /**
     * @property croppedImageQuality the 0 - 100 quality of the cropped image
     */
    private var croppedImageQuality = DefaultSetting.CROPPED_IMAGE_QUALITY

    /**
     * @property cropperOffsetWhenCornersNotFound if we can't find document corners, we set
     * corners to image size with a slight margin
     */
    private val cropperOffsetWhenCornersNotFound = 100.0

    /**
     * @property document This is the current document. Initially it's null. Once we capture
     * the photo, and find the corners we update document.
     */
    private var document: Document? = null

    /**
     * @property documents a list of documents (original photo file path, original photo
     * dimensions and 4 corner points)
     */
    private val documents = mutableListOf<Document>()

    /**
     * @property imageView container with original photo and cropper
     */
    private lateinit var imageView: ImageCropView

    private var cropperHandleColor = Color.WHITE
    private var cropperFrameColor = Color.WHITE
    private var buttonTintColor = Color.WHITE
    private var buttonContainerColor = Color.argb(128, 255, 255, 255)
    private var cropperHandleOutlineColor = buttonContainerColor
    private var cropperStrokeWidthDp = 1.2f
    private var doneButtonTintColor = buttonTintColor
    private var doneButtonContainerColor = buttonContainerColor
    private var shouldApplyButtonColors = false
    private var contentWidth = 0
    private var contentHeight = 0

    /**
     * called when activity is created
     *
     * @param savedInstanceState persisted data that maintains state
     */
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        try {
            OpenCVLoader.initLocal()
        } catch (exception: Throwable) {
            finishIntentWithError(
                "error starting OpenCV: ${exception.message}"
            )
        }

        WindowCompat.setDecorFitsSystemWindows(window, false)
        window.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN)
        window.statusBarColor = Color.TRANSPARENT
        window.navigationBarColor = Color.BLACK
        WindowCompat.getInsetsController(window, window.decorView).apply {
            isAppearanceLightStatusBars = false
            isAppearanceLightNavigationBars = false
        }

        // Show cropper, accept crop button, add new document button, and
        // retake photo button. Since we open the camera in a few lines, the user
        // doesn't see this until they finish taking a photo
        setContentView(R.layout.activity_image_crop)
        setupWindowInsets()
        imageView = findViewById(R.id.image_view)

        try {
            // validate maxNumDocuments option, and update default if user sets it
            var userSpecifiedMaxImages: Int? = null
            intent.extras?.get(DocumentScannerExtra.EXTRA_MAX_NUM_DOCUMENTS)?.let {
                if (it.toString().toIntOrNull() == null) {
                    throw Throwable(
                        "${DocumentScannerExtra.EXTRA_MAX_NUM_DOCUMENTS} must be a positive number"
                    )
                }
                userSpecifiedMaxImages = it as Int
                maxNumDocuments = userSpecifiedMaxImages
            }

            // validate letUserAdjustCrop option, and update default if user sets it
            intent.extras?.get(DocumentScannerExtra.EXTRA_LET_USER_ADJUST_CROP)?.let {
                if (!arrayOf("true", "false").contains(it.toString())) {
                    throw Throwable(
                        "${DocumentScannerExtra.EXTRA_LET_USER_ADJUST_CROP} must true or false"
                    )
                }
                letUserAdjustCrop = it as Boolean
            }

            // if we don't want user to move corners, we can let the user only take 1 photo
            if (!letUserAdjustCrop) {
                maxNumDocuments = 1

                if (userSpecifiedMaxImages != null && userSpecifiedMaxImages != 1) {
                    throw Throwable(
                        "${DocumentScannerExtra.EXTRA_MAX_NUM_DOCUMENTS} must be 1 when " +
                                "${DocumentScannerExtra.EXTRA_LET_USER_ADJUST_CROP} is false"
                    )
                }
            }

            // validate croppedImageQuality option, and update value if user sets it
            intent.extras?.get(DocumentScannerExtra.EXTRA_CROPPED_IMAGE_QUALITY)?.let {
                if (it !is Int || it < 0 || it > 100) {
                    throw Throwable(
                        "${DocumentScannerExtra.EXTRA_CROPPED_IMAGE_QUALITY} must be a number " +
                                "between 0 and 100"
                    )
                }
                croppedImageQuality = it
            }

            cropperHandleColor = intent.getIntExtra(
                DocumentScannerExtra.EXTRA_CROPPER_HANDLE_COLOR,
                cropperHandleColor
            )
            cropperFrameColor = intent.getIntExtra(
                DocumentScannerExtra.EXTRA_CROPPER_FRAME_COLOR,
                cropperFrameColor
            )
            intent.extras?.get(DocumentScannerExtra.EXTRA_CROPPER_STROKE_WIDTH_DP)?.let {
                val strokeWidthDp = (it as? Number)?.toFloat()
                    ?: throw Throwable(
                        "${DocumentScannerExtra.EXTRA_CROPPER_STROKE_WIDTH_DP} must be a number"
                    )

                if (
                    strokeWidthDp <= 0f ||
                    strokeWidthDp.isNaN() ||
                    strokeWidthDp.isInfinite()
                ) {
                    throw Throwable(
                        "${DocumentScannerExtra.EXTRA_CROPPER_STROKE_WIDTH_DP} must be greater than 0"
                    )
                }

                cropperStrokeWidthDp = strokeWidthDp
            }
            buttonTintColor = intent.getIntExtra(
                DocumentScannerExtra.EXTRA_BUTTON_TINT_COLOR,
                buttonTintColor
            )
            buttonContainerColor = intent.getIntExtra(
                DocumentScannerExtra.EXTRA_BUTTON_CONTAINER_COLOR,
                buttonContainerColor
            )
            cropperHandleOutlineColor = intent.getIntExtra(
                DocumentScannerExtra.EXTRA_CROPPER_HANDLE_OUTLINE_COLOR,
                buttonContainerColor
            )
            doneButtonTintColor = intent.getIntExtra(
                DocumentScannerExtra.EXTRA_DONE_BUTTON_TINT_COLOR,
                buttonTintColor
            )
            doneButtonContainerColor = intent.getIntExtra(
                DocumentScannerExtra.EXTRA_DONE_BUTTON_CONTAINER_COLOR,
                buttonContainerColor
            )
            shouldApplyButtonColors = listOf(
                DocumentScannerExtra.EXTRA_BUTTON_TINT_COLOR,
                DocumentScannerExtra.EXTRA_BUTTON_CONTAINER_COLOR,
                DocumentScannerExtra.EXTRA_DONE_BUTTON_TINT_COLOR,
                DocumentScannerExtra.EXTRA_DONE_BUTTON_CONTAINER_COLOR
            ).any(intent::hasExtra)
        } catch (exception: Throwable) {
            finishIntentWithError(
                "invalid extra: ${exception.message}"
            )
            return
        }

        // set click event handlers for new document button, accept and crop document button,
        // and retake document photo button
        val newPhotoButton: CircleButton = findViewById(R.id.new_photo_button)
        val completeDocumentScanButton: DoneButton = findViewById(
            R.id.complete_document_scan_button
        )
        val retakePhotoButton: CircleButton = findViewById(R.id.retake_photo_button)

        imageView.setCropperStyle(
            handleColor = cropperHandleColor,
            frameColor = cropperFrameColor,
            handleOutlineColor = cropperHandleOutlineColor,
            strokeWidthDp = cropperStrokeWidthDp
        )
        if (shouldApplyButtonColors) {
            retakePhotoButton.setColors(
                containerColor = buttonContainerColor,
                iconColor = buttonTintColor
            )
            completeDocumentScanButton.setColors(
                ringColor = buttonContainerColor,
                circleColor = doneButtonContainerColor,
                iconColor = doneButtonTintColor
            )
            newPhotoButton.setColors(
                containerColor = buttonContainerColor,
                iconColor = buttonTintColor
            )
        }

        newPhotoButton.onClick { onClickNew() }
        completeDocumentScanButton.onClick { onClickDone() }
        retakePhotoButton.onClick { onClickRetake() }

        // open camera, so user can snap document photo
        try {
            openCamera()
        } catch (exception: Throwable) {
            finishIntentWithError(
                "error opening camera: ${exception.message}"
            )
        }
    }

    private fun setupWindowInsets() {
        val root: View = findViewById(R.id.document_scanner_root)
        val initialPaddingLeft = root.paddingLeft
        val initialPaddingTop = root.paddingTop
        val initialPaddingRight = root.paddingRight
        val initialPaddingBottom = root.paddingBottom

        fun updateContentSize() {
            contentWidth = (screenWidth - root.paddingLeft - root.paddingRight).coerceAtLeast(0)
            contentHeight = (screenHeight - root.paddingTop - root.paddingBottom).coerceAtLeast(0)
        }

        ViewCompat.setOnApplyWindowInsetsListener(root) { view, windowInsets ->
            val systemBars = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
            val displayCutout = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout())
            val safeInsets = Insets.max(systemBars, displayCutout)

            view.setPadding(
                initialPaddingLeft + safeInsets.left,
                initialPaddingTop + safeInsets.top,
                initialPaddingRight + safeInsets.right,
                initialPaddingBottom + safeInsets.bottom
            )
            updateContentSize()
            windowInsets
        }

        ViewCompat.requestApplyInsets(root)
        updateContentSize()
    }

    /**
     * Pass in a photo of a document, and get back 4 corner points (top left, top right, bottom
     * right, bottom left). This tries to detect document corners, but falls back to photo corners
     * with slight margin in case we can't detect document corners.
     *
     * @param photo the original photo with a rectangular document
     * @return a List of 4 OpenCV points (document corners)
     */
    private fun getDocumentCorners(photo: Bitmap): List<Point> {
        val cornerPoints: List<Point>? = DocumentDetector.findDocumentCorners(photo)

        // if cornerPoints is null then default the corners to the photo bounds with a margin
        return cornerPoints ?: listOf(
            Point(0.0, 0.0).move(
                cropperOffsetWhenCornersNotFound,
                cropperOffsetWhenCornersNotFound
            ),
            Point(photo.width.toDouble(), 0.0).move(
                -cropperOffsetWhenCornersNotFound,
                cropperOffsetWhenCornersNotFound
            ),
            Point(0.0, photo.height.toDouble()).move(
                cropperOffsetWhenCornersNotFound,
                -cropperOffsetWhenCornersNotFound
            ),
            Point(photo.width.toDouble(), photo.height.toDouble()).move(
                -cropperOffsetWhenCornersNotFound,
                -cropperOffsetWhenCornersNotFound
            )
        )
    }

    /**
     * Set document to null since we're capturing a new document, and open the camera. If the
     * user captures a photo successfully document gets updated.
     */
    /**
     * @property photoFilePath the photo file path
     */
    private lateinit var photoFilePath: String

    /**
     * @property startForResult used to launch camera
     */
    private val startForResult = registerForActivityResult(
        ActivityResultContracts.TakePicture()
    ) { isSuccess ->
        if (isSuccess) {
            // send back photo file path on capture success
            val originalPhotoPath = photoFilePath
            // user takes photo

            // if maxNumDocuments is 3 and this is the 3rd photo, hide the new photo button since
            // we reach the allowed limit
            if (documents.size == maxNumDocuments - 1) {
                val newPhotoButton: CircleButton = findViewById(R.id.new_photo_button)
                newPhotoButton.isClickable = false
                newPhotoButton.visibility = View.INVISIBLE
            }

            // get bitmap from photo file path
            val photo: Bitmap = ImageUtil().getImageFromFilePath(originalPhotoPath)

            // get document corners by detecting them, or falling back to photo corners with
            // slight margin if we can't find the corners
            val corners = try {
                val (topLeft, topRight, bottomLeft, bottomRight) = getDocumentCorners(photo)
                Quad(topLeft, topRight, bottomRight, bottomLeft)
            } catch (exception: Throwable) {
                finishIntentWithError(
                    "unable to get document corners: ${exception.message}"
                )
                return@registerForActivityResult
            }

            document = Document(originalPhotoPath, photo.width, photo.height, corners)

            if (letUserAdjustCrop) {
                // user is allowed to move corners to make corrections
                try {
                    // set preview image height based off of photo dimensions
                    imageView.setImagePreviewBounds(
                        photo,
                        contentWidth.takeIf { it > 0 } ?: screenWidth,
                        contentHeight.takeIf { it > 0 } ?: screenHeight
                    )

                    // display original photo, so user can adjust detected corners
                    imageView.setImage(photo)

                    // document corner points are in original image coordinates, so we need to
                    // scale and move the points to account for blank space (caused by photo and
                    // photo container having different aspect ratios)
                    val cornersInImagePreviewCoordinates = corners
                        .mapOriginalToPreviewImageCoordinates(
                            imageView.imagePreviewBounds,
                            imageView.imagePreviewBounds.height() / photo.height
                        )

                    // display cropper, and allow user to move corners
                    imageView.setCropper(cornersInImagePreviewCoordinates)
                } catch (exception: Throwable) {
                    finishIntentWithError(
                        "unable get image preview ready: ${exception.message}"
                    )
                    return@registerForActivityResult
                }
            } else {
                // user isn't allowed to move corners, so accept automatically detected corners
                document?.let { document ->
                    documents.add(document)
                }

                // create cropped document image, and return file path to complete document scan
                cropDocumentAndFinishIntent()
            }
        } else {
            // delete the photo since the user didn't finish taking the photo
            File(photoFilePath).delete()
            // user exits camera
            // complete document scan if this is the first document since we can't go to crop view
            // until user takes at least 1 photo
            if (documents.isEmpty()) {
                onClickCancel()
            }
        }
    }

    /**
     * open the camera by launching an image capture intent
     *
     * @param pageNumber the current document page number
     */
    private fun openCamera(pageNumber: Int = documents.size) {
        // create new file for photo
        val photoFile: File = FileUtil().createImageFile(this, pageNumber)

        // store the photo file path, and send it back once the photo is saved
        photoFilePath = photoFile.absolutePath

        // photo gets saved to this file path
        // open camera
        startForResult.launch(
            FileProvider.getUriForFile(
                this,
                "${packageName}.DocumentScannerFileProvider",
                photoFile
            )
        )
    }

    /**
     * Once user accepts by pressing check button, or by pressing add new document button, add
     * original photo path and 4 document corners to documents list. If user isn't allowed to
     * adjust corners, call this automatically.
     */
    private fun addSelectedCornersAndOriginalPhotoPathToDocuments() {
        // only add document it's not null (the current document photo capture, and corner
        // detection are successful)
        document?.let { document ->
            // convert corners from image preview coordinates to original photo coordinates
            // (original image is probably bigger than the preview image)
            val cornersInOriginalImageCoordinates = imageView.corners
                .mapPreviewToOriginalImageCoordinates(
                    imageView.imagePreviewBounds,
                    imageView.imagePreviewBounds.height() / document.originalPhotoHeight
                )
            document.corners = cornersInOriginalImageCoordinates
            documents.add(document)
        }
    }

    /**
     * This gets called when a user presses the new document button. Store current photo path
     * with document corners. Then open the camera, so user can take a photo of the next
     * page or document
     */
    private fun onClickNew() {
        addSelectedCornersAndOriginalPhotoPathToDocuments()
        openCamera()
    }

    /**
     * This gets called when a user presses the done button. Store current photo path with
     * document corners. Then crop document using corners, and return cropped image paths
     */
    private fun onClickDone() {
        addSelectedCornersAndOriginalPhotoPathToDocuments()
        cropDocumentAndFinishIntent()
    }

    /**
     * This gets called when a user presses the retake photo button. The user presses this in
     * case the original document photo isn't good, and they need to take it again.
     */
    private fun onClickRetake() {
        // we're going to retake the photo, so delete the one we just took
        document?.let { document -> File(document.originalPhotoFilePath).delete() }
        openCamera()
    }

    /**
     * This gets called when a user doesn't want to complete the document scan after starting.
     * For example a user can quit out of the camera before snapping a photo of the document.
     */
    private fun onClickCancel() {
        setResult(RESULT_CANCELED)
        finish()
    }

    /**
     * This crops original document photo, saves cropped document photo, deletes original
     * document photo, and returns cropped document photo file path. It repeats that for
     * all document photos.
     */
    private fun cropDocumentAndFinishIntent() {
        val croppedImageResults = arrayListOf<String>()
        for ((pageNumber, document) in documents.withIndex()) {
            // crop document photo by using corners
            val croppedImage: Bitmap = try {
                ImageUtil().crop(
                    document.originalPhotoFilePath,
                    document.corners
                )
            } catch (exception: Throwable) {
                finishIntentWithError("unable to crop image: ${exception.message}")
                return
            }

            // delete original document photo
            File(document.originalPhotoFilePath).delete()

            // save cropped document photo
            try {
                val croppedImageFile = FileUtil().createImageFile(this, pageNumber)
                croppedImage.saveToFile(croppedImageFile, croppedImageQuality)
                croppedImageResults.add(Uri.fromFile(croppedImageFile).toString())
            } catch (exception: Throwable) {
                finishIntentWithError(
                    "unable to save cropped image: ${exception.message}"
                )
            }
        }

        // return array of cropped document photo file paths
        setResult(
            RESULT_OK,
            Intent().putExtra("croppedImageResults", croppedImageResults)
        )
        finish()
    }

    /**
     * This ends the document scanner activity, and returns an error message that can be
     * used to debug error
     *
     * @param errorMessage an error message
     */
    private fun finishIntentWithError(errorMessage: String) {
        setResult(
            RESULT_OK,
            Intent().putExtra("error", errorMessage)
        )
        finish()
    }
}
