#!/usr/bin/env pytest
###############################################################################
#
# Project:  GDAL/OGR Test Suite
# Purpose:  Test basic integration with numpy.
# Author:   Frank Warmerdam, warmerdam@pobox.com
#
###############################################################################
# Copyright (c) 2003, Frank Warmerdam <warmerdam@pobox.com>
# Copyright (c) 2009-2010, Even Rouault <even dot rouault at spatialys.com>
#
# SPDX-License-Identifier: MIT
###############################################################################

import gdaltest
import pytest

from osgeo import gdal

# All tests will be skipped if numpy or gdal_array are unavailable.
numpy = pytest.importorskip("numpy")
gdal_array = gdaltest.importorskip_gdal_array()

###############################################################################
@pytest.fixture(autouse=True, scope="module")
def module_disable_exceptions():
    with gdaltest.disable_exceptions():
        yield


###############################################################################
# verify that we can load the deprecated gdalnumeric module


def test_numpy_rw_1():

    # Suppress the DeprecationWarning emitted when importing gdalnumeric
    import warnings

    with warnings.catch_warnings():
        warnings.simplefilter("ignore", category=DeprecationWarning)

        from osgeo import gdalnumeric

    gdalnumeric.zeros


###############################################################################
# Load a test file into a memory Numpy array, and verify the checksum.


def test_numpy_rw_2():

    array = gdal_array.LoadFile("data/utmsmall.tif")
    assert array is not None, "Failed to load utmsmall.tif into array"

    ds = gdal_array.OpenArray(array)
    assert ds is not None, "Failed to open memory array as dataset."

    bnd = ds.GetRasterBand(1)
    assert bnd.Checksum() == 50054, "Didn't get expected checksum on reopened file"
    ds = None


###############################################################################
# Test loading complex data.


def test_numpy_rw_3():

    ds = gdal.Open("data/cint_sar.tif")
    array = ds.ReadAsArray()

    assert array[2][3] == 116 - 16j, "complex value read improperly."


###############################################################################
# Test a band read with downsampling.


def test_numpy_rw_4():

    ds = gdal.Open("data/byte.tif")
    array = ds.GetRasterBand(1).ReadAsArray(0, 0, 20, 20, 5, 5)

    assert (
        array[2][3] == 123
    ), "Read wrong value - perhaps downsampling algorithm has changed subtly?"


###############################################################################
# Test reading a multi-band file.


def test_numpy_rw_5():

    array = gdal_array.LoadFile("data/rgbsmall.tif", 35, 21, 1, 1)

    assert array[0][0][0] == 78, "value read improperly."

    assert array[1][0][0] == 117, "value read improperly."

    assert array[2][0][0] == 24, "value read improperly."

    array = gdal_array.LoadFile(
        "data/rgbsmall.tif", buf_xsize=1, buf_ysize=1, resample_alg=gdal.GRIORA_Bilinear
    )
    assert (
        array.shape[0] == 3 and array.shape[1] == 1 and array.shape[2] == 1
    ), "wrong array shape."
    assert (
        array[0][0][0] == 70 and array[1][0][0] == 97 and array[2][0][0] == 29
    ), "value read improperly."

    array = numpy.zeros([3, 1, 1], dtype=numpy.uint8)
    ds = gdal.Open("data/rgbsmall.tif")
    ds.ReadAsArray(buf_obj=array, resample_alg=gdal.GRIORA_Bilinear)
    assert (
        array[0][0][0] == 70 and array[1][0][0] == 97 and array[2][0][0] == 29
    ), "value read improperly."


###############################################################################
# Check that Band.ReadAsArray() can accept an already allocated array (#2658, #3028)


def test_numpy_rw_6():

    ds = gdal.Open("data/byte.tif")
    array = numpy.zeros([ds.RasterYSize, ds.RasterXSize], numpy.uint8)
    array_res = ds.GetRasterBand(1).ReadAsArray(buf_obj=array)

    assert array is array_res

    ds2 = gdal_array.OpenArray(array)
    assert ds2.GetRasterBand(1).Checksum() == ds.GetRasterBand(1).Checksum()


###############################################################################
# Check that Dataset.ReadAsArray() can accept an already allocated array (#2658, #3028)


def test_numpy_rw_7():

    ds = gdal.Open("data/byte.tif")
    array = numpy.zeros([1, ds.RasterYSize, ds.RasterXSize], numpy.uint8)
    array_res = ds.ReadAsArray(buf_obj=array)

    assert array is array_res

    ds2 = gdal_array.OpenArray(array)
    assert ds2.GetRasterBand(1).Checksum() == ds.GetRasterBand(1).Checksum()

    # Try again with a 2D array
    array = numpy.zeros([ds.RasterYSize, ds.RasterXSize], numpy.uint8)
    array_res = ds.ReadAsArray(buf_obj=array)

    assert array is array_res

    ds2 = gdal_array.OpenArray(array)
    assert ds2.GetRasterBand(1).Checksum() == ds.GetRasterBand(1).Checksum()

    # With a multi band file
    ds = gdal.Open("data/rgbsmall.tif")
    array = numpy.zeros([ds.RasterCount, ds.RasterYSize, ds.RasterXSize], numpy.uint8)
    array_res = ds.ReadAsArray(buf_obj=array)

    assert array is array_res

    ds2 = gdal_array.OpenArray(array)
    assert ds2.GetRasterBand(1).Checksum() == ds.GetRasterBand(1).Checksum()


