#import "PGPhotoCustomFilterPass.h"

#import "PGPhotoEditorPicture.h"

NSString *const PGPhotoFilterDefinitionsShaderString = PGShaderString
(
 precision highp float;
 
 varying vec2 texCoord;
 uniform sampler2D sourceImage;
 uniform float intensity;
);

NSString *const PGPhotoFilterMainShaderString = PGShaderString
(
 void main() {
     vec4 texel = texture2D(sourceImage, texCoord);
     vec4 result = filter(texel);
     
     gl_FragColor = vec4(mix(texel.rgb, result.rgb, intensity), texel.a);
 }
);

@interface PGPhotoCustomFilter : GPUImageFilter
{
    GLint _intensityUniform;
    
    GLuint _filterSourceTexture2;
    GLuint _filterSourceTexture3;
    GLuint _filterSourceTexture4;
    GLuint _filterSourceTexture5;
    GLuint _filterSourceTexture6;
    
    GLint _filterInputTextureUniform2;
    GLint _filterInputTextureUniform3;
    GLint _filterInputTextureUniform4;
    GLint _filterInputTextureUniform5;
    GLint _filterInputTextureUniform6;
}

@property (nonatomic, assign) CGFloat intensity;

@end

@implementation PGPhotoCustomFilter

- (instancetype)initWithFragmentShaderFromString:(NSString *)fragmentShaderString
{
    self = [super initWithFragmentShaderFromString:fragmentShaderString];
    if (self != nil)
    {
        _intensityUniform = [filterProgram uniformIndex:@"intensity"];
        self.intensity = 1.0f;
        
        _filterInputTextureUniform2 = [filterProgram uniformIndex:@"inputImageTexture2"];
        _filterInputTextureUniform3 = [filterProgram uniformIndex:@"inputImageTexture3"];
        _filterInputTextureUniform4 = [filterProgram uniformIndex:@"inputImageTexture4"];
        _filterInputTextureUniform5 = [filterProgram uniformIndex:@"inputImageTexture5"];
        _filterInputTextureUniform6 = [filterProgram uniformIndex:@"inputImageTexture6"];
    }
    return self;
}

- (void)dealloc
{
    runAsynchronouslyOnVideoProcessingQueue(^
    {
        if (_filterSourceTexture2)
            glDeleteTextures(1, &_filterSourceTexture2);
        
        if (_filterSourceTexture3)
            glDeleteTextures(1, &_filterSourceTexture3);
        
        if (_filterSourceTexture4)
            glDeleteTextures(1, &_filterSourceTexture4);
        
        if (_filterSourceTexture5)
            glDeleteTextures(1, &_filterSourceTexture5);
        
        if (_filterSourceTexture6)
            glDeleteTextures(1, &_filterSourceTexture6);
    });
}

