package mindustry.ui.layout;

import arc.math.geom.*;
import arc.struct.*;

/**
 * Algorithm taken from <a href="https://github.com/abego/treelayout">TreeLayout</a>.
 */
public class BranchTreeLayout implements TreeLayout{
    public TreeLocation rootLocation = TreeLocation.top;
    public TreeAlignment alignment = TreeAlignment.awayFromRoot;
    public float gapBetweenLevels = 10;
    public float gapBetweenNodes = 10f;

    private final FloatSeq sizeOfLevel = new FloatSeq();
    private float boundsLeft = Float.MAX_VALUE;
    private float boundsRight = Float.MIN_VALUE;
    private float boundsTop = Float.MAX_VALUE;
    private float boundsBottom = Float.MIN_VALUE;

    @Override
    public void layout(TreeNode root){
        firstWalk(root, null);
        calcSizeOfLevels(root, 0);
        secondWalk(root, -root.prelim, 0, 0);
    }

    private float getWidthOrHeightOfNode(TreeNode treeNode, boolean returnWidth){
        return returnWidth ? treeNode.calcWidth() : treeNode.height;
    }

    private float getNodeThickness(TreeNode treeNode){
        return getWidthOrHeightOfNode(treeNode, !isLevelChangeInYAxis());
    }

    private float getNodeSize(TreeNode treeNode){
        return getWidthOrHeightOfNode(treeNode, isLevelChangeInYAxis());
    }

    private boolean isLevelChangeInYAxis(){
        return rootLocation == TreeLocation.top || rootLocation == TreeLocation.bottom;
    }

    private int getLevelChangeSign(){
        return rootLocation == TreeLocation.bottom || rootLocation == TreeLocation.right ? -1 : 1;
    }

    private void updateBounds(TreeNode node, float centerX, float centerY){
        float width = node.width;
        float height = node.height;
        float left = centerX - width / 2;
        float right = centerX + width / 2;
        float top = centerY - height / 2;
        float bottom = centerY + height / 2;
        if(boundsLeft > left){
            boundsLeft = left;
        }
        if(boundsRight < right){
            boundsRight = right;
        }
        if(boundsTop > top){
            boundsTop = top;
        }
        if(boundsBottom < bottom){
            boundsBottom = bottom;
        }
    }

    public Rect getBounds(){
        return new Rect(boundsLeft, boundsBottom, boundsRight - boundsLeft, boundsTop - boundsBottom);
    }

    private void calcSizeOfLevels(TreeNode node, int level){
        float oldSize;
        if(sizeOfLevel.size <= level){
            sizeOfLevel.add(0);
            oldSize = 0;
        }else{
            oldSize = sizeOfLevel.get(level);
        }

        float size = getNodeThickness(node);
        if(oldSize < size){
            sizeOfLevel.set(level, size);
        }

        if(!node.isLeaf()){
            for(TreeNode child : node.children){
                calcSizeOfLevels(child, level + 1);
            }
        }
    }

    public int getLevelCount(){
        return sizeOfLevel.size;
    }

    public float getGapBetweenNodes(TreeNode a, TreeNode b){
        return gapBetweenNodes;
    }

    public float getSizeOfLevel(int level){
        if(!(level >= 0)) throw new IllegalArgumentException("level must be >= 0");
        if(!(level < getLevelCount())) throw new IllegalArgumentException("level must be < levelCount");

        return sizeOfLevel.get(level);
    }

    private TreeNode getAncestor(TreeNode node){
        return node.ancestor != null ? node.ancestor : node;
    }

    private float getDistance(TreeNode v, TreeNode w){
        float sizeOfNodes = getNodeSize(v) + getNodeSize(w);

        return sizeOfNodes / 2 + getGapBetweenNodes(v, w);
    }

    private TreeNode nextLeft(TreeNode v){
        return v.isLeaf() ? v.thread : v.children[0];
    }

    private TreeNode nextRight(TreeNode v){
        return v.isLeaf() ? v.thread : v.children[v.children.length - 1];
    }

    private int getNumber(TreeNode node, TreeNode parentNode){
        if(node.number == -1){
            int number = 1;
            for(TreeNode child : parentNode.children){
                child.number = number++;
            }
        }
        return node.number;
    }

    private TreeNode ancestor(TreeNode vIMinus, TreeNode parentOfV, TreeNode defaultAncestor){
        TreeNode ancestor = getAncestor(vIMinus);
        return ancestor.parent == parentOfV ? ancestor : defaultAncestor;
    }

    private void moveSubtree(TreeNode wMinus, TreeNode wPlus, TreeNode parent, float shift){
        int subtrees = getNumber(wPlus, parent) - getNumber(wMinus, parent);
        wPlus.change = wPlus.change - shift / subtrees;
        wPlus.shift = wPlus.shift + shift;
        wMinus.change = wMinus.change + shift / subtrees;
        wPlus.prelim = wPlus.prelim + shift;
        wPlus.mode = wPlus.mode + shift;
    }