###############################################################################
# Check that Dataset.ReadAsArray() with multi-band data


def test_numpy_rw_8():

    ds = gdal.Open("data/rgbsmall.tif")
    array = numpy.zeros([ds.RasterCount, ds.RasterYSize, ds.RasterXSize], numpy.uint8)
    ds.ReadAsArray(buf_obj=array)

    ds2 = gdal_array.OpenArray(array)
    for i in range(1, ds.RasterCount):
        assert ds2.GetRasterBand(i).Checksum() == ds.GetRasterBand(i).Checksum()


###############################################################################
# Test Band.WriteArray()


def test_numpy_rw_9():

    ds = gdal.Open("data/byte.tif")
    array = ds.ReadAsArray()

    out_ds = gdal.GetDriverByName("MEM").Create("", ds.RasterYSize, ds.RasterXSize)
    out_ds.GetRasterBand(1).WriteArray(array)
    cs = out_ds.GetRasterBand(1).Checksum()
    out_ds = None
    ds = None

    assert cs == 4672, "did not get expected checksum"


###############################################################################
# Test signed byte handling


def test_numpy_rw_10():

    ds = gdal.GetDriverByName("GTiff").Create(
        "/vsimem/signed8.tif", 2, 1, 1, gdal.GDT_Int8
    )
    ar = numpy.array([[-128, 127]], dtype=numpy.int8)
    ds.GetRasterBand(1).WriteArray(ar)
    ds = None

    ds = gdal.Open("/vsimem/signed8.tif")
    ar2 = ds.ReadAsArray()
    ar3 = numpy.empty_like(ar2)
    ds.GetRasterBand(1).ReadAsArray(buf_obj=ar3)
    ds = None

    gdal.Unlink("/vsimem/signed8.tif")

    assert ar2[0][0] == -128 and ar2[0][1] == 127, "did not get expected result (1)"

    assert ar3[0][0] == -128 and ar3[0][1] == 127, "did not get expected result (2)"


###############################################################################
# Test signed byte handling with all values set to nodata


@pytest.mark.parametrize("options", [[], ["SPARSE_OK=YES"]])
def test_numpy_rw_10_bis(options):
    """Reproduce https://github.com/mapbox/rasterio/issues/2180"""
    ds = gdal.GetDriverByName("GTiff").Create(
        "/vsimem/signed8.tif", 2, 1, options=["PIXELTYPE=SIGNEDBYTE"] + options
    )
    ar = numpy.array([[-1, -1]], dtype=numpy.int8)
    ds.GetRasterBand(1).WriteArray(ar)
    ds.GetRasterBand(1).SetNoDataValue(-1)
    ds = None

    ds = gdal.Open("/vsimem/signed8.tif")
    ar2 = ds.ReadAsArray()
    ds = None
    gdal.Unlink("/vsimem/signed8.tif")

    assert ar2[0][0] == -1 and ar2[0][1] == -1


###############################################################################
# Test all datatypes


def test_numpy_rw_11():

    type_tuples = [
        ("uint8", gdal.GDT_Byte, numpy.uint8, 255),
        ("uint16", gdal.GDT_UInt16, numpy.uint16, 65535),
        ("int16", gdal.GDT_Int16, numpy.int16, -32767),
        ("uint32", gdal.GDT_UInt32, numpy.uint32, 4294967295),
        ("int32", gdal.GDT_Int32, numpy.int32, -2147483648),
        ("uint64", gdal.GDT_UInt64, numpy.uint64, 4294967295 * 1000),
        ("int64", gdal.GDT_Int64, numpy.int64, -2147483648 * 1000),
        ("float32", gdal.GDT_Float32, numpy.float32, 1.23),
        ("float64", gdal.GDT_Float64, numpy.float64, 1.23456789),
        ("cint16", gdal.GDT_CInt16, numpy.complex64, -32768 + 32767j),
        ("cint32", gdal.GDT_CInt32, numpy.complex64, -32769 + 32768j),
        ("cfloat32", gdal.GDT_CFloat32, numpy.complex64, -32768.5 + 32767.5j),
        (
            "cfloat64",
            gdal.GDT_CFloat64,
            numpy.complex128,
            -32768.123456 + 32767.123456j,
        ),
    ]

    for type_tuple in type_tuples:
        ds = gdal.GetDriverByName("GTiff").Create(
            "/vsimem/" + type_tuple[0], 1, 1, 1, type_tuple[1]
        )
        tmp = ds.ReadAsArray()
        assert tmp.dtype == type_tuple[2], "did not get expected numpy type"

        ar = numpy.empty([1, 1], dtype=type_tuple[2])

        ar_ds = gdal_array.OpenArray(ar)
        got_dt = ar_ds.GetRasterBand(1).DataType
        ar_ds = None
        expected_dt = type_tuple[1]
        if expected_dt == gdal.GDT_CInt16 or expected_dt == gdal.GDT_CInt32:
            expected_dt = gdal.GDT_CFloat32
        if got_dt != expected_dt:
            print(type_tuple[1])
            pytest.fail("did not get expected result (0)")

        ar[0][0] = type_tuple[3]
        ds.GetRasterBand(1).WriteArray(ar)
        ds = None

        ds = gdal.Open("/vsimem/" + type_tuple[0])
        assert ds.GetRasterBand(1).DataType == type_tuple[1]
        ar2 = ds.ReadAsArray()
        ar3 = numpy.empty_like(ar2)
        ds.GetRasterBand(1).ReadAsArray(buf_obj=ar3)
        ds = None

        gdal.Unlink("/vsimem/" + type_tuple[0])

        assert not (
            type_tuple[0] == "float32"
            and ar2[0][0] != pytest.approx(type_tuple[3], abs=1e-6)
        ) or (
            type_tuple[0] != "float32" and ar2[0][0] != type_tuple[3]
        ), "did not get expected result (1)"

        assert not (
            type_tuple[0] == "float32"
            and ar3[0][0] != pytest.approx(type_tuple[3], abs=1e-6)
        ) or (
            type_tuple[0] != "float32" and ar3[0][0] != type_tuple[3]
        ), "did not get expected result (2)"


