#import "PGPhotoEnhanceLUTGenerator.h"

#import "PGPhotoProcessPass.h"

const NSUInteger PGPhotoEnhanceHistogramBins = 256;
const NSUInteger PGPhotoEnhanceSegments = 4;

@interface PGPhotoEnhanceLUTGenerator ()
{
    CGFloat _clipLimit;
    
    CGSize _imageSize;
    GPUImageRotationMode _inputRotation;
    
    GPUImageFramebuffer *_firstInputFramebuffer;
    GPUImageFramebuffer *_outputFramebuffer;
    GPUImageFramebuffer *_retainedFramebuffer;
    
    GLProgram *_dataProgram;
    GLint _dataPositionAttribute;
    GLint _dataTextureCoordinateAttribute;
    GLint _dataInputTextureUniform;
    
    bool _hasReadCurrentFrame;
    
    GLubyte *_rawBytesForImage;
    bool _lockNextFramebuffer;
}

@property (nonatomic, assign) BOOL enabled;

@end

@implementation PGPhotoEnhanceLUTGenerator

- (instancetype)init
{
    self = [super init];
    if (self != nil)
    {
        _clipLimit = 1.25f;
        
        self.enabled = true;
        _lockNextFramebuffer = false;
        _inputRotation = kGPUImageNoRotation;
        
        [GPUImageContext useImageProcessingContext];
        
        if (([GPUImageContext supportsFastTextureUpload]))
        {
            _dataProgram = [[GPUImageContext sharedImageProcessingContext] programForVertexShaderString:kGPUImageVertexShaderString fragmentShaderString:PGPhotoEnhanceColorSwapShaderString];
        }
        else
        {
            _dataProgram = [[GPUImageContext sharedImageProcessingContext] programForVertexShaderString:kGPUImageVertexShaderString fragmentShaderString:kGPUImagePassthroughFragmentShaderString];
        }

        if (!_dataProgram.initialized)
        {
            [_dataProgram addAttribute:@"position"];
            [_dataProgram addAttribute:@"inputTexCoord"];

            if (![_dataProgram link])
            {
                NSString *progLog = [_dataProgram programLog];
                NSLog(@"Program link log: %@", progLog);
                NSString *fragLog = [_dataProgram fragmentShaderLog];
                NSLog(@"Fragment shader compile log: %@", fragLog);
                NSString *vertLog = [_dataProgram vertexShaderLog];
                NSLog(@"Vertex shader compile log: %@", vertLog);
                _dataProgram = nil;
                NSAssert(NO, @"Filter shader link failed");
            }
        }
        
        _dataPositionAttribute = [_dataProgram attributeIndex:@"position"];
        _dataTextureCoordinateAttribute = [_dataProgram attributeIndex:@"inputTexCoord"];
        _dataInputTextureUniform = [_dataProgram uniformIndex:@"sourceImage"];
    }
    return self;
}

- (void)dealloc
{
    if (_rawBytesForImage != NULL && (![GPUImageContext supportsFastTextureUpload]))
    {
        free(_rawBytesForImage);
        _rawBytesForImage = NULL;
    }
}

#pragma mark - GPUImageInput

