// Copyright (c) 2014 - 2017 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//
//
// RUN: %target-run-simple-swift
// REQUIRES: executable_test
// REQUIRES: objc_interop

import Foundation

#if os(OSX)

#if FOUNDATION_XCTEST
import XCTest
class TestAffineTransformSuper : XCTestCase { }
#else
import StdlibUnittest
class TestAffineTransformSuper { }
#endif

func expectEqualWithAccuracy(_ lhs: Double, _ rhs: Double, accuracy: Double, _ message: String = "", file: String = #file, line: UInt = #line) {
    expectTrue(fabs(lhs - rhs) < accuracy, message, file: file, line: line)
}

extension AffineTransform {
    func transform(_ aRect: NSRect) -> NSRect {
        return NSRect(origin: transform(aRect.origin), size: transform(aRect.size))
    }
}

class TestAffineTransform : TestAffineTransformSuper {
    private let accuracyThreshold = 0.001
    
    func checkPointTransformation(_ transform: AffineTransform, point: NSPoint, expectedPoint: NSPoint, _ message: String = "", file: String = #file, line: UInt = #line) {
        let newPoint = transform.transform(point)
        expectEqualWithAccuracy(Double(newPoint.x), Double(expectedPoint.x), accuracy: accuracyThreshold,
                                   "x (expected: \(expectedPoint.x), was: \(newPoint.x)): \(message)", file: file, line: line)
        expectEqualWithAccuracy(Double(newPoint.y), Double(expectedPoint.y), accuracy: accuracyThreshold,
                                   "y (expected: \(expectedPoint.y), was: \(newPoint.y)): \(message)", file: file, line: line)
    }
    
    func checkSizeTransformation(_ transform: AffineTransform, size: NSSize, expectedSize: NSSize, _ message: String = "", file: String = #file, line: UInt = #line) {
        let newSize = transform.transform(size)
        expectEqualWithAccuracy(Double(newSize.width), Double(expectedSize.width), accuracy: accuracyThreshold,
                                   "width (expected: \(expectedSize.width), was: \(newSize.width)): \(message)", file: file, line: line)
        expectEqualWithAccuracy(Double(newSize.height), Double(expectedSize.height), accuracy: accuracyThreshold,
                                   "height (expected: \(expectedSize.height), was: \(newSize.height)): \(message)", file: file, line: line)
    }
    
    func checkRectTransformation(_ transform: AffineTransform, rect: NSRect, expectedRect: NSRect, _ message: String = "", file: String = #file, line: UInt = #line) {
        let newRect = transform.transform(rect)
        
        checkPointTransformation(transform, point: newRect.origin, expectedPoint: expectedRect.origin,
                                 "origin (expected: \(expectedRect.origin), was: \(newRect.origin)): \(message)", file: file, line: line)
        checkSizeTransformation(transform, size: newRect.size, expectedSize: expectedRect.size,
                                "size (expected: \(expectedRect.size), was: \(newRect.size)): \(message)", file: file, line: line)
    }
    
    func test_BasicConstruction() {
        let defaultAffineTransform = AffineTransform()
        let identityTransform = AffineTransform.identity

        expectEqual(defaultAffineTransform, identityTransform)
        
        // The diagonal entries (1,1) and (2,2) of the identity matrix are ones. The other entries are zeros.
        // TODO: These should use DBL_MAX but it's not available as part of Glibc on Linux
        expectEqualWithAccuracy(Double(identityTransform.m11), Double(1), accuracy: accuracyThreshold)
        expectEqualWithAccuracy(Double(identityTransform.m22), Double(1), accuracy: accuracyThreshold)
        
        expectEqualWithAccuracy(Double(identityTransform.m12), Double(0), accuracy: accuracyThreshold)
        expectEqualWithAccuracy(Double(identityTransform.m21), Double(0), accuracy: accuracyThreshold)
        expectEqualWithAccuracy(Double(identityTransform.tX), Double(0), accuracy: accuracyThreshold)
        expectEqualWithAccuracy(Double(identityTransform.tY), Double(0), accuracy: accuracyThreshold)
    }
    