###############################################################################
# Test array with slices (#3542)


def test_numpy_rw_12():

    ar = numpy.empty([2, 2], dtype=numpy.uint8)
    ar[0][0] = 0
    ar[0][1] = 1
    ar[1][0] = 2
    ar[1][1] = 3

    drv = gdal.GetDriverByName("MEM")
    ds = drv.Create("", 1, 2, 1, gdal.GDT_Byte)

    ds.GetRasterBand(1).WriteArray(ar[:, 1:])

    ar_read = numpy.zeros_like(ar)
    slice_read = ar_read[:, 1:]
    ds.GetRasterBand(1).ReadAsArray(buf_obj=slice_read)
    ds = None

    assert slice_read[0][0] == 1 and slice_read[1][0] == 3


###############################################################################
# Test expected errors


def test_numpy_rw_13():

    drv = gdal.GetDriverByName("MEM")
    ds = drv.Create("", 2, 1, 1, gdal.GDT_Byte)
    ar = numpy.empty([1, 2], dtype=numpy.uint8)
    ar[0][0] = 100
    ar[0][1] = 200
    ds.GetRasterBand(1).WriteArray(ar)

    # Try reading into unsupported array type
    ar = numpy.empty([1, 2], dtype=numpy.bool_)
    with pytest.raises(
        Exception, match="array does not have " "corresponding GDAL data type"
    ):
        ds.GetRasterBand(1).ReadAsArray(buf_obj=ar)

    # Try call with inconsistent parameters.
    ar = numpy.empty([1, 2], dtype=numpy.uint8)
    with pytest.raises(
        Exception, match="Specified buf_ysize not consistent " "with array shape"
    ):
        ds.GetRasterBand(1).ReadAsArray(buf_obj=ar, buf_xsize=2, buf_ysize=2)

    # Same with 3 dimensions
    ar = numpy.empty([1, 1, 2], dtype=numpy.uint8)
    with pytest.raises(
        Exception, match="Specified buf_ysize not consistent " "with array shape"
    ):
        ds.GetRasterBand(1).ReadAsArray(buf_obj=ar, buf_xsize=2, buf_ysize=2)

    # Try call with inconsistent parameters.
    ar = numpy.empty([1, 2], dtype=numpy.uint8)
    with pytest.raises(
        Exception, match="Specified buf_xsize not consistent " "with array shape"
    ):
        ds.GetRasterBand(1).ReadAsArray(buf_obj=ar, buf_xsize=1, buf_ysize=1)

    # Inconsistent data type
    ar = numpy.empty([1, 2], dtype=numpy.uint8)
    with pytest.raises(
        Exception, match="Specified buf_type not consistent " "with array type"
    ):
        ds.GetRasterBand(1).ReadAsArray(buf_obj=ar, buf_type=gdal.GDT_Int16)

    # This one should be OK !
    ar = numpy.zeros([1, 2], dtype=numpy.uint8)
    ds.GetRasterBand(1).ReadAsArray(buf_obj=ar, buf_xsize=2, buf_ysize=1)
    assert ar[0][0] == 100 and ar[0][1] == 200, "did not get expected values"

    # This one too
    ar = numpy.zeros([1, 1, 2], dtype=numpy.uint8)
    ds.GetRasterBand(1).ReadAsArray(buf_obj=ar)
    assert ar[0][0][0] == 100 and ar[0][0][1] == 200, "did not get expected values"

    # This one too
    ar = numpy.zeros([1, 1, 2], dtype=numpy.uint8)
    ds.ReadAsArray(buf_obj=ar)
    assert ar[0][0][0] == 100 and ar[0][0][1] == 200, "did not get expected values"

    # This one too
    ar = ds.ReadAsArray()
    assert ar[0][0] == 100 and ar[0][1] == 200, "did not get expected values"

    ds = None

    # With a multiband file
    drv = gdal.GetDriverByName("MEM")
    ds = drv.Create("", 2, 1, 3, gdal.GDT_Byte)
    ar = numpy.empty([3, 1, 2], dtype=numpy.uint8)
    ar[0][0][0] = 100
    ar[0][0][1] = 200
    ar[1][0][0] = 101
    ar[1][0][1] = 201
    ar[2][0][0] = 102
    ar[2][0][1] = 202
    for i in range(3):
        ds.GetRasterBand(i + 1).WriteArray(ar[i])

    ar = numpy.empty([3, 1, 2], dtype=numpy.bool_)
    with pytest.raises(
        Exception, match="array does not have " "corresponding GDAL data type"
    ):
        ds.ReadAsArray(buf_obj=ar)

    # Try call with inconsistent parameters.
    ar = numpy.empty([3, 1, 2], dtype=numpy.uint8)
    with pytest.raises(
        Exception, match="Specified buf_ysize not consistent " "with array shape"
    ):
        ds.ReadAsArray(buf_obj=ar, buf_xsize=2, buf_ysize=2)

    # With 2 dimensions
    ar = numpy.empty([1, 2], dtype=numpy.uint8)
    with pytest.raises(Exception, match="Array should have 3 " "dimensions"):
        ds.ReadAsArray(buf_obj=ar)

    # Try call with inconsistent parameters
    ar = numpy.empty([3, 1, 2], dtype=numpy.uint8)
    with pytest.raises(
        Exception, match="Specified buf_xsize not consistent " "with array shape"
    ):
        ds.ReadAsArray(buf_obj=ar, buf_xsize=1, buf_ysize=1)

    ar = numpy.empty([1, 2, 3], dtype=numpy.uint8)
    with pytest.raises(
        Exception, match="Specified buf_xsize not consistent " "with array shape"
    ):
        ds.ReadAsArray(buf_obj=ar, buf_xsize=1, buf_ysize=1, interleave="pixel")

    # Inconsistent data type
    ar = numpy.empty([3, 1, 2], dtype=numpy.uint8)
    with pytest.raises(
        Exception, match="Specified buf_type not consistent with array type"
    ):
        ds.ReadAsArray(buf_obj=ar, buf_type=gdal.GDT_Int16)

    # Not enough space in first dimension
    ar = numpy.empty([2, 1, 2], dtype=numpy.uint8)
    with pytest.raises(
        Exception, match="Dimension 0 of array should have size 3 to store bands"
    ):
        ds.ReadAsArray(buf_obj=ar)

    # Not enough space in third dimension
    ar = numpy.empty([1, 2, 2], dtype=numpy.uint8)
    with pytest.raises(
        Exception, match="Dimension 2 of array should have size 3 to store bands"
    ):
        ds.ReadAsArray(buf_obj=ar, interleave="pixel")

    # This one should be OK !
    ar = numpy.zeros([3, 1, 2], dtype=numpy.uint8)
    ds.ReadAsArray(buf_obj=ar, buf_xsize=2, buf_ysize=1, buf_type=gdal.GDT_Byte)
    assert (
        ar[0][0][0] == 100
        and ar[0][0][1] == 200
        and ar[1][0][0] == 101
        and ar[1][0][1] == 201
        and ar[2][0][0] == 102
        and ar[2][0][1] == 202
    ), "did not get expected values"

    # This one too
    ar = numpy.zeros([3, 1, 2], dtype=numpy.uint8)
    ds.ReadAsArray(buf_obj=ar)
    assert (
        ar[0][0][0] == 100
        and ar[0][0][1] == 200
        and ar[1][0][0] == 101
        and ar[1][0][1] == 201
        and ar[2][0][0] == 102
        and ar[2][0][1] == 202
    ), "did not get expected values"

    # This one too
    ar = ds.ReadAsArray()
    assert (
        ar[0][0][0] == 100
        and ar[0][0][1] == 200
        and ar[1][0][0] == 101
        and ar[1][0][1] == 201
        and ar[2][0][0] == 102
        and ar[2][0][1] == 202
    ), "did not get expected values"

    ds = None


