/*
 * 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.modules.fresco

import android.content.Context
import android.graphics.drawable.Drawable
import android.net.Uri
import com.facebook.common.logging.FLog
import com.facebook.imageformat.ImageFormat
import com.facebook.imageformat.ImageFormat.FormatChecker
import com.facebook.imageformat.ImageFormatCheckerUtils
import com.facebook.imagepipeline.common.ImageDecodeOptions
import com.facebook.imagepipeline.decoder.ImageDecoder
import com.facebook.imagepipeline.decoder.ImageDecoderConfig
import com.facebook.imagepipeline.drawable.DrawableFactory
import com.facebook.imagepipeline.image.CloseableImage
import com.facebook.imagepipeline.image.DefaultCloseableImage
import com.facebook.imagepipeline.image.EncodedImage
import com.facebook.imagepipeline.image.QualityInfo

public object XmlFormat {

  public fun addDecodingCapability(
      builder: ImageDecoderConfig.Builder,
      context: Context,
  ): ImageDecoderConfig.Builder {
    return builder.addDecodingCapability(
        XmlFormat.FORMAT,
        XmlFormat.XmlFormatChecker(),
        XmlFormat.XmlFormatDecoder(context),
    )
  }

  public fun getDrawableFactory(): DrawableFactory {
    return XmlDrawableFactory()
  }

  private val FORMAT: ImageFormat = ImageFormat("XML", "xml")
  private const val TAG: String = "XmlFormat"
  /**
   * These are the first 4 bytes of a binary XML file. We can only support binary XML files and not
   * raw XML files because Android explicitly disallows raw XML files when inflating drawables.
   * Binary XML files are created at build time by Android's AAPT.
   *
   * @see
   *   https://developer.android.com/reference/android/view/LayoutInflater#inflate(org.xmlpull.v1.XmlPullParser,%20android.view.ViewGroup)
   */
  private val BINARY_XML_HEADER: ByteArray =
      byteArrayOf(
          3.toByte(),
          0.toByte(),
          8.toByte(),
          0.toByte(),
      )

  private class XmlFormatChecker : FormatChecker {
    override val headerSize: Int = BINARY_XML_HEADER.size

    override fun determineFormat(headerBytes: ByteArray, headerSize: Int): ImageFormat {
      return when {
        headerSize < BINARY_XML_HEADER.size -> ImageFormat.UNKNOWN
        ImageFormatCheckerUtils.startsWithPattern(headerBytes, BINARY_XML_HEADER) -> FORMAT
        else -> ImageFormat.UNKNOWN
      }
    }
  }

  private class CloseableXmlImage(val name: String, val drawable: Drawable) :
      DefaultCloseableImage() {
    private var closed = false

    override fun getSizeInBytes(): Int {
      return getWidth() * getHeight() * 4 // 4 bytes ARGB per pixel
    }

    override fun close() {
      closed = true
    }

    override fun isClosed(): Boolean {
      return closed
    }

    override fun getWidth(): Int {
      return drawable.intrinsicWidth.takeIf { it >= 0 } ?: 0
    }

    override fun getHeight(): Int {
      return drawable.intrinsicHeight.takeIf { it >= 0 } ?: 0
    }
  }

  private class XmlFormatDecoder(private val context: Context) : ImageDecoder {
    override fun decode(
        encodedImage: EncodedImage,
        length: Int,
        qualityInfo: QualityInfo,
        options: ImageDecodeOptions
    ): CloseableImage? {
      return try {
        val xmlResourceName = encodedImage.source ?: error("No source in encoded image")
        // Use insecure URI parser since we do not care about the validity of the URI
        val xmlResource = Uri.parse(xmlResourceName)
        // Only support binary XML files from resources, not assets or raw files
        val xmlResourceId = parseImageSourceResourceId(xmlResource)
        // Use application context to avoid leaking the activity
        val drawable = context.applicationContext.resources.getDrawable(xmlResourceId, null)
        CloseableXmlImage(xmlResourceName, drawable)
      } catch (error: Throwable) {
        FLog.e(TAG, "Cannot decode xml ${error}", error)
        null
      }
    }

    /**
     * This parsing implementation is only designed to work with URI's that have been generated by
     * the ResourceDrawableIdHelper that ImageSource uses. It will ignore package names and schemes
     * in its quest to extract a basic integer resource ID.
     *
     * ResourceDrawableIdHelper generates URIs in the format of res:/[resourceId]
     *
     * @throws IllegalStateException if the resource ID cannot be parsed from the provided uri
     */
    private fun parseImageSourceResourceId(xmlResource: Uri): Int {
      return xmlResource.pathSegments.lastOrNull()?.toIntOrNull() ?: error("Invalid resource id")
    }
  }

  private class XmlDrawableFactory : DrawableFactory {
    override fun supportsImageType(image: CloseableImage): Boolean {
      return image is CloseableXmlImage
    }

    override fun createDrawable(image: CloseableImage): Drawable? {
      return (image as CloseableXmlImage).drawable
    }
  }
}
