/**
 * Copyright 2023 JogAmp Community. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without modification, are
 * permitted provided that the following conditions are met:
 *
 *    1. Redistributions of source code must retain the above copyright notice, this list of
 *       conditions and the following disclaimer.
 *
 *    2. Redistributions in binary form must reproduce the above copyright notice, this list
 *       of conditions and the following disclaimer in the documentation and/or other materials
 *       provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY JogAmp Community ``AS IS'' AND ANY EXPRESS OR IMPLIED
 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
 * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JogAmp Community OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
 * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * The views and conclusions contained in the software and documentation are those of the
 * authors and should not be interpreted as representing official policies, either expressed
 * or implied, of JogAmp Community.
 */
package com.jogamp.opengl.demos.graph.ui;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;

import com.jogamp.common.os.Clock;
import com.jogamp.common.util.IOUtil;
import com.jogamp.graph.curve.Region;
import com.jogamp.graph.font.Font;
import com.jogamp.graph.font.FontFactory;
import com.jogamp.graph.ui.Group;
import com.jogamp.graph.ui.Scene;
import com.jogamp.graph.ui.Scene.PMVMatrixSetup;
import com.jogamp.graph.ui.shapes.GlyphShape;
import com.jogamp.graph.ui.shapes.Label;
import com.jogamp.newt.MonitorDevice;
import com.jogamp.newt.event.KeyAdapter;
import com.jogamp.newt.event.KeyEvent;
import com.jogamp.newt.event.WindowAdapter;
import com.jogamp.newt.event.WindowEvent;
import com.jogamp.newt.opengl.GLWindow;
import com.jogamp.opengl.GL;
import com.jogamp.opengl.GL2ES2;
import com.jogamp.opengl.GLCapabilities;
import com.jogamp.opengl.GLProfile;
import com.jogamp.opengl.demos.graph.FontSetDemos;
import com.jogamp.opengl.demos.util.CommandlineOptions;
import com.jogamp.opengl.demos.util.MiscUtils;
import com.jogamp.opengl.fixedfunc.GLMatrixFunc;
import com.jogamp.opengl.math.FloatUtil;
import com.jogamp.opengl.math.Quaternion;
import com.jogamp.opengl.math.Recti;
import com.jogamp.opengl.math.Vec3f;
import com.jogamp.opengl.math.geom.AABBox;
import com.jogamp.opengl.util.Animator;
import com.jogamp.opengl.util.PMVMatrix;

/**
 * Res independent Shape, Scene attached to GLWindow showing simple linear Shape movement.
 * <p>
 * This variation of {@link UISceneDemo00} shows a text animation assembling one line of text,
 * each glyph coming from from a random 3D point moving to its destination all at once.
 * </p>
 * <p>
 * - Pass '-keep' to main-function to keep running.
 * - Pass '-aspeed' to vary velocity
 * </p>
 */
public class UISceneDemo03 {
    static final boolean DEBUG = false;

    static final String[] originalTexts = {
            " JOGL, Java™ Binding for the OpenGL® API ",
            " GraphUI, Resolution Independent Curves ",
            " JogAmp, Java™ libraries for 3D & Media "
    };

    static CommandlineOptions options = new CommandlineOptions(1280, 720, Region.VBAA_RENDERING_BIT);
    static float velocity = 30 / 1e3f; // [m]/[s]
    static float rot_step = velocity * 1;

    static void setVelocity(final float v) {
        velocity = v; // Math.max(1/1e3f, v);
        rot_step = velocity * 1;
    }

