/*
 * Copyright (C) 2022 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * 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.
 */

package com.android.systemui

import android.content.Context
import android.content.pm.ActivityInfo
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.ColorFilter
import android.graphics.Paint
import android.graphics.PixelFormat
import android.graphics.PorterDuff
import android.graphics.PorterDuffColorFilter
import android.graphics.PorterDuffXfermode
import android.graphics.Rect
import android.graphics.Region
import android.graphics.drawable.Drawable
import android.hardware.graphics.common.AlphaInterpretation
import android.hardware.graphics.common.DisplayDecorationSupport
import android.view.DisplayCutout.BOUNDS_POSITION_BOTTOM
import android.view.DisplayCutout.BOUNDS_POSITION_LEFT
import android.view.DisplayCutout.BOUNDS_POSITION_LENGTH
import android.view.DisplayCutout.BOUNDS_POSITION_TOP
import android.view.DisplayCutout.BOUNDS_POSITION_RIGHT
import android.view.RoundedCorner
import android.view.RoundedCorners
import android.view.Surface
import androidx.annotation.VisibleForTesting
import com.android.systemui.util.asIndenting
import java.io.PrintWriter
import kotlin.math.ceil
import kotlin.math.floor

/**
 * When the HWC of the device supports Composition.DISPLAY_DECORATION, we use this layer to draw
 * screen decorations.
 */
