package electrosphere.renderer.ui.elements;

import java.util.function.Consumer;

import org.joml.Vector3f;

import electrosphere.engine.Globals;
import electrosphere.logger.LoggerInterface;
import electrosphere.renderer.OpenGLState;
import electrosphere.renderer.RenderPipelineState;
import electrosphere.renderer.debug.DebugRendering;
import electrosphere.renderer.model.Material;
import electrosphere.renderer.model.Model;
import electrosphere.renderer.ui.elementtypes.ClickableElement;
import electrosphere.renderer.ui.elementtypes.DraggableElement;
import electrosphere.renderer.ui.elementtypes.MenuEventElement;
import electrosphere.renderer.ui.elementtypes.ValueElement;
import electrosphere.renderer.ui.events.ClickEvent;
import electrosphere.renderer.ui.events.DragEvent;
import electrosphere.renderer.ui.events.DragEvent.DragEventType;
import electrosphere.renderer.ui.events.Event;
import electrosphere.renderer.ui.events.FocusEvent;
import electrosphere.renderer.ui.events.MenuEvent;
import electrosphere.renderer.ui.events.ValueChangeEvent;

/**
 * A ui element that is a slider that lets you pick between a range of values
 */
public class Slider extends StandardDrawableElement implements ClickableElement, DraggableElement, MenuEventElement, ValueElement {

    FocusEventCallback onFocusCallback;
    FocusEventCallback onLoseFocusCallback;
    DragEventCallback onDragStart;
    DragEventCallback onDrag;
    DragEventCallback onDragRelease;
    ClickEventCallback onClick;
    MenuEventCallback onMenuEvent;
    ValueChangeEventCallback onValueChange;

    float min = 0.0f;
    float max = 1.0f;
    float value = 0.5f;

    Vector3f colorBackground = new Vector3f(0.2f,0.2f,0.2f);
    Vector3f colorForeground = new Vector3f(1,1,1);

    static Material mat;



    static final int idealMargin = 5; //5 pixels margin ideally

    static final Vector3f windowDrawDebugColor = new Vector3f(1.0f,1.0f,1.0f);

    static final int DEFAULT_HEIGHT = 20;
    static final int DEFAULT_WIDTH = 100;


    /**
     * Creates a slider element
     * @param callback the Logic to fire when the slider changes value
     * @return the slider element
     */
    public static Slider createSlider(ValueChangeEventCallback callback){
        Slider slider = new Slider();
        slider.setOnValueChangeCallback(callback);
        return slider;
    }

    /**
     * Creates a slider element
     * @param callback the Logic to fire when the slider changes value
     * @return the slider element
     */
    public static Slider createSlider(Consumer<ValueChangeEvent> callback){
        Slider slider = new Slider();
        slider.setOnValueChangeCallback(new ValueChangeEventCallback() {public void execute(ValueChangeEvent event) {
            callback.accept(event);
        }});
        return slider;
    }

    /**
     * Private constructor
     */
    private Slider(){
        super();
        if(mat == null){
            mat = new Material();
            mat.set_diffuse("Textures/ui/square.png");
            mat.set_specular("Textures/ui/square.png");
        }
        setWidth(DEFAULT_WIDTH);
        setHeight(DEFAULT_HEIGHT);
    }


    public Slider(int positionX, int positionY, int width, int height, Vector3f colorBackground, Vector3f colorForeground){
        super();
        if(mat == null){
            mat = new Material();
            mat.set_diffuse("Textures/ui/square.png");
            mat.set_specular("Textures/ui/square.png");
        }
        this.internalPositionX = positionX;
        this.internalPositionY = positionY;
        this.width = width;
        this.height = height;
        this.colorBackground.set(colorBackground);
        this.colorForeground.set(colorForeground);
    }