    public static void main(final String[] args) throws IOException {
        int autoSpeed = 0;

        if (0 != args.length) {
            final int[] idx = { 0 };
            for (idx[0] = 0; idx[0] < args.length; ++idx[0]) {
                if( options.parse(args, idx) ) {
                    continue;
                } else if (args[idx[0]].equals("-v")) {
                    ++idx[0];
                    setVelocity(MiscUtils.atoi(args[idx[0]], (int) velocity * 1000) / 1000f);
                } else if(args[idx[0]].equals("-aspeed")) {
                    autoSpeed = -1;
                    setVelocity(80/1000f);
                    options.keepRunning = true;
                }
            }
        }
        // renderModes |= Region.COLORCHANNEL_RENDERING_BIT;
        System.err.println(options);

        final GLProfile reqGLP = GLProfile.get(options.glProfileName);
        System.err.println("GLProfile: "+reqGLP);

        //
        // Resolution independent, no screen size
        //
        final Font font = FontFactory.get(IOUtil.getResource("fonts/freefont/FreeSerif.ttf",FontSetDemos.class.getClassLoader(), FontSetDemos.class).getInputStream(), true);
        // final Font font = FontFactory.get(IOUtil.getResource("jogamp/graph/font/fonts/ubuntu/Ubuntu-R.ttf",FontSetDemos.class.getClassLoader(), FontSetDemos.class).getInputStream(), true);
        System.err.println("Font: " + font.getFullFamilyName());
        final Font fontStatus = FontFactory.get(IOUtil.getResource("fonts/freefont/FreeMono.ttf", FontSetDemos.class.getClassLoader(), FontSetDemos.class).getInputStream(), true);

        final Scene scene = new Scene();
        scene.setClearParams(new float[] { 1f, 1f, 1f, 1f }, GL.GL_COLOR_BUFFER_BIT | GL.GL_DEPTH_BUFFER_BIT);
        scene.setPMVMatrixSetup(new MyPMVMatrixSetup());
        scene.setDebugBorderBox(options.debugBoxThickness);

        final Group glyphGroup = new Group();
        scene.addShape(glyphGroup);

        // scene.setFrustumCullingEnabled(true);
        glyphGroup.setFrustumCullingEnabled(true);

        final Animator animator = new Animator(0 /* w/o AWT */);
        animator.setUpdateFPSFrames(1 * 60, null); // System.err);

        final GLCapabilities caps = new GLCapabilities(reqGLP);
        caps.setAlphaBits(4);
        if( options.sceneMSAASamples > 0 ) {
            caps.setSampleBuffers(true);
            caps.setNumSamples(options.sceneMSAASamples);
        }
        System.out.println("Requested: " + caps);

        final GLWindow window = GLWindow.create(caps);
        window.setSize(options.surface_width, options.surface_height);
        window.setTitle(UISceneDemo03.class.getSimpleName() + ": " + window.getSurfaceWidth() + " x " + window.getSurfaceHeight());
        window.setVisible(true);
        window.addGLEventListener(scene);
        window.addWindowListener(new WindowAdapter() {
            @Override
            public void windowResized(final WindowEvent e) {
                window.setTitle(UISceneDemo03.class.getSimpleName() + ": " + window.getSurfaceWidth() + " x " + window.getSurfaceHeight());
            }

            @Override
            public void windowDestroyNotify(final WindowEvent e) {
                animator.stop();
            }
        });
        window.addKeyListener(new KeyAdapter() {
            @Override
            public void keyReleased(final KeyEvent e) {
                if (e.getKeySymbol() == KeyEvent.VK_PLUS ||
                    e.getKeySymbol() == KeyEvent.VK_ADD)
                {
                    if (e.isShiftDown()) {
                        setVelocity(velocity + 10 / 1000f);
                    } else {
                        setVelocity(velocity + 1 / 1000f);
                    }
                } else if (e.getKeySymbol() == KeyEvent.VK_MINUS ||
                           e.getKeySymbol() == KeyEvent.VK_SUBTRACT)
                {
                    if (e.isShiftDown()) {
                        setVelocity(velocity - 10 / 1000f);
                    } else {
                        setVelocity(velocity - 1 / 1000f);
                    }
                }
            }
        });

        scene.attachInputListenerTo(window);

        animator.add(window);
        animator.start();

        //
        // After initial display we can use screen resolution post initial
        // Scene.reshape(..)
        // However, in this example we merely use the resolution to
        // - Compute the animation values with DPI
        scene.waitUntilDisplayed();

        scene.setFrustumCullingEnabled(true);
        window.invoke(true, (drawable) -> {
            final GL gl = drawable.getGL();
            gl.glEnable(GL.GL_DEPTH_TEST);
            return true;
        });

        final GLProfile hasGLP = window.getChosenGLCapabilities().getGLProfile();
        final AABBox sceneBox = scene.getBounds();
        System.err.println("SceneBox " + sceneBox);
        System.err.println("Frustum " + scene.getMatrix().getFrustum());
        System.err.println("GlyphGroup.0: "+glyphGroup);

        final Label statusLabel;
        {
            final AABBox fbox = fontStatus.getGlyphBounds("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
            final float statusLabelScale = sceneBox.getWidth() / fbox.getWidth();
            System.err.println("StatusLabelScale: " + statusLabelScale + " = " + sceneBox.getWidth() + " / " + fbox.getWidth() + ", " + fbox);

            statusLabel = new Label(options.renderModes, fontStatus, "Nothing there yet");
            statusLabel.setScale(statusLabelScale, statusLabelScale, 1f);
            statusLabel.setColor(0.1f, 0.1f, 0.1f, 1.0f);
            statusLabel.moveTo(sceneBox.getMinX(), sceneBox.getMinY() + statusLabelScale * (fontStatus.getMetrics().getLineGap() - fontStatus.getMetrics().getDescent()), 0f);
            scene.addShape(statusLabel);
        }

        {
            final StringBuilder sb = new StringBuilder();
            for(final String s : originalTexts) {
                sb.append(s).append("\n");
            }
            final Label l = new Label(options.renderModes, font, sb.toString()); // originalTexts[0]);
            l.validate(hasGLP);
            final float scale = sceneBox.getWidth() / l.getBounds().getWidth();
            l.setScale(scale, scale, 1f);
            l.setColor(0.1f, 0.1f, 0.1f, 1.0f);
            l.moveTo(sceneBox.getMinX(), 0f, 0f);
            scene.addShape(l);

            if( options.wait_to_start ) {
                statusLabel.setText("Press enter to continue");
                MiscUtils.waitForKey("Start");
            }

            window.invoke(true, (drawable) -> {
                final GL2ES2 gl = drawable.getGL().getGL2ES2();
                scene.screenshot(drawable.getGL(), scene.nextScreenshotFile(null, UISceneDemo03.class.getSimpleName(), options.renderModes, drawable.getChosenGLCapabilities(), null));
                scene.removeShape(gl, l);
                return true;
            });
        }

        //
        // Setup the moving glyphs
        //

        final List<GlyphShape> glyphShapes = new ArrayList<GlyphShape>();

        final float pixPerMM, dpiV;
        {
            final float[] tmp = window.getPixelsPerMM(new float[2]);
            pixPerMM = tmp[0]; // [px]/[mm]
            final float[] sDPI = MonitorDevice.perMMToPerInch( tmp );
            dpiV = sDPI[1];
        }

        boolean z_only = true;
        int txt_idx = 0;

        do {
            final float fontScale;
            {
                final AABBox fbox = font.getGlyphBounds(originalTexts[txt_idx]);
                fontScale = sceneBox.getWidth() / fbox.getWidth();
                System.err.println("FontScale: " + fontScale + " = " + sceneBox.getWidth() + " / " + fbox.getWidth());
            }
            z_only = !z_only;
            window.invoke(true, (drawable) -> {
                glyphGroup.removeAllShapes(drawable.getGL().getGL2ES2(), scene.getRenderer());
                return true;
            });

            final float[] movingGlyphPixPerShapeUnit;
            {
                final Random random = new Random();

                final GlyphShape testGlyph = new GlyphShape(options.renderModes, font, 'X', 0, 0);
                testGlyph.setScale(fontScale, fontScale, 1f);
                testGlyph.validate(hasGLP);
                final PMVMatrix pmv = new PMVMatrix();
                final int[] movingGlyphSizePx = testGlyph.getSurfaceSize(scene, pmv, new int[2]); // [px]
                movingGlyphPixPerShapeUnit = testGlyph.getPixelPerShapeUnit(movingGlyphSizePx, new float[2]); // [px]/[shapeUnit]

                final AABBox box = GlyphShape.processString(glyphShapes, options.renderModes, font, originalTexts[txt_idx]);
                System.err.println("Shapes: "+box);
                for(final GlyphShape gs : glyphShapes) {
                    gs.setScale(fontScale, fontScale, 1f);
                    gs.setColor(0.1f, 0.1f, 0.1f, 1);
                    final Vec3f target = gs.getOrigPos(fontScale).add(sceneBox.getMinX(), 0f, 0f);

                    final float start_pos_x = z_only ? target.x() :
                                                       sceneBox.getMinX() + random.nextFloat() * sceneBox.getWidth();
                    final float start_pos_y = z_only ? target.y() :
                                                       sceneBox.getMinY() + random.nextFloat() * sceneBox.getHeight();
                    final float start_pos_z = 0f + random.nextFloat() * sceneBox.getHeight() * 1f;
                    gs.moveTo(start_pos_x, start_pos_y, start_pos_z);
                }
                // just add destText to scene to be cleaned up, invisible
                testGlyph.setEnabled(false);
                glyphGroup.addShape(testGlyph);
            }
            glyphGroup.addShapes(glyphShapes);

            final float pos_eps = FloatUtil.EPSILON * 5000; // ~= 0.0005960
            final float rot_eps = FloatUtil.adegToRad(0.5f); // 1 adeg ~= 0.01745 rad

            final long t0_us = Clock.currentNanos() / 1000; // [us]
            final long[] t2_us = { t0_us };
            while (!glyphShapes.isEmpty()) {
                window.invoke(true, (drawable) -> {
                    final long t3_us = Clock.currentNanos() / 1000;
                    final float dt_s = (t3_us - t2_us[0]) / 1e6f;
                    t2_us[0] = t3_us;

                    final float velocity_px = velocity * 1e3f * pixPerMM; // [px]/[s]
                    final float velocity_obj = velocity_px / movingGlyphPixPerShapeUnit[0]; // [shapeUnit]/[s]
                    final float dxy = velocity_obj * dt_s; // [shapeUnit]

                    for (int idx = glyphShapes.size() - 1; 0 <= idx; --idx) {
                        final GlyphShape glyph = glyphShapes.get(idx);
                        final Vec3f pos = new Vec3f(glyph.getPosition());
                        final Vec3f target = glyph.getOrigPos(fontScale).add(sceneBox.getMinX(), 0f, 0f);
                        final Vec3f p_t = target.minus(pos);
                        final float p_t_diff = p_t.length();
                        final Quaternion q = glyph.getRotation();
                        final Vec3f euler = q.toEuler(new Vec3f());
                        final float radY = euler.y();
                        final float radYdiff = Math.min(Math.abs(radY), FloatUtil.TWO_PI - Math.abs(radY));
                        final boolean pos_ok = p_t_diff <= pos_eps;
                        final boolean pos_near = p_t_diff <= glyph.getBounds().getSize() * fontScale * 2f;
                        final boolean rot_ok = pos_near && ( radYdiff <= rot_eps || radYdiff <= rot_step * 2f );
                        if ( pos_ok && rot_ok ) {
                            // arrived
                            if( DEBUG ) {
                                if( 0 == idx ) {
                                    System.err.println("F: rot: "+radY+" ("+FloatUtil.radToADeg(radY)+"), diff "+radYdiff+" ("+FloatUtil.radToADeg(radYdiff)+"), step "+rot_step+" ("+FloatUtil.radToADeg(rot_step)+")");
                                }
                            }
                            glyph.moveTo(target.x(), target.y(), target.z());
                            glyph.setInteractive(false);
                            q.setIdentity();
                            glyphShapes.remove(idx);
                            continue;
                        }
                        if( !pos_ok ) {
                            if( DEBUG ) {
                                if( 0 == idx ) {
                                    System.err.println("p_t_diff: "+p_t_diff+", dxy "+dxy);
                                }
                            }
                            if( p_t_diff <= dxy || p_t_diff <= pos_eps ) {
                                glyph.moveTo(target.x(), target.y(), target.z());
                            } else {
                                p_t.normalize();
                                pos.add( p_t.scale( dxy ) );
                                glyph.moveTo(pos.x(), pos.y(), pos.z());
                            }
                            if( !rot_ok ) {
                                if( pos_near ) {
                                    q.rotateByAngleY( rot_step * 2f );
                                } else {
                                    q.rotateByAngleY( rot_step );
                                }
                            }
                        } else {
                            if( DEBUG ) {
                                if( 0 == idx ) {
                                    System.err.println("P: rot: "+radY+" ("+FloatUtil.radToADeg(radY)+"), diff "+radYdiff+" ("+FloatUtil.radToADeg(radYdiff)+"), step "+rot_step+" ("+FloatUtil.radToADeg(rot_step)+")");
                                }
                            }
                            if( radYdiff <= rot_step * 3f || radYdiff <= rot_eps ) {
                                q.setIdentity();
                            } else {
                                q.rotateByAngleY( rot_step * 3f );
                            }
                        }
                    }
                    final String text = String.format("%s, v %.1f mm/s, r %.3f",
                            scene.getStatusText(drawable, options.renderModes, 0, dpiV), velocity * 1e3f, rot_step);
                    statusLabel.setText(text);
                    return true;
                });
            }
            final float has_dur_s = ((Clock.currentNanos() / 1000) - t0_us) / 1e6f; // [us]
            System.err.printf("Text travel-duration %.3f s, %d chars%n", has_dur_s, originalTexts[txt_idx].length());
            if( scene.getScreenshotCount() < 1 + originalTexts.length ) {
                scene.screenshot(true, scene.nextScreenshotFile(null, UISceneDemo03.class.getSimpleName(), options.renderModes, window.getChosenGLCapabilities(), null));
            }
            try { Thread.sleep(2000); } catch (final InterruptedException e1) { }
            if( autoSpeed > 0 ) {
                if( velocity < 60/1000f ) {
                    setVelocity(velocity + 9/1000f);
                } else {
                    setVelocity(velocity - 9/1000f);
                    autoSpeed = -1;
                }
            } else  if( autoSpeed < 0 ) {
                if( velocity > 11/1000f ) {
                    setVelocity(velocity - 9/1000f);
                } else {
                    setVelocity(velocity + 9/1000f);
                    autoSpeed = 1;
                }
            }
            txt_idx = ( txt_idx + 1 ) % originalTexts.length;
        } while (options.keepRunning && window.isNativeValid());
        if (!options.stayOpen) {
            window.destroy();
        }
    }

    /**
     * Our PMVMatrixSetup:
     * - gluPerspective like Scene's default
     * - no normal scale to 1, keep distance to near plane for rotation effects.
     */
    public static class MyPMVMatrixSetup implements PMVMatrixSetup {
        @Override
        public void set(final PMVMatrix pmv, final Recti viewport) {
            final float ratio = (float) viewport.width() / (float) viewport.height();
            pmv.glMatrixMode(GLMatrixFunc.GL_PROJECTION);
            pmv.glLoadIdentity();
            pmv.gluPerspective(Scene.DEFAULT_ANGLE, ratio, Scene.DEFAULT_ZNEAR, Scene.DEFAULT_ZFAR);
            pmv.glTranslatef(0f, 0f, Scene.DEFAULT_SCENE_DIST);

            pmv.glMatrixMode(GLMatrixFunc.GL_MODELVIEW);
            pmv.glLoadIdentity();
        }

        @Override
        public void setPlaneBox(final AABBox planeBox, final PMVMatrix pmv, final Recti viewport) {
            Scene.getDefaultPMVMatrixSetup().setPlaneBox(planeBox, pmv, viewport);
        }
    };
}
