package com.persagy.adm.diagram.core.line;

import cn.hutool.core.util.ArrayUtil;
import com.persagy.adm.diagram.core.model.Line;
import com.persagy.adm.diagram.core.model.base.XY;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Random;
import java.util.stream.Collectors;

/**
 * 连线路径计算
 *
 * @author zhaoyk
 */
public class PathBuilder {

    private static final int D_RIGHT = 1;

    private static final int D_DOWN = 2;

    private static final int D_LEFT = 3;

    private static final int D_UP = 4;

    private static int edge = 10;

    private Line line;

    private LineEnd end1;

    private LineEnd end2;

    private boolean exchange;

    private List<XY> points = new ArrayList<>();

    private XY currentPoint;

    private int fromDirection;

    private Random random = new Random();

    public PathBuilder(Line line, LineEnd end1, LineEnd end2, boolean exchange) {
        this.line = line;
        this.end1 = end1;
        this.end2 = end2;
        this.exchange = exchange;

        setCurrentPoint(end1.getPoint());
        String fromAnchor = (!exchange ? line.getFrom() : line.getTo()).getAnchorCode();
        switch (fromAnchor.charAt(0)) {
            case 'T':
                fromDirection = D_DOWN;
                break;
            case 'B':
                fromDirection = D_UP;
                break;
            case 'L':
                fromDirection = D_RIGHT;
                break;
            case 'R':
                fromDirection = D_LEFT;
                break;
            default:
        }
    }

    private void setCurrentPoint(XY point) {
        currentPoint = point;
        points.add(currentPoint);
    }

    public List<XY> calcPath(List<Block> blocks) {
        findPath(blocks);

        if (exchange) {
            Collections.reverse(points);
        }

        if (line.getFrom().getLocation() != null) {
            points.add(0, line.getFrom().getLocation());
        }
        if (line.getTo().getLocation() != null) {
            points.add(line.getTo().getLocation());
        }

        return points;
    }

