/*
 * Copyright (c) 2010-11, Oracle and/or its affiliates. All rights reserved.
 */

package com.sun.javafx.tk.quantum;

import java.util.logging.Level;
import java.util.logging.Logger;

import javafx.scene.input.InputMethodRequests;
import javafx.scene.input.Dragboard;

import com.sun.javafx.geom.CameraImpl;
import com.sun.javafx.geom.PickRay;
import com.sun.javafx.geom.Rectangle;
import com.sun.javafx.geom.Vec3d;
import com.sun.javafx.geom.transform.Affine3D;
import com.sun.javafx.geom.transform.GeneralTransform3D;
import com.sun.javafx.geom.transform.NoninvertibleTransformException;
import com.sun.javafx.sg.PGNode;
import com.sun.javafx.sg.prism.NGNode;
import com.sun.javafx.sg.prism.SceneChangeListener;
import com.sun.javafx.tk.Toolkit;
import com.sun.javafx.tk.TKDragGestureListener;
import com.sun.javafx.tk.TKDragSourceListener;
import com.sun.javafx.tk.TKDropTargetListener;
import com.sun.javafx.tk.TKScenePaintListener;
import com.sun.javafx.tk.TKScene;
import com.sun.javafx.tk.TKSceneListener;
import com.sun.prism.camera.PrismCameraImpl;
import com.sun.prism.camera.PrismParallelCameraImpl;
import com.sun.prism.camera.PrismPerspectiveCameraImpl;
import com.sun.prism.impl.PrismSettings;
import com.sun.prism.paint.Paint;

/**
 * @author Artem Ananiev
 */
public abstract class GlassScene implements TKScene, SceneChangeListener {

    protected boolean verbose;

    protected GlassStage glassStage;

    protected TKSceneListener sceneListener;
    protected TKDragGestureListener dragGestureListener;
    protected TKDragSourceListener dragSourceListener;
    protected TKDropTargetListener dropTargetListener;
    protected InputMethodRequests inputMethodRequests;
    private TKScenePaintListener scenePaintListener;

    protected NGNode root;
    protected PrismCameraImpl camera;
    protected Paint fillPaint;

    private boolean dirty;
    private boolean entireSceneDirty = true;

    private boolean depthBuffer = false;

    protected GlassScene(boolean verbose) {
        this(verbose, false);
    }

    protected GlassScene(boolean verbose, boolean depthBuffer) {
        this.verbose = verbose;
        this.depthBuffer = depthBuffer;
    }

    boolean hasDepthBuffer() {
        return depthBuffer;
    }
    
    protected abstract boolean isSynchronous();

    @Override public void setScene(Object scene) {
        // Overridden in subclasses
    }

    @Override public void setTKSceneListener(final TKSceneListener listener) {
        this.sceneListener = listener;
    }

    @Override public synchronized void setTKScenePaintListener(final TKScenePaintListener listener) {
        this.scenePaintListener = listener;
    }

    public void setTKDropTargetListener(final TKDropTargetListener listener) {
        this.dropTargetListener = listener;
    }

    public void setTKDragSourceListener(final TKDragSourceListener listener) {
        this.dragSourceListener = listener;
    }

    public void setTKDragGestureListener(final TKDragGestureListener listener) {
        this.dragGestureListener = listener;
    }

    public void setInputMethodRequests(final InputMethodRequests requests) {
        this.inputMethodRequests = requests;
    }

    @Override
    public void setRoot(PGNode root) {
        this.root = (NGNode)root;
        entireSceneNeedsRepaint();
    }
    
    protected PGNode getRoot() {
        return root;
    }

    @Override
    public void setCamera(CameraImpl camera) {
        if (camera != null) {
            this.camera = (PrismCameraImpl) camera;
        } else {
            this.camera = PrismParallelCameraImpl.getInstance();
        }
        entireSceneNeedsRepaint();
    }

    @Override
    public void setFillPaint(Object fillPaint) {
        this.fillPaint = (Paint)fillPaint;
        entireSceneNeedsRepaint();
    }

    @Override
    public void setCursor(Object cursor) {
        // TODO: implement cursors
        // assert cursor instanceof com.sun.glass.ui.Cursor;
    }

    @Override
    public void markDirty() {
        sceneChanged();
    }

    public void entireSceneNeedsRepaint() {
        entireSceneDirty = true;
        sceneChanged();
    }

    public boolean isEntireSceneDirty() {
        return entireSceneDirty;
    }

