# ISC License
#
# Copyright (c) 2016, Autonomous Vehicle Systems Lab, University of Colorado at Boulder
#
# Permission to use, copy, modify, and/or distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
# copyright notice and this permission notice appear in all copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
# Unit Test Script
# Module Name:        Camera
# Author:             Lucas Webb and Hanspeter Schaub
# Creation Date:      April 30, 2020
import colorsys
import inspect
import math
import os
import numpy as np
import pytest
filename = inspect.getframeinfo(inspect.currentframe()).filename
path = os.path.dirname(os.path.abspath(filename))
bskName = 'Basilisk'
splitPath = path.split(bskName)
# Import all of the modules that we are going to be called in this simulation
importErr = False
reasonErr = ""
try:
    from PIL import Image, ImageDraw
except ImportError:
    importErr = True
    reasonErr = "python Pillow package not installed---can't test Cameras module"
# Import all of the modules that we are going to be called in this simulation
from Basilisk.architecture import messaging
from Basilisk.utilities import macros
from Basilisk.utilities import SimulationBaseClass
try:
    from Basilisk.simulation import camera
except ImportError:
    importErr = True
    reasonErr += "\nCamera not built---check OpenCV option"
# Uncomment this line is this test is to be skipped in the global unit test run, adjust message as needed.
# @pytest.mark.skipif(conditionstring)
# Uncomment this line if this test has an expected failure, adjust message as needed.
# @pytest.mark.xfail(conditionstring)
# Provide a unique test method name, starting with 'test_'.
[docs]
@pytest.mark.skipif(importErr, reason= reasonErr)
@pytest.mark.parametrize("HSV", [
    [0, 0, 0]
    , [1.0, 20.0, -30.0]
    , [-1.0, 20.0, -30.0]
    , [3.14159, 100, -100]
])
@pytest.mark.parametrize("BGR", [
    [0, 0, 0]
    , [10, 20, 30]
    , [-10, -30, 50]
    , [-100, 200, 20]
])
# update "module" in this function name to reflect the module name
def test_module(show_plots, HSV, BGR):
    """
        **Validation Test Description**
        This module tests the color shifting capability of the camera module. Multiple HSV and BGR color
        adjustments are tests on a TV test image.
        **Description of Variables Being Tested**
        Multiple points on the test images are adjusted in python and compared to the BSK camera module
        saved image.  The HSV and BGR color corrections are applied on a series of points in the test image.
        The integer red, green and blue color values are checked to be identical.
    """
    # each test method requires a single assert method to be called
    image = "tv_test.png"
    [testResults, testMessage] = cameraColorTest(image, HSV, BGR)
    # Clean up
    imagePath = path + '/' + image
    savedImage = '/'.join(imagePath.split('/')[:-1]) + '/hsv' + str(HSV) + 'bgr' + str(BGR) + '0.000000.png'
    try:
        os.remove(savedImage)
    except FileNotFoundError:
        pass
    assert testResults < 1, testMessage 