    private boolean findPath(List<Block> blocks) {
        Block rectArea = buildRectArea();
        List<Block> currentBlocks = blocks.stream().filter(block -> block.getRect().intersects(rectArea.getRect())).collect(Collectors.toList());

        int[] directions = findDirection();

        for (int direction : directions) {
            if (direction == fromDirection) {
                continue;
            }

            boolean vDirection = isVerticalDirection(direction);
            int trackPos = vDirection ? currentPoint.x : currentPoint.y;
            int pos = vDirection ? currentPoint.y : currentPoint.x;
            boolean reverse = direction == D_LEFT || direction == D_UP;

            List<BlockLine> blockLines = buildBlockLines(currentBlocks, direction);

            int stopPos = -1;
            for (BlockLine bl : blockLines) {
                boolean valid = !reverse ? bl.base > pos : bl.base < pos;
                if (valid && bl.begin <= trackPos && bl.end >= trackPos) {
                    stopPos = bl.base;
                    break;
                }
            }

            if (stopPos != -1) {
                int move = stopPos - pos;
                //当前方向被阻挡
                if (Math.abs(move) <= edge) {
                    continue;
                }

                //预留边缘距离
                if (stopPos > pos) {
                    stopPos -= edge;
                } else {
                    stopPos += edge;
                }
            }

            List<int[]> nonBlockRoads = getNonBlockRoads(blockLines, rectArea, vDirection);
            if(reverse) {
                Collections.reverse(nonBlockRoads);
            }
            int[] nbr = findNonBlockRoad(nonBlockRoads, trackPos);

            if (nbr != null) {
                //检查终点是否在同一个无阻挡通道中
                if (checkEnd(nbr, vDirection)) {
                    connectEnd(direction, nbr, vDirection);
                    return true;
                }
            }

            //获取垂直参考方向
            int crossDirection = 0;
            for (int i = 0; i < directions.length; i++) {
                if (isVerticalDirection(directions[i]) != vDirection) {
                    crossDirection = directions[i];
                    break;
                }
            }
            boolean crossReverse = crossDirection == D_LEFT || crossDirection == D_UP;
            List<BlockLine> crossBlockLines = buildBlockLines(currentBlocks, crossDirection);
            List<int[]> crossNonBlockRoads = getNonBlockRoads(crossBlockLines, rectArea, !vDirection);
            if(reverse) {
                Collections.reverse(crossNonBlockRoads);
            }

            //位于垂直参考方向上的无阻挡通道中
            int[] crossNbr = findNonBlockRoad(crossNonBlockRoads, pos);
            if (crossNbr != null) {
                if (checkEnd(crossNbr, !vDirection)) {
                    connectEnd(crossDirection, crossNbr, !vDirection);
                    return true;
                }
            }

            //折线直达
            if (nbr != null) {
                for (int[] crossRoad : crossNonBlockRoads) {
                    boolean valid = !reverse ? crossRoad[0] > pos : crossRoad[1] < pos;
                    if (valid && checkEnd(crossRoad, !vDirection)) {
                        connectEnd(direction, crossRoad, !vDirection);
                        return true;
                    }
                }
            }
            if (crossNbr != null) {
                for (int[] road : nonBlockRoads) {
                    boolean valid = !crossReverse ? road[0] > trackPos : road[1] < trackPos;
                    if (valid && checkEnd(road, vDirection)) {
                        connectEnd(crossDirection, road, vDirection);
                        return true;
                    }
                }
            }

            //移动到垂直参考方向上的第一个无阻挡通道位置
            int targetPos = -1;
            for (int[] crossRoad : crossNonBlockRoads) {
                boolean flag = !reverse ? crossRoad[0] > pos : crossRoad[1] < pos;
                if (flag) {
                    targetPos = crossRoad[0] + (crossRoad[1] - crossRoad[0]) / 2; //TODO 调整&避免重叠

                    if(stopPos != -1) {
                        boolean valid = !reverse ? targetPos < stopPos : targetPos > stopPos;
                        if (!valid) {
                            targetPos = -1;
                        }
                    }

                    break;
                }
            }
            //垂直参考方向方向上没有找到通道
            if (targetPos == -1) {
                if (stopPos != -1) { // 移动到stopPos
                    targetPos = stopPos;
                } else { //移动到垂直参考方向上blockLine变化的位置
                    for (BlockLine crossBl : crossBlockLines) {
                        boolean valid = !crossReverse ? crossBl.base > trackPos : crossBl.base < targetPos;
                        if (valid && crossBl.begin <= pos && crossBl.end >= pos) {
                            if (!reverse) {
                                targetPos = crossBl.end;
                            } else {
                                targetPos = crossBl.begin;
                            }
                            break;
                        }
                    }
                    //计算失败，尝试下一方向
                    if (targetPos == -1) {
                        continue;
                    }
                }
            }
            moveTo(targetPos, vDirection);
            fromDirection = direction > 2 ? direction - 2 : direction + 2; //取反向
            //递归调用路径计算
            if(findPath(blocks)){
                return true;
            }
        }
        return false;
    }

    private Block buildRectArea() {
        int[] xs;
        int[] ys;
        if (end2.getPoint() != null) {
            xs = new int[]{currentPoint.x, end2.getPoint().x};
            ys = new int[]{currentPoint.y, end2.getPoint().y};
        } else {
            xs = new int[]{currentPoint.x, end2.getLine()[0].x, end2.getLine()[1].x};
            ys = new int[]{currentPoint.y, end2.getLine()[0].y, end2.getLine()[1].y};
        }

        XY location = new XY(ArrayUtil.min(xs), ArrayUtil.min(ys));
        location.x -= edge;
        location.y -= edge;
        XY size = new XY(ArrayUtil.max(xs) - location.x, ArrayUtil.max(ys) - location.y);
        size.x += edge;
        size.y += edge;

        return new Block(location, size);
    }

