using ColorTypes
using ColorTypes.FixedPointNumbers
using Test
using ColorTypes: ColorTypeResolutionError

@testset "rgb promotions" begin
    @test promote( RGB{N0f8}(0.2,0.3,0.4),  RGB(0.3,0.8,0.1)) === ( RGB{Float64}(0.2N0f8,0.3N0f8,0.4N0f8),  RGB{Float64}(0.3,0.8,0.1))
    @test promote( RGB{N0f8}(0.2,0.3,0.4), RGBA(0.3,0.8,0.1)) === (RGBA{Float64}(0.2N0f8,0.3N0f8,0.4N0f8), RGBA{Float64}(0.3,0.8,0.1))
    @test promote( RGB{N0f8}(0.2,0.3,0.4), ARGB(0.3,0.8,0.1)) === (ARGB{Float64}(0.2N0f8,0.3N0f8,0.4N0f8), ARGB{Float64}(0.3,0.8,0.1))
    @test promote(RGBA{N0f8}(0.2,0.3,0.4),  RGB(0.3,0.8,0.1)) === (RGBA{Float64}(0.2N0f8,0.3N0f8,0.4N0f8), RGBA{Float64}(0.3,0.8,0.1))
    @test promote(RGBA{N0f8}(0.2,0.3,0.4), RGBA(0.3,0.8,0.1)) === (RGBA{Float64}(0.2N0f8,0.3N0f8,0.4N0f8), RGBA{Float64}(0.3,0.8,0.1))
    @test promote(ARGB{N0f8}(0.2,0.3,0.4),  RGB(0.3,0.8,0.1)) === (ARGB{Float64}(0.2N0f8,0.3N0f8,0.4N0f8), ARGB{Float64}(0.3,0.8,0.1))
    @test promote(ARGB{N0f8}(0.2,0.3,0.4), ARGB(0.3,0.8,0.1)) === (ARGB{Float64}(0.2N0f8,0.3N0f8,0.4N0f8), ARGB{Float64}(0.3,0.8,0.1))

    @test promote( RGB24(0.2,0.3,0.4),  RGB(0.3,0.8,0.1)) === ( RGB{Float64}(0.2N0f8,0.3N0f8,0.4N0f8),  RGB{Float64}(0.3,0.8,0.1))
    @test promote( RGB24(0.2,0.3,0.4), RGBA(0.3,0.8,0.1)) === (RGBA{Float64}(0.2N0f8,0.3N0f8,0.4N0f8), RGBA{Float64}(0.3,0.8,0.1))
    @test promote( RGB24(0.2,0.3,0.4), ARGB(0.3,0.8,0.1)) === (ARGB{Float64}(0.2N0f8,0.3N0f8,0.4N0f8), ARGB{Float64}(0.3,0.8,0.1))
    @test promote(ARGB32(0.2,0.3,0.4),  RGB(0.3,0.8,0.1)) === (ARGB{Float64}(0.2N0f8,0.3N0f8,0.4N0f8), ARGB{Float64}(0.3,0.8,0.1))
    @test promote(ARGB32(0.2,0.3,0.4), ARGB(0.3,0.8,0.1)) === (ARGB{Float64}(0.2N0f8,0.3N0f8,0.4N0f8), ARGB{Float64}(0.3,0.8,0.1))

    @test promote( RGB24(0.2,0.3,0.4),  RGB{N0f8}(0.3,0.8,0.1)) === ( RGB{N0f8}(0.2,0.3,0.4),  RGB{N0f8}(0.3,0.8,0.1))
    @test promote( RGB24(0.2,0.3,0.4), RGBA{N0f8}(0.3,0.8,0.1)) === (RGBA{N0f8}(0.2,0.3,0.4), RGBA{N0f8}(0.3,0.8,0.1))
    @test promote( RGB24(0.2,0.3,0.4), ARGB{N0f8}(0.3,0.8,0.1)) === (ARGB{N0f8}(0.2,0.3,0.4), ARGB{N0f8}(0.3,0.8,0.1))
    @test promote(ARGB32(0.2,0.3,0.4),  RGB{N0f8}(0.3,0.8,0.1)) === (ARGB{N0f8}(0.2,0.3,0.4), ARGB{N0f8}(0.3,0.8,0.1))
    @test promote(ARGB32(0.2,0.3,0.4), ARGB{N0f8}(0.3,0.8,0.1)) === (ARGB{N0f8}(0.2,0.3,0.4), ARGB{N0f8}(0.3,0.8,0.1))

    @test promote(RGBX{N0f8}(0.2,0.3,0.4),  RGB(0.3,0.8,0.1)) === (RGBX{Float64}(0.2N0f8,0.3N0f8,0.4N0f8), RGBX{Float64}(0.3,0.8,0.1))
    @test promote(RGBX{N0f8}(0.2,0.3,0.4), RGBA(0.3,0.8,0.1)) === (RGBA{Float64}(0.2N0f8,0.3N0f8,0.4N0f8), RGBA{Float64}(0.3,0.8,0.1))
    @test promote(RGBX{N0f8}(0.2,0.3,0.4), ARGB(0.3,0.8,0.1)) === (ARGB{Float64}(0.2N0f8,0.3N0f8,0.4N0f8), ARGB{Float64}(0.3,0.8,0.1))
    @test promote(XRGB{N0f8}(0.2,0.3,0.4),  RGB(0.3,0.8,0.1)) === (XRGB{Float64}(0.2N0f8,0.3N0f8,0.4N0f8), XRGB{Float64}(0.3,0.8,0.1))
    @test promote(XRGB{N0f8}(0.2,0.3,0.4), RGBA(0.3,0.8,0.1)) === (RGBA{Float64}(0.2N0f8,0.3N0f8,0.4N0f8), RGBA{Float64}(0.3,0.8,0.1))
    @test promote(XRGB{N0f8}(0.2,0.3,0.4), ARGB(0.3,0.8,0.1)) === (ARGB{Float64}(0.2N0f8,0.3N0f8,0.4N0f8), ARGB{Float64}(0.3,0.8,0.1))

    @test promote(RGBX(0.2,0.3,0.4),  RGB{N0f8}(0.3,0.8,0.1)) === (RGBX{Float64}(0.2,0.3,0.4), RGBX{Float64}(0.3N0f8,0.8N0f8,0.1N0f8))
    @test promote(RGBX(0.2,0.3,0.4), RGBA{N0f8}(0.3,0.8,0.1)) === (RGBA{Float64}(0.2,0.3,0.4), RGBA{Float64}(0.3N0f8,0.8N0f8,0.1N0f8))
    @test promote(RGBX(0.2,0.3,0.4), ARGB{N0f8}(0.3,0.8,0.1)) === (ARGB{Float64}(0.2,0.3,0.4), ARGB{Float64}(0.3N0f8,0.8N0f8,0.1N0f8))
    @test promote(XRGB(0.2,0.3,0.4),  RGB{N0f8}(0.3,0.8,0.1)) === (XRGB{Float64}(0.2,0.3,0.4), XRGB{Float64}(0.3N0f8,0.8N0f8,0.1N0f8))
    @test promote(XRGB(0.2,0.3,0.4), RGBA{N0f8}(0.3,0.8,0.1)) === (RGBA{Float64}(0.2,0.3,0.4), RGBA{Float64}(0.3N0f8,0.8N0f8,0.1N0f8))
    @test promote(XRGB(0.2,0.3,0.4), ARGB{N0f8}(0.3,0.8,0.1)) === (ARGB{Float64}(0.2,0.3,0.4), ARGB{Float64}(0.3N0f8,0.8N0f8,0.1N0f8))

    @test promote(RGB24(0.2,0.3,0.4), ARGB32(0.3,0.8,0.1)) === (ARGB32(0.2,0.3,0.4), ARGB32(0.3,0.8,0.1))

    @test promote_type(RGB, RGB) === RGB
    @test promote_type(RGB, RGB{Float16}) === RGB

    @test promote_type(RGB, RGBA) === RGBA
    @test promote_type(ARGB, RGB) === ARGB
    @test promote_type(RGB, RGBA{Float16}) === RGBA
    @test promote_type(ARGB, RGB{Float16}) === ARGB

    @test promote_type(RGBA, RGBA) === RGBA
    @test promote_type(ARGB, ARGB) === ARGB
    @test promote_type(ARGB, RGBA) === ARGB
    @test promote_type(RGBA, RGBA{Float16}) === RGBA
    @test promote_type(ARGB, ARGB{Float16}) === ARGB
    @test promote_type(ARGB, RGBA{Float16}) === ARGB

    @test promote_type(RGB, RGB24) === RGB
    @test promote_type(BGR, RGB24) === RGB
    @test promote_type(XRGB, RGB24) === XRGB
    @test promote_type(RGBX, RGB24) === RGBX
    @test promote_type(RGB, ARGB32) === ARGB
    @test promote_type(BGR, ARGB32) === ARGB
    @test promote_type(XRGB, ARGB32) === ARGB
    @test promote_type(RGBX, ARGB32) === ARGB

    @test promote_type(RGB, BGR) === RGB
    @test promote_type(RGB, BGR{Float16}) === RGB
    @test promote_type(BGR, RGB{Float16}) === RGB

    @test promote_type(RGB, RGBX) === RGBX
    @test promote_type(XRGB, RGB) === XRGB
    @test promote_type(RGB, RGBX{Float16}) === RGBX
    @test promote_type(XRGB, RGB{Float16}) === XRGB

    @test promote_type(AbstractRGB, RGB{Float16}) === RGB
    @test promote_type(RGB, AbstractRGB{Float16}) === RGB
    @test promote_type(AbstractRGB{Float16}, RGB{Float16}) === RGB{Float16}
    @test promote_type(AbstractRGB{Float16}, RGB24) === RGB{Float32}
end

@testset "hsv promotions" begin
    @test promote( HSV{Float32}(100,0.3,0.4),  HSV(200,0.8,0.1)) === ( HSV{Float64}(100,0.3f0,0.4f0),  HSV{Float64}(200,0.8,0.1))
    @test promote( HSV{Float32}(100,0.3,0.4), HSVA(200,0.8,0.1)) === (HSVA{Float64}(100,0.3f0,0.4f0), HSVA{Float64}(200,0.8,0.1))
    @test promote( HSV{Float32}(100,0.3,0.4), AHSV(200,0.8,0.1)) === (AHSV{Float64}(100,0.3f0,0.4f0), AHSV{Float64}(200,0.8,0.1))
    @test promote(HSVA{Float32}(100,0.3,0.4),  HSV(200,0.8,0.1)) === (HSVA{Float64}(100,0.3f0,0.4f0), HSVA{Float64}(200,0.8,0.1))
    @test promote(HSVA{Float32}(100,0.3,0.4), HSVA(200,0.8,0.1)) === (HSVA{Float64}(100,0.3f0,0.4f0), HSVA{Float64}(200,0.8,0.1))
    @test promote(AHSV{Float32}(100,0.3,0.4),  HSV(200,0.8,0.1)) === (AHSV{Float64}(100,0.3f0,0.4f0), AHSV{Float64}(200,0.8,0.1))
    @test promote(AHSV{Float32}(100,0.3,0.4), AHSV(200,0.8,0.1)) === (AHSV{Float64}(100,0.3f0,0.4f0), AHSV{Float64}(200,0.8,0.1))

    @test promote_type(HSV, HSV) === HSV
    @test promote_type(HSV, HSV{Float16}) === HSV

    @test promote_type(HSV, HSVA) === HSVA
    @test promote_type(AHSV, HSV) === AHSV
    @test promote_type(HSV, HSVA{Float16}) === HSVA
    @test promote_type(AHSV, HSV{Float16}) === AHSV
end

@testset "gray promotions" begin
    @test promote( Gray{N0f8}(0.2),  Gray(0.3)) === ( Gray{Float64}(0.2N0f8),  Gray{Float64}(0.3))
    @test promote( Gray{N0f8}(0.2), GrayA(0.3)) === (GrayA{Float64}(0.2N0f8), GrayA{Float64}(0.3))
    @test promote( Gray{N0f8}(0.2), AGray(0.3)) === (AGray{Float64}(0.2N0f8), AGray{Float64}(0.3))
    @test promote(GrayA{N0f8}(0.2),  Gray(0.3)) === (GrayA{Float64}(0.2N0f8), GrayA{Float64}(0.3))
    @test promote(GrayA{N0f8}(0.2), GrayA(0.3)) === (GrayA{Float64}(0.2N0f8), GrayA{Float64}(0.3))
    @test promote(AGray{N0f8}(0.2),  Gray(0.3)) === (AGray{Float64}(0.2N0f8), AGray{Float64}(0.3))
    @test promote(AGray{N0f8}(0.2), AGray(0.3)) === (AGray{Float64}(0.2N0f8), AGray{Float64}(0.3))

    @test promote( Gray24(0.2),  Gray(0.3)) === ( Gray{Float64}(0.2N0f8),  Gray{Float64}(0.3))
    @test promote( Gray24(0.2), GrayA(0.3)) === (GrayA{Float64}(0.2N0f8), GrayA{Float64}(0.3))
    @test promote( Gray24(0.2), AGray(0.3)) === (AGray{Float64}(0.2N0f8), AGray{Float64}(0.3))
    @test promote(AGray32(0.2),  Gray(0.3)) === (AGray{Float64}(0.2N0f8), AGray{Float64}(0.3))
    @test promote(AGray32(0.2), AGray(0.3)) === (AGray{Float64}(0.2N0f8), AGray{Float64}(0.3))
    @test promote( Gray24(0.2),  Gray{N0f8}(0.3)) === ( Gray{N0f8}(0.2),  Gray{N0f8}(0.3))
    @test promote( Gray24(0.2), GrayA{N0f8}(0.3)) === (GrayA{N0f8}(0.2), GrayA{N0f8}(0.3))
    @test promote( Gray24(0.2), AGray{N0f8}(0.3)) === (AGray{N0f8}(0.2), AGray{N0f8}(0.3))
    @test promote(AGray32(0.2),  Gray{N0f8}(0.3)) === (AGray{N0f8}(0.2), AGray{N0f8}(0.3))
    @test promote(AGray32(0.2), AGray{N0f8}(0.3)) === (AGray{N0f8}(0.2), AGray{N0f8}(0.3))

    @test promote(AGray32(0.2), Gray24(0.3)) === (AGray32(0.2N0f8), AGray32(0.3))

    @test promote_type(Gray, Gray) === Gray
    @test promote_type(Gray, Gray{Float16}) === Gray

    @test promote_type(Gray, GrayA) === GrayA
    @test promote_type(AGray, Gray) === AGray
    @test promote_type(Gray, GrayA{Float16}) === GrayA
    @test promote_type(AGray, Gray{Float16}) === AGray

    @test promote_type(AGray, Gray{Bool}) === AGray

    @test promote_type(Gray, Gray24) === Gray
    @test promote_type(Gray, AGray32) === AGray

    @test promote_type(AbstractGray, Gray{Float16}) === Gray
    @test promote_type(Gray, AbstractGray{Float16}) === Gray
    @test promote_type(AbstractGray{Float16}, Gray{Float16}) === Gray{Float16}
    @test promote_type(AbstractGray{Float16}, Gray24) === Gray{Float32}
end

