package electrosphere.renderer.ui;

import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Stack;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;

import org.joml.Vector2i;

import electrosphere.engine.Globals;
import electrosphere.renderer.ui.elements.Window;
import electrosphere.renderer.ui.elementtypes.ContainerElement;
import electrosphere.renderer.ui.elementtypes.DraggableElement;
import electrosphere.renderer.ui.elementtypes.Element;
import electrosphere.renderer.ui.elementtypes.FocusableElement;
import electrosphere.renderer.ui.elementtypes.HoverableElement;
import electrosphere.renderer.ui.events.ClickEvent;
import electrosphere.renderer.ui.events.DragEvent;
import electrosphere.renderer.ui.events.Event;
import electrosphere.renderer.ui.events.FocusEvent;
import electrosphere.renderer.ui.events.HoverEvent;
import electrosphere.renderer.ui.events.NavigationEvent;
import electrosphere.renderer.ui.events.DragEvent.DragEventType;
import electrosphere.renderer.ui.events.NavigationEvent.NavigationEventType;

/**
 * The main interface for working with the ui system
 */
public class ElementManager {
    
    
    Map<String,Element> elementMap = new ConcurrentHashMap<String,Element>();
    List<Element> elementList = new CopyOnWriteArrayList<Element>();
    FocusableElement currentFocusedElement = null;
    DraggableElement currentDragElement = null;

    // the element currently hovered over
    HoverableElement currentHoveredElement = null;
    
    /**
     * Registers a window
     * @param name the name associated with the window
     * @param w The window element
     */
    public void registerWindow(String name, Element w){
        elementMap.put(name,w);
        if(!elementList.contains(w)){
            elementList.add(w);
        }
        if(elementList.size() < 2){
            focusFirstElement();
        }
    }

    /**
     * Gets a window element by the name associated with it
     * @param name The associated name
     * @return The window element if it exists, null otherwise
     */
    public Element getWindow(String name){
        return elementMap.get(name);
    }
    
    /**
     * Gets the list of all registered windows
     * @return The list of all registered windows
     */
    public List<Element> getWindowList(){
        return elementList;
    }
    
    /**
     * Unregisters a window stored at a given window string
     * @param name The window string
     */
    public void unregisterWindow(String name){
        Element w = elementMap.remove(name);
        while(elementList.contains(w)){
            elementList.remove(w);
        }
        if(elementList.size() > 0){
            focusFirstElement();
        } else {
            this.currentFocusedElement = null;
        }
    }

    public boolean containsWindow(String name){
        return elementMap.containsKey(name);
    }

    public void pushWindowToFront(Window window){
        elementList.remove(window);
        elementList.add(window);
    }

    /**
     * Tries to navigate-close the window at a window string
     * @param windowString The window string
     */
    public void closeWindow(String windowString){
        Element windowEl = Globals.elementManager.getWindow(windowString);
        if(windowEl instanceof Window){
            ((Window)windowEl).handleEvent(new NavigationEvent(NavigationEventType.BACKWARD));
        }
    }

    /**
     * Gets the list of focusable elements recursively
     * @param topLevel The current top level element
     * @param input The list to append to
     * @return The list to append to
     */
    List<FocusableElement> getFocusableList(Element topLevel, List<FocusableElement> input){
        if(topLevel instanceof FocusableElement){
            input.add((FocusableElement)topLevel);
        }
        if(topLevel instanceof ContainerElement){
            ContainerElement container = (ContainerElement) topLevel;
            for(Element child : container.getChildren()){
                getFocusableList(child,input);
            }
        }
        return input;
    }

    public FocusableElement getFocusedElement(){
        return currentFocusedElement;
    }

    public void focusFirstElement(){
        if(elementList.size() > 0){
            List<FocusableElement> focusables = getFocusableList(elementList.get(elementList.size() - 1),new LinkedList<FocusableElement>());
            if(focusables.size() > 0){
                if(currentFocusedElement != null){
                    currentFocusedElement.handleEvent(new FocusEvent(false));
                }
                currentFocusedElement = focusables.get(0);
                currentFocusedElement.handleEvent(new FocusEvent(true));
            } else {
                if(currentFocusedElement != null){
                    currentFocusedElement.handleEvent(new FocusEvent(false));
                }
                currentFocusedElement = null;
            }
        }
    }

