package at.hannibal2.skyhanni.utils

import at.hannibal2.skyhanni.SkyHanniMod
import at.hannibal2.skyhanni.events.LorenzTickEvent
import at.hannibal2.skyhanni.events.RenderEntityOutlineEvent
import at.hannibal2.skyhanni.mixins.transformers.CustomRenderGlobal
import at.hannibal2.skyhanni.test.command.CopyErrorCommand
import net.minecraft.client.Minecraft
import net.minecraft.client.renderer.GlStateManager
import net.minecraft.client.renderer.OpenGlHelper
import net.minecraft.client.renderer.RenderHelper
import net.minecraft.client.renderer.culling.ICamera
import net.minecraft.client.shader.Framebuffer
import net.minecraft.entity.Entity
import net.minecraft.entity.EntityLivingBase
import net.minecraft.util.BlockPos
import net.minecraftforge.client.MinecraftForgeClient
import net.minecraftforge.fml.common.eventhandler.EventPriority
import net.minecraftforge.fml.common.eventhandler.SubscribeEvent
import org.lwjgl.opengl.GL11
import org.lwjgl.opengl.GL13
import org.lwjgl.opengl.GL30
import java.lang.reflect.InvocationTargetException
import java.lang.reflect.Method

/**
 * Class to handle all entity outlining, including xray and no-xray rendering
 * Features that include entity outlining should subscribe to the {@link RenderEntityOutlineEvent}.
 *
 * Credit to SkyblockAddons and Biscuit Development
 * https://github.com/BiscuitDevelopment/SkyblockAddons/blob/main/src/main/java/codes/biscuit/skyblockaddons/features/EntityOutlines/EntityOutlineRenderer.java
 *
 */
object EntityOutlineRenderer {
    private val entityRenderCache: CachedInfo = CachedInfo(null, null, null)
    private var stopLookingForOptifine = false
    private var isMissingMixin = false
    private var isFastRender: Method? = null
    private var isShaders: Method? = null
    private var isAntialiasing: Method? = null
    private var emptyLastTick = false
    private val swapBuffer by lazy { initSwapBuffer() }
    private val logger = LorenzLogger("entity_outline_renderer")
    private val mc get() = Minecraft.getMinecraft()
    private val BUF_FLOAT_4: java.nio.FloatBuffer = org.lwjgl.BufferUtils.createFloatBuffer(4)

    private val CustomRenderGlobal.frameBuffer get() = entityOutlineFramebuffer_skyhanni
    private val CustomRenderGlobal.shader get() = entityOutlineShader_skyhanni

    /**
     * @return a new framebuffer with the size of the main framebuffer
     */
    private fun initSwapBuffer(): Framebuffer {
        val main = mc.framebuffer
        val framebuffer = Framebuffer(main.framebufferTextureWidth, main.framebufferTextureHeight, true)
        framebuffer.setFramebufferFilter(GL11.GL_NEAREST)
        framebuffer.setFramebufferColor(0.0f, 0.0f, 0.0f, 0.0f)
        return framebuffer
    }

    private fun updateFramebufferSize() {
        val width = mc.displayWidth
        val height = mc.displayHeight
        if (swapBuffer.framebufferWidth != width || swapBuffer.framebufferHeight != height) {
            swapBuffer.createBindFramebuffer(width, height)
        }
        val renderGlobal = mc.renderGlobal as CustomRenderGlobal
        val outlineBuffer = renderGlobal.frameBuffer
        if (outlineBuffer.framebufferWidth != width || outlineBuffer.framebufferHeight != height) {
            outlineBuffer.createBindFramebuffer(width, height)
            renderGlobal.shader.createBindFramebuffers(width, height)
        }
    }

