package electrosphere.renderer.ui.elements;

import static org.lwjgl.opengl.GL11.GL_COLOR_BUFFER_BIT;
import static org.lwjgl.opengl.GL11.GL_DEPTH_BUFFER_BIT;
import static org.lwjgl.opengl.GL11.glClear;
import static org.lwjgl.opengl.GL11.glClearColor;
import static org.lwjgl.opengl.GL30.GL_FRAMEBUFFER;

import java.util.LinkedList;
import java.util.List;

import org.joml.Vector3f;
import org.lwjgl.util.yoga.YGLayout;
import org.lwjgl.util.yoga.YGNode;
import org.lwjgl.util.yoga.Yoga;

import electrosphere.engine.Globals;
import electrosphere.logger.LoggerInterface;
import electrosphere.renderer.OpenGLState;
import electrosphere.renderer.RenderPipelineState;
import electrosphere.renderer.debug.DebugRendering;
import electrosphere.renderer.framebuffer.Framebuffer;
import electrosphere.renderer.framebuffer.FramebufferUtils;
import electrosphere.renderer.model.Material;
import electrosphere.renderer.model.Model;
import electrosphere.renderer.texture.Texture;
import electrosphere.renderer.ui.elementtypes.ContainerElement;
import electrosphere.renderer.ui.elementtypes.DrawableElement;
import electrosphere.renderer.ui.elementtypes.Element;
import electrosphere.renderer.ui.elementtypes.NavigableElement;
import electrosphere.renderer.ui.events.Event;
import electrosphere.renderer.ui.events.NavigationEvent;

/**
 * A window
 */
public class Window implements DrawableElement, ContainerElement, NavigableElement {

    static Vector3f color = new Vector3f(1.0f);

    List<Element> childList = new LinkedList<Element>();
    Framebuffer widgetBuffer;
    Material customMat = new Material();

    Vector3f boxPosition = new Vector3f();
    Vector3f boxDimensions = new Vector3f();
    Vector3f texPosition = new Vector3f(0,0,0);
    Vector3f texScale = new Vector3f(1,1,0);

    NavigationEventCallback navCallback;

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

    //controls whether to show window decorations (ie the frame)
    boolean showDecorations = true;

    //Yoga node for controlling placement of the window on the screen
    //IE, if you want to place a window in the upper right hand side of the screen,
    //this node can be used to control placement alignment to accomplish that
    //NOTE: It should always be set to the current size of the window (width, height)
    //NOTE: It is updated every time the applyYoga function is called
    long parentWindowYogaNode = -1;
    
    /**
     * Constructor
     * @param showDecorations
     * @param positionX
     * @param positionY
     * @param width
     * @param height
     */
    public Window(OpenGLState openGLState, int positionX, int positionY, int width, int height, boolean showDecorations){
        widgetBuffer = FramebufferUtils.generateTextureFramebuffer(openGLState, width, height);
        customMat.setTexturePointer(widgetBuffer.getTexture().getTexturePointer());
        float ndcWidth =  (float)width/Globals.WINDOW_WIDTH;
        float ndcHeight = (float)height/Globals.WINDOW_HEIGHT;
        float ndcX =      (float)positionX/Globals.WINDOW_WIDTH;
        float ndcY =      (float)positionY/Globals.WINDOW_HEIGHT;
        boxPosition = new Vector3f(ndcX,ndcY,0);
        boxDimensions = new Vector3f(ndcWidth,ndcHeight,0);
        this.showDecorations = showDecorations;
        //yoga node for the actually visible part
        this.yogaNode = Yoga.YGNodeNew();
        this.layout = YGNode.create(this.yogaNode).layout();
        //yoga node for placement
        this.parentWindowYogaNode = Yoga.YGNodeNew();
        Yoga.YGNodeInsertChild(this.parentWindowYogaNode, this.yogaNode, 0);
        setParentAlignContent(YogaAlignment.Start);
        setParentAlignItem(YogaAlignment.Start);
        setParentJustifyContent(YogaJustification.Start);
        this.setWidth(width);
        this.setHeight(height);
    }