@testset "rgb and gray promotions" begin
    @test promote( RGB(0.2,0.3,0.4),  Gray(0.8)) === (RGB{Float64}(0.2,0.3,0.4), RGB{Float64}(0.8,0.8,0.8))
    @test promote( RGB(0.2,0.3,0.4), GrayA(0.8)) === (RGBA{Float64}(0.2,0.3,0.4,1), RGBA{Float64}(0.8,0.8,0.8,1))
    @test promote( RGB(0.2,0.3,0.4), AGray(0.8)) === (ARGB{Float64}(0.2,0.3,0.4,1), ARGB{Float64}(0.8,0.8,0.8,1))
    @test promote(RGBA(0.2,0.3,0.4),  Gray(0.8)) === (RGBA{Float64}(0.2,0.3,0.4,1), RGBA{Float64}(0.8,0.8,0.8,1))
    @test promote(RGBA(0.2,0.3,0.4), GrayA(0.8)) === (RGBA{Float64}(0.2,0.3,0.4,1), RGBA{Float64}(0.8,0.8,0.8,1))
    @test promote(RGBA(0.2,0.3,0.4), AGray(0.8)) === (RGBA{Float64}(0.2,0.3,0.4,1), RGBA{Float64}(0.8,0.8,0.8,1))
    @test promote(ARGB(0.2,0.3,0.4),  Gray(0.8)) === (ARGB{Float64}(0.2,0.3,0.4,1), ARGB{Float64}(0.8,0.8,0.8,1))
    @test promote(ARGB(0.2,0.3,0.4), GrayA(0.8)) === (ARGB{Float64}(0.2,0.3,0.4,1), ARGB{Float64}(0.8,0.8,0.8,1))
    @test promote(ARGB(0.2,0.3,0.4), AGray(0.8)) === (ARGB{Float64}(0.2,0.3,0.4,1), ARGB{Float64}(0.8,0.8,0.8,1))

    @test promote( RGB{N0f8}(0.2,0.3,0.4),  Gray(0.8)) === (RGB{Float64}(0.2N0f8,0.3N0f8,0.4N0f8), RGB{Float64}(0.8,0.8,0.8))
    @test promote( RGB{N0f8}(0.2,0.3,0.4), GrayA(0.8)) === (RGBA{Float64}(0.2N0f8,0.3N0f8,0.4N0f8,1), RGBA{Float64}(0.8,0.8,0.8,1))
    @test promote( RGB{N0f8}(0.2,0.3,0.4), AGray(0.8)) === (ARGB{Float64}(0.2N0f8,0.3N0f8,0.4N0f8,1), ARGB{Float64}(0.8,0.8,0.8,1))
    @test promote(RGBA{N0f8}(0.2,0.3,0.4),  Gray(0.8)) === (RGBA{Float64}(0.2N0f8,0.3N0f8,0.4N0f8,1), RGBA{Float64}(0.8,0.8,0.8,1))
    @test promote(RGBA{N0f8}(0.2,0.3,0.4), GrayA(0.8)) === (RGBA{Float64}(0.2N0f8,0.3N0f8,0.4N0f8,1), RGBA{Float64}(0.8,0.8,0.8,1))
    @test promote(RGBA{N0f8}(0.2,0.3,0.4), AGray(0.8)) === (RGBA{Float64}(0.2N0f8,0.3N0f8,0.4N0f8,1), RGBA{Float64}(0.8,0.8,0.8,1))
    @test promote(ARGB{N0f8}(0.2,0.3,0.4),  Gray(0.8)) === (ARGB{Float64}(0.2N0f8,0.3N0f8,0.4N0f8,1), ARGB{Float64}(0.8,0.8,0.8,1))
    @test promote(ARGB{N0f8}(0.2,0.3,0.4), GrayA(0.8)) === (ARGB{Float64}(0.2N0f8,0.3N0f8,0.4N0f8,1), ARGB{Float64}(0.8,0.8,0.8,1))
    @test promote(ARGB{N0f8}(0.2,0.3,0.4), AGray(0.8)) === (ARGB{Float64}(0.2N0f8,0.3N0f8,0.4N0f8,1), ARGB{Float64}(0.8,0.8,0.8,1))

    @test promote( RGB(0.2,0.3,0.4),  Gray{N0f8}(0.8)) === (RGB{Float64}(0.2,0.3,0.4), RGB{Float64}(0.8N0f8,0.8N0f8,0.8N0f8))
    @test promote( RGB(0.2,0.3,0.4), GrayA{N0f8}(0.8)) === (RGBA{Float64}(0.2,0.3,0.4,1), RGBA{Float64}(0.8N0f8,0.8N0f8,0.8N0f8,1))
    @test promote( RGB(0.2,0.3,0.4), AGray{N0f8}(0.8)) === (ARGB{Float64}(0.2,0.3,0.4,1), ARGB{Float64}(0.8N0f8,0.8N0f8,0.8N0f8,1))
    @test promote(RGBA(0.2,0.3,0.4),  Gray{N0f8}(0.8)) === (RGBA{Float64}(0.2,0.3,0.4,1), RGBA{Float64}(0.8N0f8,0.8N0f8,0.8N0f8,1))
    @test promote(RGBA(0.2,0.3,0.4), GrayA{N0f8}(0.8)) === (RGBA{Float64}(0.2,0.3,0.4,1), RGBA{Float64}(0.8N0f8,0.8N0f8,0.8N0f8,1))
    @test promote(RGBA(0.2,0.3,0.4), AGray{N0f8}(0.8)) === (RGBA{Float64}(0.2,0.3,0.4,1), RGBA{Float64}(0.8N0f8,0.8N0f8,0.8N0f8,1))
    @test promote(ARGB(0.2,0.3,0.4),  Gray{N0f8}(0.8)) === (ARGB{Float64}(0.2,0.3,0.4,1), ARGB{Float64}(0.8N0f8,0.8N0f8,0.8N0f8,1))
    @test promote(ARGB(0.2,0.3,0.4), GrayA{N0f8}(0.8)) === (ARGB{Float64}(0.2,0.3,0.4,1), ARGB{Float64}(0.8N0f8,0.8N0f8,0.8N0f8,1))
    @test promote(ARGB(0.2,0.3,0.4), AGray{N0f8}(0.8)) === (ARGB{Float64}(0.2,0.3,0.4,1), ARGB{Float64}(0.8N0f8,0.8N0f8,0.8N0f8,1))

    @test promote( RGB24(0.2,0.3,0.4),  Gray(0.8)) === (RGB{Float64}(0.2N0f8,0.3N0f8,0.4N0f8), RGB{Float64}(0.8,0.8,0.8))
    @test promote( RGB24(0.2,0.3,0.4), GrayA(0.8)) === (RGBA{Float64}(0.2N0f8,0.3N0f8,0.4N0f8,1), RGBA{Float64}(0.8,0.8,0.8,1))
    @test promote( RGB24(0.2,0.3,0.4), AGray(0.8)) === (ARGB{Float64}(0.2N0f8,0.3N0f8,0.4N0f8,1), ARGB{Float64}(0.8,0.8,0.8,1))
    @test promote(ARGB32(0.2,0.3,0.4),  Gray(0.8)) === (ARGB{Float64}(0.2N0f8,0.3N0f8,0.4N0f8,1), ARGB{Float64}(0.8,0.8,0.8,1))
    @test promote(ARGB32(0.2,0.3,0.4), GrayA(0.8)) === (ARGB{Float64}(0.2N0f8,0.3N0f8,0.4N0f8,1), ARGB{Float64}(0.8,0.8,0.8,1))
    @test promote(ARGB32(0.2,0.3,0.4), AGray(0.8)) === (ARGB{Float64}(0.2N0f8,0.3N0f8,0.4N0f8,1), ARGB{Float64}(0.8,0.8,0.8,1))

    @test promote( RGB(0.2,0.3,0.4),  Gray24(0.8)) === (RGB{Float64}(0.2,0.3,0.4), RGB{Float64}(0.8N0f8,0.8N0f8,0.8N0f8))
    @test promote( RGB(0.2,0.3,0.4), AGray32(0.8)) === (ARGB{Float64}(0.2,0.3,0.4,1), ARGB{Float64}(0.8N0f8,0.8N0f8,0.8N0f8,1))
    @test promote(RGBA(0.2,0.3,0.4),  Gray24(0.8)) === (RGBA{Float64}(0.2,0.3,0.4,1), RGBA{Float64}(0.8N0f8,0.8N0f8,0.8N0f8,1))
    @test promote(RGBA(0.2,0.3,0.4), AGray32(0.8)) === (RGBA{Float64}(0.2,0.3,0.4,1), RGBA{Float64}(0.8N0f8,0.8N0f8,0.8N0f8,1))
    @test promote(ARGB(0.2,0.3,0.4),  Gray24(0.8)) === (ARGB{Float64}(0.2,0.3,0.4,1), ARGB{Float64}(0.8N0f8,0.8N0f8,0.8N0f8,1))
    @test promote(ARGB(0.2,0.3,0.4), AGray32(0.8)) === (ARGB{Float64}(0.2,0.3,0.4,1), ARGB{Float64}(0.8N0f8,0.8N0f8,0.8N0f8,1))

    @test promote( RGB24(0.2,0.3,0.4),  Gray24(0.8)) === (RGB24(0.2,0.3,0.4), RGB24(0.8,0.8,0.8))
    @test promote( RGB24(0.2,0.3,0.4), AGray32(0.8)) === (ARGB32(0.2,0.3,0.4,1), ARGB32(0.8,0.8,0.8,1))
    @test promote(ARGB32(0.2,0.3,0.4),  Gray24(0.8)) === (ARGB32(0.2,0.3,0.4,1), ARGB32(0.8,0.8,0.8,1))
    @test promote(ARGB32(0.2,0.3,0.4), AGray32(0.8)) === (ARGB32(0.2,0.3,0.4,1), ARGB32(0.8,0.8,0.8,1))

    @test promote(RGB(0.2,0.3,0.4), Gray{Bool}(1)) === (RGB{Float64}(0.2,0.3,0.4), RGB{Float64}(1,1,1))
    @test promote(RGB{N0f8}(0.2,0.3,0.4), Gray{Bool}(1)) === (RGB{N0f8}(0.2,0.3,0.4), RGB{N0f8}(1,1,1))

    @test promote_type(RGB, Gray) === RGB
    @test promote_type(RGB, Gray{Float16}) === RGB
    @test promote_type(Gray, RGB{Float16}) === RGB

    @test promote_type(RGB, GrayA) === RGBA
    @test promote_type(AGray, RGB) === ARGB
    @test promote_type(RGB, GrayA{Float16}) === RGBA
    @test promote_type(AGray, RGB{Float16}) === ARGB

    @test promote_type(Gray, RGBA) === RGBA
    @test promote_type(ARGB, Gray) === ARGB
    @test promote_type(Gray, RGBA{Float16}) === RGBA
    @test promote_type(ARGB, Gray{Float16}) === ARGB

    @test promote_type(RGB, Gray{Bool}) === RGB
    @test promote_type(ARGB, Gray{Bool}) === ARGB
    @test promote_type(RGBA, Gray{Bool}) === RGBA

    @test promote_type(RGB, Gray24) === RGB
    @test promote_type(RGBA, Gray24) === RGBA
    @test promote_type(ARGB, AGray32) === ARGB
    @test promote_type(RGB24, Gray) === RGB
    @test promote_type(RGB24, GrayA) === RGBA
    @test promote_type(ARGB32, Gray) === ARGB
    @test promote_type(ARGB32, GrayA) === ARGB

    @test promote_type(AbstractRGB, Gray{Float16}) === RGB
    @test promote_type(RGB, AbstractGray{Float16}) === RGB
    @test promote_type(AbstractRGB{Float16}, Gray{Float16}) === RGB{Float16}
    @test promote_type(AbstractRGB{Float16}, Gray24) === RGB{Float32}

    @test promote_type(AbstractGray, RGB{Float16}) === RGB
    @test promote_type(Gray, AbstractRGB{Float16}) === RGB
    @test promote_type(AbstractGray{Float16}, RGB{Float16}) === RGB{Float16}
    @test promote_type(AbstractGray{Float16}, RGB24) === RGB{Float32}

    @test promote_type(AbstractRGB, AGray{Float16}) === ARGB
    @test promote_type(RGB, AbstractAGray{Gray{Float16},Float16}) === ARGB
    @test promote_type(AbstractRGB{Float16}, AGray{Float16}) === ARGB{Float16}
    @test promote_type(AbstractRGB{Float16}, AGray32) === ARGB{Float32}

    @test promote_type(AbstractGray, ARGB{Float16}) === ARGB
    @test promote_type(Gray, AbstractARGB{RGB{Float16},Float16}) === ARGB
    @test promote_type(AbstractGray{Float16}, ARGB{Float16}) === ARGB{Float16}
    @test promote_type(AbstractGray{Float16}, ARGB32) === ARGB{Float32}

    @test promote_type(AbstractARGB, AGray{Float16}) === ARGB
    @test promote_type(ARGB, AbstractAGray{Gray{Float16},Float16}) === ARGB
    @test promote_type(AbstractARGB{RGB{Float16},Float16}, AGray{Float16}) === ARGB{Float16}
    @test promote_type(AbstractARGB{RGB{Float16},Float16}, AGray32) === ARGB{Float32}
end

@testset "hsv and gray promotions" begin
    @test promote_type( HSV{Float16},  Gray{N0f8}) === HSV{Float32}
    @test promote_type( HSV{Float16}, GrayA{N0f8}) === HSVA{Float32}
    @test promote_type( HSV{Float16}, AGray{N0f8}) === AHSV{Float32}
    @test promote_type(HSVA{Float16},  Gray{N0f8}) === HSVA{Float32}
    @test promote_type(HSVA{Float16}, GrayA{N0f8}) === HSVA{Float32}
    @test promote_type(HSVA{Float16}, AGray{N0f8}) === HSVA{Float32}
    @test promote_type(AHSV{Float16},  Gray{N0f8}) === AHSV{Float32}
    @test promote_type(AHSV{Float16}, GrayA{N0f8}) === AHSV{Float32}
    @test promote_type(AHSV{Float16}, AGray{N0f8}) === AHSV{Float32}

    @test promote_type( HSV{Float16},  Gray24) === HSV{Float32}
    @test promote_type( HSV{Float16}, AGray32) === AHSV{Float32}
    @test promote_type(HSVA{Float16},  Gray24) === HSVA{Float32}
    @test promote_type(HSVA{Float16}, AGray32) === HSVA{Float32}
    @test promote_type(AHSV{Float16},  Gray24) === AHSV{Float32}
    @test promote_type(AHSV{Float16}, AGray32) === AHSV{Float32}

    @test promote_type(HSV{Float64}, Gray{Bool}) === HSV{Float64}
    @test promote_type(HSV{Float32}, Gray{Bool}) === HSV{Float32}

    @test promote_type(HSV, Gray) === HSV
    @test promote_type(HSV, Gray{Float16}) === HSV
    @test promote_type(Gray, HSV{Float16}) === HSV

    @test promote_type(HSV, GrayA) === HSVA
    @test promote_type(AGray, HSV) === AHSV
    @test promote_type(HSV, GrayA{Float16}) === HSVA
    @test promote_type(AGray, HSV{Float16}) === AHSV

    @test promote_type(Gray, HSVA) === HSVA
    @test promote_type(AHSV, Gray) === AHSV
    @test promote_type(Gray, HSVA{Float16}) === HSVA
    @test promote_type(AHSV, Gray{Float16}) === AHSV

    @test promote_type(GrayA, HSVA) === HSVA
    @test promote_type(AHSV, AGray) === AHSV
    @test promote_type(AGray, HSVA) === HSVA
    @test promote_type(AHSV, GrayA) === AHSV

    @test promote_type( HSV,  Gray24) === HSV
    @test promote_type( HSV, AGray32) === AHSV
    @test promote_type(HSVA,  Gray24) === HSVA
    @test promote_type(HSVA, AGray32) === HSVA
    @test promote_type(AHSV,  Gray24) === AHSV
    @test promote_type(AHSV, AGray32) === AHSV
end