###############################################################################
# Test callback of ReadAsArray()


def numpy_rw_14_progress_callback(pct, message, user_data):
    # pylint: disable=unused-argument
    if pct != pytest.approx(user_data[0], abs=1e-5):
        print("Expected %f, got %f" % (user_data[0], pct))
        user_data[1] = False
    user_data[0] = user_data[0] + 0.05
    return 1  # 1 to continue, 0 to stop


def numpy_rw_14_progress_interrupt_callback(pct, message, user_data):
    # pylint: disable=unused-argument
    user_data[0] = pct
    if pct >= 0.5:
        return 0
    return 1  # 1 to continue, 0 to stop


def numpy_rw_14_progress_callback_2(pct, message, user_data):
    # pylint: disable=unused-argument
    if pct < user_data[0]:
        print("Got %f, last pct was %f" % (pct, user_data[0]))
        return 0
    user_data[0] = pct
    return 1  # 1 to continue, 0 to stop


def test_numpy_rw_14():

    # Progress not implemented yet
    if (
        gdal.GetConfigOption("GTIFF_DIRECT_IO") == "YES"
        or gdal.GetConfigOption("GTIFF_VIRTUAL_MEM_IO") == "YES"
    ):
        pytest.skip()

    ds = gdal.Open("data/byte.tif")

    # Test RasterBand.ReadAsArray
    tab = [0.05, True]
    data = ds.GetRasterBand(1).ReadAsArray(
        resample_alg=gdal.GRIORA_NearestNeighbour,
        callback=numpy_rw_14_progress_callback,
        callback_data=tab,
    )
    assert data is not None
    assert tab[0] == pytest.approx(1.05, abs=1e-5) and tab[1]

    # Test interruption
    tab = [0]
    data = ds.GetRasterBand(1).ReadAsArray(
        callback=numpy_rw_14_progress_interrupt_callback, callback_data=tab
    )
    assert data is None
    assert tab[0] >= 0.50

    # Test Dataset.ReadAsArray
    tab = [0.05, True]
    data = ds.ReadAsArray(
        resample_alg=gdal.GRIORA_NearestNeighbour,
        callback=numpy_rw_14_progress_callback,
        callback_data=tab,
    )
    assert data is not None
    assert tab[0] == pytest.approx(1.05, abs=1e-5) and tab[1]

    # Same with interruption
    tab = [0]
    data = ds.ReadAsArray(
        callback=numpy_rw_14_progress_interrupt_callback, callback_data=tab
    )
    assert data is None and tab[0] >= 0.50

    # Test Dataset.ReadAsArray on a multi band file
    ds = None
    ds = gdal.Open("data/rgbsmall.tif")
    last_pct = [0]
    data = ds.ReadAsArray(
        callback=numpy_rw_14_progress_callback_2, callback_data=last_pct
    )
    assert not (data is None or last_pct[0] != pytest.approx(1.0, abs=1e-5))

    last_pct = [0]

    # Same but with a provided array
    array = numpy.empty([ds.RasterCount, ds.RasterYSize, ds.RasterXSize], numpy.uint8)
    data = ds.ReadAsArray(
        buf_obj=array, callback=numpy_rw_14_progress_callback_2, callback_data=last_pct
    )
    assert not (data is None or last_pct[0] != pytest.approx(1.0, abs=1e-5))