    @Override
    public void draw(
        RenderPipelineState renderPipelineState,
        OpenGLState openGLState,
        int parentFramebufferPointer,
        int parentPosX,
        int parentPosY,
        int parentWidth,
        int parentHeight
    ) {

        float ndcWidth =  (float)this.getInternalWidth()/Globals.WINDOW_WIDTH;
        float ndcHeight = (float)this.getInternalHeight()/Globals.WINDOW_HEIGHT;
        float ndcX =      (float)this.getInternalX()/Globals.WINDOW_WIDTH;
        float ndcY =      (float)this.getInternalY()/Globals.WINDOW_HEIGHT;
        boxPosition = new Vector3f(ndcX,ndcY,0);
        boxDimensions = new Vector3f(ndcWidth,ndcHeight,0);
        
        widgetBuffer.bind(openGLState);
        openGLState.glViewport(width, height);
        
        glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

        //grab assets required to render window
        Model planeModel = Globals.assetManager.fetchModel(Globals.imagePlaneModelID);
        Texture windowFrame = Globals.assetManager.fetchTexture("Textures/ui/uiFrame1.png");
        
        for(Element child : childList){
            if(child instanceof DrawableElement){
                DrawableElement drawableChild = (DrawableElement) child;
                drawableChild.draw(renderPipelineState,openGLState,widgetBuffer.getFramebufferPointer(),parentPosX,parentPosY,width,height);
            }
        }
        //this call binds the screen as the "texture" we're rendering to
        //have to call before actually rendering
        openGLState.glBindFramebuffer(GL_FRAMEBUFFER, parentFramebufferPointer);
        openGLState.glViewport(parentWidth, parentHeight);

        //error if assets are null
        if(planeModel == null || windowFrame == null){
            LoggerInterface.loggerRenderer.ERROR("Window unable to find plane model or window frame!!", new Exception());
        }

        //render background of window
        if(planeModel != null && windowFrame != null && showDecorations){
            planeModel.pushUniformToMesh("plane", "mPosition", boxPosition);
            planeModel.pushUniformToMesh("plane", "mDimension", boxDimensions);
            planeModel.pushUniformToMesh("plane", "tPosition", texPosition);
            planeModel.pushUniformToMesh("plane", "tDimension", texScale);
            planeModel.pushUniformToMesh(planeModel.getMeshes().get(0).getMeshName(), "color", color);
            customMat.setTexturePointer(windowFrame.getTexturePointer());
            planeModel.getMeshes().get(0).setMaterial(customMat);
            planeModel.drawUI();
        }
        
        //render content of window
        if(planeModel != null){
            planeModel.pushUniformToMesh("plane", "mPosition", boxPosition);
            planeModel.pushUniformToMesh("plane", "mDimension", boxDimensions);
            planeModel.pushUniformToMesh("plane", "tPosition", texPosition);
            planeModel.pushUniformToMesh("plane", "tDimension", texScale);
            planeModel.pushUniformToMesh(planeModel.getMeshes().get(0).getMeshName(), "color", color);
            customMat.setTexturePointer(widgetBuffer.getTexture().getTexturePointer());
            planeModel.getMeshes().get(0).setMaterial(customMat);
            planeModel.drawUI();
        }

        if(Globals.RENDER_FLAG_RENDER_UI_BOUNDS && DebugRendering.RENDER_DEBUG_OUTLINE_WINDOW){
            DebugRendering.drawUIBoundsWindow(parentFramebufferPointer, boxPosition, boxDimensions, windowDrawDebugColor);
        }
    }

    /**
     * Destroys the element
     */
    public void destroy(){
        for(Element el : getChildren()){
            el.destroy();
        }
    }

    /**
     * clears all children
     */
    public void clear(){
        for(Element el : getChildren()){
            Yoga.YGNodeRemoveChild(this.yogaNode,el.getYogaNode());
            el.destroy();
        }
        getChildren().clear();
    }

    @Override
    public void setWidth(int width){
        this.width = width;
        Yoga.YGNodeStyleSetWidth(this.yogaNode, width);
        for(Element child : childList){
            if(child instanceof DrawableElement){
                DrawableElement drawableChild = (DrawableElement) child;
                drawableChild.setParentWidth(width);
                drawableChild.setParentHeight(height);
            }
        }
    }
    
    @Override
    public void setHeight(int height){
        this.height = height;
        Yoga.YGNodeStyleSetHeight(this.yogaNode, height);
        for(Element child : childList){
            if(child instanceof DrawableElement){
                DrawableElement drawableChild = (DrawableElement) child;
                drawableChild.setParentWidth(width);
                drawableChild.setParentHeight(height);
            }
        }
    }

    @Override
    public void setMaxWidth(int width) {
        Yoga.YGNodeStyleSetMaxWidth(yogaNode, width);
    }

    @Override
    public void setMaxWidthPercent(float percent) {
        Yoga.YGNodeStyleSetMaxWidthPercent(yogaNode, percent);
    }

    @Override
    public void setMaxHeight(int height) {
        Yoga.YGNodeStyleSetMaxHeight(yogaNode, height);
    }

