//
//  GraphicContext.swift
//  Memola
//
//  Created by Dscyre Scotti on 5/4/24.
//

import Combine
import MetalKit
import CoreData
import Foundation

final class GraphicContext: @unchecked Sendable {
    var tree: RTree = RTree<Element>(maxEntries: 8)
    var eraserStrokes: Set<EraserStroke> = []
    var object: GraphicContextObject?
    
    var currentElement: Element?
    var previousElement: Element?

    var currentPoint: CGPoint?
    var renderType: RenderType = .finished
    var vertices: [ViewPortVertex] = []
    var vertexCount: Int = 4
    var vertexBuffer: MTLBuffer?

    var erasers: [URL: EraserStroke] = [:]

    let barrierQueue = DispatchQueue(label: "com.memola.app.graphic-context", attributes: .concurrent)

    init() {
        setViewPortVertices()
    }

    func setViewPortVertices() {
        vertexBuffer = nil
        vertices = [
            ViewPortVertex(x: -1, y: -1, textCoord: CGPoint(x: 0, y: 1)),
            ViewPortVertex(x: -1, y: 1, textCoord: CGPoint(x: 0, y: 0)),
            ViewPortVertex(x: 1, y: -1, textCoord: CGPoint(x: 1, y: 1)),
            ViewPortVertex(x: 1, y: 1, textCoord: CGPoint(x: 1, y: 0)),
        ]
    }

    func undoGraphic(for event: HistoryEvent) {
        switch event {
        case .stroke(let stroke):
            switch stroke.style {
            case .marker:
                guard let penStroke = stroke.stroke(as: PenStroke.self) else { return }
                tree.remove(penStroke.element, in: penStroke.strokeBox)
                withPersistence(\.backgroundContext) { [weak penStroke] context in
                    penStroke?.object?.element?.graphicContext = nil
                    try context.saveIfNeeded()
                    context.refreshAllObjects()
                }
            case .eraser:
                guard let eraserStroke = stroke.stroke(as: EraserStroke.self) else { return }
                eraserStrokes.remove(eraserStroke)
                withPersistence(\.backgroundContext) { [weak eraserStroke] context in
                    guard let eraserStroke else { return }
                    for penStroke in eraserStroke.penStrokes.allObjects {
                        penStroke.eraserStrokes.remove(eraserStroke)
                        if let object = eraserStroke.object {
                            penStroke.object?.erasers.remove(object)
                        }
                    }
                    try context.saveIfNeeded()
                    context.refreshAllObjects()
                }
            }
            previousElement = nil
        case .photo(let photo):
            tree.remove(photo.element, in: photo.photoBox)
            withPersistence(\.backgroundContext) { [weak photo] context in
                photo?.object?.element?.graphicContext = nil
                try context.saveIfNeeded()
                context.refreshAllObjects()
            }
        }
    }

    func redoGraphic(for event: HistoryEvent) {
        switch event {
        case .stroke(let stroke):
            switch stroke.style {
            case .marker:
                guard let penStroke = stroke.stroke(as: PenStroke.self) else {
                    break
                }
                tree.insert(penStroke.element, in: penStroke.strokeBox)
                withPersistence(\.backgroundContext) { [weak self, weak penStroke] context in
                    penStroke?.object?.element?.graphicContext = self?.object
                    try context.saveIfNeeded()
                    context.refreshAllObjects()
                }
            case .eraser:
                guard let eraserStroke = stroke.stroke(as: EraserStroke.self) else {
                    break
                }
                eraserStrokes.insert(eraserStroke)
                withPersistence(\.backgroundContext) { [weak eraserStroke] context in
                    guard let eraserStroke else { return }
                    for penStroke in eraserStroke.penStrokes.allObjects {
                        penStroke.eraserStrokes.insert(eraserStroke)
                        if let object = eraserStroke.object {
                            penStroke.object?.erasers.add(object)
                        }
                    }
                    try context.saveIfNeeded()
                    context.refreshAllObjects()
                }
            }
            previousElement = nil
        case .photo(let photo):
            tree.insert(photo.element, in: photo.photoBox)
            withPersistence(\.backgroundContext) { [weak self, weak photo] context in
                photo?.object?.element?.graphicContext = self?.object
                try context.saveIfNeeded()
                context.refreshAllObjects()
            }
        }
    }
}