###############################################################################
# Test NumPy GetGeoTransform/SetGeoTransform


def test_numpy_rw_15():

    array = numpy.empty([1, 1, 1], numpy.uint8)
    ds = gdal_array.OpenArray(array)
    gt = ds.GetGeoTransform(can_return_null=True)
    assert gt is None
    ds.SetGeoTransform([1, 2, 3, 4, 5, -6])
    gt = ds.GetGeoTransform()
    assert gt == (1, 2, 3, 4, 5, -6)


###############################################################################
# Test errors of OpenArray()


def test_numpy_rw_16():

    # 1D
    array = numpy.empty([1], numpy.uint8)
    with gdal.quiet_errors():
        ds = gdal_array.OpenArray(array)
    assert ds is None

    with gdaltest.enable_exceptions(), pytest.raises(
        Exception, match="Illegal numpy array rank 1"
    ):
        gdal_array.OpenArray(array)

    # 4D
    array = numpy.empty([1, 1, 1, 1], numpy.uint8)
    with gdal.quiet_errors():
        ds = gdal_array.OpenArray(array)
    assert ds is None

    # Unsupported data type
    array = numpy.empty([1, 1], numpy.bool_)
    with gdal.quiet_errors():
        ds = gdal_array.OpenArray(array)
    assert ds is None


###############################################################################
# Test old deprecated way with gdal_array.GetArrayFilename()


def test_numpy_rw_17():

    # Disabled by default
    array = numpy.empty([1, 1], numpy.uint8)
    with gdal.quiet_errors():
        ds = gdal.Open(gdal_array.GetArrayFilename(array))
    assert ds is None

    with gdal.config_option("GDAL_ARRAY_OPEN_BY_FILENAME", "TRUE"):
        ds = gdal.Open(gdal_array.GetArrayFilename(array))
    assert ds is not None

    # Invalid value
    with gdal.quiet_errors():
        ds = gdal.Open("NUMPY:::invalid")
    assert ds is None


###############################################################################
# Test the pixel interleave options