# promotions between different color spaces
@testset "rgb and hsv promotions" begin
    # the current implementation is like `typejoin`.
    @test promote_type( RGB{Float64},  HSV{Float64}) == Color3{Float64}
    @test promote_type( RGB{Float64}, HSVA{Float64}) == ColorAlpha{C,Float64,4} where {C<:Color3{Float64}}
    @test promote_type( RGB{Float64}, AHSV{Float64}) == AlphaColor{C,Float64,4} where {C<:Color3{Float64}}
    @test promote_type(RGBA{Float64},  HSV{Float64}) == ColorAlpha{C,Float64,4} where {C<:Color3{Float64}}
    @test promote_type(RGBA{Float64}, HSVA{Float64}) == ColorAlpha{C,Float64,4} where {C<:Color3{Float64}}
    @test promote_type(RGBA{Float64}, AHSV{Float64}) == AlphaColor{C,Float64,4} where {C<:Color3{Float64}}
    @test promote_type(ARGB{Float64},  HSV{Float64}) == AlphaColor{C,Float64,4} where {C<:Color3{Float64}}
    @test promote_type(ARGB{Float64}, HSVA{Float64}) == AlphaColor{C,Float64,4} where {C<:Color3{Float64}}
    @test promote_type(ARGB{Float64}, AHSV{Float64}) == AlphaColor{C,Float64,4} where {C<:Color3{Float64}}

    @test promote_type( RGB{N0f8},  HSV{Float16}) == Color3{Float32}
    @test promote_type( RGB{N0f8}, HSVA{Float16}) == ColorAlpha{C,Float32,4} where {C<:Color3{Float32}}
    @test promote_type( RGB{N0f8}, AHSV{Float16}) == AlphaColor{C,Float32,4} where {C<:Color3{Float32}}
    @test promote_type(RGBA{N0f8},  HSV{Float16}) == ColorAlpha{C,Float32,4} where {C<:Color3{Float32}}
    @test promote_type(RGBA{N0f8}, HSVA{Float16}) == ColorAlpha{C,Float32,4} where {C<:Color3{Float32}}
    @test promote_type(RGBA{N0f8}, AHSV{Float16}) == AlphaColor{C,Float32,4} where {C<:Color3{Float32}}
    @test promote_type(ARGB{N0f8},  HSV{Float16}) == AlphaColor{C,Float32,4} where {C<:Color3{Float32}}
    @test promote_type(ARGB{N0f8}, HSVA{Float16}) == AlphaColor{C,Float32,4} where {C<:Color3{Float32}}
    @test promote_type(ARGB{N0f8}, AHSV{Float16}) == AlphaColor{C,Float32,4} where {C<:Color3{Float32}}

    @test promote_type( RGB24,  HSV{Float16}) == Color3{Float32}
    @test promote_type( RGB24, HSVA{Float16}) == ColorAlpha{C,Float32,4} where {C<:Color3{Float32}}
    @test promote_type( RGB24, AHSV{Float16}) == AlphaColor{C,Float32,4} where {C<:Color3{Float32}}
    @test promote_type(ARGB32,  HSV{Float16}) == AlphaColor{C,Float32,4} where {C<:Color3{Float32}}
    @test promote_type(ARGB32, HSVA{Float16}) == AlphaColor{C,Float32,4} where {C<:Color3{Float32}}
    @test promote_type(ARGB32, AHSV{Float16}) == AlphaColor{C,Float32,4} where {C<:Color3{Float32}}

    @test promote_type(RGB, HSV) == Color3
    @test promote_type(RGB, HSV{Float16}) == Color3
    @test promote_type(HSV, RGB{Float16}) == Color3

    @test promote_type(RGB, HSVA) == ColorAlpha{C,T,4} where {T, C<:Color3{T}}
    @test promote_type(AHSV, RGB) == AlphaColor{C,T,4} where {T, C<:Color3{T}}
    @test promote_type(RGB, HSVA{Float16}) == ColorAlpha{C,T,4} where {T, C<:Color3{T}}
    @test promote_type(AHSV, RGB{Float16}) == AlphaColor{C,T,4} where {T, C<:Color3{T}}

    @test promote_type(HSV, RGBA) == ColorAlpha{C,T,4} where {T, C<:Color3{T}}
    @test promote_type(ARGB, HSV) == AlphaColor{C,T,4} where {T, C<:Color3{T}}
    @test promote_type(HSV, RGBA{Float16}) == ColorAlpha{C,T,4} where {T, C<:Color3{T}}
    @test promote_type(ARGB, HSV{Float16}) == AlphaColor{C,T,4} where {T, C<:Color3{T}}

    @test promote_type(HSVA, RGBA) == ColorAlpha{C,T,4} where {T, C<:Color3{T}}
    @test promote_type(ARGB, AHSV) == AlphaColor{C,T,4} where {T, C<:Color3{T}}
    @test promote_type(HSVA, RGBA{Float16}) == ColorAlpha{C,T,4} where {T, C<:Color3{T}}
    @test promote_type(ARGB, AHSV{Float16}) == AlphaColor{C,T,4} where {T, C<:Color3{T}}

    @test promote_type(AHSV, RGBA) == AlphaColor{C,T,4} where {T, C<:Color3{T}} # != Transparent3
    @test promote_type(ARGB, HSVA) == AlphaColor{C,T,4} where {T, C<:Color3{T}} # != Transparent3
    @test promote_type(AHSV, RGBA{Float16}) == AlphaColor{C,T,4} where {T, C<:Color3{T}} # != Transparent3
    @test promote_type(ARGB, HSVA{Float16}) == AlphaColor{C,T,4} where {T, C<:Color3{T}} # != Transparent3

    @test promote_type(RGB24, HSV) == Color3
    @test promote_type(RGB24, HSVA{Float16}) == ColorAlpha{C,Float32,4} where C<:Color3{Float32}
    @test promote_type(ARGB32, HSV) == AlphaColor{C,T,4} where {T, C<:Color3{T}}
    @test promote_type(ARGB32, HSVA{Float16}) == AlphaColor{C,Float32,4} where C<:Color3{Float32}
end

@testset "promotions with abstract types" begin
    @test promote_type(Colorant, RGB{Float64}) == AlphaColor{C,T} where {T, C<:Color{T}}
    @test promote_type(Colorant{N0f8}, RGB{Float64}) == AlphaColor{C,Float64} where {C<:Color{Float64}}
    @test promote_type(Colorant{Float32,3}, RGB{Float64}) == AlphaColor{C,Float64,4} where C<:Color{Float64,3}
    @test promote_type(Color, RGB{Float64}) === Color
    @test promote_type(Color{N0f8}, RGB{Float64}) === Color{Float64}
    @test promote_type(Color{Float32,3}, RGB{Float64}) === Color{Float64,3}
    @test promote_type(AbstractRGB, RGB{Float64}) === RGB
    @test promote_type(AbstractRGB{N0f8}, RGB{Float64}) === RGB{Float64}
    @test promote_type(TransparentColor, RGB{Float64}) == AlphaColor{C,T} where {T, C<:Color{T}}
    @test promote_type(TransparentColor{RGB{N0f8}}, RGB{Float64}) === ARGB
    @test promote_type(TransparentColor{RGB{N0f8},N0f8}, RGB{Float64}) === ARGB{Float64}
    @test promote_type(TransparentColor{RGB{N0f8},N0f8,4}, RGB{Float64}) === ARGB{Float64}
    @test promote_type(AlphaColor, RGB{Float64}) == AlphaColor{C,T} where {T, C<:Color{T}}
    @test promote_type(AlphaColor{RGB{N0f8}}, RGB{Float64}) === ARGB
    @test promote_type(AlphaColor{RGB{N0f8},N0f8}, RGB{Float64}) === ARGB{Float64}
    @test promote_type(AlphaColor{RGB{N0f8},N0f8,4}, RGB{Float64}) === ARGB{Float64}
    @test promote_type(ColorAlpha, RGB{Float64}) === ColorAlpha{C,T} where {T, C<:Color{T}}
    @test promote_type(ColorAlpha{RGB{N0f8}}, RGB{Float64}) === RGBA
    @test promote_type(ColorAlpha{RGB{N0f8},N0f8}, RGB{Float64}) === RGBA{Float64}
    @test promote_type(ColorAlpha{RGB{N0f8},N0f8,4}, RGB{Float64}) === RGBA{Float64}

    @test promote_type(Colorant, ARGB{Float64}) == AlphaColor{C,T} where {T, C<:Color{T}}
    @test promote_type(Colorant{N0f8}, ARGB{Float64}) == AlphaColor{C,Float64} where C<:Color{Float64}
    @test_broken promote_type(Colorant{Float32,4}, ARGB{Float64}) == AlphaColor{C,Float64,4} where C<:Color3{Float64}
    @test promote_type(Color, ARGB{Float64}) == AlphaColor{C,T} where {T, C<:Color{T}}
    @test promote_type(AbstractRGB, ARGB{Float64}) === ARGB
    @test promote_type(AbstractRGB{N0f8}, ARGB{Float64}) === ARGB{Float64}
    @test promote_type(TransparentColor, ARGB{Float64}) == AlphaColor{C,T} where {T, C<:Color{T}}
    @test promote_type(AlphaColor, ARGB{Float64}) == AlphaColor{C,T} where {T, C<:Color{T}}
    @test promote_type(ColorAlpha, ARGB{Float64}) == AlphaColor{C,T} where {T, C<:Color{T}}

    @test promote_type(Colorant, RGBA{Float64}) == AlphaColor{C,T} where {T, C<:Color{T}}
    @test promote_type(Colorant{N0f8}, RGBA{Float64}) == AlphaColor{C,Float64} where C<:Color{Float64}
    @test promote_type(Colorant{Float32,3}, RGBA{Float64}) == AlphaColor{C,Float64,4} where C<:Color3{Float64}
    @test promote_type(Color, RGBA{Float64}) == ColorAlpha{C,T} where {T, C<:Color{T}}
    @test promote_type(AbstractRGB, RGBA{Float64}) === RGBA
    @test promote_type(AbstractRGB{N0f8}, RGBA{Float64}) === RGBA{Float64}
    @test promote_type(TransparentColor, RGBA{Float64}) == AlphaColor{C,T} where {T, C<:Color{T}}
    @test promote_type(AlphaColor, RGBA{Float64}) == AlphaColor{C,T} where {T, C<:Color{T}}
    @test promote_type(ColorAlpha, RGBA{Float64}) == ColorAlpha{C,T} where {T, C<:Color{T}}
end

@testset "rgb conversions with abstract types" begin
    c = RGB(1, 0.6, 0)
    @test convert(Colorant, c) === RGB{Float64}(1, 0.6, 0)
    @test convert(Colorant{N0f8}, c) === RGB{N0f8}(1, 0.6, 0)
    @test convert(Colorant{Float32,3}, c) === RGB{Float32}(1, 0.6, 0)
    @test convert(Color, c) === RGB{Float64}(1, 0.6, 0)
    @test convert(Color{N0f8}, c) === RGB{N0f8}(1, 0.6, 0)
    @test convert(Color{Float32,3}, c) === RGB{Float32}(1, 0.6, 0)
    @test convert(AbstractRGB, c) === RGB{Float64}(1, 0.6, 0)
    @test convert(AbstractRGB{N0f8}, c) === RGB{N0f8}(1, 0.6, 0)
    @test_throws ColorTypeResolutionError convert(TransparentColor, c)
    @test_throws ColorTypeResolutionError convert(TransparentColor{RGB{N0f8}}, c)
    @test_throws ColorTypeResolutionError convert(TransparentColor{RGB{N0f8},N0f8}, c)
    @test_throws ColorTypeResolutionError convert(TransparentColor{RGB{N0f8},N0f8,4}, c)
    @test convert(AlphaColor, c) === ARGB{Float64}(1, 0.6, 0, 1)
    @test_broken convert(AlphaColor{RGB{N0f8}}, c) === ARGB{N0f8}(1, 0.6, 0, 1)
    @test convert(AlphaColor{RGB{N0f8},N0f8}, c) === ARGB{N0f8}(1, 0.6, 0, 1)
    @test convert(AlphaColor{RGB{N0f8},N0f8,4}, c) === ARGB{N0f8}(1, 0.6, 0, 1)
    @test convert(ColorAlpha, c) === RGBA{Float64}(1, 0.6, 0, 1)
    @test_broken convert(ColorAlpha{RGB{N0f8}}, c) === RGBA{N0f8}(1, 0.6, 0, 1)
    @test convert(ColorAlpha{RGB{N0f8},N0f8}, c) === RGBA{N0f8}(1, 0.6, 0, 1)
    @test convert(ColorAlpha{RGB{N0f8},N0f8,4}, c) === RGBA{N0f8}(1, 0.6, 0, 1)

    ac = ARGB(1, 0.6, 0, 0.8)
    @test convert(Colorant, ac) === ARGB{Float64}(1, 0.6, 0, 0.8)
    @test convert(Colorant{N0f8}, ac) === ARGB{N0f8}(1, 0.6, 0, 0.8)
    @test_broken convert(Colorant{Float32,3}, ac) === RGB{Float32}(1, 0.6, 0)
    @test convert(Color, ac) === RGB{Float64}(1, 0.6, 0)
    @test convert(AbstractRGB, ac) === RGB{Float64}(1, 0.6, 0)
    @test convert(AbstractRGB{N0f8}, ac) === RGB{N0f8}(1, 0.6, 0)
    @test convert(TransparentColor, ac) === ARGB{Float64}(1, 0.6, 0, 0.8)
    @test convert(AlphaColor, ac) === ARGB{Float64}(1, 0.6, 0, 0.8) # issue #126
    @test convert(ColorAlpha, ac) === RGBA{Float64}(1, 0.6, 0, 0.8) # issue #126

    ca = RGBA(1, 0.6, 0, 0.8)
    @test convert(Colorant, ca) === RGBA{Float64}(1, 0.6, 0, 0.8)
    @test convert(Colorant{N0f8}, ca) === RGBA{N0f8}(1, 0.6, 0, 0.8)
    @test_broken convert(Colorant{Float32,3}, ca) === RGB{Float32}(1, 0.6, 0)
    @test convert(Color, ca) === RGB{Float64}(1, 0.6, 0)
    @test convert(AbstractRGB, ca) === RGB{Float64}(1, 0.6, 0)
    @test convert(AbstractRGB{N0f8}, ca) === RGB{N0f8}(1, 0.6, 0)
    @test convert(TransparentColor, ca) === RGBA{Float64}(1, 0.6, 0, 0.8)
    @test convert(AlphaColor, ca) === ARGB{Float64}(1, 0.6, 0, 0.8) # issue #126
    @test convert(ColorAlpha, ca) === RGBA{Float64}(1, 0.6, 0, 0.8) # issue #126

    rgb24 = RGB24(1, 0.6, 0)
    @test convert(Colorant, rgb24) === RGB24(1, 0.6, 0)
    @test convert(Colorant{N0f8}, rgb24) === RGB24(1, 0.6, 0)
    @test_broken convert(Colorant{Float32,3}, rgb24) === RGB{Float32}(1, 0.6, 0)
    @test convert(Color, rgb24) === RGB24(1, 0.6, 0)
    @test convert(AbstractRGB, rgb24) === RGB24(1, 0.6, 0)
    @test convert(AbstractRGB{N0f8}, rgb24) === RGB24(1, 0.6, 0)
    @test_throws ColorTypeResolutionError convert(TransparentColor, rgb24)
    @test convert(AlphaColor, rgb24) === ARGB32(1, 0.6, 0, 1)
    @test_throws MethodError convert(ColorAlpha, rgb24)

    argb32 = ARGB32(1, 0.6, 0, 0.8)
    @test convert(Colorant, argb32) === ARGB32(1, 0.6, 0, 0.8)
    @test convert(Colorant{N0f8}, argb32) === ARGB32(1, 0.6, 0, 0.8)
    @test_broken convert(Colorant{Float32,3}, argb32) === RGB{Float32}(1, 0.6, 0)
    @test convert(Color, argb32) === RGB24(1, 0.6, 0)
    @test convert(AbstractRGB, argb32) === RGB24(1, 0.6, 0)
    @test convert(AbstractRGB{N0f8}, argb32) === RGB24(1, 0.6, 0)
    @test convert(TransparentColor, argb32) === ARGB32(1, 0.6, 0, 0.8)
    @test convert(AlphaColor, argb32) === ARGB32(1, 0.6, 0, 0.8)
    @test_throws MethodError convert(ColorAlpha, argb32)

    @test convert(AbstractARGB{RGB,N0f8}, c, 0.2) === ARGB{N0f8}(1, 0.6, 0, 0.2)
    @test convert(AbstractRGBA{RGB,N0f8}, c, 0.2) === RGBA{N0f8}(1, 0.6, 0, 0.2)
    @test convert(AbstractARGB{RGB,N0f8}, rgb24, 0.2) === ARGB{N0f8}(1, 0.6, 0, 0.2)
    @test_throws MethodError convert(AbstractARGB{RGB,N0f8}, ac, 0.2)
    @test_throws MethodError convert(AbstractARGB{RGB,N0f8}, ca, 0.2)
    @test_throws MethodError convert(AbstractARGB{RGB,N0f8}, argb32, 0.2)
    @test convert(AbstractARGB{RGB24,N0f8}, rgb24, 0.2) === ARGB32(1, 0.6, 0, 0.2)