    private int[] findDirection() {
        int[] rtn = new int[4];

        if (end2.getPoint() != null) {
            XY endPoint = end2.getPoint();
            if (endPoint.x == currentPoint.x) {
                setDirection(rtn, null, endPoint.y > currentPoint.y);
            } else if (endPoint.y == currentPoint.y) {
                setDirection(rtn, endPoint.x > currentPoint.x, null);
            } else {
                setDirection(rtn, endPoint.x > currentPoint.x, endPoint.y > currentPoint.y);
            }
        } else {
            int trackPos = end2.getLineTrackPos();
            int[] scope = end2.getLineScope();
            if (end2.isLineHorizontal()) {
                boolean xMatch = scope[0] <= currentPoint.x && scope[1] >= currentPoint.x;
                if (xMatch) {
                    setDirection(rtn, null, trackPos > currentPoint.y);
                } else if (trackPos == currentPoint.y) {
                    //需要垂直交汇
                    rtn[0] = D_DOWN;
                    rtn[1] = D_UP;

                    boolean right = scope[0] > currentPoint.x;
                    rtn[2] = right ? D_RIGHT : D_LEFT;
                    rtn[3] = right ? D_LEFT : D_RIGHT;
                }
            } else {
                boolean yMatch = scope[0] <= currentPoint.y && scope[0] >= currentPoint.y;
                if (yMatch) {
                    setDirection(rtn, trackPos > currentPoint.x, null);
                } else if (trackPos == currentPoint.x) {
                    //需要垂直交汇
                    rtn[0] = D_RIGHT;
                    rtn[1] = D_LEFT;

                    boolean down = scope[0] > currentPoint.y;
                    rtn[2] = down ? D_DOWN : D_UP;
                    rtn[3] = down ? D_UP : D_DOWN;
                }
            }
            if (rtn[0] == 0) {
                XY p0 = end2.getLine()[0];
                setDirection(rtn, p0.x > currentPoint.x, p0.y > currentPoint.y);
            }
        }

        return rtn;
    }

    private void setDirection(int[] directions, Boolean right, Boolean down) {
        if (right != null && down != null) {
            directions[0] = right ? D_RIGHT : D_LEFT;
            directions[1] = down ? D_DOWN : D_UP;
            directions[2] = right ? D_LEFT : D_RIGHT;
            directions[3] = down ? D_UP : D_DOWN;
        } else if (right != null) {
            directions[0] = right ? D_RIGHT : D_LEFT;
            directions[1] = D_DOWN;
            directions[2] = D_UP;
            directions[3] = right ? D_LEFT : D_RIGHT;
        } else if (down != null) {
            directions[0] = down ? D_DOWN : D_UP;
            directions[1] = D_RIGHT;
            directions[2] = D_LEFT;
            directions[3] = down ? D_UP : D_DOWN;
        }
    }

    private List<BlockLine> buildBlockLines(List<Block> currentBlocks, int direction) {
        List<BlockLine> blockLines = new ArrayList<>();
        for (Block block : currentBlocks) {
            addBlockLine(block, direction, blockLines);
        }

        //合并相连的blockLine
        Iterator<BlockLine> iter = blockLines.iterator();
        BlockLine pre = null;
        while (iter.hasNext()) {
            BlockLine bl = iter.next();
            if (pre != null) {
                if (pre.end == bl.begin && pre.base == bl.base) {
                    pre.end = bl.end;
                    iter.remove();
                    continue;
                }
            }
            pre = bl;
        }

        return blockLines;
    }

