import OpenGLES
import UIKit

public enum PictureInputError: Error, CustomStringConvertible {
    case zeroSizedImageError
    case dataProviderNilError
    case noSuchImageError(imageName: String)

    public var errorDescription: String {
        switch self {
        case .zeroSizedImageError:
            return "Tried to pass in a zero-sized image"
        case .dataProviderNilError:
            return "Unable to retrieve image dataProvider"
        case let .noSuchImageError(imageName):
            return "No such image named: \(imageName) in your application bundle"
        }
    }

    public var description: String {
        return "<\(type(of: self)): errorDescription = \(errorDescription)>"
    }
}

public class PictureInput: ImageSource {
    public let targets = TargetContainer()
    var imageFramebuffer: Framebuffer?
    public var framebufferUserInfo: [AnyHashable: Any]?
    var hasProcessedImage: Bool = false

    public init(image: CGImage, smoothlyScaleOutput: Bool = false, orientation: ImageOrientation = .portrait) throws {
        let widthOfImage = GLint(image.width)
        let heightOfImage = GLint(image.height)

        // If passed an empty image reference, CGContextDrawImage will fail in future versions of the SDK.
        guard widthOfImage > 0, heightOfImage > 0 else { throw PictureInputError.zeroSizedImageError }

        var widthToUseForTexture = widthOfImage
        var heightToUseForTexture = heightOfImage
        var shouldRedrawUsingCoreGraphics = false

        // For now, deal with images larger than the maximum texture size by resizing to be within that limit
        let scaledImageSizeToFitOnGPU = GLSize(sharedImageProcessingContext.sizeThatFitsWithinATextureForSize(Size(width: Float(widthOfImage), height: Float(heightOfImage))))
        if scaledImageSizeToFitOnGPU.width != widthOfImage, scaledImageSizeToFitOnGPU.height != heightOfImage {
            widthToUseForTexture = scaledImageSizeToFitOnGPU.width
            heightToUseForTexture = scaledImageSizeToFitOnGPU.height
            shouldRedrawUsingCoreGraphics = true
        }

        if smoothlyScaleOutput {
            // In order to use mipmaps, you need to provide power-of-two textures, so convert to the next largest power of two and stretch to fill
            let powerClosestToWidth = ceil(log2(Float(widthToUseForTexture)))
            let powerClosestToHeight = ceil(log2(Float(heightToUseForTexture)))

            widthToUseForTexture = GLint(round(pow(2.0, powerClosestToWidth)))
            heightToUseForTexture = GLint(round(pow(2.0, powerClosestToHeight)))
            shouldRedrawUsingCoreGraphics = true
        }

        var imageData: UnsafeMutablePointer<GLubyte>!
        var dataFromImageDataProvider: CFData!
        var format = GL_BGRA

        if !shouldRedrawUsingCoreGraphics {
            /* Check that the memory layout is compatible with GL, as we cannot use glPixelStore to
             * tell GL about the memory layout with GLES.
             */
            if (image.bytesPerRow != image.width * 4) || (image.bitsPerPixel != 32) || (image.bitsPerComponent != 8)
            {
                shouldRedrawUsingCoreGraphics = true
            } else {
                /* Check that the bitmap pixel format is compatible with GL */
                let bitmapInfo = image.bitmapInfo
                if bitmapInfo.contains(.floatComponents) {
                    /* We don't support float components for use directly in GL */
                    shouldRedrawUsingCoreGraphics = true
                } else {
                    let alphaInfo = CGImageAlphaInfo(rawValue: bitmapInfo.rawValue & CGBitmapInfo.alphaInfoMask.rawValue)
                    if bitmapInfo.contains(.byteOrder32Little) {
                        /* Little endian, for alpha-first we can use this bitmap directly in GL */
                        if alphaInfo != CGImageAlphaInfo.premultipliedFirst, alphaInfo != CGImageAlphaInfo.first, alphaInfo != CGImageAlphaInfo.noneSkipFirst {
                            shouldRedrawUsingCoreGraphics = true
                        }
                    } else if bitmapInfo.contains(CGBitmapInfo()) || bitmapInfo.contains(.byteOrder32Big) {
                        /* Big endian, for alpha-last we can use this bitmap directly in GL */
                        if alphaInfo != CGImageAlphaInfo.premultipliedLast, alphaInfo != CGImageAlphaInfo.last, alphaInfo != CGImageAlphaInfo.noneSkipLast {
                            shouldRedrawUsingCoreGraphics = true
                        } else {
                            /* Can access directly using GL_RGBA pixel format */
                            format = GL_RGBA
                        }
                    }
                }
            }
        }

        try sharedImageProcessingContext.runOperationSynchronously {
            //    CFAbsoluteTime elapsedTime, startTime = CFAbsoluteTimeGetCurrent();

            if shouldRedrawUsingCoreGraphics {
                // For resized or incompatible image: redraw
                imageData = UnsafeMutablePointer<GLubyte>.allocate(capacity: Int(widthToUseForTexture * heightToUseForTexture) * 4)

                let genericRGBColorspace = CGColorSpaceCreateDeviceRGB()

                let imageContext = CGContext(data: imageData, width: Int(widthToUseForTexture), height: Int(heightToUseForTexture), bitsPerComponent: 8, bytesPerRow: Int(widthToUseForTexture) * 4, space: genericRGBColorspace, bitmapInfo: CGImageAlphaInfo.premultipliedFirst.rawValue | CGBitmapInfo.byteOrder32Little.rawValue)
                //        CGContextSetBlendMode(imageContext, kCGBlendModeCopy); // From Technical Q&A QA1708: http://developer.apple.com/library/ios/#qa/qa1708/_index.html
                imageContext?.draw(image, in: CGRect(x: 0.0, y: 0.0, width: CGFloat(widthToUseForTexture), height: CGFloat(heightToUseForTexture)))
            } else {
                // Access the raw image bytes directly
                guard let data = image.dataProvider?.data else { throw PictureInputError.dataProviderNilError }
                dataFromImageDataProvider = data
                imageData = UnsafeMutablePointer<GLubyte>(mutating: CFDataGetBytePtr(dataFromImageDataProvider))
            }

            // TODO: Alter orientation based on metadata from photo
            self.imageFramebuffer = try Framebuffer(context: sharedImageProcessingContext, orientation: orientation, size: GLSize(width: widthToUseForTexture, height: heightToUseForTexture), textureOnly: true)
            self.imageFramebuffer!.lock()

            glBindTexture(GLenum(GL_TEXTURE_2D), self.imageFramebuffer!.texture)
            if smoothlyScaleOutput {
                glTexParameteri(GLenum(GL_TEXTURE_2D), GLenum(GL_TEXTURE_MIN_FILTER), GL_LINEAR_MIPMAP_LINEAR)
            }

            glTexImage2D(GLenum(GL_TEXTURE_2D), 0, GL_RGBA, widthToUseForTexture, heightToUseForTexture, 0, GLenum(format), GLenum(GL_UNSIGNED_BYTE), imageData)

            if smoothlyScaleOutput {
                glGenerateMipmap(GLenum(GL_TEXTURE_2D))
            }
            glBindTexture(GLenum(GL_TEXTURE_2D), 0)
        }

        if shouldRedrawUsingCoreGraphics {
            imageData.deallocate()
        }
    }