    @Override
    public void draw(
        RenderPipelineState renderPipelineState,
        OpenGLState openGLState,
        int parentFramebufferPointer,
        int parentPosX,
        int parentPosY,
        int parentWidth,
        int parentHeight
    ) {
        
        Globals.renderingEngine.bindFramebuffer(parentFramebufferPointer);
        openGLState.glViewport(parentWidth, parentHeight);

        int marginX = Math.max(width - idealMargin * 2, 0);
        if(marginX < idealMargin){
            marginX = 0;
        } else {
            marginX = idealMargin;
        }
        int marginY = Math.max(height - idealMargin * 2, 0);
        if(marginY < idealMargin){
            marginY = 0;
        } else {
            marginY = idealMargin;
        }


        float ndcWidth =  (float)getInternalWidth()/parentWidth;
        float ndcHeight = (float)getInternalHeight()/parentHeight;
        float ndcX =      (float)(getInternalX() + parentPosX)/parentWidth;
        float ndcY =      (float)(getInternalY() + parentPosY)/parentHeight;
        
        Vector3f boxPosition = new Vector3f(ndcX,ndcY,0);
        Vector3f boxDimensions = new Vector3f(ndcWidth,ndcHeight,0);

        Model planeModel = Globals.assetManager.fetchModel(Globals.imagePlaneModelID);
        if(planeModel != null){
            //bounding box/margin
            planeModel.pushUniformToMesh("plane", "mPosition", boxPosition);
            planeModel.pushUniformToMesh("plane", "mDimension", boxDimensions);
            planeModel.pushUniformToMesh(planeModel.getMeshes().get(0).getMeshName(), "color", colorBackground);
            planeModel.getMeshes().get(0).setMaterial(mat);
            planeModel.drawUI();

            //actual slider
            ndcX = (float)(getInternalX() + marginX + parentPosX)/parentWidth;
            ndcY = (float)(getInternalY() + marginY + parentPosY)/parentHeight;
            ndcWidth = (float)((getInternalWidth() - marginX * 2) * getValueAsPercentage())/parentWidth;
            ndcHeight = (float)(getInternalHeight() - marginY * 2)/parentHeight;
            boxPosition = new Vector3f(ndcX,ndcY,0);
            boxDimensions = new Vector3f(ndcWidth,ndcHeight,0);
            planeModel.pushUniformToMesh("plane", "mPosition", boxPosition);
            planeModel.pushUniformToMesh("plane", "mDimension", boxDimensions);
            planeModel.pushUniformToMesh(planeModel.getMeshes().get(0).getMeshName(), "color", colorForeground);
            planeModel.getMeshes().get(0).setMaterial(mat);
            planeModel.drawUI();
        } else {
            LoggerInterface.loggerRenderer.ERROR("Window unable to find plane model!!", new Exception());
        }
        if(Globals.RENDER_FLAG_RENDER_UI_BOUNDS && DebugRendering.RENDER_DEBUG_OUTLINE_SLIDER){
            boxDimensions.x = (float)((width - marginX * 2) * 1.0f)/parentWidth;
            DebugRendering.drawUIBounds(parentFramebufferPointer, boxPosition, boxDimensions, windowDrawDebugColor);
        }
    }



    public float getMinimum(){
        return min;
    }

    public void setMinimum(float min){
        this.min = min;
    }

    public float getMaximum(){
        return max;
    }

    public void setMaximum(float max){
        this.max = max;
    }

    public float getValue(){
        return value;
    }

    public void setValue(float value){
        this.value = value;
    }

    float getValueAsPercentage(){
        return (value - min) / (max - min);
    }
    
    @Override
    public void setOnFocus(FocusEventCallback callback) {
        onFocusCallback = callback;
    }

    @Override
    public void setOnLoseFocus(FocusEventCallback callback) {
        onLoseFocusCallback = callback;
    }

    @Override
    public void setOnDragStart(DragEventCallback callback) {
        this.onDragStart = callback;
    }

    @Override
    public void setOnDrag(DragEventCallback callback) {
        this.onDrag = callback;
    }