    public void focusNextElement(){
        List<FocusableElement> focusables = getFocusableList(elementList.get(elementList.size() - 1),new LinkedList<FocusableElement>());
        if(focusables.contains(currentFocusedElement)){
            int index = focusables.indexOf(currentFocusedElement);
            if(index + 1 >= focusables.size()){
                index = 0;
            } else {
                index++;
            }
            if(currentFocusedElement != null){
                currentFocusedElement.handleEvent(new FocusEvent(false));
            }
            currentFocusedElement = focusables.get(index);
            currentFocusedElement.handleEvent(new FocusEvent(true));
        }
    }

    public void focusPreviousElement(){
        List<FocusableElement> focusables = getFocusableList(elementList.get(elementList.size() - 1),new LinkedList<FocusableElement>());
        if(focusables.contains(currentFocusedElement)){
            int index = focusables.indexOf(currentFocusedElement);
            if(index - 1 < 0){
                index = focusables.size() - 1;
            } else {
                index--;
            }
            if(currentFocusedElement != null){
                currentFocusedElement.handleEvent(new FocusEvent(false));
            }
            currentFocusedElement = focusables.get(index);
            currentFocusedElement.handleEvent(new FocusEvent(true));
        }
    }

    /**
     * Sets a specific element to be focused
     * @param focusableElement The focusable element
     */
    public void focusElement(FocusableElement focusableElement){
        if(currentFocusedElement != null){
            currentFocusedElement.handleEvent(new FocusEvent(false));
        }
        this.currentFocusedElement = focusableElement;
        focusableElement.handleEvent(new FocusEvent(true));
    }

    /**
     * Fires an event at a given position
     * @param event The event
     * @param x the x coordinate of the position
     * @param y the y coordinate of the position
     */
    public void fireEvent(Event event, int x, int y){
        boolean propagate = true;
        ListIterator<Element> windowIterator = elementList.listIterator(elementList.size());
        while(windowIterator.hasPrevious()){
            Element currentWindow = windowIterator.previous();
            Stack<Element> elementStack = buildElementPositionalStack(new Stack<Element>(), currentWindow, x, y, 0, 0);
            Element currentElement = null;
            while(elementStack.size() > 0 && propagate == true){
                currentElement = elementStack.pop();
                if(event instanceof ClickEvent){
                    ClickEvent clickEvent = (ClickEvent)event;
                    Vector2i absPos = getAbsolutePosition(currentElement);
                    clickEvent.setRelativeX(clickEvent.getCurrentX() - absPos.x);
                    clickEvent.setRelativeY(clickEvent.getCurrentY() - absPos.y);
                }
                propagate = currentElement.handleEvent(event);
            }
            if(!propagate){
                currentWindow.applyYoga(0,0);
                break;
            }
        }
    }

    /**
     * Fires an event that does not have a screen position on a given element. Event propagation works by element ancestry regardless of position.
     * @param event The event
     * @param el The element to handle the event
     */
    public boolean fireEventNoPosition(Event event, Element el){
        List<Element> ancestryList = constructNonPositionalAncestryList(el);
        boolean propagate = true;
        while(ancestryList.size() > 0 && propagate == true){
            Element currentElement = ancestryList.remove(0);
            propagate = currentElement.handleEvent(event);
        }
        return propagate;
    }

    /**
     * Fires a top level event (ie on all window elements registered)
     * @param event The top level event
     */
    public void fireTopLevelEvent(Event event){
        boolean propagate = true;
        for(Element topLevelEl : this.elementList){
            propagate = fireEventNoPosition(event, topLevelEl);
            if(!propagate){
                break;
            }
        }
    }