    func test_IdentityTransformation() {
        let identityTransform = AffineTransform.identity
        
        func checkIdentityPointTransformation(_ point: NSPoint) {
            checkPointTransformation(identityTransform, point: point, expectedPoint: point)
        }
        
        checkIdentityPointTransformation(NSPoint())
        checkIdentityPointTransformation(NSPoint(x: CGFloat(24.5), y: CGFloat(10.0)))
        checkIdentityPointTransformation(NSPoint(x: CGFloat(-7.5), y: CGFloat(2.0)))
        
        func checkIdentitySizeTransformation(_ size: NSSize) {
            checkSizeTransformation(identityTransform, size: size, expectedSize: size)
        }
        
        checkIdentitySizeTransformation(NSSize())
        checkIdentitySizeTransformation(NSSize(width: CGFloat(13.0), height: CGFloat(12.5)))
        checkIdentitySizeTransformation(NSSize(width: CGFloat(100.0), height: CGFloat(-100.0)))
    }
    
    func test_Translation() {
        let point = NSPoint(x: CGFloat(0.0), y: CGFloat(0.0))
        
        var noop = AffineTransform.identity
        noop.translate(x: CGFloat(), y: CGFloat())
        checkPointTransformation(noop, point: point, expectedPoint: point)
        
        var translateH = AffineTransform.identity
        translateH.translate(x: CGFloat(10.0), y: CGFloat())
        checkPointTransformation(translateH, point: point, expectedPoint: NSPoint(x: CGFloat(10.0), y: CGFloat()))
        
        var translateV = AffineTransform.identity
        translateV.translate(x: CGFloat(), y: CGFloat(20.0))
        checkPointTransformation(translateV, point: point, expectedPoint: NSPoint(x: CGFloat(), y: CGFloat(20.0)))
        
        var translate = AffineTransform.identity
        translate.translate(x: CGFloat(-30.0), y: CGFloat(40.0))
        checkPointTransformation(translate, point: point, expectedPoint: NSPoint(x: CGFloat(-30.0), y: CGFloat(40.0)))
    }
    
    func test_Scale() {
        let size = NSSize(width: CGFloat(10.0), height: CGFloat(10.0))
        
        var noop = AffineTransform.identity
        noop.scale(CGFloat(1.0))
        checkSizeTransformation(noop, size: size, expectedSize: size)
        
        var shrink = AffineTransform.identity
        shrink.scale(CGFloat(0.5))
        checkSizeTransformation(shrink, size: size, expectedSize: NSSize(width: CGFloat(5.0), height: CGFloat(5.0)))
        
        var grow = AffineTransform.identity
        grow.scale(CGFloat(3.0))
        checkSizeTransformation(grow, size: size, expectedSize: NSSize(width: CGFloat(30.0), height: CGFloat(30.0)))
        
        var stretch = AffineTransform.identity
        stretch.scale(x: CGFloat(2.0), y: CGFloat(0.5))
        checkSizeTransformation(stretch, size: size, expectedSize: NSSize(width: CGFloat(20.0), height: CGFloat(5.0)))
    }
    
    func test_Rotation_Degrees() {
        let point = NSPoint(x: CGFloat(10.0), y: CGFloat(10.0))
        
        var noop = AffineTransform.identity
        noop.rotate(byDegrees: CGFloat())
        checkPointTransformation(noop, point: point, expectedPoint: point)
        
        var tenEighty = AffineTransform.identity
        tenEighty.rotate(byDegrees: CGFloat(1080.0))
        checkPointTransformation(tenEighty, point: point, expectedPoint: point)
        
        var rotateCounterClockwise = AffineTransform.identity
        rotateCounterClockwise.rotate(byDegrees: CGFloat(90.0))
        checkPointTransformation(rotateCounterClockwise, point: point, expectedPoint: NSPoint(x: CGFloat(-10.0), y: CGFloat(10.0)))
        
        var rotateClockwise = AffineTransform.identity
        rotateClockwise.rotate(byDegrees: CGFloat(-90.0))
        checkPointTransformation(rotateClockwise, point: point, expectedPoint: NSPoint(x: CGFloat(10.0), y: CGFloat(-10.0)))
        
        var reflectAboutOrigin = AffineTransform.identity
        reflectAboutOrigin.rotate(byDegrees: CGFloat(180.0))
        checkPointTransformation(reflectAboutOrigin, point: point, expectedPoint: NSPoint(x: CGFloat(-10.0), y: CGFloat(-10.0)))
    }
    