    /**
     * Renders xray and no-xray entity outlines.
     *
     * @param camera       the current camera
     * @param partialTicks the progress to the next tick
     * @param x            the camera x position
     * @param y            the camera y position
     * @param z            the camera z position
     */
    @JvmStatic
    fun renderEntityOutlines(camera: ICamera, partialTicks: Float, vector: LorenzVec): Boolean {
        val shouldRenderOutlines = shouldRenderEntityOutlines()

        if (!(shouldRenderOutlines && !isCacheEmpty() && MinecraftForgeClient.getRenderPass() == 0)) {
            return !shouldRenderOutlines
        }

        val renderGlobal = mc.renderGlobal as CustomRenderGlobal
        val renderManager = mc.renderManager
        mc.theWorld.theProfiler.endStartSection("entityOutlines")
        updateFramebufferSize()

        // Clear and bind the outline framebuffer
        renderGlobal.frameBuffer.framebufferClear()
        renderGlobal.frameBuffer.bindFramebuffer(false)

        // Vanilla options
        RenderHelper.disableStandardItemLighting()
        GlStateManager.disableFog()
        mc.renderManager.setRenderOutlines(true)

        // Enable outline mode
        GL11.glTexEnvi(GL11.GL_TEXTURE_ENV, GL11.GL_TEXTURE_ENV_MODE, GL13.GL_COMBINE);
        GL11.glTexEnvi(GL11.GL_TEXTURE_ENV, GL13.GL_COMBINE_RGB, GL11.GL_REPLACE);
        GL11.glTexEnvi(GL11.GL_TEXTURE_ENV, GL13.GL_SOURCE0_RGB, GL13.GL_CONSTANT);
        GL11.glTexEnvi(GL11.GL_TEXTURE_ENV, GL13.GL_OPERAND0_RGB, GL11.GL_SRC_COLOR);
        GL11.glTexEnvi(GL11.GL_TEXTURE_ENV, GL13.GL_COMBINE_ALPHA, GL11.GL_REPLACE);
        GL11.glTexEnvi(GL11.GL_TEXTURE_ENV, GL13.GL_SOURCE0_ALPHA, GL11.GL_TEXTURE);
        GL11.glTexEnvi(GL11.GL_TEXTURE_ENV, GL13.GL_OPERAND0_ALPHA, GL11.GL_SRC_ALPHA);

        // Render x-ray outlines first, ignoring the depth buffer bit
        if (!isXrayCacheEmpty()) {
            // Xray is enabled by disabling depth testing
            GlStateManager.depthFunc(GL11.GL_ALWAYS)

            entityRenderCache.xrayCache?.forEach { (key, value) ->
                // Test if the entity should render, given the player's camera position
                if (!shouldRender(camera, key, vector)) return@forEach

                try {
                    if (key !is EntityLivingBase) outlineColor(value)
                    renderManager.renderEntityStatic(key, partialTicks, true)
                } catch (ignored: Exception) {
                }
            }

            // Reset depth function
            GlStateManager.depthFunc(GL11.GL_LEQUAL)
        }

        // Render no-xray outlines second, taking into consideration the depth bit
        if (!isNoXrayCacheEmpty()) {
            if (!isNoOutlineCacheEmpty()) {
                // Render other entities + terrain that may occlude an entity outline into a depth buffer
                swapBuffer.framebufferClear()
                copyBuffers(mc.framebuffer, swapBuffer, GL11.GL_DEPTH_BUFFER_BIT)
                swapBuffer.bindFramebuffer(false)

                // Copy terrain + other entities depth into outline frame buffer to now switch to no-xray outlines
                entityRenderCache.noOutlineCache?.forEach { entity ->
                    // Test if the entity should render, given the player's instantaneous camera position
                    if (!shouldRender(camera, entity, vector)) return@forEach

                    try {
                        renderManager.renderEntityStatic(entity, partialTicks, true)
                    } catch (ignored: Exception) {
                    }
                }

                // Copy the entire depth buffer of everything that might occlude outline to outline framebuffer
                copyBuffers(swapBuffer, renderGlobal.frameBuffer, GL11.GL_DEPTH_BUFFER_BIT)
                renderGlobal.frameBuffer.bindFramebuffer(false)
            } else {
                copyBuffers(mc.framebuffer, renderGlobal.frameBuffer, GL11.GL_DEPTH_BUFFER_BIT)
            }

            // Xray disabled by re-enabling traditional depth testing
            entityRenderCache.noXrayCache?.forEach { (key, value) ->
                // Test if the entity should render, given the player's instantaneous camera position
                if (!shouldRender(camera, key, vector)) return@forEach

                try {
                    if (key !is EntityLivingBase) outlineColor(value)
                    renderManager.renderEntityStatic(key, partialTicks, true)
                } catch (ignored: Exception) {
                }
            }
        }

        // Disable outline mode
        with(GL11.GL_TEXTURE_ENV) {
            GL11.glTexEnvi(this, GL11.GL_TEXTURE_ENV_MODE, GL11.GL_MODULATE)
            GL11.glTexEnvi(this, GL13.GL_COMBINE_RGB, GL11.GL_MODULATE)
            GL11.glTexEnvi(this, GL13.GL_SOURCE0_RGB, GL11.GL_TEXTURE)
            GL11.glTexEnvi(this, GL13.GL_OPERAND0_RGB, GL11.GL_SRC_COLOR)
            GL11.glTexEnvi(this, GL13.GL_COMBINE_ALPHA, GL11.GL_MODULATE)
            GL11.glTexEnvi(this, GL13.GL_SOURCE0_ALPHA, GL11.GL_TEXTURE)
            GL11.glTexEnvi(this, GL13.GL_OPERAND0_ALPHA, GL11.GL_SRC_ALPHA)
        }

        // Vanilla options
        RenderHelper.enableStandardItemLighting()
        mc.renderManager.setRenderOutlines(false)

        // Load the outline shader
        GlStateManager.depthMask(false)
        renderGlobal.shader.loadShaderGroup(partialTicks)
        GlStateManager.depthMask(true)

        // Reset GL/framebuffers for next render layers
        GlStateManager.enableLighting()
        mc.framebuffer.bindFramebuffer(false)
        GlStateManager.enableFog()
        GlStateManager.enableBlend()
        GlStateManager.enableColorMaterial()
        GlStateManager.enableDepth()
        GlStateManager.enableAlpha()

        return !shouldRenderOutlines
    }