    @Override
    public void setOnDragRelease(DragEventCallback callback) {
        this.onDragRelease = callback;
    }

    @Override
    public void setOnClick(ClickEventCallback callback) {
        this.onClick = callback;
    }
    
    @Override
    public void setOnMenuEventCallback(MenuEventCallback callback) {
        onMenuEvent = callback;
    }

    @Override
    public void setOnValueChangeCallback(ValueChangeEventCallback callback) {
        onValueChange = callback;
    }


    @Override
    public boolean handleEvent(Event event) {
        boolean propagate = true;
        if(event instanceof FocusEvent){
            FocusEvent focusEvent = (FocusEvent)event;
            if(focusEvent.isFocused()){
                if(this.onFocusCallback != null){
                    propagate = this.onFocusCallback.execute(focusEvent);
                } else {
                    //default behavior/
                    colorForeground = new Vector3f(1,0.5f,0.5f);
                    propagate = true;
                }
            } else {
                if(this.onLoseFocusCallback != null){
                    propagate = this.onLoseFocusCallback.execute(focusEvent);
                } else {
                    //default behavior
                    colorForeground = new Vector3f(1,1,1);
                    propagate = true;
                }
            }
        } else if(event instanceof MenuEvent){
            MenuEvent menuEvent = (MenuEvent) event;
            if(onMenuEvent != null){
                onMenuEvent.execute(menuEvent);
            } else {
                //default behavior
                switch(menuEvent.getType()){
                    case INCREMENT:
                    value = Math.min(value + ((max - min) * 0.01f),max);
                    if(onValueChange != null){
                        onValueChange.execute(new ValueChangeEvent(value));
                    }
                    propagate = false;
                    break;
                    case DECREMENT:
                    value = Math.max(value - ((max - min) * 0.01f),min);
                    if(onValueChange != null){
                        onValueChange.execute(new ValueChangeEvent(value));
                    }
                    propagate = false;
                    break;
                }
            }
        } else if(event instanceof DragEvent){
            DragEvent dragEvent = (DragEvent) event;
            if(dragEvent.getType() == DragEventType.START){
                if(onDragStart != null){
                    propagate = onDragStart.execute(dragEvent);
                } else {
                    //default behavior
                    propagate = true;
                }
            } else if(dragEvent.getType() == DragEventType.DRAG){
                if(onDrag != null){
                    propagate = onDrag.execute(dragEvent);
                } else {
                    //default behavior
                    int percentage = dragEvent.getCurrentX() - getInternalX();
                    int max = width;
                    value = Math.max(Math.min((float)percentage/max,1.0f),0.0f);
                    if(onValueChange != null){
                        onValueChange.execute(new ValueChangeEvent(value));
                    }
                    propagate = false;
                }
            } else if(dragEvent.getType() == DragEventType.RELEASE){
                if(onDragRelease != null){
                    propagate = onDragRelease.execute(dragEvent);
                } else {
                    //default behavior
                    int percentage = dragEvent.getCurrentX() - getInternalX();
                    int max = width;
                    value = Math.max(Math.min((float)percentage/max,1.0f),0.0f);
                    if(onValueChange != null){
                        onValueChange.execute(new ValueChangeEvent(value));
                    }
                    propagate = true;
                }
            }
        } else if(event instanceof ClickEvent){
            ClickEvent clickEvent = (ClickEvent) event;
            if(clickEvent.getButton1()){
                if(this.onClick != null){
                    propagate = this.onClick.execute((ClickEvent)event);
                } else {
                    //default behavior
                    int percentage = clickEvent.getRelativeX() - getInternalX();
                    int max = width;
                    value = Math.max(Math.min((float)percentage/max,1.0f),0.0f);
                    if(onValueChange != null){
                        onValueChange.execute(new ValueChangeEvent(value));
                    }
                    propagate = false;
                }
            }
        }
        return propagate;
    }
    
}