class ScreenDecorHwcLayer(
    context: Context,
    displayDecorationSupport: DisplayDecorationSupport,
    private val debug: Boolean,
) : DisplayCutoutBaseView(context) {
    val colorMode: Int
    private val useInvertedAlphaColor: Boolean
    private var color: Int = Color.BLACK
        set(value) {
            field = value
            paint.color = value
        }

    private val bgColor: Int
    private var cornerFilter: ColorFilter
    private val cornerBgFilter: ColorFilter
    private val clearPaint: Paint
    @JvmField val transparentRect: Rect = Rect()
    private val debugTransparentRegionPaint: Paint?
    private val tempRect: Rect = Rect()

    private var hasTopRoundedCorner = false
    private var hasBottomRoundedCorner = false
    private var roundedCornerTopSize = 0
    private var roundedCornerBottomSize = 0
    private var roundedCornerDrawableTop: Drawable? = null
    private var roundedCornerDrawableBottom: Drawable? = null

    init {
        if (displayDecorationSupport.format != PixelFormat.R_8) {
            throw IllegalArgumentException("Attempting to use unsupported mode " +
                    "${PixelFormat.formatToString(displayDecorationSupport.format)}")
        }
        if (debug) {
            color = Color.GREEN
            bgColor = Color.TRANSPARENT
            colorMode = ActivityInfo.COLOR_MODE_DEFAULT
            useInvertedAlphaColor = false
            debugTransparentRegionPaint = Paint().apply {
                color = 0x2f00ff00 // semi-transparent green
                style = Paint.Style.FILL
            }
        } else {
            colorMode = ActivityInfo.COLOR_MODE_A8
            useInvertedAlphaColor = displayDecorationSupport.alphaInterpretation ==
                    AlphaInterpretation.COVERAGE
            if (useInvertedAlphaColor) {
                color = Color.TRANSPARENT
                bgColor = Color.BLACK
            } else {
                color = Color.BLACK
                bgColor = Color.TRANSPARENT
            }
            debugTransparentRegionPaint = null
        }
        cornerFilter = PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN)
        cornerBgFilter = PorterDuffColorFilter(bgColor, PorterDuff.Mode.SRC_OUT)

        clearPaint = Paint()
        clearPaint.xfermode = PorterDuffXfermode(PorterDuff.Mode.CLEAR)
    }

    override fun onAttachedToWindow() {
        super.onAttachedToWindow()
        parent.requestTransparentRegion(this)
        updateColors()
    }

    private fun updateColors() {
        if (!debug) {
            viewRootImpl.setDisplayDecoration(true)
        }

        cornerFilter = PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN)

        if (useInvertedAlphaColor) {
            paint.set(clearPaint)
        } else {
            paint.color = color
            paint.style = Paint.Style.FILL
        }
    }

    fun setDebugColor(color: Int) {
        if (!debug) {
            return
        }

        if (this.color == color) {
            return
        }

        this.color = color

        updateColors()
        invalidate()
    }

    override fun onUpdate() {
        parent.requestTransparentRegion(this)
    }

    override fun onDraw(canvas: Canvas) {
        // If updating onDraw, also update gatherTransparentRegion
        if (useInvertedAlphaColor) {
            canvas.drawColor(bgColor)
        }

        // We may clear the color(if useInvertedAlphaColor is true) of the rounded corner rects
        // before drawing rounded corners. If the cutout happens to be inside one of these rects, it
        // will be cleared, so we have to draw rounded corners before cutout.
        drawRoundedCorners(canvas)
        // Cutouts are drawn in DisplayCutoutBaseView.onDraw()
        super.onDraw(canvas)

        debugTransparentRegionPaint?.let {
            canvas.drawRect(transparentRect, it)
        }
    }

    override fun gatherTransparentRegion(region: Region?): Boolean {
        region?.let {
            calculateTransparentRect()
            if (debug) {
                // Since we're going to draw a rectangle where the layer would
                // normally be transparent, treat the transparent region as
                // empty. We still want this method to be called, though, so
                // that it calculates the transparent rect at the right time
                // to match ![debug]
                region.setEmpty()
            } else {
                region.op(transparentRect, Region.Op.INTERSECT)
            }
        }
        // Always return false - views underneath this should always be visible.
        return false
    }

    /**
     * The transparent rect is calculated by subtracting the regions of cutouts, cutout protect and
     * rounded corners from the region with fullscreen display size.
     */
    @VisibleForTesting
    fun calculateTransparentRect() {
        transparentRect.set(0, 0, width, height)

        // Remove cutout region.
        removeCutoutFromTransparentRegion()

        // Remove cutout protection region.
        removeCutoutProtectionFromTransparentRegion()

        // Remove rounded corner region.
        removeRoundedCornersFromTransparentRegion()
    }

    private fun removeCutoutFromTransparentRegion() {
        displayInfo.displayCutout?.let {
                cutout ->
            if (!cutout.boundingRectLeft.isEmpty) {
                transparentRect.left =
                    cutout.boundingRectLeft.right.coerceAtLeast(transparentRect.left)
            }
            if (!cutout.boundingRectTop.isEmpty) {
                transparentRect.top =
                    cutout.boundingRectTop.bottom.coerceAtLeast(transparentRect.top)
            }
            if (!cutout.boundingRectRight.isEmpty) {
                transparentRect.right =
                    cutout.boundingRectRight.left.coerceAtMost(transparentRect.right)
            }
            if (!cutout.boundingRectBottom.isEmpty) {
                transparentRect.bottom =
                    cutout.boundingRectBottom.top.coerceAtMost(transparentRect.bottom)
            }
        }
    }

    private fun removeCutoutProtectionFromTransparentRegion() {
        if (protectionRect.isEmpty) {
            return
        }

        val centerX = protectionRect.centerX()
        val centerY = protectionRect.centerY()
        val scaledDistanceX = (centerX - protectionRect.left) * cameraProtectionProgress
        val scaledDistanceY = (centerY - protectionRect.top) * cameraProtectionProgress
        tempRect.set(
            floor(centerX - scaledDistanceX).toInt(),
            floor(centerY - scaledDistanceY).toInt(),
            ceil(centerX + scaledDistanceX).toInt(),
            ceil(centerY + scaledDistanceY).toInt()
        )

        // Find out which edge the protectionRect belongs and remove that edge from the transparent
        // region.
        val leftDistance = tempRect.left
        val topDistance = tempRect.top
        val rightDistance = width - tempRect.right
        val bottomDistance = height - tempRect.bottom
        val minDistance = minOf(leftDistance, topDistance, rightDistance, bottomDistance)
        when (minDistance) {
            leftDistance -> {
                transparentRect.left = tempRect.right.coerceAtLeast(transparentRect.left)
            }
            topDistance -> {
                transparentRect.top = tempRect.bottom.coerceAtLeast(transparentRect.top)
            }
            rightDistance -> {
                transparentRect.right = tempRect.left.coerceAtMost(transparentRect.right)
            }
            bottomDistance -> {
                transparentRect.bottom = tempRect.top.coerceAtMost(transparentRect.bottom)
            }
        }
    }

    private fun removeRoundedCornersFromTransparentRegion() {
        var hasTopOrBottomCutouts = false
        var hasLeftOrRightCutouts = false
        displayInfo.displayCutout?.let {
                cutout ->
            hasTopOrBottomCutouts = !cutout.boundingRectTop.isEmpty ||
                    !cutout.boundingRectBottom.isEmpty
            hasLeftOrRightCutouts = !cutout.boundingRectLeft.isEmpty ||
                    !cutout.boundingRectRight.isEmpty
        }
        // The goal is to remove the rounded corner areas as small as possible so that we can have a
        // larger transparent region. Therefore, we should always remove from the short edge sides
        // if possible.
        val isShortEdgeTopBottom = width < height
        if (isShortEdgeTopBottom) {
            // Short edges on top & bottom.
            if (!hasTopOrBottomCutouts && hasLeftOrRightCutouts) {
                // If there are cutouts only on left or right edges, remove left and right sides
                // for rounded corners.
                transparentRect.left = getRoundedCornerSizeByPosition(BOUNDS_POSITION_LEFT)
                    .coerceAtLeast(transparentRect.left)
                transparentRect.right =
                    (width - getRoundedCornerSizeByPosition(BOUNDS_POSITION_RIGHT))
                        .coerceAtMost(transparentRect.right)
            } else {
                // If there are cutouts on top or bottom edges or no cutout at all, remove top
                // and bottom sides for rounded corners.
                transparentRect.top = getRoundedCornerSizeByPosition(BOUNDS_POSITION_TOP)
                    .coerceAtLeast(transparentRect.top)
                transparentRect.bottom =
                    (height - getRoundedCornerSizeByPosition(BOUNDS_POSITION_BOTTOM))
                        .coerceAtMost(transparentRect.bottom)
            }
        } else {
            // Short edges on left & right.
            if (hasTopOrBottomCutouts && !hasLeftOrRightCutouts) {
                // If there are cutouts only on top or bottom edges, remove top and bottom sides
                // for rounded corners.
                transparentRect.top = getRoundedCornerSizeByPosition(BOUNDS_POSITION_TOP)
                    .coerceAtLeast(transparentRect.top)
                transparentRect.bottom =
                    (height - getRoundedCornerSizeByPosition(BOUNDS_POSITION_BOTTOM))
                        .coerceAtMost(transparentRect.bottom)
            } else {
                // If there are cutouts on left or right edges or no cutout at all, remove left
                // and right sides for rounded corners.
                transparentRect.left = getRoundedCornerSizeByPosition(BOUNDS_POSITION_LEFT)
                    .coerceAtLeast(transparentRect.left)
                transparentRect.right =
                    (width - getRoundedCornerSizeByPosition(BOUNDS_POSITION_RIGHT))
                        .coerceAtMost(transparentRect.right)
            }
        }
    }

    private fun getRoundedCornerSizeByPosition(position: Int): Int {
        val delta = displayRotation - Surface.ROTATION_0
        return when ((position + delta) % BOUNDS_POSITION_LENGTH) {
            BOUNDS_POSITION_LEFT -> roundedCornerTopSize.coerceAtLeast(roundedCornerBottomSize)
            BOUNDS_POSITION_TOP -> roundedCornerTopSize
            BOUNDS_POSITION_RIGHT -> roundedCornerTopSize.coerceAtLeast(roundedCornerBottomSize)
            BOUNDS_POSITION_BOTTOM -> roundedCornerBottomSize
            else -> throw IllegalArgumentException("Incorrect position: $position")
        }
    }

    private fun drawRoundedCorners(canvas: Canvas) {
        if (!hasTopRoundedCorner && !hasBottomRoundedCorner) {
            return
        }
        var degree: Int
        for (i in RoundedCorner.POSITION_TOP_LEFT
                until RoundedCorners.ROUNDED_CORNER_POSITION_LENGTH) {
            canvas.save()
            degree = getRoundedCornerRotationDegree(90 * i)
            canvas.rotate(degree.toFloat())
            canvas.translate(
                    getRoundedCornerTranslationX(degree).toFloat(),
                    getRoundedCornerTranslationY(degree).toFloat())
            if (hasTopRoundedCorner && (i == RoundedCorner.POSITION_TOP_LEFT ||
                            i == RoundedCorner.POSITION_TOP_RIGHT)) {
                drawRoundedCorner(canvas, roundedCornerDrawableTop, roundedCornerTopSize)
            } else if (hasBottomRoundedCorner && (i == RoundedCorner.POSITION_BOTTOM_LEFT ||
                            i == RoundedCorner.POSITION_BOTTOM_RIGHT)) {
                drawRoundedCorner(canvas, roundedCornerDrawableBottom, roundedCornerBottomSize)
            }
            canvas.restore()
        }
    }

    private fun drawRoundedCorner(canvas: Canvas, drawable: Drawable?, size: Int) {
        if (useInvertedAlphaColor) {
            canvas.drawRect(0f, 0f, size.toFloat(), size.toFloat(), clearPaint)
            drawable?.colorFilter = cornerBgFilter
        } else {
            drawable?.colorFilter = cornerFilter
        }
        drawable?.draw(canvas)
        // Clear color filter when we are done with drawing.
        drawable?.clearColorFilter()
    }

    private fun getRoundedCornerRotationDegree(defaultDegree: Int): Int {
        return (defaultDegree - 90 * displayRotation + 360) % 360
    }

    private fun getRoundedCornerTranslationX(degree: Int): Int {
        return when (degree) {
            0, 90 -> 0
            180 -> -width
            270 -> -height
            else -> throw IllegalArgumentException("Incorrect degree: $degree")
        }
    }

    private fun getRoundedCornerTranslationY(degree: Int): Int {
        return when (degree) {
            0, 270 -> 0
            90 -> -width
            180 -> -height
            else -> throw IllegalArgumentException("Incorrect degree: $degree")
        }
    }

    /**
     * Update the rounded corner drawables.
     */
    fun updateRoundedCornerDrawable(top: Drawable?, bottom: Drawable?) {
        roundedCornerDrawableTop = top
        roundedCornerDrawableBottom = bottom
        updateRoundedCornerDrawableBounds()
        invalidate()
    }

    /**
     * Update the rounded corner existence and size.
     */
    fun updateRoundedCornerExistenceAndSize(
        hasTop: Boolean,
        hasBottom: Boolean,
        topSize: Int,
        bottomSize: Int
    ) {
        if (hasTopRoundedCorner == hasTop &&
                hasBottomRoundedCorner == hasBottom &&
                roundedCornerTopSize == topSize &&
                roundedCornerBottomSize == bottomSize) {
            return
        }
        hasTopRoundedCorner = hasTop
        hasBottomRoundedCorner = hasBottom
        roundedCornerTopSize = topSize
        roundedCornerBottomSize = bottomSize
        updateRoundedCornerDrawableBounds()

        // Use requestLayout() to trigger transparent region recalculated
        requestLayout()
    }

    private fun updateRoundedCornerDrawableBounds() {
        if (roundedCornerDrawableTop != null) {
            roundedCornerDrawableTop?.setBounds(0, 0, roundedCornerTopSize,
                    roundedCornerTopSize)
        }
        if (roundedCornerDrawableBottom != null) {
            roundedCornerDrawableBottom?.setBounds(0, 0, roundedCornerBottomSize,
                    roundedCornerBottomSize)
        }
        invalidate()
    }

    override fun dump(pw: PrintWriter) {
        val ipw = pw.asIndenting()
        ipw.increaseIndent()
        ipw.println("ScreenDecorHwcLayer:")
        super.dump(pw)
        ipw.println("this=$this")
        ipw.println("transparentRect=$transparentRect")
        ipw.println("hasTopRoundedCorner=$hasTopRoundedCorner")
        ipw.println("hasBottomRoundedCorner=$hasBottomRoundedCorner")
        ipw.println("roundedCornerTopSize=$roundedCornerTopSize")
        ipw.println("roundedCornerBottomSize=$roundedCornerBottomSize")
        ipw.decreaseIndent()
    }
}