    @JvmStatic
    fun getCustomOutlineColor(entity: Entity?): Int? {
        if (entityRenderCache.xrayCache?.containsKey(entity) == true) {
            return entityRenderCache.xrayCache!![entity]
        }
        return if (entityRenderCache.noXrayCache?.containsKey(entity) == true) {
            entityRenderCache.noXrayCache!![entity]
        } else null
    }

    /**
     * Caches optifine settings and determines whether outlines should be rendered
     *
     * @return `true` iff outlines should be rendered
     */
    @JvmStatic
    fun shouldRenderEntityOutlines(): Boolean {
        // SkyBlock Conditions
        if (!LorenzUtils.inSkyBlock) {
            return false
        }

        // Main toggle for outlines features
        if (!isEnabled()) {
            return false
        }

        // Vanilla Conditions
        val renderGlobal = mc.renderGlobal as CustomRenderGlobal
        if (renderGlobal.frameBuffer == null || renderGlobal.shader == null || mc.thePlayer == null) return false

        // Optifine Conditions
        if (!stopLookingForOptifine && isFastRender == null) {
            try {
                val config = Class.forName("Config")
                try {
                    isFastRender = config.getMethod("isFastRender")
                    isShaders = config.getMethod("isShaders")
                    isAntialiasing = config.getMethod("isAntialiasing")
                } catch (ex: Exception) {
                    logger.log("Couldn't find Optifine methods for entity outlines.")
                    stopLookingForOptifine = true
                }
            } catch (ex: Exception) {
                logger.log("Couldn't find Optifine for entity outlines.")
                stopLookingForOptifine = true
            }
        }
        var isFastRenderValue = false
        var isShadersValue = false
        var isAntialiasingValue = false
        if (isFastRender != null) {
            try {
                isFastRenderValue = isFastRender!!.invoke(null) as Boolean
                isShadersValue = isShaders!!.invoke(null) as Boolean
                isAntialiasingValue = isAntialiasing!!.invoke(null) as Boolean
            } catch (ex: IllegalAccessException) {
                logger.log("An error occurred while calling Optifine methods for entity outlines... $ex")
            } catch (ex: InvocationTargetException) {
                logger.log("An error occurred while calling Optifine methods for entity outlines... $ex")
            }
        }
        return !isFastRenderValue && !isShadersValue && !isAntialiasingValue
    }

    // Add new features that need the entity outline logic here
    private fun isEnabled(): Boolean {
        if (isMissingMixin) return false
        if (SkyHanniMod.feature.fishing.rareSeaCreatureHighlight) return true
        if (SkyHanniMod.feature.misc.glowingDroppedItems.enabled) return true
        if (SkyHanniMod.feature.dungeon.highlightTeammates) return true

        return false
    }

    /**
     * Apply the same rendering standards as in [net.minecraft.client.renderer.RenderGlobal.renderEntities] lines 659 to 669
     *
     * @param camera the current camera
     * @param entity the entity to render
     * @param x      the camera x position
     * @param y      the camera y position
     * @param z      the camera z position
     * @return whether the entity should be rendered
     */
    private fun shouldRender(camera: ICamera, entity: Entity, vector: LorenzVec): Boolean =
        // Only render the view entity when sleeping or in 3rd person mode
        if (entity === mc.renderViewEntity &&
            !(mc.renderViewEntity is EntityLivingBase && (mc.renderViewEntity as EntityLivingBase).isPlayerSleeping ||
                    mc.gameSettings.thirdPersonView != 0)
        ) {
            false
        } else mc.theWorld.isBlockLoaded(BlockPos(entity)) && (mc.renderManager.shouldRender(
            entity,
            camera,
            vector.x,
            vector.y,
            vector.z
        ) || entity.riddenByEntity === mc.thePlayer)
    // Only render if renderManager would render and the world is loaded at the entity

    private fun outlineColor(color: Int) {
        BUF_FLOAT_4.put(0, (color shr 16 and 255).toFloat() / 255.0f)
        BUF_FLOAT_4.put(1, (color shr 8 and 255).toFloat() / 255.0f)
        BUF_FLOAT_4.put(2, (color and 255).toFloat() / 255.0f)
        BUF_FLOAT_4.put(3, 1f)
        GL11.glTexEnv(GL11.GL_TEXTURE_ENV, GL11.GL_TEXTURE_ENV_COLOR, BUF_FLOAT_4)
    }