    public void clearEntireSceneDirty() {
        entireSceneDirty = false;
    }

    void clearDirty() {
        dirty = false;
    }

    @Override public void requestFocus() {
        if (glassStage != null) {
            glassStage.requestFocus();
        }
    }
    
    @Override
    public Dragboard createDragboard() {
        return Toolkit.getToolkit().createDragboard();
    }

    void setGlassStage(GlassStage stage) {
        glassStage = stage;
        if (glassStage != null) {
            sceneChanged();
        } else {
            // the scene is no longer associated with a stage, remove from
            // the dirty list and clear. it will be marked dirty if it becomes
            // active again
            PaintCollector.getInstance().removeDirtyScene(this);
            clearDirty();
        }
    }

    void repaint() {
        // Overridden in subclasses
    }

    void stageVisible(boolean visible) {
        // if the stage became invisible (for example before being destroyed)
        // we need to remove the scene from the repainter list to prevent
        // potential leak
        if (!visible && PrismSettings.forceRepaint) {
            PaintCollector.getInstance().removeDirtyScene(this);
            clearDirty();
        }
        if (visible) {
            PaintCollector.getInstance().addDirtyScene(this);
        }
    }

    // Note: Class variables use with care, not MT safe.
    private static final double[] projMatValues = new double[16];
    private static final Vec3d ptCc = new Vec3d();
    private static final Vec3d ptEc = new Vec3d();
    private static final Vec3d ptWc = new Vec3d();
    private static final Vec3d eyeEc = new Vec3d();
    private static final Vec3d eyeWc = new Vec3d();

    @Override public PickRay computePickRay(float x, float y, PickRay pickRay) {
        GeneralTransform3D projTx = camera.getProjectionTransform(null);
        projTx.get(projMatValues);

        Affine3D viewTx = camera.getViewTransform(null);
        Rectangle vp = camera.getViewport(null);

        double xCc = (x / vp.width) * 2.0 - 1.0;
        double yCc = (y / vp.height) * -2.0 + 1.0;

        if (!(camera instanceof PrismPerspectiveCameraImpl)) {
            // Parallel projection
            System.err.println("Picking not implemented for parallel projection");
        } else {
            // Perspective projection
            double[] m = projMatValues;
            double zEc = (1.0 - m[15]) / m[14];
            double zCc = m[10] * zEc + m[11];
            ptCc.set(xCc, yCc, zCc);

            // Invert the projection transform. Note that we reuse projTx
            // to avoid constructing another transform.
            projTx.invert();

            // Transform the Cc point into Ec via the inverse projection transform
            projTx.transform(ptCc, ptEc);
            try {
                // Invert the view transform. Note that we reuse projTx
                // to avoid constructing another transform.
                // TODO: if we decide to define picking in Ec rather
                // than Wc then this step becomes unnecessary; instead it will
                // be handled as part of the ModelView transform
                viewTx.invert();
            } catch (NoninvertibleTransformException ex) {
                Logger.getLogger(ViewScene.class.getName()).log(Level.SEVERE, null, ex);
            }

            viewTx.transform(ptEc, ptWc);

            eyeEc.set(0.0, 0.0, 0.0);
            viewTx.transform(eyeEc, eyeWc);

            //            String msg = "computePickRay:" + "\n" +
            //                    "  ptCc = " + ptCc + "\n" +
            //                    "  ptEc = " + ptEc + "\n" +
            //                    "  ptWc = " + ptWc + "\n" +
            //                    "  eyeEc = " + eyeEc + "  eyeWc = " + eyeWc;
            //            System.err.println(msg);

            if (pickRay == null) {
                pickRay = new PickRay();
            }
            pickRay.getOriginNoClone().set(eyeWc);
            pickRay.getDirectionNoClone().sub(ptWc, eyeWc);
        }

        return pickRay;
    }

    /* com.sun.javafx.tk.TKSceneListener */

    @Override public void sceneChanged() {
        if (glassStage instanceof PopupStage) {
            ((PopupStage)glassStage).getOwnerScene().sceneChanged();
        }
        if (glassStage != null) {
            // don't mark this scene dirty and add it to the dirty scene list if
            // it is not attached to a Stage. When it does get attached the
            // scene will be marked dirty anyway.
            PaintCollector.getInstance().addDirtyScene(this);
        }
    }

    public final synchronized void frameRendered() {
        if (scenePaintListener != null) {
            scenePaintListener.frameRendered();
        }
    }
}
