from __future__ import print_function

import math
from math import pi
from typing import Tuple

import gtsam
import numpy as np
from gtsam import Cal3_S2, PinholeCameraCal3_S2, Point3, Pose3


class Options:
    """
    Options to generate test scenario
    """

    def __init__(self, triangle: bool = False, nrCameras: int = 3, K=Cal3_S2()) -> None:
        """
        Options to generate test scenario
        @param triangle: generate a triangle scene with 3 points if True, otherwise
                  a cube with 8 points
        @param nrCameras: number of cameras to generate
        @param K: camera calibration object
        """
        self.triangle = triangle
        self.nrCameras = nrCameras


class GroundTruth:
    """
    Object holding generated ground-truth data
    """

    def __init__(self, K=Cal3_S2(), nrCameras: int = 3, nrPoints: int = 4) -> None:
        self.K = K
        self.cameras = [Pose3()] * nrCameras
        self.points = [Point3(0, 0, 0)] * nrPoints

    def print(self, s: str = "") -> None:
        print(s)
        print("K = ", self.K)
        print("Cameras: ", len(self.cameras))
        for camera in self.cameras:
            print("\t", camera)
        print("Points: ", len(self.points))
        for point in self.points:
            print("\t", point)
        pass


class Data:
    """
    Object holding generated measurement data
    """

    class NoiseModels:
        pass

    def __init__(self, K=Cal3_S2(), nrCameras: int = 3, nrPoints: int = 4) -> None:
        self.K = K
        self.Z = [x[:] for x in [[gtsam.Point2()] * nrPoints] * nrCameras]
        self.J = [x[:] for x in [[0] * nrPoints] * nrCameras]
        self.odometry = [Pose3()] * nrCameras

        # Set Noise parameters
        self.noiseModels = Data.NoiseModels()
        self.noiseModels.posePrior = gtsam.noiseModel.Diagonal.Sigmas(
            np.array([0.001, 0.001, 0.001, 0.1, 0.1, 0.1]))
        # noiseModels.odometry = gtsam.noiseModel.Diagonal.Sigmas(
        #    np.array([0.001,0.001,0.001,0.1,0.1,0.1]))
        self.noiseModels.odometry = gtsam.noiseModel.Diagonal.Sigmas(
            np.array([0.05, 0.05, 0.05, 0.2, 0.2, 0.2]))
        self.noiseModels.pointPrior = gtsam.noiseModel.Isotropic.Sigma(3, 0.1)
        self.noiseModels.measurement = gtsam.noiseModel.Isotropic.Sigma(2, 1.0)


def generate_data(options) -> Tuple[Data, GroundTruth]:
    """ Generate ground-truth and measurement data. """

    K = Cal3_S2(500, 500, 0, 640. / 2., 480. / 2.)
    nrPoints = 3 if options.triangle else 8

    truth = GroundTruth(K=K, nrCameras=options.nrCameras, nrPoints=nrPoints)
    data = Data(K, nrCameras=options.nrCameras, nrPoints=nrPoints)

    # Generate simulated data
    if options.triangle:  # Create a triangle target, just 3 points on a plane
        r = 10
        for j in range(len(truth.points)):
            theta = j * 2 * pi / nrPoints
            truth.points[j] = Point3(
                r * math.cos(theta), r * math.sin(theta), 0)
    else:  # 3D landmarks as vertices of a cube
        truth.points = [
            Point3(10, 10, 10), Point3(-10, 10, 10),
            Point3(-10, -10, 10), Point3(10, -10, 10),
            Point3(10, 10, -10), Point3(-10, 10, -10),
            Point3(-10, -10, -10), Point3(10, -10, -10)
        ]

    # Create camera cameras on a circle around the triangle
    height = 10
    r = 40
    for i in range(options.nrCameras):
        theta = i * 2 * pi / options.nrCameras
        t = Point3(r * math.cos(theta), r * math.sin(theta), height)
        truth.cameras[i] = PinholeCameraCal3_S2.Lookat(t,
                                                       Point3(0, 0, 0),
                                                       Point3(0, 0, 1),
                                                       truth.K)
        # Create measurements
        for j in range(nrPoints):
            # All landmarks seen in every frame
            data.Z[i][j] = truth.cameras[i].project(truth.points[j])
            data.J[i][j] = j

    # Calculate odometry between cameras
    for i in range(1, options.nrCameras):
        data.odometry[i] = truth.cameras[i - 1].pose().between(
            truth.cameras[i].pose())

    return data, truth