- (void)newFrameReadyAtTime:(CMTime)__unused frameTime atIndex:(NSInteger)__unused textureIndex
{
    if (self.skip)
        return;
    
    _hasReadCurrentFrame = false;
    _lockNextFramebuffer = true;
    
    NSUInteger totalSegments = PGPhotoEnhanceSegments * PGPhotoEnhanceSegments;
    CGSize tileSize = CGSizeMake(CGFloor(_imageSize.width / PGPhotoEnhanceSegments), CGFloor(_imageSize.height / PGPhotoEnhanceSegments));
    NSUInteger tileArea = (NSUInteger)(tileSize.width * tileSize.height);
    NSUInteger clipLimit = (NSUInteger)MAX(1, _clipLimit * tileArea / (CGFloat)PGPhotoEnhanceHistogramBins);
    CGFloat scale = 255.0f / (CGFloat)tileArea;
    
    GLubyte *bytes = [self _rawBytes];
    NSUInteger bytesPerRow = [_retainedFramebuffer bytesPerRow];
    
    NSUInteger hist[totalSegments][PGPhotoEnhanceHistogramBins];
    NSUInteger cdfs[totalSegments][PGPhotoEnhanceHistogramBins];
    NSUInteger cdfsMin[totalSegments];
    NSUInteger cdfsMax[totalSegments];
    
    memset(hist, 0, totalSegments * PGPhotoEnhanceHistogramBins * sizeof(NSUInteger));
    memset(cdfs, 0, totalSegments * PGPhotoEnhanceHistogramBins * sizeof(NSUInteger));
    memset(cdfsMin, 0, totalSegments * sizeof(NSUInteger));
    memset(cdfsMax, 0, totalSegments * sizeof(NSUInteger));
    
    CGFloat xMul = PGPhotoEnhanceSegments / _imageSize.width;
    CGFloat yMul = PGPhotoEnhanceSegments / _imageSize.height;

    for (NSUInteger y = 0; y < _imageSize.height; y++)
    {
        NSUInteger yOffset = y * bytesPerRow;
        for (NSUInteger x = 0; x < _imageSize.width; x++)
        {
            NSUInteger index = x * 4 + yOffset;
            
            NSUInteger tx = (NSUInteger)(x * xMul);
            NSUInteger ty = (NSUInteger)(y * yMul);
            NSUInteger t = ty * PGPhotoEnhanceSegments + tx;
                        
            GLubyte value = bytes[index + 2];
            hist[t][value]++;
        }
    }
    
    [_retainedFramebuffer unlockAfterReading];
    [_retainedFramebuffer unlock];
    _retainedFramebuffer = nil;
    
    for (NSUInteger i = 0; i < totalSegments; i++)
    {
        if (clipLimit > 0)
        {
            NSUInteger clipped = 0;
            for (NSUInteger j = 0; j < PGPhotoEnhanceHistogramBins; ++j)
            {
                if (hist[i][j] > clipLimit)
                {
                    clipped += hist[i][j] - clipLimit;
                    hist[i][j] = clipLimit;
                }
            }
            
            NSUInteger redistBatch = clipped / PGPhotoEnhanceHistogramBins;
            NSUInteger residual = clipped - redistBatch * PGPhotoEnhanceHistogramBins;
            
            for (NSUInteger j = 0; j < PGPhotoEnhanceHistogramBins; ++j)
                hist[i][j] += redistBatch;
            
            for (NSUInteger j = 0; j < residual; ++j)
                hist[i][j]++;
        }
        
        memcpy(&cdfs[i], &hist[i], PGPhotoEnhanceHistogramBins * sizeof(NSUInteger));

        NSUInteger hMin = PGPhotoEnhanceHistogramBins - 1;
        for (NSUInteger j = 0; j < hMin; ++j)
        {
            if (cdfs[j] != 0)
                hMin = j;
        }
        
        NSUInteger cdf = 0;
        for (NSUInteger j = hMin; j < PGPhotoEnhanceHistogramBins; ++j)
        {
            cdf += cdfs[i][j];
            cdfs[i][j] = (uint8_t)MIN(255, cdf * scale);
        }
        
        cdfsMin[i] = cdfs[i][hMin];
        cdfsMax[i] = cdfs[i][PGPhotoEnhanceHistogramBins - 1];
    }
    
    NSUInteger resultSize = 4 * PGPhotoEnhanceHistogramBins * totalSegments;
    NSUInteger resultBytesPerRow = 4 * PGPhotoEnhanceHistogramBins;
    
    GLubyte *result = calloc(resultSize, sizeof(GLubyte));
    for (NSUInteger tile = 0; tile < totalSegments; tile++)
    {
        NSUInteger yOffset = tile * resultBytesPerRow;
        for (NSUInteger i = 0; i < PGPhotoEnhanceHistogramBins; i++)
        {
            NSUInteger index = i * 4 + yOffset;
            result[index] = (uint8_t)cdfs[tile][i];
            result[index + 1] = (uint8_t)cdfsMin[tile];
            result[index + 2] = (uint8_t)cdfsMax[tile];
        }
    }
    
    if (self.lutDataReady != nil)
        self.lutDataReady(result);
}

