#!/usr/bin/env python
#
#    Project: Sift implementation in Python + OpenCL
#             https://github.com/silx-kit/silx
#
#    Copyright (C) 2013-2024  European Synchrotron Radiation Facility, Grenoble, France
#
# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation
# files (the "Software"), to deal in the Software without
# restriction, including without limitation the rights to use,
# copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the
# Software is furnished to do so, subject to the following
# conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
# OTHER DEALINGS IN THE SOFTWARE.

"""Test suite for keypoint matching kernels
"""

__authors__ = ["Jérôme Kieffer", "Pierre Paleo"]
__contact__ = "jerome.kieffer@esrf.eu"
__license__ = "MIT"
__copyright__ = "2013-2017 European Synchrotron Radiation Facility, Grenoble, France"
__date__ = "01/08/2019"

import os
import unittest
import time
import logging
import numpy

try:
    import scipy
except ImportError:
    scipy = None
else:
    from scipy.datasets import ascent


# for Python implementation of tested functions
# from test_image_functions import
# from test_image_setup import
from ..utils import get_opencl_code
from ..plan import SiftPlan
from ..match import match_py
from silx.opencl import ocl

if ocl:
    import pyopencl.array

logger = logging.getLogger(__name__)

try:
    import feature
except ImportError:
    logger.warning(
        "feature module is not available to compare results with C++ implementation. Matching cannot be tested."
    )
    feature = None


@unittest.skipUnless(scipy and ocl, "no scipy or ocl")
class TestMatching(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        super(TestMatching, cls).setUpClass()
        if ocl:
            cls.ctx = ocl.create_context()
            if logger.getEffectiveLevel() <= logging.INFO:
                cls.PROFILE = True
                cls.queue = pyopencl.CommandQueue(
                    cls.ctx,
                    properties=pyopencl.command_queue_properties.PROFILING_ENABLE,
                )
            else:
                cls.PROFILE = False
                cls.queue = pyopencl.CommandQueue(cls.ctx)

    @classmethod
    def tearDownClass(cls):
        super(TestMatching, cls).tearDownClass()
        cls.ctx = None
        cls.queue = None

    def setUp(self):
        devtype = ocl.device_from_context(self.ctx).type
        if devtype == "CPU":
            self.use_cpu = True
        else:
            self.use_cpu = False
        kernel = "matching_gpu.cl" if not (self.use_cpu) else "matching_cpu.cl"
        kernel_src = os.linesep.join(
            (get_opencl_code(os.path.join("sift", i)) for i in ("sift", kernel))
        )
        self.program = pyopencl.Program(
            self.ctx, kernel_src
        ).build()  # .build('-D WORKGROUP_SIZE=%s' % wg_size)
        self.wg = (1, 128)

    def tearDown(self):
        self.mat = None
        self.program = None

    def test_matching(self):
        """
        tests keypoints matching kernel
        """
        image = ascent().astype(numpy.float32)

        if feature is not None:
            # get the struct keypoints : (x,y,s,angle,[descriptors])
            sc = feature.SiftAlignment()
            ref_sift = sc.sift(image)
        else:
            sp = SiftPlan(template=image)
            ref_sift = sp.keypoints(image)
        ref_sift_2 = numpy.recarray((ref_sift.shape), dtype=ref_sift.dtype)
        ref_sift_2[:] = ref_sift[::-1]

        t0_matching = time.time()
        siftmatch = match_py(ref_sift, ref_sift_2, raw_results=True)
        t1_matching = time.time()
        reference = "NumPy"

        if self.use_cpu:
            wg = (1,)
        else:
            wg = (64,)
        shape = (ref_sift.shape[0] * wg[0],)

        ratio_th = numpy.float32(0.5329)  # sift.cpp : 0.73*0.73
        keypoints_start, keypoints_end = 0, min(ref_sift.shape[0], ref_sift_2.shape[0])

        gpu_keypoints1 = pyopencl.array.to_device(self.queue, ref_sift)
        gpu_keypoints2 = pyopencl.array.to_device(self.queue, ref_sift_2)
        gpu_matchings = pyopencl.array.empty(
            self.queue,
            (keypoints_end - keypoints_start, 2),
            dtype=numpy.int32,
            order="C",
        )
        gpu_matchings.fill(0)
        keypoints_start, keypoints_end = numpy.int32(keypoints_start), numpy.int32(
            keypoints_end
        )
        nb_keypoints = numpy.int32(10000)
        counter = pyopencl.array.empty(self.queue, (1, 1), dtype=numpy.int32, order="C")
        counter.fill(0)

        t0 = time.time()
        k1 = self.program.matching(
            self.queue,
            shape,
            wg,
            gpu_keypoints1.data,
            gpu_keypoints2.data,
            gpu_matchings.data,
            counter.data,
            nb_keypoints,
            ratio_th,
            keypoints_end,
            keypoints_end,
        )
        res = gpu_matchings.get()
        cnt = counter.get()
        t1 = time.time()

        res_sort = res[numpy.argsort(res[:, 0])]
        logger.debug("%s", res_sort[0:20])
        logger.debug(
            "%s Matching took %.3f ms", reference, 1000.0 * (t1_matching - t0_matching)
        )
        logger.debug(
            "OpenCL: %d match / %s : %d match", cnt, reference, siftmatch.shape[0]
        )

        # sort to compare added keypoints
        self.assertEqual(
            cnt, siftmatch.shape[0], "number of matching element is the same"
        )
        delta = abs(res_sort - siftmatch).max()
        self.assertEqual(delta, 0, "Matching keypoints are actually the same")
        # logger.info("delta=%s" % delta)

        if self.PROFILE:
            logger.debug("Global execution time: %.3fms." % (1000.0 * (t1 - t0)))
            logger.debug(
                "Matching on device took %.3fms"
                % (1e-6 * (k1.profile.end - k1.profile.start))
            )