end

@testset "gray conversions with abstract types" begin
    c = Gray(0.4)
    @test convert(Colorant, c) === Gray{Float64}(0.4)
    @test convert(Colorant{N0f8}, c) === Gray{N0f8}(0.4)
    @test convert(Colorant{Float32,1}, c) === Gray{Float32}(0.4)
    @test convert(Color, c) === Gray{Float64}(0.4)
    @test_throws ColorTypeResolutionError convert(TransparentColor, c)
    @test convert(AlphaColor, c) === AGray{Float64}(0.4, 1)
    @test convert(ColorAlpha, c) === GrayA{Float64}(0.4, 1)

    ac = AGray(0.4, 0.8)
    @test convert(Colorant, ac) === AGray{Float64}(0.4, 0.8)
    @test convert(Colorant{N0f8}, ac) === AGray{N0f8}(0.4, 0.8)
    @test_broken convert(Colorant{Float32,1}, ac) === Gray{Float32}(0.4, 0.8)
    @test convert(Color, ac) === Gray{Float64}(0.4)
    @test convert(TransparentColor, ac) == AGray{Float64}(0.4, 0.8)
    @test convert(AlphaColor, ac) === AGray{Float64}(0.4, 0.8)
    @test convert(ColorAlpha, ac) === GrayA{Float64}(0.4, 0.8)

    ca = GrayA(0.4, 0.8)
    @test convert(Colorant, ca) === GrayA{Float64}(0.4, 0.8)
    @test convert(Colorant{N0f8}, ca) === GrayA{N0f8}(0.4, 0.8)
    @test_broken convert(Colorant{Float32,1}, ca) === Gray{Float32}(0.4)
    @test convert(Color, ca) === Gray{Float64}(0.4)
    @test convert(TransparentColor, ca) == GrayA{Float64}(0.4, 0.8)
    @test convert(AlphaColor, ca) === AGray{Float64}(0.4, 0.8)
    @test convert(ColorAlpha, ca) === GrayA{Float64}(0.4, 0.8)

    gray24 = Gray24(0.4)
    @test convert(Colorant, gray24) === Gray24(0.4)
    @test convert(Colorant{N0f8}, gray24) === Gray24(0.4)
    @test_broken convert(Colorant{Float32,1}, gray24) === Gray{Float32}(0.4)
    @test convert(Color, gray24) === Gray24(0.4)
    @test_throws ColorTypeResolutionError convert(TransparentColor, gray24)
    @test convert(AlphaColor, gray24) === AGray32(0.4, 1)
    @test_throws MethodError convert(ColorAlpha, gray24) # TODO: need docs

    agray32 = AGray32(0.4, 0.8)
    @test convert(Colorant, agray32) === AGray32(0.4, 0.8)
    @test convert(Colorant{N0f8}, agray32) === AGray32(0.4, 0.8)
    @test_broken convert(Colorant{Float32,1}, agray32) === Gray{Float32}(0.4)
    @test convert(Color, agray32) === Gray24(0.4)
    @test convert(TransparentColor, agray32) === AGray32(0.4, 0.8)
    @test convert(AlphaColor, agray32) === AGray32(0.4, 0.8)
    @test_throws MethodError convert(ColorAlpha, agray32) # TODO: need docs

    @test convert(AbstractAGray{Gray,N0f8}, c, 0.2) === AGray{N0f8}(0.4, 0.2)
    @test convert(AbstractGrayA{Gray,N0f8}, c, 0.2) === GrayA{N0f8}(0.4, 0.2)
    # the following is ok, but not consistent with the case of RGB24
    @test convert(AbstractAGray{Gray,N0f8}, gray24, 0.2) === AGray{N0f8}(0.4, 0.2)
    @test convert(AbstractAGray{Gray24,N0f8}, gray24, 0.2) === AGray32(0.4, 0.2)
    @test_throws MethodError convert(AbstractAGray{Gray,N0f8}, ac, 0.2)
    @test_throws MethodError convert(AbstractAGray{Gray,N0f8}, ca, 0.2)
    @test_throws MethodError convert(AbstractAGray{Gray,N0f8}, agray32, 0.2)
end

@testset "conversions from/to real numbers" begin
    @test convert(Float64, Gray(0.6)) === 0.6
    @test Float32(Gray(0.6)) === 0.6f0
    @test float(Gray(0.6)) === 0.6
    @test real(Gray(0.6)) === 0.6
    @test real(Gray{Float16}) === Float16

    @test convert(N0f8, Gray24(0.6)) === N0f8(0.6)
    @test convert(Float64, Gray24(0.6)) === 0.6
    @test Float32(Gray24(0.6)) === 0.6f0
    @test float(Gray24(0.6)) === 0.6
    @test real(Gray24(0.6)) === N0f8(0.6)
    @test real(Gray24) === N0f8

    @test convert(Gray, 0.6) === Gray{Float64}(0.6)
    @test convert(Gray, 0.6f0) === Gray{Float32}(0.6)
    @test convert(Gray, 0.6N0f8) === Gray{N0f8}(0.6)
    @test convert(Gray, true) === Gray{Bool}(1)
    @test convert(Gray, false) === Gray{Bool}(0)
    @test convert(Gray, 1) === Gray{N0f8}(1)

    @test convert(Gray{N0f16}, 0.6) === Gray{N0f16}(0.6)
    @test convert(Gray{N0f16}, 0.6f0) === Gray{N0f16}(0.6)
    @test convert(Gray{N0f16}, 0.6N0f8) === Gray{N0f16}(0.6)

    @test convert(GrayA{N0f8}, 0.6) === GrayA{N0f8}(0.6, 1)
    @test convert(AGray{N0f8}, 0.6) === AGray{N0f8}(0.6, 1)

    @test convert(Gray24, 0.6) === Gray24(0.6)
    @test convert(Gray24, 0.6f0) === Gray24(0.6)
    @test convert(Gray24, 0.6N0f8) === Gray24(0.6)

    @test convert(AGray32, 0.6) === AGray32(0.6, 1)
    @test convert(AGray32, 0.6f0) === AGray32(0.6, 1)
    @test convert(AGray32, 0.6N0f8) === AGray32(0.6, 1)
    @test convert(AGray32, 0.6, 0.8) === AGray32(0.6, 0.8)
    @test convert(AGray32, 0, 1) === AGray32(0, 1)

    @test_throws MethodError real(Gray) # should be a concrete type
    @test_throws MethodError real(AGray(1.0))
    @test_throws MethodError real(GrayA{Float32})

    @test_throws ColorTypeResolutionError convert(Colorant, 0.6)
    @test_throws ColorTypeResolutionError convert(Color, 0.6)
    @test_throws ColorTypeResolutionError convert(Color{N0f8,1}, 0.6)

    @test_throws MethodError convert(GrayA, 0.6, 0.8)
    @test_throws MethodError convert(AGray, 0, 1)

    @test convert(Gray, 2.0) === Gray{Float64}(2.0)
    @test_throws ArgumentError convert(Gray, 2)

    @test convert(RGB24, 0.6) === RGB24(0.6, 0.6, 0.6)
    @test convert(ARGB32, 0.6) === ARGB32(0.6, 0.6, 0.6, 1)
    @test convert(ARGB32, 0.6, 0.8) === ARGB32(0.6, 0.6, 0.6, 0.8)

    @test convert(RGB, 0.6) === RGB(0.6, 0.6, 0.6)
    @test convert(BGR, 0.6N0f8) === BGR{N0f8}(0.6, 0.6, 0.6)
    @test_broken convert(ARGB, 0.6) === ARGB(0.6, 0.6, 0.6, 1)
    @test_broken convert(RGBA, 0.6N0f8) === RGBA{N0f8}(0.6, 0.6, 0.6, 1)

    @test_broken convert(ARGB, 0.6f0, 0.8f0) === ARGB{Float32}(0.6, 0.6, 0.6, 0.8)
    @test_broken convert(RGBA{Float32}, 0.6, 0.8) === RGBA{Float32}(0.6, 0.6, 0.6, 0.8)

end

@testset "conversions from rgb to rgb" begin
    Crgb = filter(T -> T <: AbstractRGB, ColorTypes.parametric3)
    Ctransparent = unique(vcat(coloralpha.(Crgb), alphacolor.(Crgb)))
    @testset "$C conversions" for C in Crgb
        @test convert(C, C{Float64}(1,0.6,0)) === C{Float64}(1,0.6,0)
        @test convert(C{N0f8}, C{Float64}(1,0.6,0)) === C{N0f8}(1,0.6,0)
        @test convert(C, RGB24(1,0.6,0)) === C{N0f8}(1,0.6,0)
        @test convert(RGB24, C(1,0.6,0)) === RGB24(1,0.6,0)
        @test convert(C, ARGB32(1,0.6,0,0.8)) === C{N0f8}(1,0.6,0)
        @test convert(ARGB32, C(1,0.6,0)) === ARGB32(1,0.6,0)
        @test convert(ARGB32, C(1,0.6,0), 0.2) === ARGB32(1,0.6,0,0.2)
    end
    @testset "$C conversions" for C in Ctransparent
        @test convert(C, C{Float64}(1,0.6,0,0.8)) === C{Float64}(1,0.6,0,0.8)
        @test convert(C{N0f8}, C{Float64}(1,0.6,0,0.8)) === C{N0f8}(1,0.6,0,0.8)
        @test convert(C, RGB24(1,0.6,0)) === C{N0f8}(1,0.6,0,1)
        @test convert(C, RGB24(1,0.6,0), 0.2) === C{N0f8}(1,0.6,0,0.2)
        @test convert(RGB24, C(1,0.6,0,0.8)) === RGB24(1,0.6,0)
        @test convert(C, ARGB32(1,0.6,0,0.8)) === C{N0f8}(1,0.6,0,0.8)
        @test convert(ARGB32, C(1,0.6,0,0.8)) === ARGB32(1,0.6,0,0.8)
    end
    @test convert(RGB24, RGB24(1,0.6,0)) === RGB24(1,0.6,0)
    @test convert(ARGB32, RGB24(1,0.6,0)) === ARGB32(1,0.6,0,1)
    @test convert(ARGB32, RGB24(1,0.6,0), 0.2) === ARGB32(1,0.6,0,0.2)
    @test convert(RGB24, ARGB32(1,0.6,0,0.8)) === RGB24(1,0.6,0)
    @test convert(ARGB32, ARGB32(1,0.6,0,0.8)) === ARGB32(1,0.6,0,0.8)
end

@testset "conversions from gray to gray" begin
    @testset "Gray conversions" begin
        @test convert(Gray, Gray{Float64}(0.4)) === Gray{Float64}(0.4)
        @test convert(Gray{N0f8}, Gray{Float64}(0.4)) === Gray{N0f8}(0.4)
        @test convert(Gray, Gray24(0.4)) === Gray{N0f8}(0.4)
        @test convert(Gray24, Gray(0.4)) === Gray24(0.4)
        @test convert(Gray, AGray32(0.4,0.8)) === Gray{N0f8}(0.4)
        @test convert(AGray32, Gray(0.4)) === AGray32(0.4)
        @test convert(AGray32, Gray(0.4), 0.2) === AGray32(0.4, 0.2)
    end
    @testset "$C conversions" for C in (AGray, GrayA)
        @test convert(C, C{Float64}(0.4,0.8)) === C{Float64}(0.4,0.8)
        @test convert(C{N0f8}, C{Float64}(0.4,0.8)) === C{N0f8}(0.4,0.8)
        @test convert(C, Gray24(0.4)) === C{N0f8}(0.4,1)
        @test convert(C, Gray24(0.4), 0.2) === C{N0f8}(0.4,0.2)
        @test convert(Gray24, C(0.4,0.8)) === Gray24(0.4)
        @test convert(C, AGray32(0.4,0.8)) === C{N0f8}(0.4,0.8)
        @test convert(AGray32, C(0.4,0.8)) === AGray32(0.4,0.8)
    end
    @test convert(Gray24, Gray24(0.4)) === Gray24(0.4)
    @test convert(AGray32, Gray24(0.4)) === AGray32(0.4,1)
    @test convert(AGray32, Gray24(0.4), 0.2) === AGray32(0.4,0.2)
    @test convert(Gray24, AGray32(0.4,0.8)) === Gray24(0.4)
    @test convert(AGray32, AGray32(0.4,0.8)) === AGray32(0.4,0.8)
end

@testset "conversions from hsv to hsv" begin
    @testset "HSV conversions" begin
        @test convert(HSV, HSV{Float64}(100,0.4,0.6)) === HSV{Float64}(100,0.4,0.6)
        @test convert(HSV{Float32}, HSV{Float64}(100,0.4,0.6)) === HSV{Float32}(100,0.4,0.6)
        @test convert(HSV, HSVA{Float64}(100,0.4,0.6,0.8)) === HSV{Float64}(100,0.4,0.6)
        @test convert(HSV, AHSV{Float32}(100,0.4,0.6,0.8)) === HSV{Float32}(100,0.4,0.6)
    end
    @testset "$C conversions" for C in (AHSV, HSVA)
        @test convert(C, HSVA{Float64}(100,0.4,0.6,0.8)) === C{Float64}(100,0.4,0.6,0.8)
        @test convert(C, AHSV{Float32}(100,0.4,0.6,0.8)) === C{Float32}(100,0.4,0.6,0.8)
        @test convert(C{Float32}, HSVA{Float64}(100,0.4,0.6,0.8)) === C{Float32}(100,0.4,0.6,0.8)
        @test convert(C{Float32}, AHSV{Float32}(100,0.4,0.6,0.8)) === C{Float32}(100,0.4,0.6,0.8)
        @test convert(C, HSV{Float64}(100,0.4,0.6)) === C{Float64}(100,0.4,0.6,1)
        @test convert(C, HSV{Float32}(100,0.4,0.6), 0.2) === C{Float32}(100,0.4,0.6,0.2)
    end
end