    @Override
    public void setMaxHeightPercent(float percent) {
        Yoga.YGNodeStyleSetMaxHeight(yogaNode, percent);
    }

    @Override
    public void setMinWidth(int width) {
        Yoga.YGNodeStyleSetMinWidth(yogaNode, width);
    }

    @Override
    public void setMinWidthPercent(float percent) {
        Yoga.YGNodeStyleSetMinWidthPercent(yogaNode, percent);
    }

    @Override
    public void setMinHeight(int height) {
        Yoga.YGNodeStyleSetMinHeight(yogaNode, height);
    }

    @Override
    public void setMinHeightPercent(float percent) {
        Yoga.YGNodeStyleSetMinHeightPercent(yogaNode, percent);
    }

    public int width = 1;
    public int height = 1;
    
    public int positionX = 0;
    public int positionY = 0;
    
    public int parentWidth = 1;
    public int parentHeight = 1;

    int marginTop = 0;
    int marginRight = 0;
    int marginBottom = 0;
    int marginLeft = 0;
    
    public boolean visible = false;

    public int getWidth() {
        return width;
    }

    public int getHeight() {
        return height;
    }

    public int getPositionX() {
        return positionX;
    }

    public int getPositionY() {
        return positionY;
    }

    public boolean getVisible() {
        return visible;
    }

    public void setVisible(boolean draw) {
        this.visible = draw;
    }

    @Override
    public void setPositionX(int positionX) {
        this.positionX = positionX;
    }

    @Override
    public void setPositionY(int positionY) {
        this.positionY = positionY;
    }

    public int getMarginTop(){
        return marginTop;
    }

    public int getMarginRight(){
        return marginRight;
    }

    public int getMarginBottom(){
        return marginBottom;
    }

    public int getMarginLeft(){
        return marginLeft;
    }

    public void setMarginTop(int marginTop){
        this.marginTop = marginTop;
    }

    public void setMarginRight(int marginRight){
        this.marginRight = marginRight;
    }

    public void setMarginBottom(int marginBottom){
        this.marginBottom = marginBottom;
    }

    public void setMarginLeft(int marginLeft){
        this.marginLeft = marginLeft;
    }

    @Override
    public void setParentWidth(int width) {
        this.width = width;
    }

    @Override
    public void setParentHeight(int height) {
        this.height = height;
    }

    //the yoga node id
    long yogaNode = -1;
    //the layout object
    YGLayout layout;

    @Override
    public long getYogaNode() {
        return yogaNode;
    }

    @Override
    public void applyYoga(int parentX, int parentY) {
        Yoga.YGNodeStyleSetWidth(parentWindowYogaNode, Globals.WINDOW_WIDTH);
        Yoga.YGNodeStyleSetHeight(parentWindowYogaNode, Globals.WINDOW_HEIGHT);
        //calculate yoga layout
        Yoga.YGNodeCalculateLayout(parentWindowYogaNode, width, height, Yoga.YGDirectionInherit);
        //get the values from yoga
        float leftRaw = Yoga.YGNodeLayoutGetLeft(yogaNode);
        float topRaw = Yoga.YGNodeLayoutGetTop(yogaNode);
        float widthRaw = Yoga.YGNodeLayoutGetWidth(yogaNode);
        float heightRaw = Yoga.YGNodeLayoutGetHeight(yogaNode);
        // //apply the values to this component
        this.positionX = (int)leftRaw;
        this.positionY = (int)topRaw;
        this.width = (int)widthRaw;
        this.height = (int)heightRaw;
        //apply yoga values to all children
        LoggerInterface.loggerUI.DEBUG("==Apply yoga to windoow==");
        for(Element child : this.getChildren()){
            child.applyYoga(this.positionX,this.positionY);
        }
    }

    @Override
    public void setDirection(int layout) {
        Yoga.YGNodeStyleSetDirection(yogaNode, Yoga.YGFlexDirectionColumn);
    }

    @Override
    public void setFlexDirection(YogaFlexDirection layout){
        int directionInteger = Yoga.YGFlexDirectionColumn;
        switch(layout){
            case Column:
                directionInteger = Yoga.YGFlexDirectionColumn;
                break;
            case Column_Reverse:
                directionInteger = Yoga.YGFlexDirectionColumnReverse;
                break;
            case Row:
                directionInteger = Yoga.YGFlexDirectionRow;
                break;
            case Row_Reverse:
                directionInteger = Yoga.YGFlexDirectionRowReverse;
                break;
        }
        Yoga.YGNodeStyleSetFlexDirection(yogaNode, directionInteger);
    }