    func test_Rotation_Radians() {
        let point = NSPoint(x: CGFloat(10.0), y: CGFloat(10.0))
        
        var noop = AffineTransform.identity
        noop.rotate(byRadians: CGFloat())
        checkPointTransformation(noop, point: point, expectedPoint: point)
        
        var tenEighty = AffineTransform.identity
        tenEighty.rotate(byRadians: 6 * .pi)
        checkPointTransformation(tenEighty, point: point, expectedPoint: point)
        
        var rotateCounterClockwise = AffineTransform.identity
        rotateCounterClockwise.rotate(byRadians: .pi / 2)
        checkPointTransformation(rotateCounterClockwise, point: point, expectedPoint: NSPoint(x: CGFloat(-10.0), y: CGFloat(10.0)))
        
        var rotateClockwise = AffineTransform.identity
        rotateClockwise.rotate(byRadians: -.pi / 2)
        checkPointTransformation(rotateClockwise, point: point, expectedPoint: NSPoint(x: CGFloat(10.0), y: CGFloat(-10.0)))
        
        var reflectAboutOrigin = AffineTransform.identity
        reflectAboutOrigin.rotate(byRadians: .pi)
        checkPointTransformation(reflectAboutOrigin, point: point, expectedPoint: NSPoint(x: CGFloat(-10.0), y: CGFloat(-10.0)))
        
        var scaleThenRotate = AffineTransform(scale: 2)
        scaleThenRotate.rotate(byRadians: .pi / 2)
        checkPointTransformation(scaleThenRotate, point: point, expectedPoint: NSPoint(x: CGFloat(-20.0), y: CGFloat(20.0)))
    }
    
    func test_Inversion() {
        let point = NSPoint(x: CGFloat(10.0), y: CGFloat(10.0))
        
        var translate = AffineTransform.identity
        translate.translate(x: CGFloat(-30.0), y: CGFloat(40.0))
        
        var rotate = AffineTransform.identity
        translate.rotate(byDegrees: CGFloat(30.0))
        
        var scale = AffineTransform.identity
        scale.scale(CGFloat(2.0))
        
        var identityTransform = AffineTransform.identity
        
        // append transformations
        identityTransform.append(translate)
        identityTransform.append(rotate)
        identityTransform.append(scale)
        
        // invert transformations
        scale.invert()
        rotate.invert()
        translate.invert()
        
        // append inverse transformations in reverse order
        identityTransform.append(scale)
        identityTransform.append(rotate)
        identityTransform.append(translate)
        
        checkPointTransformation(identityTransform, point: point, expectedPoint: point)
    }
    
    func test_TranslationComposed() {
        var xyPlus5 = AffineTransform.identity
        xyPlus5.translate(x: CGFloat(2.0), y: CGFloat(3.0))
        xyPlus5.translate(x: CGFloat(3.0), y: CGFloat(2.0))
        
        checkPointTransformation(xyPlus5, point: NSPoint(x: CGFloat(-2.0), y: CGFloat(-3.0)),
                                 expectedPoint: NSPoint(x: CGFloat(3.0), y: CGFloat(2.0)))
    }
    
    func test_Scaling() {
        var xyTimes5 = AffineTransform.identity
        xyTimes5.scale(CGFloat(5.0))
        
        checkPointTransformation(xyTimes5, point: NSPoint(x: CGFloat(-2.0), y: CGFloat(3.0)),
                                 expectedPoint: NSPoint(x: CGFloat(-10.0), y: CGFloat(15.0)))
        
        var xTimes2YTimes3 = AffineTransform.identity
        xTimes2YTimes3.scale(x: CGFloat(2.0), y: CGFloat(-3.0))
        
        checkPointTransformation(xTimes2YTimes3, point: NSPoint(x: CGFloat(-1.0), y: CGFloat(3.5)),
                                 expectedPoint: NSPoint(x: CGFloat(-2.0), y: CGFloat(-10.5)))
    }
    
    func test_TranslationScaling() {
        var xPlus2XYTimes5 = AffineTransform.identity
        xPlus2XYTimes5.translate(x: CGFloat(2.0), y: CGFloat())
        xPlus2XYTimes5.scale(x: CGFloat(5.0), y: CGFloat(-5.0))
        
        checkPointTransformation(xPlus2XYTimes5, point: NSPoint(x: CGFloat(1.0), y: CGFloat(2.0)),
                                 expectedPoint: NSPoint(x: CGFloat(7.0), y: CGFloat(-10.0)))
    }
    