@testset "conversions from gray to rgb" begin
    Crgb = filter(T -> T <: AbstractRGB, ColorTypes.parametric3)
    Ctransparent = unique(vcat(coloralpha.(Crgb), alphacolor.(Crgb)))
    @testset "$C conversions" for C in Crgb
        @test convert(C,  Gray{N0f8}(0.4)    ) === C{N0f8}(0.4,0.4,0.4)
        @test convert(C, GrayA{N0f8}(0.4,0.8)) === C{N0f8}(0.4,0.4,0.4)
        @test convert(C, AGray{N0f8}(0.4,0.8)) === C{N0f8}(0.4,0.4,0.4)
        @test convert(C{N0f8},  Gray{N0f8}(0.4)    ) === C{N0f8}(0.4,0.4,0.4)
        @test convert(C{N0f8}, GrayA{N0f8}(0.4,0.8)) === C{N0f8}(0.4,0.4,0.4)
        @test convert(C{N0f8}, AGray{N0f8}(0.4,0.8)) === C{N0f8}(0.4,0.4,0.4)
        @test convert(C{Float32},  Gray{N0f8}(0.4)    ) === C{Float32}(0.4N0f8,0.4N0f8,0.4N0f8)
        @test convert(C{Float32}, GrayA{N0f8}(0.4,0.8)) === C{Float32}(0.4N0f8,0.4N0f8,0.4N0f8)
        @test convert(C{Float32}, AGray{N0f8}(0.4,0.8)) === C{Float32}(0.4N0f8,0.4N0f8,0.4N0f8)
        @test convert(C{N0f8},  Gray{Float32}(0.4)    ) === C{N0f8}(0.4,0.4,0.4)
        @test convert(C{N0f8}, GrayA{Float32}(0.4,0.8)) === C{N0f8}(0.4,0.4,0.4)
        @test convert(C{N0f8}, AGray{Float32}(0.4,0.8)) === C{N0f8}(0.4,0.4,0.4)
        @test convert(C,  Gray24(0.4)    ) === C{N0f8}(0.4,0.4,0.4)
        @test convert(C, AGray32(0.4,0.8)) === C{N0f8}(0.4,0.4,0.4)
        @test convert(C{N0f8},  Gray24(0.4)    ) === C{N0f8}(0.4,0.4,0.4)
        @test convert(C{N0f8}, AGray32(0.4,0.8)) === C{N0f8}(0.4,0.4,0.4)
        @test convert(C{Float32},  Gray24(0.4)    ) === C{Float32}(0.4N0f8,0.4N0f8,0.4N0f8)
        @test convert(C{Float32}, AGray32(0.4,0.8)) === C{Float32}(0.4N0f8,0.4N0f8,0.4N0f8)
    end
    @testset "$C conversions" for C in Ctransparent
        @test convert(C,  Gray{N0f8}(0.4)    ) === C{N0f8}(0.4,0.4,0.4,1)
        @test convert(C,  Gray{N0f8}(0.4),0.2) === C{N0f8}(0.4,0.4,0.4,0.2)
        @test convert(C, GrayA{N0f8}(0.4,0.8)) === C{N0f8}(0.4,0.4,0.4,0.8)
        @test convert(C, AGray{N0f8}(0.4,0.8)) === C{N0f8}(0.4,0.4,0.4,0.8)
        @test convert(C{N0f8},  Gray{N0f8}(0.4)    ) === C{N0f8}(0.4,0.4,0.4,1)
        @test convert(C{N0f8},  Gray{N0f8}(0.4),0.2) === C{N0f8}(0.4,0.4,0.4,0.2)
        @test convert(C{N0f8}, GrayA{N0f8}(0.4,0.8)) === C{N0f8}(0.4,0.4,0.4,0.8)
        @test convert(C{N0f8}, AGray{N0f8}(0.4,0.8)) === C{N0f8}(0.4,0.4,0.4,0.8)
        @test convert(C{Float32},  Gray{N0f8}(0.4)    ) === C{Float32}(0.4N0f8,0.4N0f8,0.4N0f8,1)
        @test convert(C{Float32},  Gray{N0f8}(0.4),0.2) === C{Float32}(0.4N0f8,0.4N0f8,0.4N0f8,0.2)
        @test convert(C{Float32}, GrayA{N0f8}(0.4,0.8)) === C{Float32}(0.4N0f8,0.4N0f8,0.4N0f8,0.8N0f8)
        @test convert(C{Float32}, AGray{N0f8}(0.4,0.8)) === C{Float32}(0.4N0f8,0.4N0f8,0.4N0f8,0.8N0f8)
        @test convert(C{N0f8},  Gray{Float32}(0.4)    ) === C{N0f8}(0.4,0.4,0.4,1)
        @test convert(C{N0f8},  Gray{Float32}(0.4),0.2) === C{N0f8}(0.4,0.4,0.4,0.2)
        @test convert(C{N0f8}, GrayA{Float32}(0.4,0.8)) === C{N0f8}(0.4,0.4,0.4,0.8)
        @test convert(C{N0f8}, AGray{Float32}(0.4,0.8)) === C{N0f8}(0.4,0.4,0.4,0.8)
        @test convert(C,  Gray24(0.4)    ) === C{N0f8}(0.4,0.4,0.4,1)
        @test convert(C,  Gray24(0.4),0.2) === C{N0f8}(0.4,0.4,0.4,0.2)
        @test convert(C, AGray32(0.4,0.8)) === C{N0f8}(0.4,0.4,0.4,0.8)
        @test convert(C{N0f8},  Gray24(0.4)    ) === C{N0f8}(0.4,0.4,0.4,1)
        @test convert(C{N0f8},  Gray24(0.4),0.2) === C{N0f8}(0.4,0.4,0.4,0.2)
        @test convert(C{N0f8}, AGray32(0.4,0.8)) === C{N0f8}(0.4,0.4,0.4,0.8)
        @test convert(C{Float32},  Gray24(0.4)    ) === C{Float32}(0.4N0f8,0.4N0f8,0.4N0f8,1)
        @test convert(C{Float32},  Gray24(0.4),0.2) === C{Float32}(0.4N0f8,0.4N0f8,0.4N0f8,0.2)
        @test convert(C{Float32}, AGray32(0.4,0.8)) === C{Float32}(0.4N0f8,0.4N0f8,0.4N0f8,0.8N0f8)
    end
    @testset "RGB24 conversions" begin
        @test convert(RGB24,  Gray{N0f8}(0.4)    ) === RGB24(0.4,0.4,0.4)
        @test convert(RGB24, GrayA{N0f8}(0.4,0.8)) === RGB24(0.4,0.4,0.4)
        @test convert(RGB24, AGray{Float32}(0.4,0.8)) === RGB24(0.4,0.4,0.4)
        @test convert(RGB24,  Gray24(0.4)    ) === RGB24(0.4,0.4,0.4)
        @test convert(RGB24, AGray32(0.4,0.8)) === RGB24(0.4,0.4,0.4)
    end
    @testset "ARGB32 conversions" begin
        @test convert(ARGB32,  Gray{N0f8}(0.4)    ) === ARGB32(0.4,0.4,0.4,1)
        @test convert(ARGB32,  Gray{N0f8}(0.4),0.2) === ARGB32(0.4,0.4,0.4,0.2)
        @test convert(ARGB32, GrayA{N0f8}(0.4,0.8)) === ARGB32(0.4,0.4,0.4,0.8)
        @test convert(ARGB32, AGray{Float32}(0.4,0.8)) === ARGB32(0.4,0.4,0.4,0.8)
        @test convert(ARGB32,  Gray24(0.4)    ) === ARGB32(0.4,0.4,0.4,1)
        @test convert(ARGB32,  Gray24(0.4),0.2) === ARGB32(0.4,0.4,0.4,0.2)
        @test convert(ARGB32, AGray32(0.4,0.8)) === ARGB32(0.4,0.4,0.4,0.8)
    end
end

@testset "conversions between different spaces" begin
    @test_throws ErrorException convert(HSV, RGB(1,0,1))
    @test_throws ErrorException convert(AHSV, RGB(1,0,1), 0.5)
end

@testset "alphacolor/coloralpha for instances" begin
    Cp3 = ColorTypes.parametric3
    @testset "alphacolor/coloralpha: $C" for C in Cp3
        cf64, cf32 = C{Float64}(1, 0.6, 0), C{Float32}(1, 0.6, 0)
        for f in (alphacolor, coloralpha)
            A = f(C)
            @test f(cf64) === A{Float64}(1, 0.6, 0, 1)
            @test f(cf64, 0.8) === A{Float64}(1, 0.6, 0, 0.8)
            @test f(cf32, 0.8) === A{Float32}(1, 0.6, 0, 0.8)
            @test f(cf32, 0.8N0f8) === A{Float32}(1, 0.6, 0, 0.8N0f8)
        end
    end
    Ct3 = unique(vcat(coloralpha.(Cp3), alphacolor.(Cp3)))
    @testset "alphacolor/coloralpha: $C" for C in Ct3
        cf64, cf32 = C{Float64}(1, 0.6, 0, 0.4), C{Float32}(1, 0.6, 0, 0.4)
        for f in (alphacolor, coloralpha)
            A = f(C)
            @test f(cf64) === A{Float64}(1, 0.6, 0, 0.4)
            @test f(cf64, 0.8) === A{Float64}(1, 0.6, 0, 0.8)
            @test f(cf32, 0.8) === A{Float32}(1, 0.6, 0, 0.8)
            @test f(cf32, 0.8N0f8) === A{Float32}(1, 0.6, 0, 0.8N0f8)
        end
    end
    @testset "alphacolor/coloralpha: $C" for C in (Gray, AGray, GrayA)
        cf64 = C === Gray ? Gray(0.6)   : C{Float64}(0.6, 0.4)
        cf32 = C === Gray ? Gray(0.6f0) : C{Float32}(0.6, 0.4)
        for f in (alphacolor, coloralpha)
            A = f(C)
            @test f(cf64) === A{Float64}(0.6, C === Gray ? 1 : 0.4)
            @test f(cf64, 0.8) === A{Float64}(0.6, 0.8)
            @test f(cf32, 0.8) === A{Float32}(0.6, 0.8)
            @test f(cf32, 0.8N0f8) === A{Float32}(0.6, 0.8N0f8)
        end
    end

    @test alphacolor(RGB{N0f8}(1,0.6,0), 0.8) === ARGB{N0f8}(1,0.6,0,0.8)
    @test coloralpha(RGB{N0f8}(1,0.6,0), 0.8) === RGBA{N0f8}(1,0.6,0,0.8)

    @test alphacolor(RGB24(1,0.6,0)) === ARGB32(1,0.6,0, 1)
    @test alphacolor(RGB24(1,0.6,0), 0.8) === ARGB32(1,0.6,0, 0.8)
    @test_throws MethodError coloralpha(RGB24(1,0.6,0))
    @test_throws MethodError coloralpha(RGB24(1,0.6,0), 0.8)

    @test alphacolor(ARGB32(1,0.6,0)) === ARGB32(1,0.6,0, 1)
    @test alphacolor(ARGB32(1,0.6,0), 0.8) === ARGB32(1,0.6,0, 0.8)
    @test_throws MethodError coloralpha(ARGB32(1,0.6,0))
    @test_throws MethodError coloralpha(ARGB32(1,0.6,0), 0.8)

    @test alphacolor(Gray24(0.6)) === AGray32(0.6, 1)
    @test alphacolor(Gray24(0.6), 0.8) === AGray32(0.6, 0.8)
    @test_throws MethodError coloralpha(Gray24(0.6))
    @test_throws MethodError coloralpha(Gray24(0.6), 0.8)

    @test alphacolor(AGray32(0.6)) === AGray32(0.6, 1)
    @test alphacolor(AGray32(0.6), 0.8) === AGray32(0.6, 0.8)
    @test_throws MethodError coloralpha(AGray32(0.6))
    @test_throws MethodError coloralpha(AGray32(0.6), 0.8)
end

@testset "reinterpret" begin
    @test reinterpret(UInt32, RGB24()) === 0x00000000
    @test reinterpret(UInt32, ARGB32()) === 0xff000000
    @test reinterpret(UInt32, Gray24()) === 0x00000000
    @test reinterpret(UInt32, AGray32()) === 0xff000000

    @test reinterpret(UInt32, RGB24(0.071,0.204,0.337)) === 0x00123456
    @test reinterpret(UInt32, ARGB32(0.071,0.204,0.337,0.671)) === 0xab123456
    @test reinterpret(UInt32, Gray24(0.071)) === 0x00121212
    @test reinterpret(UInt32, AGray32(0.071, 0.671)) === 0xab121212

    @test reinterpret(RGB24, 0x00123456) === RGB24(0.071,0.204,0.337)
    @test reinterpret(RGB24, 0xdc123456) ==  RGB24(0.071,0.204,0.337)
    @test reinterpret(RGB24, 0x00123456) !== reinterpret(RGB24, 0xdc123456)
    @test reinterpret(UInt32, reinterpret(RGB24, 0xdc123456)) === 0xdc123456

    @test reinterpret(ARGB32, 0xab123456) === ARGB32(0.071,0.204,0.337,0.671)

    @test reinterpret(Gray24, 0x00121212) === Gray24(0.071)
    @test reinterpret(Gray24, 0xdc121212) ==  Gray24(0.071)
    @test reinterpret(Gray24, 0x00121212) !== reinterpret(Gray24, 0xdc121212)
    @test reinterpret(UInt32, reinterpret(Gray24, 0xdc121212)) === 0xdc121212
    # the following behavior is undefined, so this is just for the regression test.
    @test reinterpret(Gray24, 0xdc00ff12) ==  Gray24(0.071)
    @test reinterpret(UInt32, reinterpret(Gray24, 0xdc00ff12)) === 0xdc00ff12

    @test reinterpret(AGray32, 0xab121212) === AGray32(0.071, 0.671)

    @test_throws ErrorException reinterpret(UInt32, ARGB{N0f8}(1, 0, 0))
    @test_throws ErrorException reinterpret(ARGB{N0f8}, 0x12345678)
end

### Prevent ambiguous definitions

# Certain types, like Gray24, reinterpret a UInt32 as having a
# particular color meaning. The problem with defining `convert`
# methods is that a UInt32 has a value, e.g., 0 = black and 1 = white.
# Users who want to interpret a UInt32 as a bit pattern should
# explicitly use `reinterpret`.
@testset "bit pattern ambiguities" begin
    @test_throws MethodError convert(UInt32, RGB24(1,1,1))
    @test_throws MethodError convert(UInt32, ARGB32(1,1,1,0))
    @test convert(UInt32, Gray24(1)) === UInt32(1)
    @test_throws InexactError convert(UInt32, Gray24(0.5))
    @test_throws MethodError convert(UInt32, AGray32(1,0))
    @test !(RGB24(1,1,1) == 0x00ffffff)
    @test !(ARGB32(1,1,1,0) == 0x00ffffff)
    @test Gray24(1) == 1
    @test !(Gray24(1) == 0x00ffffff)
    @test !(AGray32(1,0) == 0x00ffffff)
    ret = @test_throws ArgumentError RGB24(0x00ffffff)
    @test occursin("Use `reinterpret(RGB24, 0x00ffffff)`", ret.value.msg)
    ret = @test_throws ArgumentError ARGB32(0x00ffffff)
    @test_broken occursin("Use `reinterpret(ARGB32, 0x00ffffff)`", ret.value.msg)
    ret = @test_throws ArgumentError Gray24(0x00ffffff)
    @test occursin("Use `reinterpret(Gray24, 0x00ffffff)`", ret.value.msg)
    ret = @test_throws ArgumentError AGray32(0x00ffffff)
    @test occursin("Use `reinterpret(AGray32, 0x00ffffff)`", ret.value.msg)
    c = 0x123456
    ret = @test_throws ArgumentError RGB24(c >> 16 & 0xFF, c >> 8 & 0xFF, c & 0xFF)
    @test !occursin("Use `reinterpret(RGB24", ret.value.msg)
    @test occursin("(0x00000012, 0x00000034, 0x00000056) do not lie", ret.value.msg)
    @test_throws ArgumentError RGB24[0x00000000,0x00808080]
    @test_throws ArgumentError RGB24[0x00000000,0x00808080,0x00ffffff]
    @test_throws ArgumentError RGB24[0x00000000,0x00808080,0x00ffffff,0x000000ff]
end