    private void addBlockLine(Block block, int direction, List<BlockLine> blockLines) {
        BlockLine toHandle = new BlockLine();
        XY loc = block.getLocation();
        XY size = block.getSize();
        switch (direction) {
            case D_UP:
                if (currentPoint.y <= loc.y) {
					return;
				}
                toHandle.base = loc.y + size.y;
                toHandle.begin = loc.x;
                toHandle.end = loc.x + size.x;
                break;
            case D_DOWN:
                if (currentPoint.y >= loc.y + size.y) {
					return;
				}
                toHandle.base = loc.y;
                toHandle.begin = loc.x;
                toHandle.end = loc.x + size.x;
                break;
            case D_LEFT:
                if (currentPoint.x <= loc.x) {
					return;
				}
                toHandle.base = loc.x + size.x;
                toHandle.begin = loc.y;
                toHandle.end = loc.y + size.y;
                break;
            case D_RIGHT:
                if (currentPoint.x >= loc.x + size.x) {
					return;
				}
                toHandle.base = loc.x;
                toHandle.begin = loc.y;
                toHandle.end = loc.y + size.y;
                break;
			default:
        }

        //使用toHandle的端点对blockLines中线段进行切分
        int pos = toHandle.begin;
        for (int i = 0; i < blockLines.size(); i++) {
            BlockLine item = blockLines.get(i);
            if (pos > item.begin && pos < item.end) {
                blockLines.add(i + 1, new BlockLine(pos, item.end, item.base));
                item.end = pos;

                if (pos == toHandle.begin) {
					pos = toHandle.end;
				} else {
					break;
				}
            }
        }

        //使用blockLines中线段端点对toHandle进行切分
        LinkedList<BlockLine> nls = new LinkedList<>();
        boolean finish = false;
        for (BlockLine item : blockLines) {
            int[] posArr = new int[]{item.begin, item.end};
            for (int p : posArr) {
                if (p > toHandle.begin && p < toHandle.end) {
                    nls.add(new BlockLine(toHandle.begin, p, toHandle.base));
                    toHandle.begin = p;
                } else if (p > toHandle.end) {
                    finish = true;
                    break;
                }
            }

            if (finish) {
				break;
			}
        }
        nls.add(toHandle);

        //合并blockLines并设置base
        for (int i = 0; i < blockLines.size(); i++) {
            if (nls.size() == 0) {
				break;
			}

            BlockLine item = blockLines.get(i);
            BlockLine nl = nls.getFirst();
            if (nl.begin < item.begin) { //插入
                blockLines.add(i, nl);

                nls.removeFirst();
            } else if (nl.begin == item.begin) { //重合
                item.base = (direction == D_UP || direction == D_LEFT) ? Math.max(nl.base, item.base) : Math.min(nl.base, item.base);

                nls.removeFirst();
            }
        }
        if (nls.size() > 0) {
            blockLines.addAll(nls);
        }
    }

    private List<int[]> getNonBlockRoads(List<BlockLine> blockLines, Block rectArea, boolean vDirection) {
        int from, to;
        if (vDirection) {
            from = rectArea.getLocation().x;
            to = rectArea.getLocation().x + rectArea.getSize().x;
        } else {
            from = rectArea.getLocation().y;
            to = rectArea.getLocation().y + rectArea.getSize().y;
        }

        List<int[]> list = new ArrayList<>();
        int tmp = from;
        for (BlockLine bl : blockLines) {
            if (tmp < bl.begin) {
				list.add(new int[]{tmp, bl.begin});
			}
            tmp = bl.end;
        }
        if (tmp < to) {
			list.add(new int[]{tmp, to});
		}

        return list;
    }

    private int[] findNonBlockRoad(List<int[]> nonBlockRoads, int pos) {
        for (int[] road : nonBlockRoads) {
            if (road[0] <= pos && road[1] >= pos) {
                return road;
            }
        }
        return null;
    }

    /**
     * 检查终点是否在某个无阻挡通道上
     */
    private boolean checkEnd(int[] nonBlockRoad, boolean vDirection) {
        if (end2.getPoint() != null) {
            int pos = vDirection ? end2.getPoint().x : end2.getPoint().y;
            return nonBlockRoad[0] <= pos && nonBlockRoad[1] >= pos;
        } else {
            boolean hLine = end2.isLineHorizontal();
            if (hLine == vDirection) { //垂直
                int[] scope = end2.getLineScope();
                boolean notJoin = scope[0] > nonBlockRoad[1] || nonBlockRoad[0] > scope[1];
                return !notJoin;
            } else { //平行
                int trackPos = end2.getLineTrackPos();
                return nonBlockRoad[0] <= trackPos && nonBlockRoad[1] >= trackPos;
            }
        }
    }