    /**
     * Function that copies a portion of a framebuffer to another framebuffer.
     *
     *
     * Note that this requires GL3.0 to function properly
     *
     *
     * The major use of this function is to copy the depth-buffer portion of the world framebuffer to the entity outline framebuffer.
     * This enables us to perform no-xray outlining on entities, as we can use the world framebuffer's depth testing on the outline frame buffer
     *
     * @param frameToCopy   the framebuffer from which we are copying data
     * @param frameToPaste  the framebuffer onto which we are copying the data
     * @param buffersToCopy the bit mask indicating the sections to copy (see [GL11.GL_DEPTH_BUFFER_BIT], [GL11.GL_COLOR_BUFFER_BIT], [GL11.GL_STENCIL_BUFFER_BIT])
     */
    private fun copyBuffers(frameToCopy: Framebuffer?, frameToPaste: Framebuffer?, buffersToCopy: Int) {
        if (OpenGlHelper.isFramebufferEnabled()) {
            OpenGlHelper.glBindFramebuffer(GL30.GL_READ_FRAMEBUFFER, frameToCopy!!.framebufferObject)
            OpenGlHelper.glBindFramebuffer(GL30.GL_DRAW_FRAMEBUFFER, frameToPaste!!.framebufferObject)
            GL30.glBlitFramebuffer(
                0, 0, frameToCopy.framebufferWidth, frameToCopy.framebufferHeight,
                0, 0, frameToPaste.framebufferWidth, frameToPaste.framebufferHeight,
                buffersToCopy, GL11.GL_NEAREST
            )
        }
    }

    fun isCacheEmpty() = isXrayCacheEmpty() && isNoXrayCacheEmpty()

    private fun isXrayCacheEmpty() = entityRenderCache.xrayCache?.isEmpty() ?: true
    private fun isNoXrayCacheEmpty() = entityRenderCache.noXrayCache?.isEmpty() ?: true
    private fun isNoOutlineCacheEmpty() = entityRenderCache.noOutlineCache?.isEmpty() ?: true

    /**
     * Updates the cache at the start of every minecraft tick to improve efficiency.
     * Identifies and caches all entities in the world that should be outlined.
     *
     *
     * Calls to [.shouldRender] are frustum based, rely on partialTicks,
     * and so can't be updated on a per-tick basis without losing information.
     *
     *
     * This works since entities are only updated once per tick, so the inclusion or exclusion of an entity
     * to be outlined can be cached each tick with no loss of data
     *
     * @param event the client tick event
     */
    @SubscribeEvent
    fun onTick(event: LorenzTickEvent) {
        if (!(event.phase == EventPriority.NORMAL && isEnabled())) return;

        val renderGlobal = try {
            mc.renderGlobal as CustomRenderGlobal
        } catch (e: NoClassDefFoundError) {
            CopyErrorCommand.logError(e, "Unable to enable entity outlines, the required mixin is not loaded")
            isMissingMixin = true
            return
        }

        if (mc.theWorld != null && shouldRenderEntityOutlines()) {
            // These events need to be called in this specific order for the xray to have priority over the no xray
            // Get all entities to render xray outlines
            val xrayOutlineEvent = RenderEntityOutlineEvent(RenderEntityOutlineEvent.Type.XRAY, null)
            xrayOutlineEvent.postAndCatch()
            // Get all entities to render no xray outlines, using pre-filtered entities (no need to test xray outlined entities)
            val noxrayOutlineEvent = RenderEntityOutlineEvent(
                RenderEntityOutlineEvent.Type.NO_XRAY,
                xrayOutlineEvent.entitiesToChooseFrom
            )
            noxrayOutlineEvent.postAndCatch()
            // Cache the entities for future use
            entityRenderCache.xrayCache = xrayOutlineEvent.entitiesToOutline
            entityRenderCache.noXrayCache = noxrayOutlineEvent.entitiesToOutline
            entityRenderCache.noOutlineCache = noxrayOutlineEvent.entitiesToChooseFrom
            emptyLastTick = if (isCacheEmpty()) {
                if (!emptyLastTick) {
                    renderGlobal.frameBuffer.framebufferClear()
                }
                true
            } else false
        } else if (!emptyLastTick) {
            entityRenderCache.xrayCache = null
            entityRenderCache.noXrayCache = null
            entityRenderCache.noOutlineCache = null
            if (renderGlobal.frameBuffer != null) renderGlobal.frameBuffer.framebufferClear()
            emptyLastTick = true
        }
    }

    private class CachedInfo(
        var xrayCache: HashMap<Entity, Int>?,
        var noXrayCache: HashMap<Entity, Int>?,
        var noOutlineCache: HashSet<Entity>?
    )
}