import Foundation
import CoreImage
import Metal

// MARK: - LUT Filter Implementation
// Applies LUT (Look-Up Table) transformations to images using Core Image CIColorCube

/// LUT filter errors
public enum LUTFilterError: Error {
    case invalidLUTData
    case filterCreationFailed
    case incompatibleImageFormat
    case metalNotAvailable

    var localizedDescription: String {
        switch self {
        case .invalidLUTData: return "Invalid LUT data"
        case .filterCreationFailed: return "Failed to create LUT filter"
        case .incompatibleImageFormat: return "Image format incompatible with LUT"
        case .metalNotAvailable: return "Metal acceleration not available"
        }
    }
}

/// LUT Filter with intensity blending support
public class LUTFilter {

    // MARK: - Properties

    private let lutData: LUTData
    private var cachedFilter: CIFilter?
    private let ciContext: CIContext

    // MARK: - Initialization

    /// Initialize LUT filter with parsed LUT data
    /// - Parameters:
    ///   - lutData: Parsed LUT data structure
    ///   - ciContext: Core Image context for rendering
    public init(lutData: LUTData, ciContext: CIContext) {
        self.lutData = lutData
        self.ciContext = ciContext
    }

    /// Convenience initializer from .cube file
    /// - Parameters:
    ///   - fileURL: URL to .cube file
    ///   - ciContext: Core Image context for rendering
    /// - Throws: LUTParserError or LUTFilterError
    public convenience init(fileURL: URL, ciContext: CIContext) throws {
        let lutData = try LUTParser.parse(fileURL: fileURL)
        self.init(lutData: lutData, ciContext: ciContext)
    }

    // MARK: - Public Methods

    /// Apply LUT to image with intensity blending
    /// - Parameters:
    ///   - image: Input CIImage
    ///   - intensity: Blend intensity (0.0 = original, 1.0 = full LUT)
    /// - Returns: Filtered CIImage
    /// - Throws: LUTFilterError if filter application fails
    public func apply(to image: CIImage, intensity: Float = 1.0) throws -> CIImage {
        let clampedIntensity = max(0.0, min(1.0, intensity))

        // If intensity is 0, return original
        if clampedIntensity < 0.001 {
            return image
        }

        // Get or create the filter
        let filter = try getOrCreateFilter()

        // Apply filter
        filter.setValue(image, forKey: kCIInputImageKey)

        guard let outputImage = filter.outputImage else {
            throw LUTFilterError.filterCreationFailed
        }

        // If full intensity, return filtered image
        if abs(clampedIntensity - 1.0) < 0.001 {
            return outputImage
        }

        // Blend original and filtered based on intensity
        return blendImages(original: image, filtered: outputImage, intensity: clampedIntensity)
    }

    /// Clear cached filter (useful for memory management)
    public func clearCache() {
        cachedFilter = nil
    }

    // MARK: - Private Methods

    private func getOrCreateFilter() throws -> CIFilter {
        if let cached = cachedFilter {
            return cached
        }

        let filter = try createFilter()
        cachedFilter = filter
        return filter
    }

    private func createFilter() throws -> CIFilter {
        switch lutData.type {
        case .lut3D(let size):
            return try create3DLUTFilter(size: size)
        case .lut1D(let size):
            return try create1DLUTFilter(size: size)
        }
    }

    private func create3DLUTFilter(size: Int) throws -> CIFilter {
        // CIColorCube expects data in a specific format
        // Data must be arranged as: [R0G0B0A0, R1G0B0A0, ..., RnGnBnAn]
        // where n = size^3

        let cubeData = convertTo3DCubeData(size: size)

        guard let filter = CIFilter(name: "CIColorCube") else {
            throw LUTFilterError.filterCreationFailed
        }

        let data = Data(bytes: cubeData, count: cubeData.count * MemoryLayout<Float>.size)

        filter.setValue(size, forKey: "inputCubeDimension")
        filter.setValue(data, forKey: "inputCubeData")

        return filter
    }