export iauAtoc13
"""
Observed place at a groundbased site to to ICRS astrometric RA,Dec.
The caller supplies UTC, site coordinates, ambient air conditions
and observing wavelength.

This function is part of the International Astronomical Union's
SOFA (Standards of Fundamental Astronomy) software collection.

Status:  support function.

Given:
   type   char[]   type of coordinates - "R", "H" or "A" (Notes 1,2)
   ob1    double   observed Az, HA or RA (radians; Az is N=0,E=90)
   ob2    double   observed ZD or Dec (radians)
   utc1   double   UTC as a 2-part...
   utc2   double   ...quasi Julian Date (Notes 3,4)
   dut1   double   UT1-UTC (seconds, Note 5)
   elong  double   longitude (radians, east +ve, Note 6)
   phi    double   geodetic latitude (radians, Note 6)
   hm     double   height above ellipsoid (m, geodetic Notes 6,8)
   xp,yp  double   polar motion coordinates (radians, Note 7)
   phpa   double   pressure at the observer (hPa = mB, Note 8)
   tc     double   ambient temperature at the observer (deg C)
   rh     double   relative humidity at the observer (range 0-1)
   wl     double   wavelength (micrometers, Note 9)

Returned:
   rc,dc  double   ICRS astrometric RA,Dec (radians)

Returned (function value):
         int      status: +1 = dubious year (Note 4)
                           0 = OK
                           -1 = unacceptable date

Notes:

   1.  "Observed" Az,ZD means the position that would be seen by a
   perfect geodetically aligned theodolite.  (Zenith distance is
   used rather than altitude in order to reflect the fact that no
   allowance is made for depression of the horizon.)  This is
   related to the observed HA,Dec via the standard rotation, using
   the geodetic latitude (corrected for polar motion), while the
   observed HA and RA are related simply through the Earth rotation
   angle and the site longitude.  "Observed" RA,Dec or HA,Dec thus
   means the position that would be seen by a perfect equatorial
   with its polar axis aligned to the Earth's axis of rotation.

   2.  Only the first character of the type argument is significant.
   "R" or "r" indicates that ob1 and ob2 are the observed right
   ascension and declination;  "H" or "h" indicates that they are
   hour angle (west +ve) and declination;  anything else ("A" or
   "a" is recommended) indicates that ob1 and ob2 are azimuth
   (north zero, east 90 deg) and zenith distance.

   3.  utc1+utc2 is quasi Julian Date (see Note 2), apportioned in any
   convenient way between the two arguments, for example where utc1
   is the Julian Day Number and utc2 is the fraction of a day.

   However, JD cannot unambiguously represent UTC during a leap
   second unless special measures are taken.  The convention in the
   present function is that the JD day represents UTC days whether
   the length is 86399, 86400 or 86401 SI seconds.

   Applications should use the function iauDtf2d to convert from
   calendar date and time of day into 2-part quasi Julian Date, as
   it implements the leap-second-ambiguity convention just
   described.

   4.  The warning status "dubious year" flags UTCs that predate the
   introduction of the time scale or that are too far in the
   future to be trusted.  See iauDat for further details.

   5.  UT1-UTC is tabulated in IERS bulletins.  It increases by exactly
   one second at the end of each positive UTC leap second,
   introduced in order to keep UT1-UTC within +/- 0.9s.  n.b. This
   practice is under review, and in the future UT1-UTC may grow
   essentially without limit.

   6.  The geographical coordinates are with respect to the WGS84
   reference ellipsoid.  TAKE CARE WITH THE LONGITUDE SIGN:  the
   longitude required by the present function is east-positive
   (i.e. right-handed), in accordance with geographical convention.

   7.  The polar motion xp,yp can be obtained from IERS bulletins.  The
   values are the coordinates (in radians) of the Celestial
   Intermediate Pole with respect to the International Terrestrial
   Reference System (see IERS Conventions 2003), measured along the
   meridians 0 and 90 deg west respectively.  For many
   applications, xp and yp can be set to zero.

   8.  If hm, the height above the ellipsoid of the observing station
   in meters, is not known but phpa, the pressure in hPa (=mB), is
   available, an adequate estimate of hm can be obtained from the
   expression

         hm = -29.3 * tsl * log ( phpa / 1013.25 );

   where tsl is the approximate sea-level air temperature in K
   (See Astrophysical Quantities, C.W.Allen, 3rd edition, section
   52).  Similarly, if the pressure phpa is not known, it can be
   estimated from the height of the observing station, hm, as
   follows:

         phpa = 1013.25 * exp ( -hm / ( 29.3 * tsl ) );

   Note, however, that the refraction is nearly proportional to
   the pressure and that an accurate phpa value is important for
   precise work.

   9.  The argument wl specifies the observing wavelength in
   micrometers.  The transition from optical to radio is assumed to
   occur at 100 micrometers (about 3000 GHz).

   10. The accuracy of the result is limited by the corrections for
   refraction, which use a simple A*tan(z) + B*tan^3(z) model.
   Providing the meteorological parameters are known accurately and
   there are no gross local effects, the predicted astrometric
   coordinates should be within 0.05 arcsec (optical) or 1 arcsec
   (radio) for a zenith distance of less than 70 degrees, better
   than 30 arcsec (optical or radio) at 85 degrees and better
   than 20 arcmin (optical) or 30 arcmin (radio) at the horizon.

   Without refraction, the complementary functions iauAtco13 and
   iauAtoc13 are self-consistent to better than 1 microarcsecond
   all over the celestial sphere.  With refraction included,
   consistency falls off at high zenith distances, but is still
   better than 0.05 arcsec at 85 degrees.

   11. It is advisable to take great care with units, as even unlikely
   values of the input parameters are accepted and processed in
   accordance with the models used.

Called:
   iauApco13    astrometry parameters, ICRS-observed
   iauAtoiq     quick observed to CIRS
   iauAticq     quick CIRS to ICRS

This revision:   2013 October 9

SOFA release 2018-01-30

Copyright (C) 2018 IAU SOFA Board.  See notes at end.
"""
# int iauAtoc13(const char *type, double ob1, double ob2,
#               double utc1, double utc2, double dut1,
#               double elong, double phi, double hm, double xp, double yp,
#               double phpa, double tc, double rh, double wl,
#               double *rc, double *dc)

function iauAtoc13(type_::Char, ob1::Real, ob2::Real,
               utc1::Real, utc2::Real, dut1::Real,
               elong::Real, phi::Real, hm::Real, xp::Real, yp::Real,
               phpa::Real, tc::Real, rh::Real, wl::Real)

   # Allocate return values
   ref_type_  = Ref{UInt8}(convert(UInt8, type_))
   ref_rc     = Ref{Float64}(0.0)
   ref_dc     = Ref{Float64}(0.0)

   status = ccall((:iauAtoc13, libsofa_c), Cint,
       (Ref{UInt8}, Cdouble, Cdouble, Cdouble, Cdouble, Cdouble,
       Cdouble, Cdouble, Cdouble, Cdouble, Cdouble,
       Cdouble, Cdouble, Cdouble, Cdouble, Ref{Cdouble}, Ref{Cdouble}),
       ref_type_, convert(Float64, ob1), convert(Float64, ob2),
       convert(Float64, utc1), convert(Float64, utc2),
       convert(Float64, dut1), convert(Float64, elong),
       convert(Float64, phi), convert(Float64, hm),
       convert(Float64, xp), convert(Float64, yp),
       convert(Float64, phpa), convert(Float64, tc),
       convert(Float64, rh), convert(Float64, wl),
       ref_rc, ref_dc)

   return status, ref_rc[], ref_dc[]
end


# # Example: long, single end reads
#
# This is an example generated from this source
# file: [`se-example.jl`](@__REPO_ROOT_URL__/examples/sequencing/se-example.jl)
# You are seeing the
#md # online documentation version. The corresponding notebook can be found
#md # here: [`se-example.ipynb`](@__NBVIEWER_ROOT_URL__/man/sequencing/examples/se-example.ipynb)
#nb # jupyter notebook version. The corresponding online documentation page can
#nb # be found here: [`se-example.html`](https://bioinfologics.github.io/Pseudoseq.jl/latest/examples/sequencing/se-example),
# and the script can be found here: [`se-example.jl`](./se-example.jl)

# Let's see how you might simulate something like an Oxford Nanopore sequencing
# experiment.
#
# For the simulation we are going to:
#
# 1. Create a pool of 5000 copies of a reference genome.
# 2. Fragment the DNA molecules in the pool, to an average length of 40,000bp.
# 3. Subsample the molecules in the pool to achieve approximatly 30x coverage.
# 4. Create a set of single-end reads, the enitre length of each molecule.
# 5. Apply errors to the reads at a rate of 0.10 (1 error every 10bp).
# 6. Generate an output FASTQ file.

using Pseudoseq.Sequencing

# Starting with a FASTA formatted file containing the genome we want to sequence,
# we create a pool with a copy of the genome.

m = Molecules("ecoli-ref.fasta")

# We want to first have 5000 full copies of the genome, so we will make an amplifier.

amp = amplify(5000)

# Next we want a fragmenter to make a pool of shorter DNA molecules, with an average
# length of 40,000bp.

frag = fragment(40000bp)

# Next we create a subsampler which will randomly sample molecules, to give us
# a desired expected coverage. We specify 40000bp as our predicted average read
# length.

ssmpl = subsample(50X, 40000bp)

# We then want a read-maker that will give us single end reads.
# Since we call `makereads` without providing a read length, the function will
# generate reads from the entire length of each molecule in the pool. We do this
# to emulate what Nanopore sequencing is supposed to do: It takes an entire DNA
# fragment, feeds it through an electrically charged pore, producing a read for
# the entire fragment.

readmaker = makereads()

# Once we have reads, we will mark positions in the reads that are incorrectly
# detected by the sequencer: errors.
# We will construct a `FixedProbSubstitutions` function with a per base error
# probability of 0.1 and pass it to the `make_substitutions` method.
# This will make errors fall totally randomly over each read.

errmaker = make_substitutions(FixedProbSubstitutions(0.1))

# Now we can push our molecules through a pipeline of these processors, and out
# to a FASTQ file:

m |> amp |> frag |> ssmpl |> readmaker |> errmaker |> generate("se-reads.fastq")

# You can also compose the processors together into one whole function.
# Typing \circ in the julia repl and then hitting tab gives you the circular
# composition symbol. Note how pipelining above progresses from left to right,
# but composition is right to left in order.

my_protocol = errmaker ∘ readmaker ∘ ssmpl ∘ frag ∘ amp

m |> my_protocol |> generate("se-reads.fastq")


mutable struct FixedSimParams <: RayTracer.FixedParams
    rng::MersenneTwister
    _map::Map
    max_steps::Int              # Maximum number of steps per episode
    draw_curve::Bool                # Flag to draw the road curve
    draw_bbox::Bool                 # Flag to draw bounding boxes
    domain_rand::Bool               # Flag to enable/disable domain randomization
    randomizer::Union{Randomizer, Nothing}
    randomization_settings::Union{Dict, Nothing}
    graphics::Bool
    frame_rate::Int                 # Frame rate to run at
    frame_skip::Int                 # Number of frames to skip per action
    delta_time::Float32
    camera_width::Int
    camera_height::Int
    robot_speed::Float32
    action_space::Box
    observation_space::Box
    reward_range::NTuple{2, Int}
    #window                          # Window for displaying the environment to humans
    #img_array::Array               # Array to render the image into (for observation rendering)
    accept_start_angle_deg::Float32    # allowed angle in lane for starting position
    full_transparency::Bool
    user_tile_start                 # Start tile
    distortion::Bool
    randomize_maps_on_reset::Bool
    camera_model::Nothing
    map_names::Union{Vector{String}, Nothing}
    undistort::Bool
    horizon_color::Vec3
    ground_color::Vec3
    wheel_dist::Float32
    cam_height::Float32
    cam_angle::Vector
    cam_fov_y::Float32
    cam_offset::Vector
    raytrace::Bool	# If false, then uses rasterizer
    train::Bool		# to incorporate some hacks for training purpose
end

function FixedSimParams(map_name::String=DEFAULT_MAP_NAME,
                        max_steps::Int=DEFAULT_MAX_STEPS;
                        draw_curve::Bool=false,
                        draw_bbox::Bool=false,
                        domain_rand::Bool=true,
                        frame_rate::Int=DEFAULT_FRAMERATE,
                        frame_skip::Int=DEFAULT_FRAME_SKIP,
                        camera_width::Int=DEFAULT_CAMERA_WIDTH,
                        camera_height::Int=DEFAULT_CAMERA_HEIGHT,
                        robot_speed::Float32=DEFAULT_ROBOT_SPEED,
                        accept_start_angle_deg::Real=DEFAULT_ACCEPT_START_ANGLE_DEG,
                        full_transparency::Bool=false,
                        user_tile_start=nothing,
                        seed=nothing,
                        distortion::Bool=false,
                        randomize_maps_on_reset::Bool=false,
						raytrace::Bool=true,
						train::Bool=false)
    #=
    :param map_name:
    :param max_steps:
    :param draw_curve:
    :param draw_bbox:
    :param domain_rand: If true, applies domain randomization
    :param frame_rate:
    :param frame_skip:
    :param camera_width:
    :param camera_height:
    :param robot_speed:
    :param accept_start_angle_deg:
    :param full_transparency:   # If true, then we publish all transparency information
    :param user_tile_start: If None, sample randomly. Otherwise (i,j). Overrides map start tile
    :param seed:
    :param distortion: If true, distorts the image with fish-eye approximation
    :param randomize_maps_on_reset: If true, randomizes the map on reset (Slows down training)
    =#

    # first initialize the RNG
    rng = MersenneTwister(seed)

    _map = Map(map_name, domain_rand)

    randomizer, randomization_settings = nothing, nothing
    if domain_rand
        randomizer = Randomizer()
        randomization_settings = randomize(randomizer)
    end

    delta_time = 1f0 / frame_rate

    # Produce graphical output
    graphics = true

    # Two-tuple of wheel torques, each in the range [-1, 1]
    action_space = Box(-1, 1, (2,), Float32)

    # We observe an RGB image with pixels in [0, 255]
    # Note: the pixels are in UInt8 format because this is more compact
    # than Float32 if sent over the network or stored in a dataset
    observation_space = Box(0, 255, (camera_height, camera_width, 3), UInt8)

    reward_range = (-1000, 1000)

    # Distortion params, if so, load the library, only if not bbox mode
    distortion = distortion && !draw_bbox
    camera_model = nothing
    if !draw_bbox && distortion
        if distortion
            throw(error("Currently not supporting distortion!"))
            #include("distortion.jl")
            #camera_model = Distortion()
        end
    end

    map_names = nothing
    if randomize_maps_on_reset
        map_names = readdir("src/maps")
        map_names = map(mapfile->replace(mapfile, ".yaml"=>""), map_names)
    end

    # Used by the UndistortWrapper, always initialized to False
    undistort = false

    # Robot's current speed
    speed = 0f0

    horizon_color = BLUE_SKY_COLOR
    ground_color = GROUND_COLOR

    # Distance between the robot's wheels
    wheel_dist = WHEEL_DIST

    # Distance bewteen camera and ground
    cam_height = CAMERA_FLOOR_DIST

    # Angle at which the camera is rotated
    cam_angle = [CAMERA_ANGLE, 0, 0]

    # Field of view angle of the camera
    cam_fov_y = CAMERA_FOV_Y

    # Camera offset for use in free camera mode
    cam_offset = zeros(Float32, 3)

    FixedSimParams(rng, _map, max_steps, draw_curve, draw_bbox, domain_rand,
                    randomizer, randomization_settings, graphics, frame_rate,
                    frame_skip, delta_time, camera_width, camera_height,
                    robot_speed, action_space, observation_space, reward_range,
                    accept_start_angle_deg, full_transparency, user_tile_start,
                    distortion, randomize_maps_on_reset, camera_model,
                    map_names, undistort, horizon_color, ground_color, wheel_dist, cam_height,
                    cam_angle, cam_fov_y, cam_offset, raytrace, train)
end