    func test_ScalingTranslation() {
        var xyTimes5XPlus3 = AffineTransform.identity
        xyTimes5XPlus3.scale(CGFloat(5.0))
        xyTimes5XPlus3.translate(x: CGFloat(3.0), y: CGFloat())
        
        checkPointTransformation(xyTimes5XPlus3, point: NSPoint(x: CGFloat(1.0), y: CGFloat(2.0)),
                                 expectedPoint: NSPoint(x: CGFloat(20.0), y: CGFloat(10.0)))
    }
    
    func test_AppendTransform() {
        let point = NSPoint(x: CGFloat(10.0), y: CGFloat(10.0))
        
        var identityTransform = AffineTransform.identity
        identityTransform.append(identityTransform)
        checkPointTransformation(identityTransform, point: point, expectedPoint: point)
        
        let translate = AffineTransform(translationByX: 10.0, byY: 0.0)
        
        let scale = AffineTransform(scale: 2.0)
        
        var translateThenScale = translate
        translateThenScale.append(scale)
        checkPointTransformation(translateThenScale, point: point, expectedPoint: NSPoint(x: CGFloat(40.0), y: CGFloat(20.0)))
    }
    
    func test_PrependTransform() {
        let point = NSPoint(x: CGFloat(10.0), y: CGFloat(10.0))
        
        var identityTransform = AffineTransform.identity
        identityTransform.append(identityTransform)
        checkPointTransformation(identityTransform, point: point, expectedPoint: point)
        
        let translate = AffineTransform(translationByX: 10.0, byY: 0.0)
        
        let scale = AffineTransform(scale: 2.0)
        
        var scaleThenTranslate = translate
        scaleThenTranslate.prepend(scale)
        checkPointTransformation(scaleThenTranslate, point: point, expectedPoint: NSPoint(x: CGFloat(30.0), y: CGFloat(20.0)))
    }
    
    func test_TransformComposition() {
        let origin = NSPoint(x: CGFloat(10.0), y: CGFloat(10.0))
        let size = NSSize(width: CGFloat(40.0), height: CGFloat(20.0))
        let rect = NSRect(origin: origin, size: size)
        let center = NSPoint(x: NSMidX(rect), y: NSMidY(rect))
        
        let rotate = AffineTransform(rotationByDegrees: 90.0)
        
        let moveOrigin = AffineTransform(translationByX: -center.x, byY: -center.y)
        
        var moveBack = moveOrigin
        moveBack.invert()
        
        var rotateAboutCenter = rotate
        rotateAboutCenter.prepend(moveOrigin)
        rotateAboutCenter.append(moveBack)
        
        // center of rect shouldn't move as its the rotation anchor
        checkPointTransformation(rotateAboutCenter, point: center, expectedPoint: center)
    }
    
    func test_hashing_identity() {
        let ref = NSAffineTransform()
        let val = AffineTransform.identity
        expectEqual(ref.hashValue, val.hashValue)
    }
    
    func test_hashing_values() {
        // the transforms are made up and the values don't matter
        let values = [
            AffineTransform(m11: 1.0, m12: 2.5, m21: 66.2, m22: 40.2, tX: -5.5, tY: 3.7),
            AffineTransform(m11: -55.66, m12: 22.7, m21: 1.5, m22: 0.0, tX: -22, tY: -33),
            AffineTransform(m11: 4.5, m12: 1.1, m21: 0.025, m22: 0.077, tX: -0.55, tY: 33.2),
            AffineTransform(m11: 7.0, m12: -2.3, m21: 6.7, m22: 0.25, tX: 0.556, tY: 0.99),
            AffineTransform(m11: 0.498, m12: -0.284, m21: -0.742, m22: 0.3248, tX: 12, tY: 44)
        ]
        for val in values {
            let ref = val as NSAffineTransform
            expectEqual(ref.hashValue, val.hashValue)
        }
    }

    func test_AnyHashableContainingAffineTransform() {
        let values: [AffineTransform] = [
            AffineTransform.identity,
            AffineTransform(m11: -55.66, m12: 22.7, m21: 1.5, m22: 0.0, tX: -22, tY: -33),
            AffineTransform(m11: -55.66, m12: 22.7, m21: 1.5, m22: 0.0, tX: -22, tY: -33)
        ]
        let anyHashables = values.map(AnyHashable.init)
        expectEqual(AffineTransform.self, type(of: anyHashables[0].base))
        expectEqual(AffineTransform.self, type(of: anyHashables[1].base))
        expectEqual(AffineTransform.self, type(of: anyHashables[2].base))
        expectNotEqual(anyHashables[0], anyHashables[1])
        expectEqual(anyHashables[1], anyHashables[2])
    }

