import Quick
import Nimble
import simd
import GameKit

@testable import PerspectiveTransform

class ProjectionSpec: QuickSpec {
    override func spec() {
        describe("general2DProjection") {
            context("2 random Quadrilateral") {
                let source = GKRandomSource.sharedRandom()
                var a: Perspective!
                var b: Perspective!

                beforeEach {
                    a = Perspective(source.nextFourPoints())
                    b = Perspective(source.nextFourPoints())
                }

                context("reverse projection") {
                    it("should be invert of original") {
                        let forward = a.projection(to: b)
                        let reverse = b.projection(to: a)
                        expect(forward.inverse) ≈ (reverse, 0.0001)
                    }
                }
            }
            context("isosceles trapezoid") {
                var from: Perspective!
                var to: Perspective!

                beforeEach {
                    from = Perspective(CGRect(origin: CGPoint.zero, size: CGSize(width: 1, height: 1)))
                    to = Perspective(
                        CGPoint(x: -1, y: 0), // shifted left by 1
                        CGPoint(x: 2, y: 0), // shifted right by 1
                        CGPoint(x: 1, y: 1),
                        CGPoint(x: 0, y: 1)
                    )
                }

                it("should be non affine and have 3D rotation") {
                    let transform = from.projectiveTransform(destination: to)
                    expect(CATransform3DIsIdentity(transform)) == false
                    expect(CATransform3DIsAffine(transform)) == false
                }
            }
            context("to the same perspective") {
                var from: Perspective!
                var to: Perspective!

                beforeEach {
                    from = Perspective(CGRect(origin: CGPoint.zero, size: CGSize(width: 10, height: 10)))
                    to = from
                }

                it("should be identity") {
                    let projection = from.projection(to: to)
                    let transform3D = CATransform3D(projection.to3d())
                    expect(CATransform3DIsIdentity(transform3D)) == true

                    expect(transform3D.component(for: .scale)) == Vector3(1, 1, 1)
                    expect(transform3D.component(for: .rotation)) == Vector3(0, 0, 0)
                    expect(transform3D.component(for: .translation)) == Vector3(0, 0, 0)
                }
            }

            context("rotation") {
                // Image showing 3D transform matrix values
                // https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/CoreAnimation_guide/Art/transform_manipulations_2x.png
                var from: Perspective!
                var to: Perspective!

                beforeEach {
                    let points = Quadrilateral(CGRect(origin: CGPoint.zero, size: CGSize(width: 10, height: 10))).corners
                    from = Perspective(points)
                    let turnedRight = [points[1], points[3], points[0], points[2]]
                    to = Perspective(turnedRight)
                }

                it("should match rotate around 0,0 and translate back to 0") {
                    let rotate3D = CATransform3DMakeRotation(.pi / 2, 0, 0, 1)
                    let translate3D = CATransform3DMakeTranslation(10, 0, 0)
                    let combined = CATransform3DConcat(rotate3D, translate3D)

                    let projection = from.projection(to: to)
                    expect(CATransform3D(projection.to3d())) == combined
                }

                context("matrix values") {
                    var matrix: CATransform3D!

                    beforeEach {
                        let projection = from.projection(to: to)
                        matrix = CATransform3D(projection.to3d())
                    }

                    it("should contain rotation around z axis") {
                        let angle = CGFloat.pi / 2
                        expect(matrix.m11) ≈ cos(angle)
                        expect(matrix.m22) ≈ cos(angle)
                        expect(matrix.m33) == 1
                        expect(matrix.m12) ≈ sin(angle)
                        expect(matrix.m21) ≈ -sin(angle)
                        expect(matrix.m13) == 0
                        expect(matrix.m31) == 0
                    }

                    it("should have key path components of rotation and translation") {
                        expect(matrix.component(for: .scale)) == Vector3(1, 1, 1)
                        expect(matrix.component(for: .rotation)) == Vector3(0, 0, Double.pi / 2)
                        expect(matrix.component(for: .translation)) == Vector3(10, 0, 0)
                    }
                }
            }

            context("scale and translate") {
                let expected = Matrix3x3([200.0/152, 0, 100,
                                          0, 200.0/122, 100,
                                          0, 0, 1])
                var start: Perspective!
                var destination: Perspective!

                beforeEach {
                    start = Perspective(CGRect(origin: CGPoint.zero, size: CGSize(width: 152, height: 122)))
                    destination = Perspective(
                        CGRect(
                            origin: CGPoint(x: 100, y: 100),
                            size: CGSize(width: 200, height: 200)
                        )
                    )
                }

                it("should match expected") {
                    let projection = start.projection(to: destination)
                    expect(projection) ≈ expected
                }

                context("reverse") {
                    it("should be invert of original") {
                        let projection = start.projection(to: destination)
                        let reverse = destination.projection(to: start)
                        expect(projection.inverse) ≈ (reverse, 0.001)
                    }
                }

                context("concat") {
                    var expected3D: CATransform3D!
                    var translate3D: CATransform3D!
                    var scale3D: CATransform3D!

                    beforeEach {
                        expected3D = CATransform3D(expected.to3d())
                        translate3D = CATransform3DMakeTranslation(100, 100, 0)
                        scale3D = CATransform3DMakeScale(200.0/152, 200.0/122, 1)
                    }

                    it("should create 3D transformation scale + translate, in that order only") {
                        expect(CATransform3DConcat(scale3D, translate3D)) == expected3D
                        expect(CATransform3DConcat(translate3D, scale3D)) != expected3D
                    }

                    context("expected 3d matrix") {
                        it("should contain scale values on the diagonal") {
                            [expected3D, scale3D].forEach { transform3d in
                                let matrix = transform3d!
                                expect(matrix.m11) == 200.0/152
                                expect(matrix.m22) == 200.0/122
                                expect(matrix.m33) == 1
                                expect(matrix.m44) == 1
                            }
                        }

                        it("should contain translate values in row 4") {
                            [expected3D, translate3D].forEach { transform3d in
                                let matrix = transform3d!
                                expect(matrix.m41) == 100
                                expect(matrix.m42) == 100
                                expect(matrix.m43) == 0
                                expect(matrix.m44) == 1
                            }
                        }

                        context("without scale and translate") {
                            var matrix: CATransform3D!

                            beforeEach {
                                matrix = expected3D!
                                matrix.m11 = 1
                                matrix.m22 = 1
                                matrix.m41 = 0
                                matrix.m42 = 0
                            }

                            it("should have no other value and thus be identity") {
                                expect(CATransform3DIsIdentity(matrix)) == true
                            }
                        }
                    }

                    context("affine 2D") {
                        var scale2D: CGAffineTransform!
                        var translate2D: CGAffineTransform!
                        var expected2D: CGAffineTransform!

                        beforeEach {
                            scale2D = CATransform3DGetAffineTransform(scale3D)
                            translate2D = CATransform3DGetAffineTransform(translate3D)
                            expected2D = CATransform3DGetAffineTransform(expected3D)
                        }

                        it("should all be affine") {
                            let isAffine = [scale3D, translate3D, expected3D].map {CATransform3DIsAffine($0)}
                            expect(isAffine).to(allPass(beTrue()))
                        }

                        it("should create 2D affine tranformation") {
                            expect(scale2D.concatenating(translate2D)) == expected2D
                        }
                    }
                }
            }
        }
    }
}