extension GraphicContext {
    func loadStrokes(_ bounds: CGRect) {
        guard let object else { return }
        let queue = OperationQueue()
        queue.qualityOfService = .userInteractive
        object.elements.forEach { element in
            guard let element = element as? ElementObject else { return }
            switch element.type {
            case 0:
                guard let stroke = element.stroke, stroke.style == 0 else { return }
                let _stroke = PenStroke(object: stroke)
                tree.insert(_stroke.element, in: _stroke.strokeBox)
                if _stroke.isVisible(in: bounds) {
                    let id = stroke.objectID
                    queue.addOperation { [weak self] in
                        guard let self else { return }
                        withPersistenceSync(\.newBackgroundContext) { [weak _stroke] context in
                            guard let stroke = try? context.existingObject(with: id) as? StrokeObject else { return }
                            _stroke?.loadQuads(from: stroke, with: self)
                            context.refreshAllObjects()
                        }
                    }
                } else {
                    withPersistence(\.backgroundContext) { [weak self, weak _stroke] context in
                        guard let self else { return }
                        _stroke?.loadQuads(with: self)
                        context.refreshAllObjects()
                    }
                }
            case 1:
                guard let photo = element.photo, photo.file?.imageURL != nil else { return }
                let _photo = Photo(object: photo)
                tree.insert(_photo.element, in: _photo.photoBox)
            default:
                break
            }

        }
        queue.waitUntilAllOperationsAreFinished()
    }

    func loadQuads(_ bounds: CGRect, on context: NSManagedObjectContext) {
        for element in self.tree.search(box: bounds.box) {
            guard let stroke = element.stroke(as: PenStroke.self), stroke.isEmpty else { continue }
            stroke.loadQuads(with: self)
        }
    }
}

extension GraphicContext: Drawable {
    func prepare(device: MTLDevice) {
        vertexCount = vertices.count
        vertexBuffer = device.makeBuffer(bytes: vertices, length: vertexCount * MemoryLayout<ViewPortVertex>.stride, options: [])
    }

    func draw(device: MTLDevice, renderEncoder: MTLRenderCommandEncoder) {
        prepare(device: device)
        renderEncoder.setVertexBuffer(vertexBuffer, offset: 0, index: 0)
        renderEncoder.drawPrimitives(type: .triangleStrip, vertexStart: 0, vertexCount: vertices.count)
    }
}

// MARK: - Stroke
extension GraphicContext {
    func beginStroke(at point: CGPoint, pen: Pen) -> any Stroke {
        let stroke: any Stroke
        switch pen.strokeStyle {
        case .marker:
            let penStroke = PenStroke(
                bounds: [point.x - pen.thickness, point.y - pen.thickness, point.x + pen.thickness, point.y + pen.thickness],
                color: pen.rgba,
                style: pen.strokeStyle,
                createdAt: .now,
                thickness: pen.thickness
            )
            withPersistence(\.backgroundContext) { [weak graphicContext = object, weak _stroke = penStroke] context in
                guard let _stroke else { return }
                let stroke = StrokeObject(\.backgroundContext)
                stroke.bounds = _stroke.bounds
                stroke.color = _stroke.color
                stroke.style = _stroke.style.rawValue
                stroke.thickness = _stroke.thickness
                stroke.createdAt = _stroke.createdAt
                stroke.quads = []
                stroke.erasers = .init()
                let element = ElementObject(\.backgroundContext)
                element.createdAt = _stroke.createdAt
                element.type = 0
                element.graphicContext = graphicContext
                stroke.element = element
                element.stroke = stroke
                graphicContext?.elements.add(element)
                _stroke.object = stroke
                try context.saveIfNeeded()
            }
            stroke = penStroke
        case .eraser:
            let eraserStroke = EraserStroke(
                bounds: [point.x - pen.thickness, point.y - pen.thickness, point.x + pen.thickness, point.y + pen.thickness],
                color: pen.rgba,
                style: pen.strokeStyle,
                createdAt: .now,
                thickness: pen.thickness
            )
            eraserStroke.graphicContext = self
            withPersistence(\.backgroundContext) { [weak _stroke = eraserStroke] context in
                guard let _stroke else { return }
                let stroke = EraserObject(\.backgroundContext)
                stroke.bounds = _stroke.bounds
                stroke.color = _stroke.color
                stroke.style = _stroke.style.rawValue
                stroke.thickness = _stroke.thickness
                stroke.createdAt = _stroke.createdAt
                stroke.quads = []
                stroke.strokes = .init()
                _stroke.object = stroke
                try context.saveIfNeeded()
            }
            stroke = eraserStroke
        }
        currentElement = .stroke(stroke.anyStroke)
        currentPoint = point
        currentElement?.stroke()?.begin(at: point)
        return stroke
    }