    private void connectEnd(int direction, int[] endNbr, boolean vEndNbr) {
        boolean vDirection = isVerticalDirection(direction);
        if (end2.getPoint() != null) {
            XY endPoint = end2.getPoint();
            if (currentPoint.x != endPoint.x && currentPoint.y != endPoint.y) {
                moveTo(vDirection ? endPoint.y : endPoint.x, vDirection);
            }
            points.add(endPoint);
        } else {
            //XY[] line = end2.getLine();
            boolean hLine = end2.isLineHorizontal();
            int lineTrackPos = end2.getLineTrackPos();
            int[] lineScope = end2.getLineScope();

            if (vDirection == vEndNbr) { //当前方向与终点通道方向相同
                if (vEndNbr == hLine) { //endLine与通道垂直
                    int safeOffset = 5;
                    int[] targetScope = new int[]{Math.max(lineScope[0], endNbr[0] + safeOffset), Math.min(lineScope[1], endNbr[1] - safeOffset)};

                    //当前点和线有偏差时，先做调整
                    int pos = vEndNbr ? currentPoint.x : currentPoint.y;
                    if(!(pos > targetScope[0] && pos < targetScope[1])){
                        //临时避免和节点边线重合
                        int target0 = targetScope[0] + random.nextInt(targetScope[1] - targetScope[0]);
                        if (vEndNbr) {
                            currentPoint.x = target0;
                        } else {
                            currentPoint.y = target0;
                        }
                    }

                    moveTo(lineTrackPos, hLine); //线横向时做纵向移动
                } else { //endLine与通道平行
                    //TODO 可能需要调整，避免和线水平连接
                    int target1 = hLine ? currentPoint.x : currentPoint.y;
                    int offset = random.nextInt((lineScope[1] - lineScope[0]) / 2);
                    if (Math.abs(lineScope[0] - target1) < Math.abs((lineScope[1] - target1))) {
                        target1 = lineScope[0] + offset;
                    } else {
                        target1 = lineScope[1] - offset;
                    }
                    moveTo(target1, vDirection);

                    moveTo(lineTrackPos, !vDirection);
                }
            } else { //当前方向与终点通道方向垂直，折线连接
                if (vEndNbr == hLine) { //endLine与通道垂直
                    int safeOffset = 5;//临时避免和节点边线重合
                    int[] targetScope = new int[]{Math.max(lineScope[0], endNbr[0] + safeOffset), Math.min(lineScope[1], endNbr[1] - safeOffset)};
                    int target0 = targetScope[0] + random.nextInt(targetScope[1] - targetScope[0]); //TODO 调整&避免重叠
                    moveTo(target0, vDirection);
                    moveTo(lineTrackPos, hLine);
                } else { //endLine与通道平行
                    //TODO 调整&避免重叠
                    int target0;
                    if (endNbr[1] - lineTrackPos > lineTrackPos - endNbr[0]) {
                        target0 = lineTrackPos + LineEnd.offset;
                    } else {
                        target0 = lineTrackPos - LineEnd.offset;
                    }
                    if (target0 > endNbr[1]) {
                        target0 = endNbr[1];
                    } else if (target0 < endNbr[0]) {
                        target0 = endNbr[0];
                    }
                    XY point0 = moveTo(target0, vDirection);

                    int target1 = hLine ? point0.x : point0.y;
                    int offset = random.nextInt((lineScope[1] - lineScope[0]) / 2);
                    if (Math.abs(lineScope[0] - target1) < Math.abs((lineScope[1] - target1))) {
                        target1 = lineScope[0] + offset;
                    } else {
                        target1 = lineScope[1] - offset;
                    }
                    moveTo(target1, !vDirection);

                    moveTo(lineTrackPos, vDirection);
                }
            }
        }
    }

    private XY moveTo(int targetPos, boolean vDirection) {
        XY point = new XY(currentPoint);
        if (vDirection) {
			point.y = targetPos;
		} else {
			point.x = targetPos;
		}

        //TODO 连线检查，处理重叠等

        setCurrentPoint(point);
        return point;
    }

    private boolean isVerticalDirection(int direction) {
        return direction == D_DOWN || direction == D_UP;
    }

    class BlockLine {

        BlockLine() {

        }

        BlockLine(int begin, int end, int base) {
            this.begin = begin;
            this.end = end;
            this.base = base;
        }

        int begin;

        int end;

        int base;

    }

}