    private func create1DLUTFilter(size: Int) throws -> CIFilter {
        // For 1D LUT, we create a 3D LUT with the same values along each axis
        // This effectively creates a 1D color curve

        let cubeSize = size
        let cubeData = convertTo3DCubeDataFrom1D(size: cubeSize)

        guard let filter = CIFilter(name: "CIColorCube") else {
            throw LUTFilterError.filterCreationFailed
        }

        let data = Data(bytes: cubeData, count: cubeData.count * MemoryLayout<Float>.size)

        filter.setValue(cubeSize, forKey: "inputCubeDimension")
        filter.setValue(data, forKey: "inputCubeData")

        return filter
    }

    private func convertTo3DCubeData(size: Int) -> [Float] {
        // CIColorCube expects RGBA data with alpha = 1.0
        var cubeData: [Float] = []
        cubeData.reserveCapacity(size * size * size * 4)

        // LUT data is in order: [R0, G0, B0, R1, G1, B1, ...]
        // For b, g, r (z, y, x) iteration order
        for i in 0..<(size * size * size) {
            let baseIndex = i * 3
            cubeData.append(lutData.data[baseIndex])     // R
            cubeData.append(lutData.data[baseIndex + 1]) // G
            cubeData.append(lutData.data[baseIndex + 2]) // B
            cubeData.append(1.0)                         // A
        }

        return cubeData
    }

    private func convertTo3DCubeDataFrom1D(size: Int) -> [Float] {
        // Convert 1D LUT to 3D cube by applying 1D curve to each channel independently
        var cubeData: [Float] = []
        cubeData.reserveCapacity(size * size * size * 4)

        for b in 0..<size {
            for g in 0..<size {
                for r in 0..<size {
                    // Get interpolated values from 1D LUT for each channel
                    let rValue = interpolate1D(value: Float(r) / Float(size - 1), channel: 0)
                    let gValue = interpolate1D(value: Float(g) / Float(size - 1), channel: 1)
                    let bValue = interpolate1D(value: Float(b) / Float(size - 1), channel: 2)

                    cubeData.append(rValue)
                    cubeData.append(gValue)
                    cubeData.append(bValue)
                    cubeData.append(1.0)
                }
            }
        }

        return cubeData
    }

    private func interpolate1D(value: Float, channel: Int) -> Float {
        guard case .lut1D(let size) = lutData.type else {
            return value
        }

        let scaledValue = value * Float(size - 1)
        let lowerIndex = Int(floor(scaledValue))
        let upperIndex = min(lowerIndex + 1, size - 1)
        let fraction = scaledValue - Float(lowerIndex)

        let lowerValue = lutData.data[lowerIndex * 3 + channel]
        let upperValue = lutData.data[upperIndex * 3 + channel]

        return lowerValue + (upperValue - lowerValue) * fraction
    }

    private func blendImages(original: CIImage, filtered: CIImage, intensity: Float) -> CIImage {
        // Use CIBlendWithAlphaMask for smooth blending
        guard let blendFilter = CIFilter(name: "CIBlendWithAlphaMask") else {
            // Fallback: simple linear interpolation using CIColorMatrix
            return fallbackBlend(original: original, filtered: filtered, intensity: intensity)
        }

        // Create alpha mask with uniform intensity
        let maskColor = CIColor(red: CGFloat(intensity), green: CGFloat(intensity), blue: CGFloat(intensity), alpha: 1.0)
        let maskImage = CIImage(color: maskColor).cropped(to: original.extent)

        blendFilter.setValue(filtered, forKey: kCIInputImageKey)
        blendFilter.setValue(original, forKey: kCIInputBackgroundImageKey)
        blendFilter.setValue(maskImage, forKey: kCIInputMaskImageKey)

        return blendFilter.outputImage ?? filtered
    }