function reset!(fsp::FixedSimParams)
    # Step count since episode start
    if fsp.randomize_maps_on_reset
        map_name = rand(fsp.map_names)
        fsp._map = Map(map_name, fsp._map.map_file_path)
    end

    #TODO: Randomizer
    if fsp.domain_rand
        fsp.randomization_settings = randomize(fsp.randomizer)
    end

    # Horizon color
    # Note: we explicitly sample white and grey/black because
    # these colors are easily confused for road and lane markings
    if fsp.domain_rand
        horz_mode = fsp.randomization_settings["horz_mode"]
        if horz_mode == 0
            fsp.horizon_color = _perturb(fsp, BLUE_SKY_COLOR)
        elseif horz_mode == 1
            fsp.horizon_color = _perturb(fsp, WALL_COLOR)
        elseif horz_mode == 2
            fsp.horizon_color = _perturb(fsp, ones(Float32, 3)*0.15f0, 0.4f0)
        elseif horz_mode == 3
            fsp.horizon_color = _perturb(fsp, ones(Float32, 3)*0.9f0, 0.4f0)
        end
    end

    # Ground color
    fsp.ground_color = _perturb(fsp, GROUND_COLOR, 0.3f0)

    # Distance between the robot's wheels
    fsp.wheel_dist = _perturb(fsp, WHEEL_DIST)

    # Distance between camera and ground
    fsp.cam_height = _perturb(fsp, CAMERA_FLOOR_DIST, 0.08f0)

    # Angle at which the camera is rotated
    fsp.cam_angle = vcat(_perturb(fsp, CAMERA_ANGLE, 0.2f0), 0, 0)

    # Field of view angle of the camera
    fsp.cam_fov_y = _perturb(fsp, CAMERA_FOV_Y, 0.2f0)

    # Camera offset for use in free camera mode
    fsp.cam_offset = zeros(Float32, 3)

    # Create the vertex list for the ground/noise triangles
    # These are distractors, junk on the floor
    numTris = 12
    verts = []
    colors = []
    for _ in 0 : 3numTris
        p = [rand(fsp.rng, Uniform(-20f0, 20f0)),
             rand(fsp.rng, Uniform(-0.6f0, -0.3f0)),
             rand(fsp.rng, Uniform(-20f0, 20f0))]
        c = Float32(rand(fsp.rng, Uniform(0f0, 0.9f0)))
        c = _perturb(fsp, ones(Float32, 3)*c, 0.1f0)
        verts = vcat(verts, p)
        colors = vcat(colors, c)
    end

    #=
    import pyglet
    self.tri_vlist = pyglet.graphics.vertex_list(3 * numTris, ('v3f', verts), ('c3f', colors))
    =#

    # Randomize tile parameters
    for tile in _grid(fsp)._grid
        rng = fsp.domain_rand ? fsp.rng : nothing
        # Randomize the tile texture
        # comment till textures are implemented
        tile["texture"] = Graphics.get(tile["kind"], fsp.rng)

        # Random tile color multiplier
        tile["color"] = _perturb(fsp, Vec3([1f0]), 0.2f0)
    end

    # Randomize object parameters
    for obj in _objects(fsp)
        # Randomize the object color
        _set_color!(obj, _perturb(fsp, Vec3([1f0]), 0.3f0))

        # Randomize whether the object is visible or not
        if _optional(obj) && fsp.domain_rand
            _set_visible!(obj, rand(fsp.rng, 0:1) == 0)
        else
            _set_visible!(obj, true)
        end
    end

    # If the map specifies a starting tile
    if !isnothing(fsp.user_tile_start)
        #logger.info('using user tile start: %s' % self.user_tile_start)
        i, j = fsp.user_tile_start
        tile = _get_tile(_grid(fsp), i, j)
        if isnothing(tile)
            msg = "The tile specified does not exist."
            throw(error(msg))
        end
        #logger.debug('tile: %s' % tile)
    else
        if !isnothing(_start_tile(fsp))
            tile = _start_tile(fsp)
        else
            # Select a random drivable tile to start on
            tile_idx = rand(fsp.rng, 1:length(_drivable_tiles(fsp)))
            tile = _drivable_tiles(fsp)[tile_idx]
        end
    end

    propose_pos, propose_angle = nothing, nothing

    for iter in 1:MAX_SPAWN_ATTEMPTS
        i, j = tile["coords"]

        # Choose a random position on this tile
        x = rand(fsp.rng, Uniform(i-1f0, i)) * _road_tile_size(fsp)
        z = rand(fsp.rng, Uniform(j-1f0, j)) * _road_tile_size(fsp)
        propose_pos = Float32.([x, 0f0, z])

        # Choose a random direction
        propose_angle = Float32(rand(fsp.rng, Uniform(0f0, 2π)))

        # logger.debug('Sampled %s %s angle %s' % (propose_pos[0],
        #                                          propose_pos[1],
        #                                          np.rad2deg(propose_angle)))

        # If this is too close to an object or not a valid pose, retry
        inconvenient = _inconvenient_spawn(_objects(fsp), propose_pos)

        inconvenient && continue

        invalid = !_valid_pose(fsp, propose_pos, propose_angle, 1.3f0)
        invalid && continue

        # If the angle is too far away from the driving direction, retry
        lp = get_lane_pos2(fsp, propose_pos, propose_angle)

        !is_inlane(lp) && continue

        M = fsp.accept_start_angle_deg
        ok = -M < lp.angle_deg < +M
        if !ok
            if iter == MAX_SPAWN_ATTEMPTS
                msg = "Could not find a valid starting pose after $MAX_SPAWN_ATTEMPTS attempts"
                throw(error(msg))
            end
            continue
        end
        # Found a valid initial pose
        break
    end

    return propose_pos, propose_angle

end

function _perturb(fsp::FixedSimParams, val::Vec3, scale=0.1f0)
    val = [val.x[1], val.y[1], val.z[1]]
    val = _perturb(fsp, val, scale)
    return Vec3(val...)
end

function _perturb(fsp::FixedSimParams, val, scale=0.1f0)
    ##
    #Add noise to a value. This is used for domain randomization.
    ##
    #@assert 0f0 ≤ scale < 1f0

    !fsp.domain_rand && (return val)

    noise = Float32.(rand(fsp.rng, Uniform(1f0-scale, 1f0+scale), size(val)...))

    return val .* noise
end

function _inconvenient_spawn(objects::Vector, pos)
    ##
    #Check that agent spawn is not too close to any object
    ##

    cond(x) = norm(_pos(x) .- pos) <
               maximum(_max_coords(x)) * 0.5f0 * _scale(x) + MIN_SPAWN_OBJ_DIST
    arr = filter(x->_visible(x), objects)
    results = map(x->cond(x), arr)

    return any(results)
end

function _valid_pose(fsp::FixedSimParams, pos, angle, safety_factor=1f0)
    ##
    #    Check that the agent is in a valid pose
    #
    #    safety_factor = minimum distance
    ##

    # Compute the coordinates of the base of both wheels
    pos = _actual_center(pos, angle)
    f_vec = get_dir_vec(angle)
    r_vec = get_right_vec(angle)

    l_pos = pos .- (safety_factor * 0.5f0 * ROBOT_WIDTH) .* r_vec
    r_pos = pos .+ (safety_factor * 0.5f0 * ROBOT_WIDTH) .* r_vec
    f_pos = pos .+ (safety_factor * 0.5f0 * ROBOT_LENGTH) .* f_vec

    # Check that the center position and
    # both wheels are on drivable tiles and no collisions

    all_drivable = (_drivable_pos(_grid(fsp), pos) &&
                    _drivable_pos(_grid(fsp), l_pos) &&
                    _drivable_pos(_grid(fsp), r_pos) &&
                    _drivable_pos(_grid(fsp), f_pos))

    # Recompute the bounding boxes (BB) for the agent
    agent_corners = get_agent_corners(pos, angle)
    no_collision = !_collision(fsp, agent_corners)

    res = (no_collision && all_drivable)

    if !res
        #logger.debug(f'Invalid pose. Collision free: {no_collision} On drivable area: {all_drivable}')
        #logger.debug(f'safety_factor: {safety_factor}')
        #logger.debug(f'pos: {pos}')
        #logger.debug(f'l_pos: {l_pos}')
        #logger.debug(f'r_pos: {r_pos}')
        #logger.debug(f'f_pos: {f_pos}')
    end

    return res
end

Zygote.@nograd _valid_pose

function _collision(fsp::FixedSimParams, agent_corners::Matrix{Float32})
    ##
    #Tensor-based OBB Collision detection
    ##

    # If there are no objects to collide against, stop
    length(_collidable_corners(fsp)) == 0 && (return false)

    # Generate the norms corresponding to each face of BB
    agent_norm = generate_norm(agent_corners)

    # Check collisions with static objects
    collision = intersects(
            agent_corners,
            _collidable_corners(fsp),
            agent_norm,
            _collidable_norms(fsp)
    )

    collision && (return true)

    # Check collisions with Dynamic Objects
    for obj in _objects(fsp)
        check_collision(obj, agent_corners, agent_norm) && (return true)
    end

    # No collision with any object
    return false
end

# FIXME: this does not follow the same signature as WorldOb
# NOTE: This is actually function meant for DuckiebotObj, defined here to break
#       cyclic dependency of types
function step!(db_obj::DuckiebotObj, delta_time::Float32, fp::FixedSimParams, closest_curve_point, objects::Vector)
    ##
    #Take a step, implemented as a PID controller
    ##

    # Find the curve point closest to the agent, and the tangent at that point
    closest_point, closest_tangent = closest_curve_point(fp, db_obj.wobj.pos, db_obj.wobj.angle)

    iterations = 0

    lookup_distance = db_obj.follow_dist
    curve_point = nothing
    while iterations < db_obj.max_iterations
        # Project a point ahead along the curve tangent,
        # then find the closest point to to that
        follow_point = closest_point .+ closest_tangent * lookup_distance
        curve_point, _ = closest_curve_point(fp, follow_point, db_obj.wobj.angle)

        # If we have a valid point on the curve, stop
        isnothing(curve_point) && break

        iterations += 1
        lookup_distance *= 0.5f0
    end

    # Compute a normalized vector to the curve point
    point_vec = curve_point .- db_obj.wobj.pos
    point_vec = point_vec ./ norm(point_vec)

    dot_ = dot(get_right_vec(db_obj, db_obj.wobj.angle), point_vec)
    steering = db_obj.gain * (-dot_)

    _update_pos(db_obj, Float32.([db_obj.velocity, steering]), delta_time)
end


@testset "CPReward" begin
    @testset "set_reward!(DecisionPhase)" begin
        trailer = SeaPearl.Trailer()
        model = SeaPearl.CPModel(trailer)
        lh = SeaPearl.LearnedHeuristic{SeaPearl.DefaultStateRepresentation{SeaPearl.DefaultFeaturization, SeaPearl.DefaultTrajectoryState}, SeaPearl.CPReward, SeaPearl.FixedOutput}(agent)
        SeaPearl.update_with_cpmodel!(lh, model)

        lh.reward.value = 0
        SeaPearl.set_reward!(SeaPearl.DecisionPhase, lh, model)
        @test lh.reward.value == 0
    end

    @testset "set_reward!(StepPhase)" begin
        trailer = SeaPearl.Trailer()
        model = SeaPearl.CPModel(trailer)
        lh = SeaPearl.LearnedHeuristic{SeaPearl.DefaultStateRepresentation{SeaPearl.DefaultFeaturization, SeaPearl.DefaultTrajectoryState}, SeaPearl.CPReward, SeaPearl.FixedOutput}(agent)
        SeaPearl.update_with_cpmodel!(lh, model)

        lh.reward.value = 0
        SeaPearl.set_reward!(SeaPearl.StepPhase, lh, model,nothing)
        @test lh.reward.value == 0
    end

    @testset "set_reward!(EndingPhase)" begin
        trailer = SeaPearl.Trailer()
        model = SeaPearl.CPModel(trailer)
        x = SeaPearl.IntVar(2, 3, "x", trailer)
        y = SeaPearl.IntVar(3, 4, "y", trailer)
        z = SeaPearl.IntVar(4, 5, "z", trailer)
        SeaPearl.addVariable!(model, x)
        SeaPearl.addVariable!(model, y)
        SeaPearl.addVariable!(model, z; branchable=false)
        lh = SeaPearl.LearnedHeuristic{SeaPearl.DefaultStateRepresentation{SeaPearl.DefaultFeaturization, SeaPearl.DefaultTrajectoryState}, SeaPearl.CPReward, SeaPearl.FixedOutput}(agent)
        SeaPearl.update_with_cpmodel!(lh, model)

        lh.reward.value = 5
        model.statistics.numberOfNodes = 30
        SeaPearl.set_reward!(SeaPearl.EndingPhase, lh, model, :FoundSolution)
        @test lh.reward.value == 6
        SeaPearl.assign!(x,2)
        SeaPearl.set_reward!(SeaPearl.EndingPhase, lh, model, :Infeasible)
        @test lh.reward.value == 5
    end
end


# ==================================================================================================================
function solveStokes_penalty( U, P,  θ, r, Ucartesian,Upolar, g, ρ, η, 𝓒,
                      theta, rr, TT,
                      ∂Ωu, ufix, ifree,
                      PhaseID, EL2NOD, EL2NODP,
                      KKidx, GGidx, invMMidx,
                      to)
    @timeit to "Stokes" begin
        @timeit to "Assembly" KK, GG, invMM, Rhs =
            assembly_stokes_penalty(EL2NOD, EL2NODP, theta, rr, g, ρ, η, 𝓒, PhaseID, KKidx, GGidx, invMMidx)
        @timeit to "BCs" begin
            KK, GG, Rhs = _prepare_matrices_penalty(KK, GG, Rhs, TT)
            U, Rhs = _apply_bcs(U,KK,Rhs,∂Ωu,ufix)
        end
        @timeit to "Solve" U, P = PowellHesteness(U, P, KK, invMM, GG, Rhs, ifree)
        @timeit to "Remove net rotation" U, Ucart, Upolar, Ucartesian = updatevelocity(U,Ucartesian, Upolar,  θ, r,TT)
        # @timeit to "Remove nullspace"  U, Ucart, Upolar, Ucartesian = updatevelocity2(U, Ucartesian, Upolar, ρ, EL2NOD, TT, theta, rr, r)

    end
    return Ucartesian,Upolar,U,Ucart,P,to
end
# ==================================================================================================================

@inline function _apply_bcs_penalty(U, KK, Rhs, ∂Ω, vfix)
    U[∂Ω] = vfix # write prescribed temperature values
    Rhs -= KK[:,∂Ω] * vfix
    return U,Rhs
end

