#!/usr/bin/env python
"""
PanelResolver class

Copyright 2017 MicaSense, Inc.

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.
"""

import math
import re

import cv2
import matplotlib.pyplot as plt
import numpy as np
from pyzbar import pyzbar
from skimage import measure

class Panel:
    def __init__(self, img, panelCorners=None, ignore_autocalibration=False):
        # if we have panel images with QR metadata, panel detection is not called,
        # so this can be forced here
        if img is None:
            raise OSError("Must provide an image")

        self.image = img
        bias = img.radiance().min()
        scale = img.radiance().max() - bias
        self.gray8b = np.zeros(img.radiance().shape, dtype="uint8")
        cv2.convertScaleAbs(
            img.undistorted(img.radiance()),
            self.gray8b,
            256.0 / scale,
            -1.0 * scale * bias,
        )

        if (self.image.auto_calibration_image) and ~ignore_autocalibration:
            self.__panel_type = "auto"  ## panels the camera found we call auto
            if panelCorners is not None:
                self.__panel_bounds = np.array(panelCorners)
            else:
                self.__panel_bounds = np.array(self.image.panel_region)
            self.panel_albedo = self.image.panel_albedo
            self.serial = self.image.panel_serial
            self.qr_area = None
            self.qr_bounds = None
            self.panel_std = None
            self.saturated_panel_pixels_pct = None
            self.panel_pixels_mean = None
            self.panel_version = None
            if re.search(r"RP\d{2}-(\d{7})-\D{2}", self.image.panel_serial):
                self.serial = self.image.panel_serial
                self.panel_version = int(self.image.panel_serial[2:4])
        else:
            self.__panel_type = "search"  ## panels we search for we call search
            self.serial = None
            self.qr_area = None
            self.qr_bounds = None
            self.panel_std = None
            self.saturated_panel_pixels_pct = None
            self.panel_pixels_mean = None
            self.panel_version = None
            if panelCorners is not None:
                self.__panel_bounds = np.array(panelCorners)
            else:
                self.__panel_bounds = None

    def __expect_panel(self):
        return self.image.band_name.upper() != "LWIR"

    def __find_qr(self):
        decoded = pyzbar.decode(self.gray8b, symbols=[pyzbar.ZBarSymbol.QRCODE])
        for symbol in decoded:
            serial_str = symbol.data.decode("UTF-8")
            m = re.search(r"RP\d{2}-(\d{7})-\D{2}", serial_str)
            if m:
                self.serial = serial_str
                self.panel_version = int(self.serial[2:4])
                self.qr_bounds = []
                for point in symbol.polygon:
                    self.qr_bounds.append([point.x, point.y])
                self.qr_bounds = np.asarray(self.qr_bounds, np.int32)
                self.qr_area = cv2.contourArea(self.qr_bounds)
                # print (symbol.polygon)
                # print (self.qr_bounds)
                break

    def __pt_in_image_bounds(self, pt):
        width, height = self.image.size()
        if pt[0] >= width or pt[0] < 0:
            return False
        if pt[1] >= height or pt[1] < 0:
            return False
        return True

    def reflectance_from_panel_serial(self):
        if self.__panel_type == "auto":
            return self.panel_albedo

        if self.serial is None:
            self.__find_qr()
        if self.serial is None:
            raise ValueError("Panel serial number not found")
        if self.panel_version >= 4:
            min_wl = float(self.serial[-14:-10])
            min_rf = float(self.serial[-10:-7]) / 1000.0
            max_wl = float(self.serial[-7:-3])
            max_rf = float(self.serial[-3:]) / 1000.0
            c = np.polyfit([min_wl, max_wl], [min_rf, max_rf], 1)
            p = np.poly1d(c)
            return p(self.image.center_wavelength)
        return None

    def qr_corners(self):
        if self.__panel_type == "auto":
            return None

        if self.qr_bounds is None:
            self.__find_qr()
        return self.qr_bounds

    def panel_detected(self):
        if self.__expect_panel() == False:
            return False

        if self.__panel_type == "auto":
            return True

        if self.serial is None:
            self.__find_qr()
        return self.qr_bounds is not None

    def panel_corners(self):
        """
        Get the corners of a panel region based on the qr code location
        Our algorithm to do this uses a 'reference' qr code location and
        it's associate panel region.  We find the affine transform
        between the reference qr and our qr, and apply that same transform to the
        reference panel region to find our panel region. Because of a limitation
        of the pyzbar library, the rotation of the absolute QR code isn't known,
        so we then try all 4 rotations and test against a cost function which is the
        minimum of the standard devation divided by the mean value for the panel region
        """
        if self.__panel_bounds is not None:
            return self.__panel_bounds
        if self.serial is None:
            self.__find_qr()
        if self.serial is None:  # didn't find a panel in this image
            return None

        if self.panel_version < 3:
            # reference_panel_pts = np.asarray([[894, 469], [868, 232], [630, 258], [656, 496]],
            #                                 dtype=np.int32)
            # reference_qr_pts = np.asarray([[898, 748], [880, 567], [701, 584], [718, 762]],
            #                             dtype=np.int32)

            # use the actual panel measures here - we use units of [mm]
            # the panel is 154.4 x 152.4 mm , vs. the 84 x 84 mm for the QR code
            # it is left 143.20 mm from the QR code
            # use the inner 50% square of the panel
            s = 76.2
            p = 42
            T = np.array([-143.2, 0])

        elif (self.panel_version >= 3) and (self.panel_version < 6):
            s = 50
            p = 45
            T = np.array([-145.8, 0])
            # reference_panel_pts = np.asarray([[557, 350], [550, 480], [695, 480], [700, 350]], dtype=np.int32)
            # reference_qr_pts = np.asarray([[821, 324], [819, 506], [996, 509], [999, 330]], dtype=np.int32)
        elif self.panel_version >= 6:
            # use the actual panel measures here - we use units of [mm]
            # the panel is 100 x 100 mm , vs. the 91 x 91 mm for the QR code
            # it is down 125.94 mm from the QR code
            # use the inner 50% square of the panel
            p = 41
            s = 50
            T = np.array([0, -130.84])

        reference_panel_pts = (
            np.asarray([[-s, s], [s, s], [s, -s], [-s, -s]], dtype=np.float32) * 0.5 + T
        )
        reference_qr_pts = np.asarray(
            [[-p, p], [p, p], [p, -p], [-p, -p]], dtype=np.float32,
        )
        bounds = []
        costs = []
        for rotation in range(4):
            qr_points = np.roll(reference_qr_pts, rotation, axis=0)

            src = np.asarray([tuple(row) for row in qr_points[:]], np.float32)
            dst = np.asarray([tuple(row) for row in self.qr_corners()[:]], np.float32)

            # we determine the homography from the 4 corner points
            warp_matrix = cv2.getPerspectiveTransform(src, dst)

            # warp_matrix = cv2.getAffineTransform(src, dst)

            pts = np.asarray([reference_panel_pts], "float32")
            panel_bounds = cv2.convexHull(
                cv2.perspectiveTransform(pts, warp_matrix), clockwise=False,
            )
            panel_bounds = np.squeeze(panel_bounds)  # remove nested lists

            bounds_in_image = True
            for i, point in enumerate(panel_bounds):
                if not self.__pt_in_image_bounds(point):
                    bounds_in_image = False
            if bounds_in_image:
                mean, std, _, _ = self.region_stats(
                    self.image.raw(), panel_bounds, sat_threshold=65000,
                )
                bounds.append(panel_bounds.astype(np.int32))
                costs.append(std / mean)

        idx = costs.index(min(costs))
        self.__panel_bounds = bounds[idx]
        return self.__panel_bounds

    def ordered_panel_coordinates(self):
        """
        Return panel region coordinates in a predictable order. Panel region coordinates that are automatically
        detected by the camera are ordered differently than coordinates detected by Panel.panel_corners().
        :return: [ (ur), (ul), (ll), (lr) ] to mirror Image.panel_region attribute order
        """
        pc = self.panel_corners()
        pc = sorted(pc, key=lambda x: x[0])

        # get the coordinates on the "left" and "right" side of the bounding box
        left_coords = pc[:2]
        right_coords = pc[2:]

        # sort y values ascending for correct order
        left_coords = sorted(left_coords, key=lambda y: y[0])
        right_coords = sorted(right_coords, key=lambda y: y[0])

        return [
            tuple(right_coords[1]),
            tuple(left_coords[1]),
            tuple(left_coords[0]),
            tuple(right_coords[0]),
        ]

    def region_stats(self, img, region, sat_threshold=None):
        """
        Provide regional statistics for a image over a region
        Inputs: img is any image ndarray, region is a skimage shape
        Outputs: mean, std, count, and saturated count tuple for the region
        """
        rev_panel_pts = np.fliplr(region)  # skimage and opencv coords are reversed
        w, h = img.shape
        mask = measure.grid_points_in_poly((w, h), rev_panel_pts)
        num_pixels = mask.sum()
        panel_pixels = img[mask]
        stdev = panel_pixels.std()
        mean_value = panel_pixels.mean()
        saturated_count = 0
        if sat_threshold is not None:
            saturated_count = (panel_pixels > sat_threshold).sum()
            # set saturated pixels here
            if num_pixels > 0:
                self.saturated_panel_pixels_pct = (100.0 * saturated_count) / num_pixels
        return mean_value, stdev, num_pixels, saturated_count

    def raw(self):
        raw_img = self.image.undistorted(self.image.raw())
        return self.region_stats(raw_img, self.panel_corners(), sat_threshold=65000)

    def intensity(self):
        intensity_img = self.image.undistorted(self.image.intensity())
        return self.region_stats(
            intensity_img, self.panel_corners(), sat_threshold=65000,
        )

    def radiance(self):
        radiance_img = self.image.undistorted(self.image.radiance())
        return self.region_stats(radiance_img, self.panel_corners())

    def reflectance_mean(self):
        reflectance_image = self.image.reflectance()
        if reflectance_image is None:
            print(
                "First calculate the reflectance image by providing a\n band specific irradiance to the calling image.reflectance(irradiance)",
            )
        mean, _, _, _ = self.region_stats(reflectance_image, self.panel_corners())
        return mean

    def irradiance_mean(self, reflectance):
        radiance_mean, _, _, _ = self.radiance()
        return radiance_mean * math.pi / reflectance

    def plot_image(self):
        display_img = cv2.cvtColor(self.gray8b, cv2.COLOR_GRAY2RGB)
        if self.panel_detected():
            if self.qr_corners() is not None:
                cv2.drawContours(display_img, [self.qr_corners()], 0, (255, 0, 0), 3)
            cv2.drawContours(display_img, [self.panel_corners()], 0, (0, 0, 255), 3)

        font = cv2.FONT_HERSHEY_DUPLEX
        if self.panel_detected():
            if self.qr_corners() is not None:
                xloc = self.qr_corners()[0][0] - 100
                yloc = self.qr_corners()[0][1] + 100
            else:
                xloc = self.panel_corners()[0][0] - 100
                yloc = self.panel_corners()[0][1] + 100
            cv2.putText(
                display_img,
                str(self.serial).split("_")[0],
                (xloc, yloc),
                font,
                1,
                255,
                2,
            )
        return display_img

    def plot(self, figsize=(14, 14)):
        display_img = self.plot_image()
        fig, ax = plt.subplots(figsize=figsize)
        ax.imshow(display_img)
        plt.tight_layout()
        plt.show()
        return fig, ax