- (void)addTextureWithImage:(UIImage *)image textureIndex:(NSInteger)textureIndex
{
    bool redrawNeeded = false;
    CGImageRef cgImage = image.CGImage;
    
    if (image.imageOrientation != UIImageOrientationUp)
        redrawNeeded = true;
    
    CGSize imageSize = CGSizeMake(CGImageGetWidth(cgImage), CGImageGetHeight(cgImage));
    if (image.imageOrientation == UIImageOrientationLeft || image.imageOrientation == UIImageOrientationRight)
        imageSize = CGSizeMake(imageSize.height, imageSize.width);
    
    GLubyte *imageData = NULL;
    CFDataRef dataFromImageDataProvider = NULL;
    GLenum format = GL_BGRA;
    
    if (!redrawNeeded)
    {
        if (CGImageGetBytesPerRow(cgImage) != CGImageGetWidth(cgImage) * 4 ||
            CGImageGetBitsPerPixel(cgImage) != 32 ||
            CGImageGetBitsPerComponent(cgImage) != 8)
        {
            redrawNeeded = true;
        }
        else
        {
            CGBitmapInfo bitmapInfo = CGImageGetBitmapInfo(cgImage);
            if ((bitmapInfo & kCGBitmapFloatComponents) != 0)
            {
                redrawNeeded = true;
            }
            else
            {
                CGBitmapInfo byteOrderInfo = bitmapInfo & kCGBitmapByteOrderMask;
                if (byteOrderInfo == kCGBitmapByteOrder32Little)
                {
                    /* Little endian, for alpha-first we can use this bitmap directly in GL */
                    CGImageAlphaInfo alphaInfo = bitmapInfo & kCGBitmapAlphaInfoMask;
                    if (alphaInfo != kCGImageAlphaPremultipliedFirst && alphaInfo != kCGImageAlphaFirst &&
                        alphaInfo != kCGImageAlphaNoneSkipFirst)
                    {
                        redrawNeeded = true;
                    }
                }
                else if (byteOrderInfo == kCGBitmapByteOrderDefault || byteOrderInfo == kCGBitmapByteOrder32Big)
                {
                    /* Big endian, for alpha-last we can use this bitmap directly in GL */
                    CGImageAlphaInfo alphaInfo = bitmapInfo & kCGBitmapAlphaInfoMask;
                    if (alphaInfo != kCGImageAlphaPremultipliedLast && alphaInfo != kCGImageAlphaLast &&
                        alphaInfo != kCGImageAlphaNoneSkipLast)
                    {
                        redrawNeeded = true;
                    } else
                    {
                        /* Can access directly using GL_RGBA pixel format */
                        format = GL_RGBA;
                    }
                }
            }
        }
    }
    
    if (redrawNeeded)
    {
        imageData = (GLubyte *) calloc(1, (int)imageSize.width * (int)imageSize.height * 4);
        
        CGColorSpaceRef genericRGBColorspace = CGColorSpaceCreateDeviceRGB();
        
        CGContextRef imageContext = CGBitmapContextCreate(imageData, (size_t)imageSize.width, (size_t)imageSize.height, 8, (size_t)imageSize.width * 4, genericRGBColorspace,  kCGBitmapByteOrder32Little | kCGImageAlphaPremultipliedFirst);
        
        CGSize imageDrawSize = CGSizeMake(imageSize.width, imageSize.height);
        if (image.imageOrientation == UIImageOrientationLeft || image.imageOrientation == UIImageOrientationRight)
            imageDrawSize = CGSizeMake(imageDrawSize.height, imageDrawSize.width);
        
        CGAffineTransform transform = CGAffineTransformIdentity;
        switch (image.imageOrientation)
        {
            case UIImageOrientationDown:
            case UIImageOrientationDownMirrored:
                transform = CGAffineTransformTranslate(transform, imageSize.width, imageSize.height);
                transform = CGAffineTransformRotate(transform, (CGFloat)M_PI);
                break;
                
            case UIImageOrientationLeft:
            case UIImageOrientationLeftMirrored:
                transform = CGAffineTransformTranslate(transform, imageSize.width, 0);
                transform = CGAffineTransformRotate(transform, (CGFloat)M_PI_2);
                break;
                
            case UIImageOrientationRight:
            case UIImageOrientationRightMirrored:
                transform = CGAffineTransformTranslate(transform, 0, imageSize.height);
                transform = CGAffineTransformRotate(transform, (CGFloat)-M_PI_2);
                break;
                
            default:
                break;
        }
        
        switch (image.imageOrientation)
        {
            case UIImageOrientationUpMirrored:
            case UIImageOrientationDownMirrored:
                transform = CGAffineTransformTranslate(transform, imageSize.width,0);
                transform = CGAffineTransformScale(transform, -1, 1);
                break;
                
            case UIImageOrientationLeftMirrored:
            case UIImageOrientationRightMirrored:
                transform = CGAffineTransformTranslate(transform, imageSize.height, 0);
                transform = CGAffineTransformScale(transform, -1, 1);
                break;
                
            default:
                break;
        }
        
        CGContextConcatCTM(imageContext, transform);
        
        CGContextDrawImage(imageContext, CGRectMake(0.0f, 0.0f, imageDrawSize.width, imageDrawSize.height), cgImage);
        CGContextRelease(imageContext);
        CGColorSpaceRelease(genericRGBColorspace);
    }
    else
    {
        dataFromImageDataProvider = CGDataProviderCopyData(CGImageGetDataProvider(cgImage));
        imageData = (GLubyte *)CFDataGetBytePtr(dataFromImageDataProvider);
    }
    
    runSynchronouslyOnVideoProcessingQueue(^
    {
        [GPUImageContext useImageProcessingContext];
        
        switch (textureIndex)
        {
            case 0:
            {
                glActiveTexture(GL_TEXTURE3);
                glGenTextures(1, &_filterSourceTexture2);
                glBindTexture(GL_TEXTURE_2D, _filterSourceTexture2);
            }
                break;
                
            case 1:
            {
                glActiveTexture(GL_TEXTURE4);
                glGenTextures(1, &_filterSourceTexture3);
                glBindTexture(GL_TEXTURE_2D, _filterSourceTexture3);
            }
                break;
                
            case 2:
            {
                glActiveTexture(GL_TEXTURE5);
                glGenTextures(1, &_filterSourceTexture4);
                glBindTexture(GL_TEXTURE_2D, _filterSourceTexture4);
            }
                break;
                
            case 3:
            {
                glActiveTexture(GL_TEXTURE6);
                glGenTextures(1, &_filterSourceTexture5);
                glBindTexture(GL_TEXTURE_2D, _filterSourceTexture5);
                break;
            }
                
            case 4:
            {
                glActiveTexture(GL_TEXTURE7);
                glGenTextures(1, &_filterSourceTexture6);
                glBindTexture(GL_TEXTURE_2D, _filterSourceTexture6);
                break;
            }
                
            default:
                break;
        }
        
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
        
        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, (GLsizei)imageSize.width, (GLsizei)imageSize.height, 0, GL_RGBA, GL_UNSIGNED_BYTE, imageData);
        
        if (redrawNeeded)
            free(imageData);
        else if (dataFromImageDataProvider)
            CFRelease(dataFromImageDataProvider);
    });
}