    func appendStroke(with point: CGPoint) {
        guard let currentStroke = currentElement?.stroke() else { return }
        currentStroke.append(to: point)
        self.currentPoint = point
    }

    func endStroke(at point: CGPoint) {
        guard currentPoint != nil, let currentStroke = currentElement?.stroke() else { return }
        currentStroke.finish(at: point)
        if let penStroke = currentStroke.stroke(as: PenStroke.self) {
            penStroke.saveQuads()
            tree.insert(currentStroke.element, in: currentStroke.strokeBox)
            withPersistence(\.backgroundContext) { [weak penStroke] context in
                guard let penStroke else { return }
                penStroke.object?.bounds = penStroke.bounds
                try context.saveIfNeeded()
                context.refreshAllObjects()
            }
        } else if let eraserStroke = currentStroke.stroke(as: EraserStroke.self) {
            eraserStroke.saveQuads()
            eraserStrokes.insert(eraserStroke)
            withPersistence(\.backgroundContext) { [weak eraserStroke] context in
                guard let eraserStroke else { return }
                eraserStroke.object?.bounds = eraserStroke.bounds
                try context.saveIfNeeded()
                context.refreshAllObjects()
            }
        }
        previousElement = currentElement
        self.currentElement = nil
        self.currentPoint = nil
    }

    func cancelStroke() {
        if let stroke = currentElement?.stroke() {
            switch stroke.style {
            case .marker:
                guard let _stroke = stroke.stroke(as: PenStroke.self) else { break }
                withPersistence(\.backgroundContext) { [weak graphicContext = object, weak _stroke] context in
                    guard let _stroke else { return }
                    if let element = _stroke.object?.element {
                        graphicContext?.elements.remove(element)
                    }
                    try context.saveIfNeeded()
                }
            case .eraser:
                guard let eraserStroke = stroke.stroke(as: EraserStroke.self) else { break }
                eraserStrokes.remove(eraserStroke)
                withPersistence(\.backgroundContext) { [weak eraserStroke] context in
                    if let stroke = eraserStroke?.object {
                        context.delete(stroke)
                    }
                    try context.saveIfNeeded()
                }
            }
        }
        currentElement = nil
        currentPoint = nil
    }
}

// MARK: - Photo
extension GraphicContext {
    func insertPhoto(at point: CGPoint, photoFile: PhotoFileObject) -> Photo {
        let size = photoFile.photoDimension()
        let origin = point
        let bounds = [origin.x - size.width / 2, origin.y - size.height / 2, origin.x + size.width / 2, origin.y + size.height / 2]
        let photo = Photo(url: photoFile.imageURL, size: size, origin: origin, bounds: bounds, createdAt: .now, bookmark: photoFile.bookmark)
        tree.insert(photo.element, in: photo.photoBox)
        let photoFileID = photoFile.objectID
        withPersistence(\.backgroundContext) { [weak _photo = photo, weak graphicContext = object] context in
            guard let _photo, let photoFile = try context.existingObject(with: photoFileID) as? PhotoFileObject else {
                return
            }
            let photo = PhotoObject(\.backgroundContext)
            photo.bounds = _photo.bounds
            photo.width = _photo.size.width
            photo.originY = _photo.origin.y
            photo.originX = _photo.origin.x
            photo.height = _photo.size.height
            photo.createdAt = _photo.createdAt
            photo.file = photoFile
            photoFile.photos?.add(photo)
            let element = ElementObject(\.backgroundContext)
            element.createdAt = _photo.createdAt
            element.type = 1
            element.graphicContext = graphicContext
            photo.element = element
            element.photo = photo
            graphicContext?.elements.add(element)
            _photo.object = photo
            try context.saveIfNeeded()
        }
        self.previousElement = .photo(photo)
        return photo
    }
}

extension GraphicContext {
    enum RenderType {
        case inProgress
        case newlyFinished
        case finished
    }
}