def test_numpy_rw_18():

    img = numpy.random.randint(0, 255, size=(256, 200, 3)).astype("uint8")
    ds = gdal_array.OpenArray(img, interleave="pixel")
    assert ds is not None, "Failed to open memory array as dataset."

    bnd1 = ds.GetRasterBand(1).ReadAsArray()
    bnd2 = ds.GetRasterBand(2).ReadAsArray()
    bnd3 = ds.GetRasterBand(3).ReadAsArray()

    res = numpy.dstack((bnd1, bnd2, bnd3))
    assert numpy.all(img == res)

    res = ds.ReadAsArray(interleave="pixel")
    assert numpy.all(img == res)

    res = numpy.zeros([256, 200, 3])
    ds.ReadAsArray(buf_obj=res, interleave="pixel")
    assert numpy.all(img == res)


###############################################################################
# The VRT references a non existing TIF file, but using the proxy pool dataset API (#2837)


@pytest.mark.skipif(
    not gdaltest.vrt_has_open_support(),
    reason="VRT driver open missing",
)
def test_numpy_rw_failure_in_readasarray():

    ds = gdal.Open("data/idontexist2.vrt")
    assert ds is not None

    exception_raised = False
    with gdal.ExceptionMgr():
        try:
            ds.ReadAsArray()
        except RuntimeError:
            exception_raised = True
    assert exception_raised

    exception_raised = False
    with gdal.ExceptionMgr():
        try:
            ds.GetRasterBand(1).ReadAsArray()
        except RuntimeError:
            exception_raised = True
    assert exception_raised


###############################################################################
# Test permission handling


def test_numpy_rw_gdal_array_openarray_permissions():

    # Writeable array
    ar = numpy.zeros([1, 1], dtype=numpy.uint8)
    ds = gdal_array.OpenArray(ar)
    assert ds.GetRasterBand(1).Fill(1) == 0
    assert ds.GetRasterBand(1).Checksum() != 0

    # Non-writeable array
    ar = numpy.zeros([1, 1], dtype=numpy.uint8)
    ar.setflags(write=False)
    ds = gdal_array.OpenArray(ar)
    with gdal.quiet_errors():
        assert ds.GetRasterBand(1).Fill(1) != 0
    assert ds.GetRasterBand(1).Checksum() == 0

    # Cannot read in non-writeable array
    with gdal.quiet_errors():
        assert ds.ReadAsArray(buf_obj=ar) is None
        assert ds.GetRasterBand(1).ReadAsArray(buf_obj=ar) is None


###############################################################################
# Test ReadAsArray RMS subsampling.


def test_numpy_rw_rms_resemple_alg():

    wrk_ds = gdal.Open("../gdrivers/data/int16.tif")
    assert wrk_ds is not None, "Failed to open test dataset."

    res = wrk_ds.ReadAsArray(
        0,
        0,
        wrk_ds.RasterXSize,
        wrk_ds.RasterYSize,
        buf_xsize=wrk_ds.RasterXSize // 2,
        buf_ysize=wrk_ds.RasterYSize // 2,
        resample_alg=gdal.GRIORA_RMS,
    )
    cs = res.sum()
    exp_cs = 12773

    assert cs == exp_cs, "got wrong rms sum"


###############################################################################
# Test Dataset.ReadAsArray() with band_list


def test_numpy_rw_dataset_read_as_array():

    wrk_ds = gdal.Open("../gdrivers/data/rgbsmall.tif")

    assert numpy.array_equal(
        wrk_ds.ReadAsArray(band_list=[2]), wrk_ds.GetRasterBand(2).ReadAsArray()
    )

    assert numpy.array_equal(
        wrk_ds.ReadAsArray(band_list=[2, 1]),
        numpy.stack(
            [
                wrk_ds.GetRasterBand(2).ReadAsArray(),
                wrk_ds.GetRasterBand(1).ReadAsArray(),
            ]
        ),
    )


###############################################################################
# Test Dataset.WriteArray()