[docs]
def cameraColorTest(image, HSV, BGR):
    """
    Test method to apply the HSV and BGR image adjustments.
    :param image: image name to load from local folder
    :param HSV: 3d vector of HSV adjustments
    :param BGR: 3d vector of BGR adjustments
    """
    if importErr:
        print(reasonErr)
        exit()
    # Truth values from python
    imagePath = path + '/' + image
    input_image = Image.open(imagePath)
    input_image.load()
    #################################################
    testFailCount = 0  # zero unit test result counter
    testMessages = []  # create empty array to store test log messages
    unitTaskName = "unitTask"  # arbitrary name (don't change)
    unitProcessName = "TestProcess"  # arbitrary name (don't change)
    # Create a sim module as an empty container
    unitTestSim = SimulationBaseClass.SimBaseClass()
    # Create test thread
    testProcessRate = macros.sec2nano(0.5)  # update process rate update time
    testProc = unitTestSim.CreateNewProcess(unitProcessName)
    testProc.addTask(unitTestSim.CreateNewTask(unitTaskName, testProcessRate))
    # Construct algorithm and associated C++ container
    module = camera.Camera()
    module.ModelTag = "cameras"
    # Add test module to runtime call list
    unitTestSim.AddModelToTask(unitTaskName, module)
    module.filename = imagePath
    module.saveImages = True
    # make each image saved have a unique name for this test case
    module.saveDir = '/'.join(imagePath.split('/')[:-1]) + '/hsv' + str(HSV) + 'bgr' + str(BGR)
    # Create input message and size it because the regular creator of that message
    # is not part of the test.
    inputMessageData = messaging.CameraImageMsgPayload()
    inputMessageData.timeTag = int(1E9)
    inputMessageData.cameraID = 1
    inCamMsg = messaging.CameraImageMsg().write(inputMessageData)
    module.imageInMsg.subscribeTo(inCamMsg)
    module.cameraIsOn = 1
    module.sigma_CB = [0, 0, 1]
    # Noise parameters
    module.bgrPercent = np.array(BGR)
    module.hsv = np.array(HSV)
    # Setup logging on the test module output message so that we get all the writes to it
    dataLog = module.cameraConfigOutMsg.recorder()
    unitTestSim.AddModelToTask(unitTaskName, dataLog)
    unitTestSim.InitializeSimulation()
    unitTestSim.TotalSim.SingleStepProcesses()
    corruptedPath = module.saveDir + '0.000000.png'
    #   print out error message if test failed
    if not trueColorAdjust(imagePath, corruptedPath, HSV, BGR):
        testFailCount += 1
        testMessages.append("Test failed color adjustment  " + image)
    # each test method requires a single assert method to be called
    # this check below just makes sure no sub-test failures were found
    return [testFailCount, ''.join(testMessages)] 
def rgb_to_hsv(rgb):
    hsv = colorsys.rgb_to_hsv(rgb[0], rgb[1], rgb[2])
    return [hsv[0] * 180., hsv[1] * 255., hsv[2]]
def hsv_to_rgb(hsv):
    rgb = colorsys.hsv_to_rgb(hsv[0]/180., hsv[1]/255., hsv[2]/255.)
    return [rgb[0] * 255, rgb[1] * 255, rgb[2] * 255]
def trueColorAdjust(image, corrupted, HSV, BGR):
    input_rgb = Image.open(image).load()
    output = Image.open(corrupted).load()
    # these points correspond to the included 'tv_test.png'
    testPoints = [(100, 300), (250, 300), (450, 300), (600, 300), (700, 300), (950, 300), (1100, 300), (300, 800),
                  (880, 780)]
    for point in testPoints:
        px = point[0]
        py = point[1]
        # do HSV adjustment
        hsv = rgb_to_hsv(input_rgb[px, py])
        expected = [0, 0, 0]
        input_degrees = math.degrees(HSV[0])
        h_360 = (hsv[0] * 2) + input_degrees
        h_360 -= 360. * math.floor(h_360 * (1. / 360.))
        h_360 = int(h_360 / 2)
        if h_360 == 180:
            h_360 = 0
        expected[0] = int(h_360)
        for i in range(2):
            expected[i+1] = int(hsv[i+1] * (HSV[i+1]/100. + 1.))
            if expected[i+1] < 0:
                expected[i+1] = 0
            if expected[i+1] > 255:
                expected[i+1] = 255
        expectedAfterHSV = hsv_to_rgb(expected)
        expectedAfterHSV = [int(i) for i in expectedAfterHSV]
        # do BGR adjustment
        for i in range(3):
            expected[i] = int((BGR[2 - i] / 100. + 1.) * expectedAfterHSV[i])
            if expected[i] > 255:
                expected[i] = 255
            if expected[i] < 0:
                expected[i] = 0
        for i in range(3):
            if abs(int(output[px, py][i]) - expected[i]) > 3:
                print("Failed HSV at point: px=" + str(px) + " py= + " + str(py))
                return False
    print("Passed Color Check")
    return True
#
# This statement below ensures that the unitTestScript can be run as a
# stand-along python script
#
if __name__ == "__main__":
    hsvAdjust = [1.0, 20.0, -30.0]
    bgrAdjust = [-100, 0, 0]
    cameraColorTest("tv_test.png", hsvAdjust, bgrAdjust)