@inline function _prepare_matrices_penalty(KK, GG, Fb,TT)

    dropzeros!(GG)
    KK = SparseMatrixCSC(Symmetric(KK,:L))
    dropzeros!(KK)
    transTT = SparseMatrixCSC(TT')
    KK = transTT*KK*TT
    GG = transTT*GG
    Fb = transTT*Fb
    KK = SparseMatrixCSC(Symmetric(KK,:L))
    return KK,GG,Fb
end

function assembly_stokes_penalty(EL2NOD, EL2NODP, theta, r, g, ρ, η, 𝓒, PhaseID, KKidx, GGidx, invMMidx)
    penalty = 1e6
    # ============================================ MODEL AND BLOCKING PARAMETERS
    ndim = 2
    # nvert = 3
    # EL2NOD0 = deepcopy(EL2NOD)
    nnodel = size(EL2NOD,1)
    nel = size(EL2NOD,2)
    nelblk = min(nel, 1200)
    nblk = ceil(Int,nel/nelblk)
    il = one(nelblk); iu = nelblk
    # =========== PREPARE INTEGRATION POINTS & DERIVATIVES wrt LOCAL COORDINATES
    nip = 6
    # nUdof = ndim*maximum(EL2NOD)
    # nPdof = maximum(EL2NODP)
    nUdofel = ndim * nnodel
    nPdofel = size(EL2NODP,1)
    ni, nn, nnP, nn3 = Val(nip), Val(nnodel), Val(3), Val(3)
    N,dNds,_,w_ip = _get_SF(ni,nn)
    NP,_,_,_ = _get_SF(ni,nnP)
    N3,_,dN3ds,_ = _get_SF(ni,nn3)
    # ============================================================= ALLOCATIONS
    detJ_PL = Vector{Float64}(undef,nelblk)
    R_31 = similar(detJ_PL) # =  detJa_PL*dxi_dth
    R_21 = similar(detJ_PL) # = -detJa_PL*deta_dth
    Th_31 = similar(detJ_PL) # = -detJa_PL*dxi_dr
    Th_21 = similar(detJ_PL) # =  detJa_PL*deta_dr
    # NxN = Array{Float64,2}(undef,3,3)
    # ang_dist        = Array{Float64,2}(undef,3,nelblk)
    # VCOORD_th       = similar(ang_dist)
    # VCOORD_r        = similar(ang_dist)
    dNdx = Array{Float64,2}(undef,nelblk,size(dNds[1],2))
    dNdy = similar(dNdx)
    ω = Vector{Float64}(undef,Int(nelblk))
    invJx_double = Array{Float64,2}(undef,Int(nelblk),ndim)    # storage for x-components of Jacobi matrix
    invJz_double = similar(invJx_double)                     # storage for z-components of Jacobi matrix
    # ==================================== STORAGE FOR DATA OF ELEMENTS IN BLOCK
    K_blk = fill(0.0, nelblk, Int(nUdofel*(nUdofel+1)/2))
        # symmetric stiffness matrix, dim=vel dofs, but only upper triangle
    M_blk = fill(0.0, nelblk, Int(nPdofel*(nPdofel+1)/2))
        # symmetric mass matrix, dim=pressure nodes, but only upper triangle
    G_blk = fill(0.0, nelblk, nPdofel*nUdofel)
        # asymmetric gradient matrix, vel dofs x pressure nodes
    Fb_blk = fill(0.0, nelblk, nUdofel)
        # storage for entries in bouyancy force vector
    invM_blk = fill(0.0, nelblk, nPdofel*nPdofel)
    invMG_blk = fill(0.0, nelblk, nPdofel*nUdofel)

    # ====================================  STORAGE FOR DATA OF ALL ELEMENT MATRICES/VECTORS
    K_all = Array{Float64,2}(undef, Int(nUdofel*(nUdofel+1)/2),nel)
    M_all = Array{Float64,2}(undef, Int(nPdofel*(nPdofel+1)/2),nel)
    G_all = Array{Float64,2}(undef, nUdofel*nPdofel,nel)
    Fb_all = Array{Float64,2}(undef, nUdofel,nel)
    invM_all = Array{Float64,2}(undef, nPdofel*nPdofel,nel)

    #=========================================================================
    BLOCK LOOP - MATRIX COMPUTATION
    =========================================================================#
    for ib in 1:nblk
        #===========================================================================
        CALCULATE JACOBIAN, ITS DETERMINANT AND INVERSE
        ===========================================================================#
        """
        NOTE: For triangular elements with non-curved edges the Jacobian is
              the same for all integration points (i.e. calculated once
              before the integration loop). Further, linear 3-node shape are
              sufficient to calculate the Jacobian.
        """
        # idx       = @views EL2NOD[1:nvert,il:iu]
        # VCOORD_th = reshape(theta[idx],nvert,nelblk)
        # VCOORD_r  = reshape(r[idx],nvert,nelblk)
        VCOORD_th = view(theta,:,il:iu)
        VCOORD_r = view(r,:,il:iu)

        J_th    = (VCOORD_th'*dN3ds')
        J_r     = (VCOORD_r'*dN3ds')

        _fill_R_J!(J_th,J_r,VCOORD_th,VCOORD_r,
            R_31,R_21,Th_31,Th_21,detJ_PL)

        # ------------------------ NUMERICAL INTEGRATION LOOP (GAUSS QUADRATURE)
        _ip_loop_penalty!(K_blk, M_blk, G_blk, Fb_blk,
                  g, η, 𝓒, ρ, EL2NOD, PhaseID,il:iu,
                  dNds, w_ip, nip, nnodel, nelblk, nPdofel,nUdofel,
                  dNdx,dNdy,N,N3,NP,
                  ω,invJx_double, invJz_double, detJ_PL,
                  R_21,R_31,Th_21,Th_31,
                  VCOORD_th,VCOORD_r)

        _invM!(invM_blk, M_blk, detJ_PL)

        _fill_invMxG!(invMG_blk, invM_blk, G_blk, nPdofel, nUdofel)

        _fill_KplusGxinvMxGt(K_blk, invMG_blk, G_blk, nPdofel, nUdofel, penalty)

        # --------------------------------------- WRITE DATA INTO GLOBAL STORAGE
        @views begin
            K_all[:,il:iu] .= K_blk'
            G_all[:,il:iu] .= G_blk'
            M_all[:,il:iu] .= M_blk'
            Fb_all[:,il:iu] .= Fb_blk'
            invM_all[:,il:iu] .= invM_blk'
        end

        # -------------------------------------------------- RESET BLOCK MATRICES
        fill!(K_blk,0.0)
        fill!(G_blk,0.0)
        fill!(M_blk,0.0)
        fill!(Fb_blk,0.0)
        fill!(invM_blk,0.0)
        fill!(invMG_blk,0.0)

        # -------------------------------- READJUST START, END AND SIZE OF BLOCK
        il += nelblk;
        if ib == nblk-1
           # ------------ Account for different number of elements in last block
           nelblk = nel - il + 1  # new block size
           # --------------------------------- Reallocate at blocks at the edge
           detJ_PL = Vector{Float64}(undef,nelblk)
           R_31 = similar(detJ_PL)
           R_21 = similar(detJ_PL)
           Th_31 = similar(detJ_PL)
           Th_21 = similar(detJ_PL)
        #    ang_dist = Array{Float64,2}(undef,3,nelblk)
           dNdx = Array{Float64,2}(undef,nelblk,size(dNds[1],2))
           dNdy = similar(dNdx)
           K_blk = fill(0.0, nelblk, Int(nUdofel*(nUdofel+1)/2))
           M_blk = fill(0.0, nelblk, Int(nPdofel*(nPdofel+1)/2))
           G_blk = fill(0.0, nelblk, nPdofel*nUdofel)
           Fb_blk = fill(0.0, nelblk, nUdofel)
           ω = Vector{Float64}(undef,Int(nelblk))
           invJx_double = Array{Float64,2}(undef,Int(nelblk),ndim)
           invJz_double = similar(invJx_double)
           invM_blk = fill(0.0, nelblk, nPdofel*nPdofel)
           invMG_blk = fill(0.0, nelblk, nPdofel*nUdofel)
        end
        iu += nelblk

    end # end block loop

    # #===========================================================================
    # ASSEMBLY OF GLOBAL SPARSE MATRICES AND RHS-VECTOR
    # ===========================================================================#
    KK, GG, invMM, Fb =
        _create_stokes_matrices_penalty(EL2NOD, nUdofel, ndim,
                                        KKidx, GGidx, invMMidx,
                                        K_all, G_all, invM_all, Fb_all)

    return KK, GG, invMM, Fb

end # END OF ASSEMBLY FUNCTION

function _ip_loop_penalty!( K_blk, M_blk, G_blk, Fb_blk,
                            g, η, 𝓒, ρ, EL2NOD, PhaseID,els,
                            dNds, w_ip, nip, nnodel, nelblk, nPdofel,nUdofel,
                            dNdx,dNdy,N,N3,NP,
                            ω,invJx_double, invJz_double, detJ_PL,
                            R_21,R_31,Th_21,Th_31,
                            VCOORD_th,VCOORD_r)

    sin_ip  = similar(detJ_PL)
    cos_ip  = similar(detJ_PL)

    for ip=1:nip

        N_ip = N3[ip]
        # NP_blk = repeat(NP[ip],nelblk,1)
        NP_blk = NP[ip].*ones(nelblk)

        #=======================================================================
        PROPERTIES OF ELEMENTS AT ip-TH EVALUATION POINT
        =======================================================================#
        Dens_blk    = _element_density(ρ,EL2NOD,PhaseID,els,NP[ip])
        Visc_blk    = _element_viscosity(η,EL2NOD,PhaseID,els,NP[ip])
        #  Gravitational force at ip-th integration point
        Fg_blk      = g * Dens_blk

        #==========================================================================================================
        CALCULATE 2nd JACOBIAN (FROM CARTESIAN TO POLAR COORDINATES --> curved edges), ITS DETERMINANT AND INVERSE
        ===========================================================================================================
        NOTE: For triangular elements with curved edges the Jacobian needs to be computed at each integration
               point (inside the integration loop). =#
        th_ip   = VCOORD_th'*N_ip'
        r_ip    = VCOORD_r'*N_ip'

        _derivative_weights!(dNds[ip],ω,dNdx,dNdy,w_ip[ip],th_ip,r_ip,sin_ip,cos_ip,
            R_21,R_31,Th_21,Th_31,detJ_PL,invJx_double,invJz_double)

        _fill_Kblk!(K_blk, 𝓒, Visc_blk, Val(η), ω, dNdx, dNdy, nnodel, els, ip)

        _fill_Gblk!(G_blk, NP_blk, ω, dNdx, dNdy, nPdofel,nUdofel,nnodel)

        _fill_Mblk_penalty!(M_blk, NP_blk, ω, nPdofel)

        _fill_Fbblk!(Fb_blk,Fg_blk, N[ip],
            ω, sin_ip,cos_ip,nUdofel)

    end # end integration point loop
end # ENF OF IP_LOOP FUNCTION

@inline function _fill_Mblk_penalty!(M_blk, NP_blk, ω, nPdofel)
    # "weight" divided by viscosity at integration point in all elements
    indx = 1;
    for i in 1:nPdofel
        @inbounds @simd for j in i:nPdofel
            M_blk[:,indx] .+=  ω.*view(NP_blk,:,i).*view(NP_blk,:,j)
            indx += 1
        end
    end
end

function _invM!(invM_blk, M_blk, detJ)
    TMP   = 1.0./detJ
    M_blk = M_blk .* TMP
    # M_blk = M_blk .* TMP[:,ones[1,nPdofel*[nPdofel+1]/2]];

    # How to calculate the determinante of several symmetric 3x3 matrices:
    # det[M] =   M11*M22*M33 + M12*M23*M31 + M13*M21*M32
    #          - M13*M22*M31 - M12*M21*M33 - M11*M23*M32
    #   written in 1-index notation:
    #        =   M1 *M5 *M9  + M4 *M8 *M3  + M7 *M2 *M6
    #          - M7 *M5 *M3  - M4 *M2 *M9  - M1 *M8 *M6
    #   considering symmetry and using lower triangular part only
    #   [M4=M2, M7=M3, M8=M6]
    #        =   M1 *M5 *M9  + M2 *M6 *M3  + M3 *M2 *M6
    #          - M3 *M5 *M3  - M2 *M2 *M9  - M1 *M6 *M6
    #   and knowing where the 6 different values are stored in M_blk
    #   [1-->M1, 2-->M2, 3-->M3, 4-->M5, 5-->M6, 6-->M9]
    #        =   M_blk1*M_blk4*M_blk6 + M_blk2*M_blk5*M_blk3 + M_blk3*M_blk2*M_blk5
    #          - M_blk3*M_blk4*M_blk3 - M_blk2*M_blk2*M_blk6 - M_blk1*M_blk5*M_blk5
    #   re-arranging
    #        =   M_blk1 * [M_blk4*M_blk6 - M_blk5*M_blk5]
    #          + M_blk2 * [M_blk5*M_blk3 - M_blk2*M_blk6]
    #          + M_blk3 * [M_blk2*M_blk5 - M_blk4*M_blk3]
    detM_blk = @. M_blk[:,1] * (M_blk[:,4]*M_blk[:,6] - M_blk[:,5]*M_blk[:,5]) +
                  M_blk[:,2] * (M_blk[:,5]*M_blk[:,3] - M_blk[:,2]*M_blk[:,6]) +
                  M_blk[:,3] * (M_blk[:,2]*M_blk[:,5] - M_blk[:,4]*M_blk[:,3])
    detM_blk ./=TMP

    # The determinante is used to calculate the inverse of the symmetric
    # 3x3 element mass matrices. The same logic as above is used.
    invM_blk[:,1] = @views @. (M_blk[:,4]*M_blk[:,6] - M_blk[:,5]*M_blk[:,5])/detM_blk;
    invM_blk[:,2] = @views @. (M_blk[:,5]*M_blk[:,3] - M_blk[:,2]*M_blk[:,6])/detM_blk;
    invM_blk[:,3] = @views @. (M_blk[:,2]*M_blk[:,5] - M_blk[:,4]*M_blk[:,3])/detM_blk;
    invM_blk[:,4] = @views invM_blk[:,2];
    invM_blk[:,5] = @views @. (M_blk[:,1]*M_blk[:,6] - M_blk[:,3]*M_blk[:,3])/detM_blk;
    invM_blk[:,6] = @views @. (M_blk[:,2]*M_blk[:,3] - M_blk[:,1]*M_blk[:,5])/detM_blk;
    invM_blk[:,7] = @views invM_blk[:,3];
    invM_blk[:,8] = @views invM_blk[:,6];
    invM_blk[:,9] = @views @. (M_blk[:,1]*M_blk[:,4] - M_blk[:,5]*M_blk[:,5])/detM_blk;

end

function _fill_invMxG!(invMG_blk, invM_blk, G_blk, nPdofel, nUdofel)
    # --------------------------invM*G'----------------------------
    for i=1:nPdofel
        for j=1:nUdofel
            for k=1:nPdofel
                invMG_blk[:,(i-1)*nUdofel+j] = @views invMG_blk[:,(i-1)*nUdofel+j] +
                    invM_blk[:,(i-1)*nPdofel+k].*G_blk[:,(k-1)*nUdofel+j];
            end
        end
    end
end

function _fill_KplusGxinvMxGt(K_blk, invMG_blk, G_blk, nPdofel, nUdofel, penalty)
    # -----------------K = K + penalty*G*invM*G'-------------------
    indx = 1;
    for i=1:nUdofel
        for j=i:nUdofel
            for k=1:nPdofel
                K_blk[:,indx] = @views K_blk[:,indx] +
                     penalty*G_blk[:,(k-1)*nUdofel+i].*invMG_blk[:,(k-1)*nUdofel+j];
            end
            indx += 1;
        end
    end
end

@inline function _create_stokes_matrices_penalty(EL2NOD,nUdofel,ndim,
    KKidx, GGidx, invMMidx,
    K_all, G_all, invM_all, Fb_all)

    nel     = size(EL2NOD,2)

    #==========================================================================
    ASSEMBLY OF GLOBAL STIFFNESS MATRIX
    ==========================================================================#
    #  convert triplet data to sparse global stiffness matrix (assembly)
    KK = sparse(KKidx._i ,KKidx._j, vec(K_all))

    #==========================================================================
    ASSEMBLY OF GLOBAL GRADIENT MATRIX
    ==========================================================================#
    # convert triplet data to sparse global gradient matrix (assembly)
    GG = sparse(GGidx._i ,GGidx._j ,vec(G_all))

    #==========================================================================
    ASSEMBLY OF GLOBAL FORCE VECTOR
    ==========================================================================#
    EL2DOF = Array{Int64}(undef,nUdofel, nel)
    @views EL2DOF[1:ndim:nUdofel,:] .= @. ndim*(EL2NOD-1)+1
    @views EL2DOF[2:ndim:nUdofel,:] .= @. ndim*(EL2NOD-1)+2
    Fb = accumarray(vec(EL2DOF),vec(Fb_all));

    #==========================================================================
    ASSEMBLY OF GLOBAL (1/VISCOSITY)-SCALED MASS MATRIX
    ==========================================================================#
    # convert triplet data to sparse global matrix
    invMM = sparse(invMMidx._i ,invMMidx._j , vec(invM_all))

    return KK, GG, invMM, Fb

end

# POWELL-HESTENESS SOLVER ================================================================
@inline  function PowellHesteness(U, P, KK, M⁻¹, GG, Rhs, ifree)

    itmax_PH = 50
    itmin_PH = 2
    # itnum = 0
    norm∇U = Vector{Float64}(undef, itmax_PH)

    Rhs .+= GG*P

    LL = factorize(KK[ifree, ifree])
    # LL = cholesky(KK[ifree, ifree])
    GGᵀ = GG'
    # Uifree = U[ifree]
    # Rhsifree = Rhs[ifree]
    penalty = 1e6

    for itPH = 1:itmax_PH
        @inbounds U[ifree] .= LL\Rhs[ifree]
        ∇U = -GGᵀ*U
        # rm0space!(∇U)
        ΔP = penalty*M⁻¹*∇U
        Rhs .+= GG*ΔP
        P .+= ΔP
        # Check convergence -----------------------------------------------
        norm∇U[itPH] = mynorm(∇U)

        if (itPH > itmin_PH) && (norm∇U[itPH]/norm∇U[itPH-1] > 0.1)
            println("\n", itPH," Powell-Hesteness iterations\n")
            break
        end

    end

    # Ux = U[1:2:end]
    # Uz = U[2:2:end]
    # M = sum(mean(ρ[EL2NOD],dims=1)'.*area_el)
    # Ax =  sum(mean(ρ[EL2NOD].*Ux[EL2NOD], dims=1)'.*area_el)
    # Az =  sum(mean(ρ[EL2NOD].*Uz[EL2NOD], dims=1)'.*area_el)
    # A = @. (Ax+Az)/M
    # U .-=A

    return U, P

end # END POWELL-HESTENESS SOLVER