def test_numpy_rw_dataset_writearray():

    # Write 2D array in single-band dataset
    ds = gdal.GetDriverByName("MEM").Create("", 3, 2)
    ar = numpy.array([[0, 1, 2], [3, 4, 5]], dtype=numpy.uint8)
    assert ds.WriteArray(ar) == 0
    assert numpy.array_equal(ds.ReadAsArray(), ar)

    # Use WriteRaster interface
    ds = gdal.GetDriverByName("MEM").Create("", 3, 2)
    assert (
        ds.WriteRaster(0, 0, ds.RasterXSize, ds.RasterYSize, ar.astype(numpy.uint32))
        == 0
    )
    assert numpy.array_equal(ds.ReadAsArray(), ar)

    with pytest.raises(Exception):
        ds.WriteArray(None)

    with pytest.raises(Exception):
        ds.WriteArray("not an array")

    # 1D array
    with pytest.raises(Exception):
        ds.WriteArray(numpy.array([0, 1, 2], dtype=numpy.uint8))

    # Too big 2D array in X
    with pytest.raises(Exception):
        ds.WriteArray(numpy.array([[0, 1, 2, 10], [3, 4, 5, 20]], dtype=numpy.uint8))

    # Write 3D array in single-band dataset
    ds = gdal.GetDriverByName("MEM").Create("", 3, 2)
    ar = numpy.array([[[0, 1, 2], [3, 4, 5]]], dtype=numpy.uint8)
    assert ds.WriteArray(ar) == 0
    assert numpy.array_equal(ds.ReadAsArray(), ar[0])

    # ar.shape[0] != 1
    with pytest.raises(Exception):
        ds.WriteArray(numpy.array([[[0, 1, 2], [3, 4, 5]], [[0, 1, 2], [3, 4, 5]]]))

    # Write 3D array in two-band dataset
    ds = gdal.GetDriverByName("MEM").Create("", 3, 2, 2)
    ar = numpy.array(
        [[[0, 1, 2], [3, 4, 5]], [[10, 11, 12], [13, 14, 15]]], dtype=numpy.uint8
    )
    assert ds.WriteRaster(0, 0, 3, 2, memoryview(ar)) == 0
    assert numpy.array_equal(ds.ReadAsArray(), ar)

    # Non-native data type
    ds = gdal.GetDriverByName("MEM").Create("", 3, 2, 2)
    assert ds.WriteArray(ar.astype(numpy.int32)) == 0
    assert numpy.array_equal(ds.ReadAsArray(), ar)

    # interleave='pixel'
    ds = gdal.GetDriverByName("MEM").Create("", 3, 2, 2)
    assert ds.WriteArray(numpy.transpose(ar, (1, 2, 0)), interleave="pixel") == 0
    assert numpy.array_equal(ds.ReadAsArray(), ar)

    # band_list
    ds = gdal.GetDriverByName("MEM").Create("", 3, 2, 2)
    assert ds.WriteArray(ar[::-1, ...], band_list=[2, 1]) == 0
    assert numpy.array_equal(ds.ReadAsArray(), ar)

    # Use WriteRaster interface
    ds = gdal.GetDriverByName("MEM").Create("", 3, 2, 2)
    assert (
        ds.WriteRaster(0, 0, ds.RasterXSize, ds.RasterYSize, ar.astype(numpy.uint32))
        == 0
    )
    assert numpy.array_equal(ds.ReadAsArray(), ar)

    # 2D array
    with pytest.raises(Exception):
        ds.WriteArray(numpy.array([[0, 1, 2, 0], [3, 4, 5, 0]]))

    # Too big 3D array in X
    with pytest.raises(Exception):
        ds.WriteArray(
            numpy.array(
                [[[0, 1, 2, 0], [3, 4, 5, 0]], [[10, 11, 12, 0], [13, 14, 15, 0]]]
            )
        )

    # Too big 3D array in Y
    with pytest.raises(Exception):
        ds.WriteArray(
            numpy.array(
                [
                    [[0, 1, 2], [3, 4, 5], [3, 4, 5]],
                    [[10, 11, 12], [13, 14, 15], [13, 14, 15]],
                ]
            )
        )

    # Too big 3D array in band dimension
    with pytest.raises(Exception):
        ds.WriteArray(
            numpy.array(
                [
                    [[0, 1, 2], [3, 4, 5]],
                    [[10, 11, 12], [13, 14, 15]],
                    [[10, 11, 12], [13, 14, 15]],
                ]
            )
        )


###############################################################################
# Test Band.ReadAsArray() error cases


def test_numpy_rw_band_read_as_array_error_cases():

    ds = gdal.GetDriverByName("MEM").Create("", 3, 2)
    band = ds.GetRasterBand(1)
    assert band.ReadAsArray(buf_obj=numpy.empty((3, 2), dtype=numpy.uint8)) is not None
    assert (
        band.ReadAsArray(buf_obj=numpy.empty((1, 3, 2), dtype=numpy.uint8)) is not None
    )

    # 1D
    with pytest.raises(Exception, match="expected array of dimension 2 or 3"):
        band.ReadAsArray(buf_obj=numpy.empty((3,), dtype=numpy.uint8))

    # 3D of wrong size in first dimension
    with pytest.raises(Exception, match="expected size of first dimension should be 0"):
        band.ReadAsArray(buf_obj=numpy.empty((2, 3, 2), dtype=numpy.uint8))


###############################################################################
# Test that we can get an error (#5374)


@pytest.mark.skipif(
    not gdaltest.vrt_has_open_support(),
    reason="VRT driver open missing",
)
def test_numpy_rw_band_read_as_array_getlasterrormsg():

    ds = gdal.Open(
        """<VRTDataset rasterXSize="20" rasterYSize="20">
  <VRTRasterBand dataType="Float64" band="1" subClass="VRTDerivedRasterBand">
    <Description>Scaling</Description>
    <PixelFunctionType>invalid</PixelFunctionType>
    <SourceTransferType>Float64</SourceTransferType>
    <SimpleSource>
      <SourceFilename relativeToVRT="0">notexisting</SourceFilename>
      <SourceBand>1</SourceBand>
    </SimpleSource>
  </VRTRasterBand>
</VRTDataset>"""
    )
    gdal.ErrorReset()
    with gdal.quiet_errors():
        assert ds.GetRasterBand(1).ReadAsArray() is None
    assert gdal.GetLastErrorMsg() != ""