    /**
     * Constructs a list of elements starting with el and containing all its ancestor elements, parent, grandparent, etc
     * @param el The target element
     * @return The ancestry list
     */
    private List<Element> constructNonPositionalAncestryList(Element el){
        //a list of elements with 0 being the target of the event, 1 being the parent of the target, 2 being the parent of 1, etc
        List<Element> elementPropagation = new LinkedList<Element>();
        
        //if we are calling this on the current focused element, it could be null
        if(el != null){
            elementPropagation.add(el);
        }
        Element targetElement = el;
        while(targetElement != null && targetElement.getParent() != null){
            targetElement = targetElement.getParent();
            elementPropagation.add(targetElement);
        }
        return elementPropagation;
    }

    /**
     * Recursively searched for a target element starting at the searchRoot
     * @param searchRoot The root element to search from propagating down the tree
     * @param target The target to search for
     * @return The parent of target if it exists, null otherwise
     */
    public Element recursivelySearchParent(ContainerElement searchRoot, Element target){
        if(searchRoot instanceof ContainerElement){
            for(Element child : searchRoot.getChildren()){
                if(child == target){
                    return searchRoot;
                } else if(child instanceof ContainerElement){
                    Element result = recursivelySearchParent((ContainerElement)child, target);
                    if(result != null){
                        return result;
                    }
                }
            }
        }
        return null;
    }

    /**
     * Recursively uilds a stack of elements at a given position based on their depth into the tree
     * @param inputStack The empty stack to fill
     * @param current The current element
     * @param x the x position to query
     * @param y the y position to query
     * @param offsetX the x offset accumulated
     * @param offsetY the y offset accumulated
     * @return the stack, filled with all relevant elements
     */
    Stack<Element> buildElementPositionalStack(Stack<Element> inputStack, Element current, int x, int y, int offsetX, int offsetY){
        //if contains x,y, call function on el
        if(elementContainsPoint(current,x,y,offsetX,offsetY)){
            inputStack.push(current);
        }
        if(current instanceof ContainerElement){
            ContainerElement container = (ContainerElement)current;
            int xLoc = x - container.getInternalX() - container.getChildOffsetX();
            int yLoc = y - container.getInternalY() - container.getChildOffsetY();
            for(Element el : ((ContainerElement)current).getChildren()){
                buildElementPositionalStack(inputStack, el, xLoc, yLoc, offsetX, offsetY);
            }
        }
        return inputStack;
    }

    public Element resolveFirstDraggable(DragEvent event){
        ListIterator<Element> windowIterator = elementList.listIterator(elementList.size());
        while(windowIterator.hasPrevious()){
            Element currentWindow = windowIterator.previous();
            Stack<Element> elementStack = buildElementPositionalStack(new Stack<Element>(), currentWindow, event.getCurrentX(), event.getCurrentY(), 0, 0);
            Element currentElement = null;
            while(elementStack.size() > 0){
                currentElement = elementStack.pop();
                if(currentElement instanceof DraggableElement){
                    return currentElement;
                }
            }
        }
        return null;
    }

    /**
     * Gets the first hoverable element in the view stack
     * @param currentX the current x position of the mouse
     * @param currentY the current y position of the mouse
     * @return The first hoverable element if it exists, null otherwise
     */
    public Element resolveFirstHoverable(int currentX, int currentY){
        ListIterator<Element> windowIterator = elementList.listIterator(elementList.size());
        while(windowIterator.hasPrevious()){
            Element currentWindow = windowIterator.previous();
            Stack<Element> elementStack = buildElementPositionalStack(new Stack<Element>(), currentWindow, currentX, currentY, 0, 0);
            Element currentElement = null;
            while(elementStack.size() > 0){
                currentElement = elementStack.pop();
                if(currentElement instanceof HoverableElement){
                    return currentElement;
                }
            }
        }
        return null;
    }

    /**
     * Checks if an element contains a given screen coordinate
     * @param el The element
     * @param x the x component of the coordinate
     * @param y the y component of the coordinate
     * @return True if it contains that point, false otherwise
     */
    boolean elementContainsPoint(Element el, int x, int y, int offsetX, int offsetY){
        return
        x >= el.getInternalX() + offsetX &&
        x <= el.getInternalX() + offsetX + el.getInternalWidth() &&
        y >= el.getInternalY() + offsetY &&
        y <= el.getInternalY() + offsetY + el.getInternalHeight();
    }