- (void)renderToTextureWithVertices:(const GLfloat *)vertices textureCoordinates:(const GLfloat *)textureCoordinates
{
    if (self.preventRendering)
    {
        [firstInputFramebuffer unlock];
        return;
    }
    
    [GPUImageContext setActiveShaderProgram:filterProgram];
    
    outputFramebuffer = [[GPUImageContext sharedFramebufferCache] fetchFramebufferForSize:[self sizeOfFBO] textureOptions:self.outputTextureOptions onlyTexture:false];
    [outputFramebuffer activateFramebuffer];
    if (usingNextFrameForImageCapture)
    {
        [outputFramebuffer lock];
    }
    
    [self setUniformsForProgramAtIndex:0];
    
    glClearColor(backgroundColorRed, backgroundColorGreen, backgroundColorBlue, backgroundColorAlpha);
    glClear(GL_COLOR_BUFFER_BIT);
    
    glActiveTexture(GL_TEXTURE2);
    glBindTexture(GL_TEXTURE_2D, [firstInputFramebuffer texture]);
    glUniform1i(filterInputTextureUniform, 2);
    
    if (_filterSourceTexture2)
    {
        glActiveTexture(GL_TEXTURE3);
        glBindTexture(GL_TEXTURE_2D, _filterSourceTexture2);
        glUniform1i(_filterInputTextureUniform2, 3);
    }
    
    if (_filterSourceTexture3)
    {
        glActiveTexture(GL_TEXTURE4);
        glBindTexture(GL_TEXTURE_2D, _filterSourceTexture3);
        glUniform1i(_filterInputTextureUniform3, 4);
    }
    
    if (_filterSourceTexture4)
    {
        glActiveTexture(GL_TEXTURE5);
        glBindTexture(GL_TEXTURE_2D, _filterSourceTexture4);
        glUniform1i(_filterInputTextureUniform4, 5);
    }
    
    if (_filterSourceTexture5)
    {
        glActiveTexture(GL_TEXTURE6);
        glBindTexture(GL_TEXTURE_2D, _filterSourceTexture5);
        glUniform1i(_filterInputTextureUniform5, 6);
    }
    
    if (_filterSourceTexture6)
    {
        glActiveTexture(GL_TEXTURE7);
        glBindTexture(GL_TEXTURE_2D, _filterSourceTexture6);
        glUniform1i(_filterInputTextureUniform6, 7);
    }
    
    glVertexAttribPointer(filterPositionAttribute, 2, GL_FLOAT, 0, 0, vertices);
    glVertexAttribPointer(filterTextureCoordinateAttribute, 2, GL_FLOAT, 0, 0, textureCoordinates);
    
    glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
    
    [firstInputFramebuffer unlock];
    
    if (usingNextFrameForImageCapture)
        dispatch_semaphore_signal(imageCaptureSemaphore);
}

