/*
 * 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.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,
    borderRadius: BorderRadiusStyle? = null,
    private val shadowColor: Int,
    private val offsetX: Float,
    private val offsetY: Float,
    private val blurRadius: Float,
    private val spread: Float,
) : Drawable() {
  public var borderRadius = borderRadius
    set(value) {
      if (value != field) {
        field = value
        invalidateSelf()
      }
    }

  private val shadowPaint =
      Paint().apply {
        color = shadowColor
        if (blurRadius > 0) {
          maskFilter =
              BlurMaskFilter(
                  FilterHelper.sigmaToRadius(blurRadius * BLUR_RADIUS_SIGMA_SCALE),
                  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()
  }

  override fun getOpacity(): Int =
      ((shadowPaint.alpha / 255f) / (Color.alpha(shadowColor) / 255f) * 255f).roundToInt()

  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)
  }
}
