package org.helioviewer.jhv.camera;

import org.helioviewer.jhv.astronomy.Sun;
import org.helioviewer.jhv.astronomy.UpdateViewpoint;
import org.helioviewer.jhv.display.Display;
import org.helioviewer.jhv.layers.Movie;
import org.helioviewer.jhv.math.Mat4f;
import org.helioviewer.jhv.math.Quat;
import org.helioviewer.jhv.math.Transform;
import org.helioviewer.jhv.math.Vec2;
import org.helioviewer.jhv.opengl.GLSLShape;
import org.helioviewer.jhv.position.Position;
import org.helioviewer.jhv.time.JHVDate;
import org.json.JSONArray;
import org.json.JSONObject;

import com.jogamp.opengl.GL2;

public class Camera {

    public static final double INITFOV = 1. * Math.PI / 180.;
    private static final double MIN_FOV = INITFOV * 0.1;
    private static final double MAX_FOV = INITFOV * 30;
    private double fov = INITFOV;

    private Quat rotation = Quat.ZERO;
    private final Vec2 currentTranslation = new Vec2(0, 0);
    private Quat currentDragRotation = Quat.ZERO;
    private double cameraWidth = 1;

    private boolean tracking;

    private Position viewpoint = Sun.StartEarth;
    private UpdateViewpoint updateViewpoint = UpdateViewpoint.observer;

    ////
    private static final float depthClose = (float) (32 * Sun.Radius); // bit more than LASCO C3
    private static final float depthFar = (float) (50 * Sun.MeanEarthDistance); // bit further than Pluto

    private final float[] invProj = new float[16];

    public void projectionOrtho2D(double aspect) {
        Transform.setOrthoProjection(-(float) (cameraWidth * aspect), (float) (cameraWidth * aspect), -(float) cameraWidth, (float) cameraWidth, -1, 1);
        Transform.setTranslateView((float) currentTranslation.x, (float) currentTranslation.y, 0);
        Transform.cacheMVP();
    }

    public void projectionOrtho(double aspect, GL2 gl, GLSLShape blackCircle) {
        Transform.setOrthoProjection(-(float) (cameraWidth * aspect), (float) (cameraWidth * aspect), -(float) cameraWidth, (float) cameraWidth, -depthClose, depthClose);
        Transform.setTranslateView((float) currentTranslation.x, (float) currentTranslation.y, 0);
        Transform.cacheMVP();

        blackCircle.renderShape(gl, GL2.GL_TRIANGLE_STRIP);

        Transform.rotateView(rotation);
        Transform.cacheMVP();
    }

    public float[] getTransformationInverse(double aspect) {
        Mat4f.orthoInverse(invProj, -(float) (cameraWidth * aspect), (float) (cameraWidth * aspect), -(float) cameraWidth, (float) cameraWidth, -1, 1);
        Mat4f.translate(invProj, -(float) currentTranslation.x, -(float) currentTranslation.y, 0);
        return invProj;
    }

    public void projectionOrthoFar(double aspect) {
        Transform.setOrthoProjection(-(float) (cameraWidth * aspect), (float) (cameraWidth * aspect), -(float) cameraWidth, (float) cameraWidth, -depthFar, depthFar);
    }
////

    public void updateCamera(Position _viewpoint) { // for layers sync
        if (viewpoint != _viewpoint) {
            viewpoint = _viewpoint;
            updateRotation();
            updateWidth();
        }
    }

    private void updateCamera(JHVDate time) {
        viewpoint = Display.mode == Display.DisplayMode.Orthographic ? updateViewpoint.update(time) : UpdateViewpoint.earthFixedDistance.update(time);
        updateRotation();
        updateWidth();
    }

    private void updateRotation() {
        rotation = Quat.rotate(currentDragRotation, viewpoint.toQuat());
    }

    private void updateWidth() {
        cameraWidth = viewpoint.distance * Math.tan(0.5 * fov);
    }

    public void refresh() {
        updateCamera(Movie.getTime());
        Display.render(1);
    }

    public void reset() {
        currentTranslation.x = 0;
        currentTranslation.y = 0;
        currentDragRotation = Quat.ZERO;

        updateCamera(Movie.getTime());
        CameraHelper.zoomToFit(this);
        Display.render(1);
    }

    public Position getViewpoint() {
        return viewpoint;
    }

    public UpdateViewpoint getUpdateViewpoint() {
        return updateViewpoint;
    }

    public Camera setViewpointUpdate(UpdateViewpoint _updateViewpoint) {
        updateViewpoint = _updateViewpoint;
        reset();
        return this;
    }

    public Vec2 getCurrentTranslation() {
        return currentTranslation;
    }

    public void setCurrentTranslation(double x, double y) {
        currentTranslation.x = x;
        currentTranslation.y = y;
    }

    public Quat getCurrentDragRotation() {
        return currentDragRotation;
    }

    void rotateCurrentDragRotation(Quat _currentDragRotation) {
        currentDragRotation = Quat.rotate(currentDragRotation, _currentDragRotation);
        updateRotation();
    }

    public void setFOV(double _fov) {
        if (_fov < MIN_FOV) {
            fov = MIN_FOV;
        } else if (_fov > MAX_FOV) {
            fov = MAX_FOV;
        } else {
            fov = _fov;
        }
        updateWidth();
    }

    public double getFOV() {
        return fov;
    }

    public void setTrackingMode(boolean _tracking) {
        if (tracking != _tracking) {
            tracking = _tracking;
            refresh();
        }
    }

    public boolean getTrackingMode() {
        return tracking;
    }

    public double getWidth() {
        return cameraWidth;
    }

    public void zoom(double wr) {
        setFOV(fov * (1 + 0.015 * wr));
    }

    public void timeChanged(JHVDate date) {
        if (!tracking) {
            updateCamera(date);
        }
    }

    public JSONObject toJson() {
        JSONObject jo = new JSONObject();
        jo.put("dragRotation", currentDragRotation.toJson());
        jo.put("translationX", currentTranslation.x);
        jo.put("translationY", currentTranslation.y);
        jo.put("fov", fov);
        return jo;
    }

    public void fromJson(JSONObject jo) {
        JSONArray ja = jo.optJSONArray("dragRotation");
        if (ja != null)
            currentDragRotation = Quat.fromJson(ja);
        currentTranslation.x = jo.optDouble("translationX", currentTranslation.x);
        currentTranslation.y = jo.optDouble("translationY", currentTranslation.y);
        fov = jo.optDouble("fov", fov);
    }

}

@@ -1200,26 +1200,26 @@
 l float 
-depthClose
+clipNarrow
  = (floa
@@ -1297,24 +1297,24 @@
 l float 
-depthFar
+clipWide
  = (floa
@@ -1995,30 +1995,30 @@
 h, -
-depthClose, depthClose
+clipNarrow, clipNarrow
 );%0A 
@@ -2482,36 +2482,50 @@
 ) cameraWidth, -
-1, 1
+clipWide, clipWide
 );%0A        Mat4f
@@ -2843,26 +2843,26 @@
 h, -
-depthFar, depthFar
+clipWide, clipWide
 );%0A 

Set wide clip planes on inverse projection matrix