    func test_AnyHashableCreatedFromNSAffineTransform() {
        func makeNSAffineTransform(rotatedByDegrees angle: CGFloat) -> NSAffineTransform {
            let result = NSAffineTransform()
            result.rotate(byDegrees: angle)
            return result
        }
        let values: [NSAffineTransform] = [
            makeNSAffineTransform(rotatedByDegrees: 0),
            makeNSAffineTransform(rotatedByDegrees: 10),
            makeNSAffineTransform(rotatedByDegrees: 10),
        ]
        let anyHashables = values.map(AnyHashable.init)
        expectEqual(AffineTransform.self, type(of: anyHashables[0].base))
        expectEqual(AffineTransform.self, type(of: anyHashables[1].base))
        expectEqual(AffineTransform.self, type(of: anyHashables[2].base))
        expectNotEqual(anyHashables[0], anyHashables[1])
        expectEqual(anyHashables[1], anyHashables[2])
    }

    func test_unconditionallyBridgeFromObjectiveC() {
        expectEqual(AffineTransform(), AffineTransform._unconditionallyBridgeFromObjectiveC(nil))
    }

    func test_rotation_compose() {
        var t = AffineTransform.identity
        t.translate(x: 1.0, y: 1.0)
        t.rotate(byDegrees: 90)
        t.translate(x: -1.0, y: -1.0)
        let result = t.transform(NSPoint(x: 1.0, y: 2.0))
        expectEqualWithAccuracy(0.0, Double(result.x), accuracy: accuracyThreshold)
        expectEqualWithAccuracy(1.0, Double(result.y), accuracy: accuracyThreshold)
    }
}

#if !FOUNDATION_XCTEST
var AffineTransformTests = TestSuite("TestAffineTransform")
AffineTransformTests.test("test_BasicConstruction") { TestAffineTransform().test_BasicConstruction() }
AffineTransformTests.test("test_IdentityTransformation") { TestAffineTransform().test_IdentityTransformation() }
AffineTransformTests.test("test_Translation") { TestAffineTransform().test_Translation() }
AffineTransformTests.test("test_Scale") { TestAffineTransform().test_Scale() }
AffineTransformTests.test("test_Rotation_Degrees") { TestAffineTransform().test_Rotation_Degrees() }
AffineTransformTests.test("test_Rotation_Radians") { TestAffineTransform().test_Rotation_Radians() }
AffineTransformTests.test("test_Inversion") { TestAffineTransform().test_Inversion() }
AffineTransformTests.test("test_TranslationComposed") { TestAffineTransform().test_TranslationComposed() }
AffineTransformTests.test("test_Scaling") { TestAffineTransform().test_Scaling() }
AffineTransformTests.test("test_TranslationScaling") { TestAffineTransform().test_TranslationScaling() }
AffineTransformTests.test("test_ScalingTranslation") { TestAffineTransform().test_ScalingTranslation() }
AffineTransformTests.test("test_AppendTransform") { TestAffineTransform().test_AppendTransform() }
AffineTransformTests.test("test_PrependTransform") { TestAffineTransform().test_PrependTransform() }
AffineTransformTests.test("test_TransformComposition") { TestAffineTransform().test_TransformComposition() }
AffineTransformTests.test("test_hashing_identity") { TestAffineTransform().test_hashing_identity() }
AffineTransformTests.test("test_hashing_values") { TestAffineTransform().test_hashing_values() }
AffineTransformTests.test("test_AnyHashableContainingAffineTransform") { TestAffineTransform().test_AnyHashableContainingAffineTransform() }
AffineTransformTests.test("test_AnyHashableCreatedFromNSAffineTransform") { TestAffineTransform().test_AnyHashableCreatedFromNSAffineTransform() }
AffineTransformTests.test("test_unconditionallyBridgeFromObjectiveC") { TestAffineTransform().test_unconditionallyBridgeFromObjectiveC() }
AffineTransformTests.test("test_rotation_compose") { TestAffineTransform().test_rotation_compose() }
runAllTests()
#endif
    

#endif
