/**
 * Copyright 2010-2024 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.graph.ui.widgets;

import com.jogamp.graph.curve.Region;
import com.jogamp.graph.curve.opengl.GLRegion;
import com.jogamp.graph.curve.opengl.RegionRenderer;
import com.jogamp.graph.ui.Group;
import com.jogamp.graph.ui.Shape;
import com.jogamp.graph.ui.layout.Alignment;
import com.jogamp.graph.ui.layout.GridLayout;
import com.jogamp.math.Vec2f;
import com.jogamp.math.Vec3f;
import com.jogamp.math.Vec4f;
import com.jogamp.math.geom.AABBox;
import com.jogamp.math.geom.Cube;
import com.jogamp.math.geom.Frustum;
import com.jogamp.math.util.PMVMatrix4f;
import com.jogamp.newt.event.MouseEvent;
import com.jogamp.opengl.GL2ES2;
import com.jogamp.opengl.GLProfile;
import com.jogamp.opengl.util.texture.TextureSequence;

/**
 * Ranged {@link Group} {@link Widget}, displaying a clipped content {@link Group}
 * with optional horizontal and/or vertical {@link RangeSlider}.
 */
public class RangedGroup extends Widget {
    private final Group content;
    private final Group clippedContent;
    private final RangeSlider horizSlider, vertSlider;
    private final Vec2f contentPosZero = new Vec2f();

    /** {@link RangeSlider} configuration parameter for {@link RangedGroup}. */
    public static final class SliderParam {
        /** spatial dimension of the slider box. A horizontal slider has width >= height. */
        public final Vec2f size;
        /** size of one unit (element) in sliding direction */
        public final float unitSize;
        /**
         * Toggle whether the slider uses an inverted value range,
         * e.g. top 0% and bottom 100% for an vertical inverted slider
         * instead of bottom 0% and top 100% for a vertical non-inverted slider.
         */
        public final boolean inverted;

        /**
         *
         * @param size spatial dimension of this slider box. A horizontal slider has width >= height.
         * @param unitSize size of one unit (element) in sliding direction
         * @param inverted toggle to invert value range, see {@link #inverted}
         */
        public SliderParam(final Vec2f size, final float unitSize, final boolean inverted) {
            this.size = size;
            this.unitSize = unitSize;
            this.inverted = inverted;
        }
    }

    /**
     * Construct a {@link RangedGroup}
     * @param renderModes Graph's {@link Region} render modes, see {@link GLRegion#create(GLProfile, int, TextureSequence) create(..)}.
     * @param content the {@link Group} with content to view
     * @param contentSize the fixed spatial size of the clipped content to view, i.e. page-size
     * @param horizSliderParam optional initial horizontal slider parameters, null for none
     * @param vertSliderParam optional initial vertical slider parameters, null for none
     */
    public RangedGroup(final int renderModes, final Group content, final Vec2f contentSize,
                       final SliderParam horizSliderParam, final SliderParam vertSliderParam)
    {
        super( new GridLayout(1 + (null != vertSliderParam ? 1 : 0), 0f, 0f, Alignment.None)); // vertical slider adds to the right column
        this.content = content;
        this.clippedContent = new Group( new GridLayout(1, 0f, 0f, Alignment.None));
        this.clippedContent.setFixedSize(contentSize);
        this.clippedContent.addShape(content);
        addShape(clippedContent);

        if( null != horizSliderParam ) {
            horizSlider = new RangeSlider(renderModes, horizSliderParam.size,
                                          new Vec2f(0, content.getBounds().getWidth()), horizSliderParam.unitSize, contentSize.x(), 0).setInverted(horizSliderParam.inverted);
            addShape(horizSlider);
            horizSlider.addChangeListener((final RangeSlider w, final float old_val, final float val, final float old_val_pct, final float val_pct, final Vec3f pos, final MouseEvent e) -> {
                final Vec3f oldPos = content.getPosition();
                if( horizSlider.isInverted() ) {
                    content.moveTo(contentPosZero.x()-val, oldPos.y(), oldPos.z());
                } else {
                    content.moveTo(contentPosZero.x()+val, oldPos.y(), oldPos.z());
                }
            } );
        } else {
            horizSlider = null;
        }
        if( null != vertSliderParam ) {
            vertSlider = new RangeSlider(renderModes, vertSliderParam.size,
                                         new Vec2f(0, content.getBounds().getHeight()), vertSliderParam.unitSize, contentSize.y(), 0).setInverted(vertSliderParam.inverted);
            addShape(vertSlider);
            vertSlider.addChangeListener((final RangeSlider w, final float old_val, final float val, final float old_val_pct, final float val_pct, final Vec3f pos, final MouseEvent e) -> {
                final Vec3f oldPos = content.getPosition();
                if( vertSlider.isInverted() ) {
                    content.moveTo(oldPos.x(), contentPosZero.y()+val, oldPos.z());
                } else {
                    content.moveTo(oldPos.x(), contentPosZero.y()-val, oldPos.z());
                }
            } );
        } else {
            vertSlider = null;
        }
        this.onDraw( (final Shape shape, final GL2ES2 gl_, final RegionRenderer renderer_) -> {
            content.moveTo(contentPosZero.x(), contentPosZero.y(), 0);
            return true;
        });
    }

    public Group getContent() { return content; }
    public Vec2f getContentSize(final Vec2f out) { return clippedContent.getFixedSize(out); }
    public Group getClippedContent() { return clippedContent; }
    /** Returns the used horizontal {@link RangeSlider} or {@code null}. */
    public RangeSlider getHorizSlider() { return horizSlider; }
    /** Returns the used vertical {@link RangeSlider} or {@code null}. */
    public RangeSlider getVertSlider() { return vertSlider; }

    @Override
    protected void validateImpl(final GL2ES2 gl, final GLProfile glp) {
        if( isShapeDirty() ) {
            final Vec3f pos = content.getPosition().copy(); // preserve content position
            super.validateImpl(gl, glp);

            final AABBox cb = content.getBounds();
            final Vec3f contentSize = clippedContent.getFixedSize();
            contentPosZero.set(0, 0);
            if( null != horizSlider ) {
                horizSlider.setMinMax(new Vec2f(0, content.getBounds().getWidth()));
                if( horizSlider.isInverted() ) {
                    contentPosZero.setX( cb.getWidth() - contentSize.x() );
                }
            }
            if( null != vertSlider ) {
                vertSlider.setMinMax(new Vec2f(0, content.getBounds().getHeight()));
                if( vertSlider.isInverted() ) {
                    contentPosZero.setY( contentSize.y() - cb.getHeight() );
                }
            }
            content.moveTo( pos );
        }
    }
    @Override
    protected void drawImpl0(final GL2ES2 gl, final RegionRenderer renderer, final Vec4f rgba) {
        if( content.isVisible() ) {
            final PMVMatrix4f pmv = renderer.getMatrix();

            // Mv pre-multiplied Frustum, clippedContent is on same PMV
            final Frustum clipFrustum = tempC00.set( clippedContent.getBounds() ).transform( pmv.getMv() ).updateFrustumPlanes(tempF00);
            content.setClipMvFrustum(clipFrustum);
            super.drawImpl0(gl, renderer, rgba);
            content.setClipMvFrustum(null);
        }
    }
    private final Frustum tempF00 = new Frustum(); // OK, synchronized
    private final Cube tempC00 = new Cube(); // OK, synchronized
}