    private TreeNode apportion(TreeNode v, TreeNode defaultAncestor, TreeNode leftSibling, TreeNode parentOfV){
        if(leftSibling == null){
            return defaultAncestor;
        }

        TreeNode vOPlus = v;
        TreeNode vIPlus = v;
        TreeNode vIMinus = leftSibling;

        TreeNode vOMinus = parentOfV.children[0];

        float sIPlus = (vIPlus).mode;
        float sOPlus = (vOPlus).mode;
        float sIMinus = (vIMinus).mode;
        float sOMinus = (vOMinus).mode;

        TreeNode nextRightVIMinus = nextRight(vIMinus);
        TreeNode nextLeftVIPlus = nextLeft(vIPlus);

        while(nextRightVIMinus != null && nextLeftVIPlus != null){
            vIMinus = nextRightVIMinus;
            vIPlus = nextLeftVIPlus;
            vOMinus = nextLeft(vOMinus);
            vOPlus = nextRight(vOPlus);
            vOPlus.ancestor = v;
            float shift = (vIMinus.prelim + sIMinus)
            - (vIPlus.prelim + sIPlus)
            + getDistance(vIMinus, vIPlus);

            if(shift > 0){
                moveSubtree(ancestor(vIMinus, parentOfV, defaultAncestor),
                v, parentOfV, shift);
                sIPlus = sIPlus + shift;
                sOPlus = sOPlus + shift;
            }
            sIMinus += vIMinus.mode;
            sIPlus += vIPlus.mode;
            sOMinus += vOMinus.mode;
            sOPlus += vOPlus.mode;

            nextRightVIMinus = nextRight(vIMinus);
            nextLeftVIPlus = nextLeft(vIPlus);
        }

        if(nextRightVIMinus != null && nextRight(vOPlus) == null){
            vOPlus.thread = nextRightVIMinus;
            vOPlus.mode += sIMinus - sOPlus;
        }

        if(nextLeftVIPlus != null && nextLeft(vOMinus) == null){
            vOMinus.thread = nextLeftVIPlus;
            vOMinus.mode += sIPlus - sOMinus;
            defaultAncestor = v;
        }
        return defaultAncestor;
    }

    private void executeShifts(TreeNode v){
        float shift = 0;
        float change = 0;

        for(int i = v.children.length - 1; i >= 0; i--){
            TreeNode w = v.children[i];
            change = change + w.change;
            w.prelim += shift;
            w.mode += shift;
            shift += w.shift + change;
        }
    }

    private void firstWalk(TreeNode v, TreeNode leftSibling){
        if(v.isLeaf()){
            if(leftSibling != null){
                v.prelim = leftSibling.prelim + getDistance(v, leftSibling);
            }

        }else{
            TreeNode defaultAncestor = v.children[0];
            TreeNode previousChild = null;
            for(TreeNode w : v.children){
                firstWalk(w, previousChild);
                defaultAncestor = apportion(w, defaultAncestor, previousChild, v);
                previousChild = w;
            }
            executeShifts(v);
            float midpoint = (v.children[0].prelim + v.children[v.children.length - 1].prelim) / 2f;
            if(leftSibling != null){
                v.prelim = leftSibling.prelim + getDistance(v, leftSibling);
                v.mode = v.prelim - midpoint;
            }else{
                v.prelim = midpoint;
            }
        }
    }

    private void secondWalk(TreeNode v, float m, int level, float levelStart){
        float levelChangeSign = getLevelChangeSign();
        boolean levelChangeOnYAxis = isLevelChangeInYAxis();
        float levelSize = getSizeOfLevel(level);

        float x = v.prelim + m;

        float y;
        if(alignment == TreeAlignment.center){
            y = levelStart + levelChangeSign * (levelSize / 2);
        }else if(alignment == TreeAlignment.towardsRoot){
            y = levelStart + levelChangeSign * (getNodeThickness(v) / 2);
        }else{
            y = levelStart + levelSize - levelChangeSign * (getNodeThickness(v) / 2);
        }

        if(!levelChangeOnYAxis){
            float t = x;
            x = y;
            y = t;
        }

        v.x = x;
        v.y = y;
        updateBounds(v, x, y);

        if(!v.isLeaf()){
            float nextLevelStart = levelStart
            + (levelSize + gapBetweenLevels)
            * levelChangeSign;
            for(TreeNode w : v.children){
                secondWalk(w, m + v.mode, level + 1, nextLevelStart);
            }
        }
    }

    public enum TreeLocation{
        top, left, bottom, right
    }

    public enum TreeAlignment{
        center, towardsRoot, awayFromRoot
    }
}