- (void)setIntensity:(CGFloat)intensity
{
    _intensity = intensity;
    
    [self setFloat:(float)_intensity forUniform:_intensityUniform program:filterProgram];
}

@end

@implementation PGPhotoCustomFilterPass

@dynamic intensity;

- (instancetype)initWithShaderFile:(NSString *)shaderFile textureFiles:(NSArray *)textureFiles
{
    return [self initWithShaderFile:shaderFile textureFiles:textureFiles optimized:false];
}

- (instancetype)initWithShaderFile:(NSString *)shaderFile textureFiles:(NSArray *)textureFiles optimized:(bool)optimized
{
    NSString *fragmentShaderPathname = [[NSBundle mainBundle] pathForResource:shaderFile ofType:@"fsh"];
    NSString *fragmentShaderString = [NSString stringWithContentsOfFile:fragmentShaderPathname encoding:NSUTF8StringEncoding error:nil];
    
    NSMutableArray *textureImages = [[NSMutableArray alloc] init];
    for (id textureDefinition in textureFiles)
    {
        NSString *textureFile = nil;
        if ([textureDefinition isKindOfClass:[NSString class]])
        {
            textureFile = (NSString *)textureDefinition;
        }
        else if ([textureDefinition isKindOfClass:[NSArray class]])
        {
            NSArray *textureDefinitions = (NSArray *)textureDefinition;
            textureFile = optimized ? textureDefinitions.lastObject : textureDefinitions.firstObject;
        }

        NSString *name = [[textureFile lastPathComponent] stringByDeletingPathExtension];
        NSString *extension = [textureFile pathExtension];
        
        NSString *texturePathname = [[NSBundle mainBundle] pathForResource:name ofType:extension];
        UIImage *textureImage = [UIImage imageWithContentsOfFile:texturePathname];
        
        [textureImages addObject:textureImage];
    }

    return [self initWithShaderString:fragmentShaderString textureImages:textureImages];
}

- (instancetype)initWithShaderString:(NSString *)shaderString textureImages:(NSArray *)textureImages
{
    self = [super init];
    if (self != nil)
    {
        NSMutableString *fullShaderString = [[NSMutableString alloc] initWithString:PGPhotoFilterDefinitionsShaderString];
        [fullShaderString appendString:shaderString];
        [fullShaderString appendString:PGPhotoFilterMainShaderString];
        
        PGPhotoCustomFilter *filter = [[PGPhotoCustomFilter alloc] initWithFragmentShaderFromString:fullShaderString];
        
        NSInteger index = 0;
        for (UIImage *image in textureImages)
        {
            [filter addTextureWithImage:image textureIndex:index];
            index++;
        }
        
        _filter = filter;
    }
    return self;
}

- (void)setIntensity:(CGFloat)intensity
{
    [(PGPhotoCustomFilter *)_filter setIntensity:intensity];
}

@end
