/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

package com.facebook.react.uimanager.drawable

import android.content.Context
import android.graphics.BlurMaskFilter
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.ColorFilter
import android.graphics.Paint
import android.graphics.Path
import android.graphics.PixelFormat
import android.graphics.RectF
import android.graphics.drawable.Drawable
import androidx.annotation.RequiresApi
import com.facebook.react.uimanager.FilterHelper
import com.facebook.react.uimanager.PixelUtil.dpToPx
import com.facebook.react.uimanager.PixelUtil.pxToDp
import com.facebook.react.uimanager.style.BorderRadiusStyle
import com.facebook.react.uimanager.style.ComputedBorderRadius
import com.facebook.react.uimanager.style.CornerRadii
import kotlin.math.roundToInt

internal const val MIN_OUTSET_BOX_SHADOW_SDK_VERSION = 28

// "the resulting shadow must approximate {...} a Gaussian blur with a standard deviation equal
// to half the blur radius"
// https://www.w3.org/TR/css-backgrounds-3/#shadow-blur
private const val BLUR_RADIUS_SIGMA_SCALE = 0.5f

/** Draws an outer box-shadow https://www.w3.org/TR/css-backgrounds-3/#shadow-shape */
@RequiresApi(MIN_OUTSET_BOX_SHADOW_SDK_VERSION)
internal class OutsetBoxShadowDrawable(
    private val context: Context,
    private val shadowColor: Int,
    private val offsetX: Float,
    private val offsetY: Float,
    private val blurRadius: Float,
    private val spread: Float,
    /*
     * We assume borderRadius to be shared across multiple drawables
     * therefore we should manually invalidate this drawable when changing it
     */
    var borderRadius: BorderRadiusStyle? = null,
) : Drawable() {
  private val shadowPaint =
      Paint().apply {
        color = shadowColor
        val convertedBlurRadius = FilterHelper.sigmaToRadius(blurRadius * BLUR_RADIUS_SIGMA_SCALE)
        if (convertedBlurRadius > 0) {
          maskFilter = BlurMaskFilter(convertedBlurRadius, BlurMaskFilter.Blur.NORMAL)
        }
      }

  override fun setAlpha(alpha: Int) {
    shadowPaint.alpha = (((alpha / 255f) * (Color.alpha(shadowColor) / 255f)) * 255f).roundToInt()
    invalidateSelf()
  }

  override fun setColorFilter(colorFilter: ColorFilter?) {
    shadowPaint.colorFilter = colorFilter
    invalidateSelf()
  }

  @Deprecated("Deprecated in Java")
  override fun getOpacity(): Int {
    val alpha = shadowPaint.alpha
    return when (alpha) {
      255 -> PixelFormat.OPAQUE
      in 1..254 -> PixelFormat.TRANSLUCENT
      else -> PixelFormat.TRANSPARENT
    }
  }

  override fun draw(canvas: Canvas) {
    val resolutionWidth = bounds.width().toFloat().pxToDp()
    val resolutionHeight = bounds.height().toFloat().pxToDp()
    val computedBorderRadii =
        borderRadius?.resolve(layoutDirection, context, resolutionWidth, resolutionHeight)?.let {
          ComputedBorderRadius(
              topLeft = CornerRadii(it.topLeft.horizontal.dpToPx(), it.topLeft.vertical.dpToPx()),
              topRight =
                  CornerRadii(it.topRight.horizontal.dpToPx(), it.topRight.vertical.dpToPx()),
              bottomLeft =
                  CornerRadii(it.bottomLeft.horizontal.dpToPx(), it.bottomLeft.vertical.dpToPx()),
              bottomRight =
                  CornerRadii(it.bottomRight.horizontal.dpToPx(), it.bottomRight.vertical.dpToPx()),
          )
        }

    val spreadExtent = spread.dpToPx()
    val shadowRect =
        RectF(bounds).apply {
          inset(-spreadExtent, -spreadExtent)
          offset(offsetX.dpToPx(), offsetY.dpToPx())
        }

    canvas.save().let { saveCount ->
      if (computedBorderRadii?.hasRoundedBorders() == true) {
        drawShadowRoundRect(canvas, shadowRect, spreadExtent, computedBorderRadii)
      } else {
        drawShadowRect(canvas, shadowRect)
      }
      canvas.restoreToCount(saveCount)
    }
  }

  private fun drawShadowRoundRect(
      canvas: Canvas,
      shadowRect: RectF,
      spreadExtent: Float,
      computedBorderRadii: ComputedBorderRadius,
  ) {
    // We inset the clip slightly, to avoid Skia artifacts with antialiased
    // clipping. This inset is only visible when no background is present.
    // https://neugierig.org/software/chromium/notes/2010/07/clipping.html
    val subpixelInsetBounds = RectF(bounds).apply { inset(0.4f, 0.4f) }
    canvas.clipOutPath(
        Path().apply {
          addRoundRect(
              subpixelInsetBounds,
              floatArrayOf(
                  computedBorderRadii.topLeft.horizontal,
                  computedBorderRadii.topLeft.vertical,
                  computedBorderRadii.topRight.horizontal,
                  computedBorderRadii.topRight.vertical,
                  computedBorderRadii.bottomRight.horizontal,
                  computedBorderRadii.bottomRight.vertical,
                  computedBorderRadii.bottomLeft.horizontal,
                  computedBorderRadii.bottomLeft.vertical,
              ),
              Path.Direction.CW,
          )
        }
    )

    canvas.drawPath(
        Path().apply {
          addRoundRect(
              shadowRect,
              floatArrayOf(
                  adjustRadiusForSpread(computedBorderRadii.topLeft.horizontal, spreadExtent),
                  adjustRadiusForSpread(computedBorderRadii.topLeft.vertical, spreadExtent),
                  adjustRadiusForSpread(computedBorderRadii.topRight.horizontal, spreadExtent),
                  adjustRadiusForSpread(computedBorderRadii.topRight.vertical, spreadExtent),
                  adjustRadiusForSpread(computedBorderRadii.bottomRight.horizontal, spreadExtent),
                  adjustRadiusForSpread(computedBorderRadii.bottomRight.vertical, spreadExtent),
                  adjustRadiusForSpread(computedBorderRadii.bottomLeft.horizontal, spreadExtent),
                  adjustRadiusForSpread(computedBorderRadii.bottomLeft.vertical, spreadExtent),
              ),
              Path.Direction.CW,
          )
        },
        shadowPaint,
    )
  }

  private fun drawShadowRect(canvas: Canvas, shadowRect: RectF) {
    canvas.clipOutRect(bounds)
    canvas.drawRect(shadowRect, shadowPaint)
  }
}