- (NSInteger)nextAvailableTextureIndex
{
    return 0;
}

- (void)setInputFramebuffer:(GPUImageFramebuffer *)newInputFramebuffer atIndex:(NSInteger)__unused textureIndex
{
    _firstInputFramebuffer = newInputFramebuffer;
    [_firstInputFramebuffer lock];
}

- (void)setInputRotation:(GPUImageRotationMode)newInputRotation atIndex:(NSInteger)__unused textureIndex
{
    _inputRotation = newInputRotation;
}

- (void)setInputSize:(CGSize)newSize atIndex:(NSInteger)__unused textureIndex
{
    _imageSize = newSize;
}

- (CGSize)maximumOutputSize
{
    return CGSizeMake(PGPhotoEnhanceHistogramBins, PGPhotoEnhanceSegments * PGPhotoEnhanceSegments);
}

- (void)endProcessing
{
    
}

- (BOOL)shouldIgnoreUpdatesToThisTarget
{
    return false;
}

- (BOOL)wantsMonochromeInput
{
    return false;
}

- (void)setCurrentlyReceivingMonochromeInput:(BOOL)__unused newValue
{
    
}

- (void)_render
{
    [GPUImageContext setActiveShaderProgram:_dataProgram];
    
    _outputFramebuffer = [[GPUImageContext sharedFramebufferCache] fetchFramebufferForSize:_imageSize onlyTexture:false];
    [_outputFramebuffer activateFramebuffer];
    
    if(_lockNextFramebuffer)
    {
        _retainedFramebuffer = _outputFramebuffer;
        [_retainedFramebuffer lock];
        [_retainedFramebuffer lockForReading];
        _lockNextFramebuffer = false;
    }
    
    glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    
    static const GLfloat squareVertices[] = {
        -1.0f, -1.0f,
        1.0f, -1.0f,
        -1.0f,  1.0f,
        1.0f,  1.0f,
    };
    
    static const GLfloat textureCoordinates[] = {
        0.0f, 0.0f,
        1.0f, 0.0f,
        0.0f, 1.0f,
        1.0f, 1.0f,
    };
    
    glActiveTexture(GL_TEXTURE4);
    glBindTexture(GL_TEXTURE_2D, [_firstInputFramebuffer texture]);
    glUniform1i(_dataInputTextureUniform, 4);
    
    glVertexAttribPointer(_dataPositionAttribute, 2, GL_FLOAT, 0, 0, squareVertices);
    glVertexAttribPointer(_dataTextureCoordinateAttribute, 2, GL_FLOAT, 0, 0, textureCoordinates);
    
    glEnableVertexAttribArray(_dataPositionAttribute);
    glEnableVertexAttribArray(_dataTextureCoordinateAttribute);
    
    glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
    [_firstInputFramebuffer unlock];
}

- (GLubyte *)_rawBytes
{
    if ((_rawBytesForImage == NULL) && (![GPUImageContext supportsFastTextureUpload]))
    {
        _rawBytesForImage = (GLubyte *)calloc((NSInteger)(_imageSize.width * _imageSize.height) * 4, sizeof(GLubyte));
        _hasReadCurrentFrame = false;
    }
    
    if (!_hasReadCurrentFrame)
    {
        runSynchronouslyOnVideoProcessingQueue(^
        {
            [GPUImageContext useImageProcessingContext];
            [self _render];
            
            if ([GPUImageContext supportsFastTextureUpload])
            {
                glFinish();
                _rawBytesForImage = [_outputFramebuffer byteBuffer];
            }
            else
            {
                glReadPixels(0, 0, (GLsizei)_imageSize.width, (GLsizei)_imageSize.height, GL_RGBA, GL_UNSIGNED_BYTE, _rawBytesForImage);
            }
            
            _hasReadCurrentFrame = true;
        });
    }
    
    return _rawBytesForImage;
}

@end