###############################################################################
# Test a band read into a masked array


def test_numpy_rw_masked_array_1():

    ds = gdal.Open("data/byte.tif")
    band = ds.GetRasterBand(1)

    masked_arr = band.ReadAsMaskedArray()
    assert not numpy.any(masked_arr.mask)

    ds_masked_arr = ds.ReadAsMaskedArray()
    assert not numpy.any(ds_masked_arr.mask)
    assert numpy.all(masked_arr == ds_masked_arr)


def test_numpy_rw_masked_array_2():

    ds = gdal.Open("data/test3_with_mask_8bit.tif")

    band = ds.GetRasterBand(1)

    arr = band.ReadAsArray()
    mask = band.GetMaskBand().ReadAsArray()

    masked_arr = band.ReadAsMaskedArray()

    assert not numpy.any(masked_arr.mask[mask == 255])
    assert numpy.all(masked_arr.mask[mask != 255])

    assert masked_arr.sum() == arr[mask == 255].sum()

    ds_masked_arr = ds.ReadAsMaskedArray()
    assert numpy.all(ds_masked_arr.mask == masked_arr.mask)
    assert numpy.all(ds_masked_arr.data == masked_arr.data)


###############################################################################
# Test dataset read into a masked array


def test_numpy_rw_masked_array_3():

    nx = 6
    ny = 4
    nz = 3
    nodata = -999

    ds = gdal.GetDriverByName("MEM").Create("", nx, ny, nz, eType=gdal.GDT_Int16)
    for i in range(nz):
        ds.GetRasterBand(i + 1).SetNoDataValue(nodata)

    data = numpy.arange(nx * ny * nz).reshape(nz, ny, nx).astype(numpy.int16)
    data[:, 1, 1] = nodata
    ds.WriteArray(data)

    masked_data = numpy.ma.masked_array(data, mask=data == nodata)

    assert numpy.ma.allequal(masked_data, ds.ReadAsMaskedArray())

    # read a single band
    assert numpy.ma.allequal(masked_data[1, :, :], ds.ReadAsMaskedArray(band_list=[2]))

    # read multiple bands
    assert numpy.ma.allequal(
        numpy.ma.stack((masked_data[2, :, :], masked_data[0, :, :])),
        ds.ReadAsMaskedArray(band_list=[3, 1]),
    )

    # read a sub-window (square)
    assert numpy.ma.allequal(
        numpy.ma.stack((masked_data[2, 0:2, 1:3], masked_data[0, 0:2, 1:3])),
        ds.ReadAsMaskedArray(band_list=[3, 1], xsize=2, ysize=2, xoff=1, yoff=0),
    )

    # read a sub-window (single pixel)
    assert numpy.ma.allequal(
        numpy.ma.stack((masked_data[2, 1:2, 1:2], masked_data[0, 1:2, 1:2])),
        ds.ReadAsMaskedArray(band_list=[3, 1], xsize=1, ysize=1, xoff=1, yoff=1),
    )

    # read a sub-window (read 2x2 square into 4x4 square)
    assert numpy.ma.allequal(
        numpy.ma.repeat(
            numpy.ma.repeat(
                numpy.ma.stack((masked_data[2, 0:2, 1:3], masked_data[0, 0:2, 1:3])),
                2,
                axis=2,
            ),
            2,
            axis=1,
        ),
        ds.ReadAsMaskedArray(
            band_list=[3, 1], xsize=2, ysize=2, xoff=1, yoff=0, buf_xsize=4, buf_ysize=4
        ),
    )

    # read a sub-window (read 2x2 square into single pixel)
    assert numpy.ma.allequal(
        numpy.ma.mean(
            numpy.ma.stack((masked_data[2, 0:2, 1:3], masked_data[0, 0:2, 1:3])),
            axis=(1, 2),
            keepdims=True,
        ).round(),
        ds.ReadAsMaskedArray(
            band_list=[3, 1],
            xsize=2,
            ysize=2,
            xoff=1,
            yoff=0,
            buf_xsize=1,
            buf_ysize=1,
            resample_alg=gdal.GRIORA_Average,
        ),
    )


###############################################################################
# Test type code mapping


def test_gdal_type_code_to_numeric_type_code():

    assert gdal_array.GDALTypeCodeToNumericTypeCode(gdal.GDT_Float32) == numpy.float32

    # invalid type code
    assert gdal_array.GDALTypeCodeToNumericTypeCode(802) is None


def test_numeric_type_code_to_gdal_type_code():

    assert gdal_array.NumericTypeCodeToGDALTypeCode(numpy.float32) == gdal.GDT_Float32
    assert (
        gdal_array.NumericTypeCodeToGDALTypeCode(numpy.dtype("int16")) == gdal.GDT_Int16
    )


def test_flip_code():

    assert gdal_array.flip_code(numpy.float32) == gdal.GDT_Float32
    assert gdal_array.flip_code(gdal.GDT_Int16) == numpy.int16