    /**
     * Fires an event where the mouse presses a button and clicks
     * @param event The event to fire
     */
    public void click(ClickEvent event){
        fireEvent(event,event.getCurrentX(),event.getCurrentY());
    }

    /**
     * Fires an event where the mouse presses down a button and begins to drag an element
     * @param x
     * @param y
     * @param lastX
     * @param lastY
     * @param deltaX
     * @param deltaY
     */
    public void dragStart(int x, int y, int lastX, int lastY, int deltaX, int deltaY){
        DragEvent event = new DragEvent(x, y, lastX, lastY, deltaX, deltaY, DragEventType.START, null);
        currentDragElement = (DraggableElement)resolveFirstDraggable(event);
        event.setTarget(currentDragElement);
        fireEvent(event,x,y);
    }

    /**
     * Fires an event where the mouse is dragging an element
     * @param x
     * @param y
     * @param lastX
     * @param lastY
     * @param deltaX
     * @param deltaY
     */
    public void drag(int x, int y, int lastX, int lastY, int deltaX, int deltaY){
        if(currentDragElement != null){
            //need to calculate offset based on parent positions
            //the positions for x, y, lastX, and lastY are all in absolute-to-screen coordinates
            //the logic for things like slider depend on the 
            Vector2i absPos = getAbsolutePosition(currentDragElement);
            DragEvent event = new DragEvent(x - absPos.x, y - absPos.y, lastX - absPos.x, lastY - absPos.y, deltaX, deltaY, DragEventType.DRAG, currentDragElement);
            currentDragElement.handleEvent(event);
        }
        // fireEvent(event,event.getCurrentX(),event.getCurrentY());
    }

    /**
     * Fires an event where the mouse is dragging an element and releases the mouse button
     * @param x
     * @param y
     * @param lastX
     * @param lastY
     * @param deltaX
     * @param deltaY
     */
    public void dragRelease(int x, int y, int lastX, int lastY, int deltaX, int deltaY){
        if(currentDragElement != null){
            //need to calculate offset based on parent positions
            //the positions for x, y, lastX, and lastY are all in absolute-to-screen coordinates
            //the logic for things like slider depend on the 
            Vector2i absPos = getAbsolutePosition(currentDragElement);
            DragEvent event = new DragEvent(x - absPos.x, y - absPos.y, lastX - absPos.x, lastY - absPos.y, deltaX, deltaY, DragEventType.RELEASE, currentDragElement);
            currentDragElement.handleEvent(event);
            currentDragElement = null;
        }
        // fireEvent(event,event.getCurrentX(),event.getCurrentY());
    }

    /**
     * Gets the absolute position of an element
     * @param target the element
     * @return The absolute position
     */
    private Vector2i getAbsolutePosition(Element target){
        List<Element> ancestryList = constructNonPositionalAncestryList(target);
        ancestryList.remove(target);
        int relX = 0;
        int relY = 0;
        for(Element el : ancestryList){
            relX = relX + el.getInternalX();
            relY = relY + el.getInternalY();
        }
        return new Vector2i(relX,relY);
    }

    /**
     * Navigates backwards
     */
    public void navigateBackwards(){
        NavigationEvent event = new NavigationEvent(NavigationEventType.BACKWARD);
        if(currentFocusedElement != null){
            //fires on the currently focused element
            fireEventNoPosition(event, currentFocusedElement);
        } else if(this.elementList.size() > 0){
            fireTopLevelEvent(event);
        }
    }

    /**
     * Updates the hover state
     * @param currentX The current mouse X
     * @param currentY The current mouse Y
     */
    public void updateHover(int currentX, int currentY){
        Element newHoverableElement = resolveFirstHoverable(currentX,currentY);
        if(currentHoveredElement != newHoverableElement){
            if(currentHoveredElement != null){
                currentHoveredElement.handleEvent(new HoverEvent(false));
            }
            if(newHoverableElement != null){
                newHoverableElement.handleEvent(new HoverEvent(true));
            }
            currentHoveredElement = (HoverableElement)newHoverableElement;
        }
    }
    
}
