/*
 * Copyright (c) 2019, 2025, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

#include "EncoderManager.h"
#include "MTLContext.h"
#include "sun_java2d_SunGraphics2D.h"
#import "common.h"

// NOTE: uncomment to disable comparing cached encoder states with requested (for debugging)
// #define ALWAYS_UPDATE_ENCODER_STATES

const SurfaceRasterFlags defaultRasterFlags = { JNI_FALSE, JNI_TRUE };

// Internal utility class that represents the set of 'mutable' encoder properties
@interface EncoderStates : NSObject
@property (readonly) MTLClip * clip;

- (id)init;
- (void)dealloc;

- (void)reset:(id<MTLTexture>)destination
         isAA:(jboolean)isAA
       isText:(jboolean)isText
        isLCD:(jboolean)isLCD;

- (void)updateEncoder:(id<MTLRenderCommandEncoder>)encoder
              context:(MTLContext *)mtlc
        renderOptions:(const RenderOptions *)renderOptions
          forceUpdate:(jboolean)forceUpdate;
@property (assign) jboolean aa;
@property (assign) jboolean text;
@property (assign) jboolean lcd;
@property (assign) jboolean aaShader;
@property (retain) MTLPaint* paint;
@end

@implementation EncoderStates {
    MTLPipelineStatesStorage * _pipelineStateStorage;
    id<MTLDevice> _device;

    // Persistent encoder properties
    id<MTLTexture> _destination;

    jboolean _isAA;
    jboolean _isText;
    jboolean _isLCD;
    jboolean _isAAShader;

    //
    // Cached 'mutable' states of encoder
    //

    // Composite rule and source raster flags (it affects the CAD-multipliers (of pipelineState))
    MTLComposite * _composite;
    SurfaceRasterFlags _srcFlags;

    // Paint mode (it affects shaders (of pipelineState) and corresponding buffers)
    MTLPaint * _paint;

    // If true, indicates that encoder is used for texture drawing (user must do [encoder setFragmentTexture:] before drawing)
    jboolean _isTexture;
    int _interpolationMode;

    // Clip rect or stencil
    MTLClip * _clip;

    // Transform (affects transformation inside vertex shader)
    MTLTransform * _transform;
}
@synthesize aa = _isAA;
@synthesize text = _isText;
@synthesize lcd = _isLCD;
@synthesize aaShader = _isAAShader;
@synthesize paint = _paint;

- (id)init {
    self = [super init];
    if (self) {
        _destination = nil;
        _composite = [[MTLComposite alloc] init];
        _paint = [[MTLPaint alloc] init];
        _transform = [[MTLTransform alloc] init];
        _clip = [[MTLClip alloc] init];
    }
    return self;
}

- (void)dealloc {
    [_composite release];
    [_paint release];
    [_transform release];
    [super dealloc];
}

- (void)setContext:(MTLContext * _Nonnull)mtlc {
    self->_pipelineStateStorage = mtlc.pipelineStateStorage;
    self->_device = mtlc.device;
}

- (void)reset:(id<MTLTexture>)destination
         isAA:(jboolean)isAA
       isText:(jboolean)isText
        isLCD:(jboolean)isLCD {
    _destination = destination;
    _isAA = isAA;
    _isText = isText;
    _isLCD = isLCD;
    // NOTE: probably it's better to invalidate/reset all cached states now
}

- (void)updateEncoder:(id<MTLRenderCommandEncoder>)encoder
              context:(MTLContext *)mtlc
        renderOptions:(const RenderOptions *)renderOptions
          forceUpdate:(jboolean)forceUpdate
{
    // 1. Process special case for stencil mask generation
    if (mtlc.clip.stencilMaskGenerationInProgress == JNI_TRUE) {
        // use separate pipeline state for stencil generation
        if (forceUpdate || (_clip.stencilMaskGenerationInProgress != JNI_TRUE)) {
            [_clip copyFrom:mtlc.clip];
            [_clip setMaskGenerationPipelineState:encoder
                                        destWidth:_destination.width
                                       destHeight:_destination.height
                             pipelineStateStorage:_pipelineStateStorage];
        }

        [self updateTransform:encoder transform:mtlc.transform forceUpdate:forceUpdate];
        return;
    }

    // 2. Otherwise update all 'mutable' properties of encoder
    [self updatePipelineState:encoder
                      context:mtlc
                renderOptions:renderOptions
                  forceUpdate:forceUpdate];
    [self updateTransform:encoder transform:mtlc.transform forceUpdate:forceUpdate];
    [self updateClip:encoder clip:mtlc.clip forceUpdate:forceUpdate];
}

//
// Internal methods that update states when necessary (compare with cached states)
//

// Updates pipelineState (and corresponding buffers) with use of paint+composite+flags
- (void)updatePipelineState:(id<MTLRenderCommandEncoder>)encoder
                    context:(MTLContext *)mtlc
              renderOptions:(const RenderOptions *)renderOptions
                forceUpdate:(jboolean)forceUpdate
{
    if (!forceUpdate
        && [_paint isEqual:mtlc.paint]
        && [_composite isEqual:mtlc.composite]
        && (_isTexture == renderOptions->isTexture && (!renderOptions->isTexture || _interpolationMode == renderOptions->interpolation)) // interpolation is used only in texture mode
        && _isAA == renderOptions->isAA
        && _isAAShader == renderOptions->isAAShader
        && _isText == renderOptions->isText
        && _isLCD == renderOptions->isLCD
        && _srcFlags.isOpaque == renderOptions->srcFlags.isOpaque && _srcFlags.isPremultiplied == renderOptions->srcFlags.isPremultiplied)
        return;

    self.paint = mtlc.paint;
    [_composite copyFrom:mtlc.composite];
    _isTexture = renderOptions->isTexture;
    _interpolationMode = renderOptions->interpolation;
    _isAA = renderOptions->isAA;
    _isAAShader = renderOptions->isAAShader;
    _isText = renderOptions->isText;
    _isLCD = renderOptions->isLCD;
    _srcFlags = renderOptions->srcFlags;

    if ((jint)[mtlc.composite getCompositeState] == sun_java2d_SunGraphics2D_COMP_XOR) {

        [mtlc.paint setXorModePipelineState:encoder
                               context:mtlc
                         renderOptions:renderOptions
                  pipelineStateStorage:_pipelineStateStorage];
    } else {
        [mtlc.paint  setPipelineState:encoder
                              context:mtlc
                        renderOptions:renderOptions
                 pipelineStateStorage:_pipelineStateStorage];
    }
}

- (void) updateClip:(id<MTLRenderCommandEncoder>)encoder clip:(MTLClip *)clip forceUpdate:(jboolean)forceUpdate
{
    if (clip.stencilMaskGenerationInProgress == JNI_TRUE) {
        // don't set setScissorOrStencil when generation in progress
        return;
    }

    if (!forceUpdate && [_clip isEqual:clip])
        return;

    [_clip copyFrom:clip];
    [_clip setScissorOrStencil:encoder
                     destWidth:_destination.width
                    destHeight:_destination.height
                        device:_device];
}

- (void)updateTransform:(id <MTLRenderCommandEncoder>)encoder
              transform:(MTLTransform *)transform
            forceUpdate:(jboolean)forceUpdate
{
    if (!forceUpdate
        && [_transform isEqual:transform])
        return;

    [_transform copyFrom:transform];
    [_transform setVertexMatrix:encoder
                        destWidth:_destination.width
                       destHeight:_destination.height];
}

@end

@implementation EncoderManager {
    MTLContext * _mtlc; // used to obtain CommandBufferWrapper and Composite/Paint/Transform

    id<MTLRenderCommandEncoder> _encoder;

    // 'Persistent' properties of encoder
    id<MTLTexture> _destination;
    id<MTLTexture> _aaDestination;
    BOOL _useStencil;

    // 'Mutable' states of encoder
    EncoderStates * _encoderStates;
}

- (id _Nonnull)init {
    self = [super init];
    if (self) {
        _encoder = nil;
        _destination = nil;
        _aaDestination = nil;
        _useStencil = NO;
        _encoderStates = [[EncoderStates alloc] init];

    }
    return self;
}

- (void)dealloc {
    [_encoderStates release];
    [super dealloc];
}

- (void)setContext:(MTLContex * _Nonnull)mtlc {
    self->_mtlc = mtlc;
    [self->_encoderStates setContext:mtlc];
}

- (id<MTLRenderCommandEncoder> _Nonnull) getRenderEncoder:(const BMTLSDOps * _Nonnull)dstOps
{
    return [self getRenderEncoder:dstOps->pTexture isDstOpaque:dstOps->isOpaque];
}

- (id<MTLRenderCommandEncoder> _Nonnull)getAARenderEncoder:(const BMTLSDOps * _Nonnull)dstOps {
  id<MTLTexture> dstTxt = dstOps->pTexture;
  RenderOptions roptions = {JNI_FALSE, JNI_TRUE, INTERPOLATION_NEAREST_NEIGHBOR, defaultRasterFlags, JNI_FALSE, JNI_FALSE, JNI_FALSE};
  return [self getEncoder:dstTxt renderOptions:&roptions];
}

- (id<MTLRenderCommandEncoder> _Nonnull)getAAShaderRenderEncoder:(const BMTLSDOps * _Nonnull)dstOps
{
    RenderOptions roptions = {JNI_FALSE, JNI_FALSE, INTERPOLATION_NEAREST_NEIGHBOR, defaultRasterFlags, JNI_FALSE, JNI_FALSE, JNI_TRUE};
    return [self getEncoder:dstOps->pTexture renderOptions:&roptions];
}

- (id<MTLRenderCommandEncoder> _Nonnull)getRenderEncoder:(id<MTLTexture> _Nonnull)dest
                                             isDstOpaque:(bool)isOpaque
{
    RenderOptions roptions = {JNI_FALSE, JNI_FALSE, INTERPOLATION_NEAREST_NEIGHBOR, defaultRasterFlags, JNI_FALSE, JNI_FALSE, JNI_FALSE};
    return [self getEncoder:dest renderOptions:&roptions];
}

- (id<MTLRenderCommandEncoder> _Nonnull) getTextureEncoder:(const BMTLSDOps * _Nonnull)dstOps
                                      isSrcOpaque:(bool)isSrcOpaque
{
    return [self getTextureEncoder:dstOps->pTexture
                       isSrcOpaque:isSrcOpaque
                       isDstOpaque:dstOps->isOpaque
                     interpolation:INTERPOLATION_NEAREST_NEIGHBOR];
}

- (id<MTLRenderCommandEncoder> _Nonnull) getTextureEncoder:(id<MTLTexture> _Nonnull)dest
                                               isSrcOpaque:(bool)isSrcOpaque
                                               isDstOpaque:(bool)isDstOpaque
{
    return [self getTextureEncoder:dest
                       isSrcOpaque:isSrcOpaque
                       isDstOpaque:isDstOpaque
                     interpolation:INTERPOLATION_NEAREST_NEIGHBOR
                              isAA:JNI_FALSE];
}

- (id<MTLRenderCommandEncoder> _Nonnull) getLCDEncoder:(id<MTLTexture> _Nonnull)dest
                                               isSrcOpaque:(bool)isSrcOpaque
                                               isDstOpaque:(bool)isDstOpaque
{
    RenderOptions roptions = {JNI_TRUE, JNI_FALSE, INTERPOLATION_NEAREST_NEIGHBOR, {isSrcOpaque, JNI_TRUE }, JNI_FALSE, JNI_TRUE, JNI_FALSE};
    return [self getEncoder:dest renderOptions:&roptions];
}

- (id<MTLRenderCommandEncoder> _Nonnull) getTextureEncoder:(id<MTLTexture> _Nonnull)dest
                                      isSrcOpaque:(bool)isSrcOpaque
                                      isDstOpaque:(bool)isDstOpaque
                                    interpolation:(int)interpolation
                                             isAA:(jboolean)isAA
{
    RenderOptions roptions = {JNI_TRUE, isAA, interpolation, { isSrcOpaque, JNI_TRUE }, JNI_FALSE, JNI_FALSE, JNI_FALSE};
    return [self getEncoder:dest renderOptions:&roptions];
}

- (id<MTLRenderCommandEncoder> _Nonnull) getTextureEncoder:(id<MTLTexture> _Nonnull)dest
                                               isSrcOpaque:(bool)isSrcOpaque
                                               isDstOpaque:(bool)isDstOpaque
                                             interpolation:(int)interpolation
{
    return [self getTextureEncoder:dest isSrcOpaque:isSrcOpaque isDstOpaque:isDstOpaque interpolation:interpolation isAA:JNI_FALSE];
}

- (id<MTLRenderCommandEncoder> _Nonnull) getTextEncoder:(const BMTLSDOps * _Nonnull)dstOps
                                      isSrcOpaque:(bool)isSrcOpaque
{
    RenderOptions roptions = {JNI_TRUE, JNI_FALSE, INTERPOLATION_NEAREST_NEIGHBOR, { isSrcOpaque, JNI_TRUE },
                              JNI_TRUE, JNI_FALSE, JNI_FALSE};
    return [self getEncoder:dstOps->pTexture renderOptions:&roptions];
}

- (id<MTLRenderCommandEncoder> _Nonnull) getEncoder:(id <MTLTexture> _Nonnull)dest
                                      renderOptions:(const RenderOptions * _Nonnull)renderOptions
{
  //
  // 1. check whether it's necessary to call endEncoder
  //
  jboolean needEnd = JNI_FALSE;
  if (_encoder != nil) {
    if (_destination != dest || renderOptions->isAA != _encoderStates.aa) {
      J2dTraceLn(J2D_TRACE_VERBOSE,
                 "end common encoder because of dest change: %p -> %p",
                 _destination, dest);
      needEnd = JNI_TRUE;
    } else if ((_useStencil == NO) != ([_mtlc.clip isShape] == NO)) {
      // 1. When mode changes RECT -> SHAPE we must recreate encoder with
      // stencilAttachment (todo: consider the case when current encoder already
      // has stencil)
      //
      // 2. When mode changes SHAPE -> RECT it seems that we can use the same
      // encoder with disabled stencil test, but [encoder
      // setDepthStencilState:nil] causes crash, so we have to recreate encoder
      // in such case
      J2dTraceLn(J2D_TRACE_VERBOSE,
                 "end common encoder because toggle stencil: %d -> %d",
                 (int)_useStencil, (int)[_mtlc.clip isShape]);
      needEnd = JNI_TRUE;
    }
  }
  if (needEnd)
    [self endEncoder];

  //
  // 2. recreate encoder if necessary
  //
  jboolean forceUpdate = JNI_FALSE;
#ifdef ALWAYS_UPDATE_ENCODER_STATES
  forceUpdate = JNI_TRUE;
#endif // ALWAYS_UPDATE_ENCODER_STATES

  if (_encoder == nil) {
    _destination = dest;
    _useStencil = [_mtlc.clip isShape] && !_mtlc.clip.stencilMaskGenerationInProgress;
    forceUpdate = JNI_TRUE;

    MTLCommandBufferWrapper *cbw = [_mtlc getCommandBufferWrapper];
    MTLRenderPassDescriptor *rpd =
        [MTLRenderPassDescriptor renderPassDescriptor];
    MTLRenderPassColorAttachmentDescriptor *ca = rpd.colorAttachments[0];
    ca.texture = dest;

    // TODO: Find out why we cannot use
    // if (_mtlc.clip.stencilMaskGenerationInProgress == YES) {
    //     ca.loadAction = MTLLoadActionClear;
    //     ca.clearColor = MTLClearColorMake(0.0f, 0.0f,0.0f, 0.0f);
    // }
    // here to avoid creation of clearEncoder in beginShapeClip

    ca.loadAction = MTLLoadActionLoad;
    ca.storeAction = MTLStoreActionStore;

    if (_useStencil && !renderOptions->isAA) {
        // If you enable stencil testing or stencil writing, the
        // MTLRenderPassDescriptor must include a stencil attachment.
        rpd.stencilAttachment.loadAction = MTLLoadActionLoad;
        rpd.stencilAttachment.storeAction = MTLStoreActionStore;
        rpd.stencilAttachment.texture = _mtlc.clip.stencilTextureRef;
    } else if (_mtlc.clip.stencilMaskGenerationInProgress == YES) {
        rpd.stencilAttachment.texture = _mtlc.clip.dstOps->pStencilTexture;
        rpd.stencilAttachment.clearStencil = 0;
        rpd.stencilAttachment.loadAction = _mtlc.clip.stencilMaskGenerationStarted? MTLLoadActionLoad : MTLLoadActionClear;
        _mtlc.clip.stencilMaskGenerationStarted = YES;
        rpd.stencilAttachment.storeAction = MTLStoreActionStore;
    }

    // J2dTraceLn(J2D_TRACE_VERBOSE, "created render encoder to draw on
    // tex=%p", dest);
    _encoder = [[cbw getCommandBuffer] renderCommandEncoderWithDescriptor:rpd];

    [_encoderStates reset:dest
                     isAA:renderOptions->isAA
                   isText:renderOptions->isText
                    isLCD:renderOptions->isLCD];
  }

  //
  // 3. update encoder states
  //
  [_encoderStates updateEncoder:_encoder
                        context:_mtlc
                  renderOptions:renderOptions
                    forceUpdate:forceUpdate];

  return _encoder;
}

- (id<MTLBlitCommandEncoder> _Nonnull) createBlitEncoder {
    [self endEncoder];
    return [[[_mtlc getCommandBufferWrapper] getCommandBuffer] blitCommandEncoder];
}

- (void) endEncoder {
    if (_encoder != nil) {
      [_encoder endEncoding];
      _encoder = nil;
      _destination = nil;
    }
}

@end
