class RenderingPipeline(DebugObject):
    def __init__(self, showbase):
        DebugObject.__init__(self, "RenderingPipeline")
        self.showbase = showbase
        self.lightManager = LightManager()
        self.size = self._getSize()
        self.precomputeSize = Vec2(0)
        self.camera = base.cam
        self.cullBounds = None
        self.patchSize = Vec2(32, 32)

        self.temporalProjXOffs = 0
        self.temporalProjFactor = 2

        self.forwardScene = NodePath("Forward Rendering")
        self.lastMVP = None

        self._setup()

    def _setup(self):
        self.debug("Setting up render pipeline")
        # First, we need no transparency
        render.setAttrib(TransparencyAttrib.make(TransparencyAttrib.MNone),
                         100)

        # Now create deferred render buffers
        self._makeDeferredTargets()

        # Setup compute shader for lighting
        self._createLightingPipeline()

        # Setup combiner
        self._createCombiner()

        self.deferredTarget.setShader(
            BetterShader.load("Shader/DefaultPostProcess.vertex",
                              "Shader/TextureDisplay.fragment"))
        self._setupAntialiasing()

        self._createFinalPass()

        self.antialias.getFirstBuffer().setShaderInput(
            "lastFrame", self.lightingComputeCombinedTex)
        self.antialias.getFirstBuffer().setShaderInput("lastPosition",
                                                       self.lastPositionBuffer)
        self.antialias.getFirstBuffer().setShaderInput(
            "currentPosition", self.deferredTarget.getColorTexture())

        # self.deferredTarget.setShaderInput("sampler", self.lightingComputeCombinedTex)
        # self.deferredTarget.setShaderInput("sampler", self.antialias.getResultTexture())
        self.deferredTarget.setShaderInput("sampler",
                                           self.finalPass.getColorTexture())
        # self.deferredTarget.setShaderInput("sampler", self.combiner.getColorTexture())

        # self.deferredTarget.setShaderInput("sampler", self.lightingComputeCombinedTex)
        # self.deferredTarget.setShaderInput("sampler", self.antialias._neighborBuffer.getColorTexture())
        # self.deferredTarget.setShaderInput("sampler", self.antialias._blendBuffer.getColorTexture())
        # self.deferredTarget.setShaderInput("sampler", self.lightingComputeCombinedTex)

        # add update task
        self._attachUpdateTask()

        # compute first mvp
        self._computeMVP()
        self.lastLastMVP = self.lastMVP

        # DirectFrame(frameColor=(1, 1, 1, 0.2), frameSize=(-0.28, 0.28, -0.27, 0.4), pos=(base.getAspectRatio() - 0.35, 0.0, 0.49))
        self.atlasDisplayImage = OnscreenImage(
            image=self.lightManager.getAtlasTex(),
            pos=(base.getAspectRatio() - 0.35, 0, 0.5),
            scale=(0.25, 0, 0.25))
        self.lastPosImage = OnscreenImage(
            image=self.lightingComputeCombinedTex,
            pos=(base.getAspectRatio() - 0.35, 0, -0.05),
            scale=(0.25, 0, 0.25))
        # self.atlasDisplayImage =  OnscreenImage(image = self.lightManager.getAtlasTex(), pos = (0,0,0), scale=(0.8,1,0.8))
        # self.atlasDisplayImage =  OnscreenImage(image = self.lightPerTileStorage, pos = (base.getAspectRatio() - 0.35, 0, 0.5), scale=(0.25,0,0.25))

    def _createCombiner(self):
        self.combiner = RenderTarget("Combine-Temporal")
        self.combiner.setColorBits(8)
        self.combiner.addRenderTexture(RenderTargetType.Color)
        self.combiner.prepareOffscreenBuffer()
        self.combiner.setShaderInput(
            "currentComputation",
            self.lightingComputeContainer.getColorTexture())
        self.combiner.setShaderInput("lastFrame",
                                     self.lightingComputeCombinedTex)
        self.combiner.setShaderInput("positionBuffer",
                                     self.deferredTarget.getColorTexture())
        self.combiner.setShaderInput("velocityBuffer",
                                     self.deferredTarget.getAuxTexture(1))
        self.combiner.setShaderInput("lastPosition", self.lastPositionBuffer)

        self._setCombinerShader()

    def _setupAntialiasing(self):
        self.debug("Creating antialiasing handler ..")
        self.antialias = Antialiasing()

        # self.antialias.setColorTexture(self.lightingComputeContainer.getColorTexture())
        self.antialias.setColorTexture(self.combiner.getColorTexture())
        self.antialias.setDepthTexture(self.deferredTarget.getDepthTexture())
        self.antialias.setup()

    # Creates all the render targets
    def _makeDeferredTargets(self):
        self.debug("Creating deferred targets")
        self.deferredTarget = RenderTarget("DeferredTarget")
        self.deferredTarget.addRenderTexture(RenderTargetType.Color)
        self.deferredTarget.addRenderTexture(RenderTargetType.Depth)
        self.deferredTarget.addRenderTexture(RenderTargetType.Aux0)
        self.deferredTarget.addRenderTexture(RenderTargetType.Aux1)
        self.deferredTarget.setAuxBits(16)
        self.deferredTarget.setColorBits(16)
        self.deferredTarget.setDepthBits(32)
        # self.deferredTarget.setSize(400, 240) # check for overdraw
        self.deferredTarget.prepareSceneRender()

    def _createFinalPass(self):
        self.debug("Creating final pass")
        self.finalPass = RenderTarget("FinalPass")
        self.finalPass.addRenderTexture(RenderTargetType.Color)
        self.finalPass.prepareOffscreenBuffer()

        colorTex = self.antialias.getResultTexture()
        # Set wrap for motion blur
        colorTex.setWrapU(Texture.WMMirror)
        colorTex.setWrapV(Texture.WMMirror)
        self.finalPass.setShaderInput("colorTex", colorTex)
        self.finalPass.setShaderInput("velocityTex",
                                      self.deferredTarget.getAuxTexture(1))
        self.finalPass.setShaderInput("depthTex",
                                      self.deferredTarget.getDepthTexture())
        self._setFinalPassShader()

    # Creates the storage to store the list of visible lights per tile
    def _makeLightPerTileStorage(self):

        storageSizeX = int(self.precomputeSize.x * 8)
        storageSizeY = int(self.precomputeSize.y * 8)

        self.debug("Creating per tile storage of size", storageSizeX, "x",
                   storageSizeY)

        self.lightPerTileStorage = Texture("LightsPerTile")
        self.lightPerTileStorage.setup2dTexture(storageSizeX, storageSizeY,
                                                Texture.TUnsignedShort,
                                                Texture.FR32i)
        self.lightPerTileStorage.setMinfilter(Texture.FTNearest)
        self.lightPerTileStorage.setMagfilter(Texture.FTNearest)

    # Inits the lighting pipeline
    def _createLightingPipeline(self):
        self.debug("Creating lighting pipeline ..")

        # size has to be a multiple of the compute unit size
        # but still has to cover the whole screen
        sizeX = int(math.ceil(self.size.x / self.patchSize.x))
        sizeY = int(math.ceil(self.size.y / self.patchSize.y))

        self.precomputeSize = Vec2(sizeX, sizeY)

        self.debug("Batch size =", sizeX, "x", sizeY, "Actual Buffer size=",
                   int(sizeX * self.patchSize.x), "x",
                   int(sizeY * self.patchSize.y))

        self._makeLightPerTileStorage()

        # Create a buffer which computes which light affects which tile
        self._makeLightBoundsComputationBuffer(sizeX, sizeY)

        # Create a buffer which applies the lighting
        self._makeLightingComputeBuffer()

        # Register for light manager
        self.lightManager.setLightingComputator(self.lightingComputeContainer)
        self.lightManager.setLightingCuller(self.lightBoundsComputeBuff)

        self.lightingComputeContainer.setShaderInput("lightsPerTile",
                                                     self.lightPerTileStorage)

        self.lightingComputeContainer.setShaderInput("cameraPosition",
                                                     base.cam.getPos(render))

        # Ensure the images have the correct filter mode
        for bmode in [RenderTargetType.Color]:
            tex = self.lightBoundsComputeBuff.getTexture(bmode)
            tex.setMinfilter(Texture.FTNearest)
            tex.setMagfilter(Texture.FTNearest)

        self._loadFallbackCubemap()

        # Create storage for the bounds computation

        # Set inputs
        self.lightBoundsComputeBuff.setShaderInput("destination",
                                                   self.lightPerTileStorage)
        self.lightBoundsComputeBuff.setShaderInput(
            "depth", self.deferredTarget.getDepthTexture())

        self.lightingComputeContainer.setShaderInput(
            "data0", self.deferredTarget.getColorTexture())
        self.lightingComputeContainer.setShaderInput(
            "data1", self.deferredTarget.getAuxTexture(0))
        self.lightingComputeContainer.setShaderInput(
            "data2", self.deferredTarget.getAuxTexture(1))

        self.lightingComputeContainer.setShaderInput(
            "shadowAtlas", self.lightManager.getAtlasTex())
        self.lightingComputeContainer.setShaderInput(
            "destination", self.lightingComputeCombinedTex)
        # self.lightingComputeContainer.setShaderInput("sampleTex", loader.loadTexture("Data/Antialiasing/Unigine01.png"))

    def _loadFallbackCubemap(self):
        cubemap = loader.loadCubeMap("Cubemap/#.png")
        cubemap.setMinfilter(Texture.FTLinearMipmapLinear)
        cubemap.setMagfilter(Texture.FTLinearMipmapLinear)
        cubemap.setFormat(Texture.F_srgb_alpha)
        self.lightingComputeContainer.setShaderInput("fallbackCubemap",
                                                     cubemap)

    def _makeLightBoundsComputationBuffer(self, w, h):
        self.debug("Creating light precomputation buffer of size", w, "x", h)
        self.lightBoundsComputeBuff = RenderTarget("ComputeLightTileBounds")
        self.lightBoundsComputeBuff.setSize(w, h)
        self.lightBoundsComputeBuff.addRenderTexture(RenderTargetType.Color)
        self.lightBoundsComputeBuff.setColorBits(16)
        self.lightBoundsComputeBuff.prepareOffscreenBuffer()

        self.lightBoundsComputeBuff.setShaderInput("mainCam", base.cam)
        self.lightBoundsComputeBuff.setShaderInput("mainRender", base.render)

        self._setPositionComputationShader()

    def _makeLightingComputeBuffer(self):
        self.lightingComputeContainer = RenderTarget("ComputeLighting")
        self.lightingComputeContainer.setSize(
            base.win.getXSize() / self.temporalProjFactor, base.win.getYSize())
        self.lightingComputeContainer.addRenderTexture(RenderTargetType.Color)
        self.lightingComputeContainer.setColorBits(16)
        self.lightingComputeContainer.prepareOffscreenBuffer()

        self.lightingComputeCombinedTex = Texture("Lighting-Compute-Combined")
        self.lightingComputeCombinedTex.setup2dTexture(base.win.getXSize(),
                                                       base.win.getYSize(),
                                                       Texture.TFloat,
                                                       Texture.FRgba16)
        self.lightingComputeCombinedTex.setMinfilter(Texture.FTLinear)
        self.lightingComputeCombinedTex.setMagfilter(Texture.FTLinear)

        self.lastPositionBuffer = Texture("Last-Position-Buffer")
        self.lastPositionBuffer.setup2dTexture(base.win.getXSize(),
                                               base.win.getYSize(),
                                               Texture.TFloat, Texture.FRgba16)
        self.lastPositionBuffer.setMinfilter(Texture.FTNearest)
        self.lastPositionBuffer.setMagfilter(Texture.FTNearest)

    def _setLightingShader(self):

        lightShader = BetterShader.load("Shader/DefaultPostProcess.vertex",
                                        "Shader/ApplyLighting.fragment")
        self.lightingComputeContainer.setShader(lightShader)

    def _setCombinerShader(self):
        cShader = BetterShader.load("Shader/DefaultPostProcess.vertex",
                                    "Shader/Combiner.fragment")
        self.combiner.setShader(cShader)

    def _setPositionComputationShader(self):
        pcShader = BetterShader.load("Shader/DefaultPostProcess.vertex",
                                     "Shader/PrecomputeLights.fragment")
        self.lightBoundsComputeBuff.setShader(pcShader)

    def _setFinalPassShader(self):
        fShader = BetterShader.load("Shader/DefaultPostProcess.vertex",
                                    "Shader/Final.fragment")
        self.finalPass.setShader(fShader)

    def _getSize(self):
        return Vec2(int(self.showbase.win.getXSize()),
                    int(self.showbase.win.getYSize()))

    def debugReloadShader(self):
        self.lightManager.debugReloadShader()
        self._setPositionComputationShader()
        self._setCombinerShader()
        self._setLightingShader()
        self._setFinalPassShader()
        self.antialias.reloadShader()

    def _attachUpdateTask(self):
        self.showbase.addTask(self._update,
                              "UpdateRenderingPipeline",
                              sort=-10000)

    def _computeCameraBounds(self):
        # compute camera bounds in render space
        cameraBounds = self.camera.node().getLens().makeBounds()
        cameraBounds.xform(self.camera.getMat(render))
        return cameraBounds

    def _update(self, task=None):

        self.temporalProjXOffs += 1
        self.temporalProjXOffs = self.temporalProjXOffs % self.temporalProjFactor

        self.cullBounds = self._computeCameraBounds()

        self.lightManager.setCullBounds(self.cullBounds)
        self.lightManager.update()

        self.lightingComputeContainer.setShaderInput("cameraPosition",
                                                     base.cam.getPos(render))
        self.lightingComputeContainer.setShaderInput(
            "temporalProjXOffs", LVecBase2i(self.temporalProjXOffs))
        self.combiner.setShaderInput("lastMVP", self.lastMVP)
        render.setShaderInput("lastMVP", self.lastMVP)
        self.combiner.setShaderInput("temporalProjXOffs",
                                     LVecBase2i(self.temporalProjXOffs))
        self._computeMVP()
        self.combiner.setShaderInput("currentMVP", self.lastMVP)

        self.combiner.setShaderInput("cameraPosition", base.cam.getPos(render))

        if task is not None:
            return task.cont

    def _computeMVP(self):
        projMat = Mat4.convertMat(CSYupRight, base.camLens.getCoordinateSystem(
        )) * base.camLens.getProjectionMat()
        transformMat = TransformState.makeMat(
            Mat4.convertMat(base.win.getGsg().getInternalCoordinateSystem(),
                            CSZupRight))
        modelViewMat = transformMat.invertCompose(render.getTransform(
            base.cam)).getMat()
        self.lastMVP = modelViewMat * projMat
        # print "Self.lastMVP is now from frame",globalClock.getFrameTime()

    def getLightManager(self):
        return self.lightManager

    def getDefaultObjectShader(self):
        shader = BetterShader.load("Shader/DefaultObjectShader.vertex",
                                   "Shader/DefaultObjectShader.fragment")
        return shader