    public convenience init(image: UIImage, smoothlyScaleOutput: Bool = false, orientation: ImageOrientation = .portrait) throws {
        try self.init(image: image.cgImage!, smoothlyScaleOutput: smoothlyScaleOutput, orientation: orientation)
    }

    public convenience init(imageName: String, smoothlyScaleOutput: Bool = false, orientation: ImageOrientation = .portrait) throws {
        guard let image = UIImage(named: imageName) else { throw PictureInputError.noSuchImageError(imageName: imageName) }
        try self.init(image: image.cgImage!, smoothlyScaleOutput: smoothlyScaleOutput, orientation: orientation)
    }

    deinit {
        // debugPrint("Deallocating operation: \(self)")

        self.imageFramebuffer?.unlock()
    }

    public func processImage(synchronously: Bool = false) {
        if synchronously {
            sharedImageProcessingContext.runOperationSynchronously {
                self.imageFramebuffer?.userInfo = self.framebufferUserInfo

                if let framebuffer = self.imageFramebuffer {
                    self.updateTargetsWithFramebuffer(framebuffer)
                    self.hasProcessedImage = true
                }
            }
        } else {
            sharedImageProcessingContext.runOperationAsynchronously {
                self.imageFramebuffer?.userInfo = self.framebufferUserInfo

                if let framebuffer = self.imageFramebuffer {
                    self.updateTargetsWithFramebuffer(framebuffer)
                    self.hasProcessedImage = true
                }
            }
        }
    }

    public func transmitPreviousImage(to _: ImageConsumer, atIndex _: UInt) {
        // This gets called after the pipline gets adjusted and needs an image it
        // Disabled so we can adjust/prepare the pipline freely without worrying an old framebuffer will get pushed through it
        // If after changing the pipline you need the prior frame buffer to be reprocessed, call processImage() again.
        /* if hasProcessedImage {
             imageFramebuffer.lock()
             target.newFramebufferAvailable(imageFramebuffer, fromSourceIndex:atIndex)
         } */
    }
}