    @Override
    public void setJustifyContent(YogaJustification justification){
        int justificationInteger = Yoga.YGJustifyFlexStart;
        switch(justification){
            case Center:
                justificationInteger = Yoga.YGJustifyCenter;
                break;
            case Start:
                justificationInteger = Yoga.YGJustifyFlexStart;
                break;
            case End:
                justificationInteger = Yoga.YGJustifyFlexEnd;
                break;
            case Around:
                justificationInteger = Yoga.YGJustifySpaceAround;
                break;
            case Between:
                justificationInteger = Yoga.YGJustifySpaceBetween;
                break;
            case Evenly:
                justificationInteger = Yoga.YGJustifySpaceEvenly;
                break;
        }
        Yoga.YGNodeStyleSetJustifyContent(this.yogaNode, justificationInteger);
    }

    @Override
    public void setAlignItems(YogaAlignment alignment){
        int alignmentInteger = Yoga.YGAlignAuto;
        switch(alignment){
            case Auto:
                alignmentInteger = Yoga.YGAlignAuto;
                break;
            case Start:
                alignmentInteger = Yoga.YGAlignFlexStart;
                break;
            case End:
                alignmentInteger = Yoga.YGAlignFlexEnd;
                break;
            case Around:
                alignmentInteger = Yoga.YGAlignSpaceAround;
                break;
            case Between:
                alignmentInteger = Yoga.YGAlignSpaceBetween;
                break;
            case Stretch:
                alignmentInteger = Yoga.YGAlignStretch;
                break;
            case Baseline:
                alignmentInteger = Yoga.YGAlignBaseline;
                break;
            case Center:
                alignmentInteger = Yoga.YGAlignCenter;
                break;
        }
        Yoga.YGNodeStyleSetAlignItems(this.yogaNode, alignmentInteger);
    }

    @Override
    public void setAlignContent(YogaAlignment alignment){
        int alignmentInteger = Yoga.YGAlignAuto;
        switch(alignment){
            case Auto:
                alignmentInteger = Yoga.YGAlignAuto;
                break;
            case Start:
                alignmentInteger = Yoga.YGAlignFlexStart;
                break;
            case End:
                alignmentInteger = Yoga.YGAlignFlexEnd;
                break;
            case Around:
                alignmentInteger = Yoga.YGAlignSpaceAround;
                break;
            case Between:
                alignmentInteger = Yoga.YGAlignSpaceBetween;
                break;
            case Stretch:
                alignmentInteger = Yoga.YGAlignStretch;
                break;
            case Baseline:
                alignmentInteger = Yoga.YGAlignBaseline;
                break;
            case Center:
                alignmentInteger = Yoga.YGAlignCenter;
                break;
        }
        Yoga.YGNodeStyleSetAlignContent(this.yogaNode, alignmentInteger);
    }

    @Override
    public void addChild(Element child) {
        childList.add(child);
        child.setParent(this);
        if(child instanceof DrawableElement){
            DrawableElement drawableChild = (DrawableElement) child;
            drawableChild.setParentWidth(width);
            drawableChild.setParentHeight(height);
            drawableChild.setVisible(false);
            Yoga.YGNodeInsertChild(yogaNode, drawableChild.getYogaNode(), childList.size() - 1);
        }
    }

    @Override
    public List<Element> getChildren() {
        return childList;
    }

    @Override
    public void removeChild(Element child) {
        childList.remove(child);
        child.setParent(null);
        Yoga.YGNodeRemoveChild(yogaNode, child.getYogaNode());
    }

    @Override
    public void clearChildren(){
        Yoga.YGNodeRemoveAllChildren(yogaNode);
        childList.clear();
    }

    public boolean handleEvent(Event event){
        boolean propagate = true;
        if(event instanceof NavigationEvent && navCallback != null){
            if(!navCallback.execute((NavigationEvent)event)){
                propagate = false;
            }
        }
        return propagate;
    }

    @Override
    public void setOnNavigationCallback(NavigationEventCallback callback) {
        navCallback = callback;
    }

    public Element getParent(){
        return null;
    }

    @Override
    public int getInternalX() {
        return positionX;
    }

    @Override
    public int getInternalY() {
        return positionY;
    }

    @Override
    public int getInternalWidth() {
        return width;
    }

    @Override
    public int getInternalHeight() {
        return height;
    }

    @Override
    public int getChildOffsetX(){
        return 0;
    }

    @Override
    public int getChildOffsetY(){
        return 0;
    }

    @Override
    public float getChildScaleX(){
        return 1;
    }

    @Override
    public float getChildScaleY(){
        return 1;
    }