    private func fallbackBlend(original: CIImage, filtered: CIImage, intensity: Float) -> CIImage {
        // Simple lerp: result = original * (1 - intensity) + filtered * intensity
        // Using CIColorMatrix for each image then add

        let inv = 1.0 - intensity

        // Scale original by (1 - intensity)
        guard let originalMatrix = CIFilter(name: "CIColorMatrix") else {
            return filtered
        }
        originalMatrix.setValue(original, forKey: kCIInputImageKey)
        originalMatrix.setValue(CIVector(x: CGFloat(inv), y: 0, z: 0, w: 0), forKey: "inputRVector")
        originalMatrix.setValue(CIVector(x: 0, y: CGFloat(inv), z: 0, w: 0), forKey: "inputGVector")
        originalMatrix.setValue(CIVector(x: 0, y: 0, z: CGFloat(inv), w: 0), forKey: "inputBVector")
        originalMatrix.setValue(CIVector(x: 0, y: 0, z: 0, w: 1), forKey: "inputAVector")

        // Scale filtered by intensity
        guard let filteredMatrix = CIFilter(name: "CIColorMatrix") else {
            return filtered
        }
        filteredMatrix.setValue(filtered, forKey: kCIInputImageKey)
        filteredMatrix.setValue(CIVector(x: CGFloat(intensity), y: 0, z: 0, w: 0), forKey: "inputRVector")
        filteredMatrix.setValue(CIVector(x: 0, y: CGFloat(intensity), z: 0, w: 0), forKey: "inputGVector")
        filteredMatrix.setValue(CIVector(x: 0, y: 0, z: CGFloat(intensity), w: 0), forKey: "inputBVector")
        filteredMatrix.setValue(CIVector(x: 0, y: 0, z: 0, w: 1), forKey: "inputAVector")

        // Add them together
        guard let addFilter = CIFilter(name: "CIAdditionCompositing"),
              let originalScaled = originalMatrix.outputImage,
              let filteredScaled = filteredMatrix.outputImage else {
            return filtered
        }

        addFilter.setValue(filteredScaled, forKey: kCIInputImageKey)
        addFilter.setValue(originalScaled, forKey: kCIInputBackgroundImageKey)

        return addFilter.outputImage ?? filtered
    }
}

// MARK: - LUT Cache Manager

/// Manages cached LUT filters for reuse
public class LUTCache {

    private var cache: [String: LUTFilter] = [:]
    private let queue = DispatchQueue(label: "com.imageSDK.lutcache", attributes: .concurrent)
    private var maxCacheSize: Int
    private var accessOrder: [String] = []  // For LRU eviction

    /// Initialize cache with maximum size
    /// - Parameter maxCacheSize: Maximum number of LUTs to cache (default: 10)
    public init(maxCacheSize: Int = 10) {
        self.maxCacheSize = maxCacheSize
    }

    /// Load and cache a LUT from file
    /// - Parameters:
    ///   - name: Unique name identifier for the LUT
    ///   - fileURL: URL to .cube file
    ///   - ciContext: Core Image context
    /// - Returns: LUT filter instance
    /// - Throws: LUTParserError or LUTFilterError
    public func loadLUT(name: String, fileURL: URL, ciContext: CIContext) throws -> LUTFilter {
        // Check cache first
        if let cached = getLUT(name: name) {
            return cached
        }

        // Load and cache
        let filter = try LUTFilter(fileURL: fileURL, ciContext: ciContext)
        setLUT(name: name, filter: filter)
        return filter
    }

    /// Get cached LUT
    public func getLUT(name: String) -> LUTFilter? {
        return queue.sync {
            if let filter = cache[name] {
                // Update access order (LRU)
                accessOrder.removeAll { $0 == name }
                accessOrder.append(name)
                return filter
            }
            return nil
        }
    }

    /// Store LUT in cache
    public func setLUT(name: String, filter: LUTFilter) {
        queue.async(flags: .barrier) {
            // Evict oldest if cache is full
            if self.cache.count >= self.maxCacheSize, !self.cache.keys.contains(name) {
                self.evictOldest()
            }

            self.cache[name] = filter
            self.accessOrder.removeAll { $0 == name }
            self.accessOrder.append(name)
        }
    }

    /// Remove LUT from cache
    public func removeLUT(name: String) {
        queue.async(flags: .barrier) {
            self.cache[name]?.clearCache()
            self.cache.removeValue(forKey: name)
            self.accessOrder.removeAll { $0 == name }
        }
    }

    /// Clear all cached LUTs
    public func clearAll() {
        queue.async(flags: .barrier) {
            self.cache.values.forEach { $0.clearCache() }
            self.cache.removeAll()
            self.accessOrder.removeAll()
        }
    }

    private func evictOldest() {
        guard let oldest = accessOrder.first else { return }
        cache[oldest]?.clearCache()
        cache.removeValue(forKey: oldest)
        accessOrder.removeFirst()
    }

    /// Get current cache size
    public var count: Int {
        return queue.sync { cache.count }
    }
}
