/*
 * 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.Canvas
import android.graphics.Color
import android.graphics.ColorFilter
import android.graphics.Paint
import android.graphics.Path
import android.graphics.PixelFormat
import android.graphics.Rect
import android.graphics.RectF
import android.graphics.drawable.Drawable
import com.facebook.react.uimanager.PixelUtil.dpToPx
import com.facebook.react.uimanager.PixelUtil.pxToDp
import com.facebook.react.uimanager.style.BorderInsets
import com.facebook.react.uimanager.style.BorderRadiusStyle
import com.facebook.react.uimanager.style.ComputedBorderRadius
import kotlin.math.roundToInt

internal class BackgroundDrawable(
    private val context: Context,
    /*
     * We assume borderRadius & borderInsets to be shared across multiple drawables
     * therefore we should manually invalidate this drawable when changing either of them
     */
    var borderRadius: BorderRadiusStyle? = null,
    var borderInsets: BorderInsets? = null,
) : Drawable() {
  /**
   * There is a small gap between the edges of adjacent paths, such as between its Border and its
   * Outline. The smallest amount (found to be 0.8f) is used to shrink outline's path, overlapping
   * them and closing the visible gap.
   */
  private val pathAdjustment = 0.8f
  private var computedBorderInsets: RectF? = null
  private var computedBorderRadius: ComputedBorderRadius? = null
  private var needUpdatePath = true

  var backgroundColor: Int = Color.TRANSPARENT
    set(value) {
      if (field != value) {
        field = value
        backgroundPaint.color = value
        invalidateSelf()
      }
    }

  private var backgroundRect: RectF = RectF()
  private var backgroundRenderPath: Path? = null

  private val backgroundPaint: Paint =
      Paint(Paint.ANTI_ALIAS_FLAG).apply {
        style = Paint.Style.FILL
        color = backgroundColor
      }

  override fun invalidateSelf() {
    needUpdatePath = true
    super.invalidateSelf()
  }

  override fun onBoundsChange(bounds: Rect) {
    super.onBoundsChange(bounds)
    needUpdatePath = true
  }

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

  override fun setColorFilter(colorFilter: ColorFilter?) {
    // do nothing
  }

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

  override fun draw(canvas: Canvas) {
    updatePath()
    canvas.save()

    // Draws the View without its border first (with background color fill)
    if (backgroundPaint.alpha != 0) {
      if (computedBorderRadius?.isUniform() == true && borderRadius?.hasRoundedBorders() == true) {
        canvas.drawRoundRect(
            backgroundRect,
            computedBorderRadius?.topLeft?.horizontal?.dpToPx() ?: 0f,
            computedBorderRadius?.topLeft?.vertical?.dpToPx() ?: 0f,
            backgroundPaint,
        )
      } else if (borderRadius?.hasRoundedBorders() != true) {
        canvas.drawRect(backgroundRect, backgroundPaint)
      } else {
        canvas.drawPath(checkNotNull(backgroundRenderPath), backgroundPaint)
      }
    }

    canvas.restore()
  }

  private fun computeBorderInsets(): RectF =
      borderInsets?.resolve(layoutDirection, context).let {
        RectF(
            it?.left?.dpToPx() ?: 0f,
            it?.top?.dpToPx() ?: 0f,
            it?.right?.dpToPx() ?: 0f,
            it?.bottom?.dpToPx() ?: 0f,
        )
      }

  private fun updatePath() {
    if (!needUpdatePath) {
      return
    }
    needUpdatePath = false

    backgroundRect.set(bounds)

    computedBorderInsets = computeBorderInsets()
    computedBorderRadius =
        borderRadius?.resolve(
            layoutDirection,
            context,
            bounds.width().pxToDp(),
            bounds.height().pxToDp(),
        )
    val hasBorder =
        (computedBorderInsets?.left != 0f ||
            computedBorderInsets?.top != 0f ||
            computedBorderInsets?.right != 0f ||
            computedBorderInsets?.bottom != 0f)

    if (
        computedBorderRadius?.hasRoundedBorders() == true &&
            computedBorderRadius?.isUniform() == false
    ) {
      backgroundRenderPath = backgroundRenderPath ?: Path()
      backgroundRenderPath?.reset()
    }

    /**
     * The background bleeds a bit outside of the borderDrawable. pathAdjustment is used to slightly
     * shrink the rectangle (backgroundRect), ensuring the border can be drawn on top without the
     * gap.
     */
    if (hasBorder && borderRadius?.hasRoundedBorders() == true) {
      backgroundRect.left += pathAdjustment
      backgroundRect.top += pathAdjustment
      backgroundRect.right -= pathAdjustment
      backgroundRect.bottom -= pathAdjustment
    }

    if (borderRadius?.hasRoundedBorders() == true && computedBorderRadius?.isUniform() != true) {

      backgroundRenderPath?.addRoundRect(
          backgroundRect,
          floatArrayOf(
              computedBorderRadius?.topLeft?.horizontal?.dpToPx() ?: 0f,
              computedBorderRadius?.topLeft?.vertical?.dpToPx() ?: 0f,
              computedBorderRadius?.topRight?.horizontal?.dpToPx() ?: 0f,
              computedBorderRadius?.topRight?.vertical?.dpToPx() ?: 0f,
              computedBorderRadius?.bottomRight?.horizontal?.dpToPx() ?: 0f,
              computedBorderRadius?.bottomRight?.vertical?.dpToPx() ?: 0f,
              computedBorderRadius?.bottomLeft?.horizontal?.dpToPx() ?: 0f,
              computedBorderRadius?.bottomLeft?.vertical?.dpToPx() ?: 0f,
          ),
          Path.Direction.CW,
      )
    }
  }
}