    /**
     * Sets the alignment of items on the parent to the window yoga node
     * @param alignment The alignment value
     */
    public void setParentAlignItem(YogaAlignment alignment){
        int alignmentInteger = Yoga.YGAlignAuto;
        switch(alignment){
            case Auto:
                alignmentInteger = Yoga.YGAlignAuto;
                break;
            case Start:
                alignmentInteger = Yoga.YGAlignFlexStart;
                break;
            case End:
                alignmentInteger = Yoga.YGAlignFlexEnd;
                break;
            case Around:
                alignmentInteger = Yoga.YGAlignSpaceAround;
                break;
            case Between:
                alignmentInteger = Yoga.YGAlignSpaceBetween;
                break;
            case Stretch:
                alignmentInteger = Yoga.YGAlignStretch;
                break;
            case Baseline:
                alignmentInteger = Yoga.YGAlignBaseline;
                break;
            case Center:
                alignmentInteger = Yoga.YGAlignCenter;
                break;
        }
        Yoga.YGNodeStyleSetAlignItems(this.parentWindowYogaNode, alignmentInteger);
    }

    /**
     * Sets the alignment of content on the parent to the window yoga node
     * @param alignment The alignment
     */
    public void setParentAlignContent(YogaAlignment alignment){
        int alignmentInteger = Yoga.YGAlignAuto;
        switch(alignment){
            case Auto:
                alignmentInteger = Yoga.YGAlignAuto;
                break;
            case Start:
                alignmentInteger = Yoga.YGAlignFlexStart;
                break;
            case End:
                alignmentInteger = Yoga.YGAlignFlexEnd;
                break;
            case Around:
                alignmentInteger = Yoga.YGAlignSpaceAround;
                break;
            case Between:
                alignmentInteger = Yoga.YGAlignSpaceBetween;
                break;
            case Stretch:
                alignmentInteger = Yoga.YGAlignStretch;
                break;
            case Baseline:
                alignmentInteger = Yoga.YGAlignBaseline;
                break;
            case Center:
                alignmentInteger = Yoga.YGAlignCenter;
                break;
        }
        Yoga.YGNodeStyleSetAlignContent(this.parentWindowYogaNode, alignmentInteger);
    }

    /**
     * Sets the justification of the parent yoga node containing this window
     * @param justification The justification mode
     */
    public void setParentJustifyContent(YogaJustification justification){
        int justificationInteger = Yoga.YGJustifyFlexStart;
        switch(justification){
            case Center:
                justificationInteger = Yoga.YGJustifyCenter;
                break;
            case Start:
                justificationInteger = Yoga.YGJustifyFlexStart;
                break;
            case End:
                justificationInteger = Yoga.YGJustifyFlexEnd;
                break;
            case Around:
                justificationInteger = Yoga.YGJustifySpaceAround;
                break;
            case Between:
                justificationInteger = Yoga.YGJustifySpaceBetween;
                break;
            case Evenly:
                justificationInteger = Yoga.YGJustifySpaceEvenly;
                break;
        }
        Yoga.YGNodeStyleSetJustifyContent(this.parentWindowYogaNode, justificationInteger);
    }

    @Override
    public void setWrap(YogaWrap wrap) {
        switch(wrap){
            case WRAP:
                Yoga.YGNodeStyleSetFlexWrap(yogaNode, Yoga.YGWrapWrap);
                break;
            case NO_WRAP:
                Yoga.YGNodeStyleSetFlexWrap(yogaNode, Yoga.YGWrapNoWrap);
                break;
            case REVERSE:
                Yoga.YGNodeStyleSetFlexWrap(yogaNode, Yoga.YGWrapReverse);
                break;
        }
    }

    @Override
    public void setOverflow(YogaOverflow overflow){
        switch(overflow){
            case Visible:
                Yoga.YGNodeStyleSetOverflow(yogaNode, Yoga.YGOverflowVisible);
                break;
            case Hidden:
                Yoga.YGNodeStyleSetOverflow(yogaNode, Yoga.YGOverflowHidden);
                break;
            case Scroll:
                Yoga.YGNodeStyleSetOverflow(yogaNode, Yoga.YGOverflowScroll);
                break;
        }
    }

    @Override
    public int getRelativeX() {
        return this.positionX;
    }

    @Override
    public int getRelativeY() {
        return this.positionY;
    }

    @Override
    public int getAbsoluteX() {
        return this.positionX;
    }

    @Override
    public int getAbsoluteY() {
        return this.positionY;
    }

    @Override
    public void setAbsolutePosition(boolean useAbsolutePosition) {
        //not implemented
        throw new UnsupportedOperationException();
    }

    @Override
    public void setParent(Element parent) {
        //not implemented
        throw new UnsupportedOperationException();
    }

}
