package com.persagy.dmp.rwd.digital.service.calstrategy;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.NumberUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.persagy.dmp.basic.dto.RelationCalRuleDTO;
import com.persagy.dmp.common.helper.SpringHelper;
import com.persagy.dmp.digital.entity.ObjectRelation;
import com.persagy.dmp.rwd.basic.constant.EquipmentTypeEnum;
import com.persagy.dmp.rwd.basic.utils.DigitalCommonUtils;
import com.persagy.dmp.rwd.basic.utils.GeoToolsUtil;
import com.persagy.dmp.rwd.digital.dao.ObjectDigitalMapper;
import com.persagy.dmp.rwd.digital.dao.ObjectRelationMapper;
import com.persagy.dmp.rwd.digital.domain.CalculatingDTO;
import com.persagy.dmp.rwd.digital.domain.OpenPointDTO;
import com.persagy.dmp.rwd.digital.domain.SpaceCalDTO;
import com.persagy.dmp.rwd.digital.service.CalculateService;
import com.persagy.dmp.rwd.digital.service.IObjectRelationService;
import com.persagy.dmp.rwd.digital.utils.ObjectDigitalCriteriaHelper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

/***
 * Description: 空间连通关系计算
 * @author : lijie
 * @date :2021/10/9 16:26
 * Update By lijie 2021/10/9 16:26
 */
@Service
@Slf4j
@RequiredArgsConstructor
public class SpaceConnectionRelCalculateServiceImpl implements CalculateService {

    private final IObjectRelationService objectRelationService;
    private final ObjectRelationMapper objectRelationMapper;
    private final ObjectDigitalMapper objectDigitalMapper;


    /***
     * Description: 空间连通关系计算
     * @param calRule : 计算规则
     * @param calculatingDTO : 请求参数
     * @return : void
     * @author : lijie
     * @date :2021/10/9 16:26
     * Update By lijie 2021/10/9 16:26
     */
    @Override
    public void calculateRelation(RelationCalRuleDTO calRule, CalculatingDTO calculatingDTO) {
        // 1.获取有所在楼层的所有业务空间, 并按照所在楼层和业务空间类型排序
        List<SpaceCalDTO> spaceCalDTOS = objectRelationMapper.querySpacesByProjectId(calculatingDTO);
        if (CollUtil.isEmpty(spaceCalDTOS)){
            return;
        }
        // 1.1 获得[[space,space]]的二维数组
        Map<String,List<SpaceCalDTO>> spaceMap = spaceCalDTOS.stream()
                .collect(Collectors.groupingBy(spaceCalDTO -> spaceCalDTO.getFloorId()+StrUtil.UNDERLINE+spaceCalDTO.getClassCode()));

        // 存储的关系列表
        List<ObjectRelation> resultList = new ArrayList<>();
        // 2.门的计算
        calculateRelationInternal(resultList,spaceCalDTOS,calculatingDTO,spaceMap, EquipmentTypeEnum.CFBEDR.name(),
                calRule.getSpaceConnectionCal().getDoorCalFlag());
        // 3.窗的计算
        calculateRelationInternal(resultList,spaceCalDTOS,calculatingDTO,spaceMap,EquipmentTypeEnum.CFBEWN.name(),
                calRule.getSpaceConnectionCal().getWindowCalFlag());
        // 4.虚拟墙的计算(暂时没有虚拟墙的概念,先置为false)
        calculateRelationInternal(resultList,spaceCalDTOS,calculatingDTO,spaceMap,EquipmentTypeEnum.CFBEXX.name(),
                calRule.getSpaceConnectionCal().getVirtualWindowCalFlag());
        // 5.元空间的计算
        ispaceCalculateRelationInternal(resultList,spaceCalDTOS,calculatingDTO,spaceMap,
                calRule.getSpaceConnectionCal().getIspaceCalFlag());
        if (CollUtil.isNotEmpty(resultList)){
            // 3.根据图类型编码,边类型编码,relValue,项目id,集团编码删除关系
            deleteRelDataByCondition(calculatingDTO);
            // 4.新增关系
            objectRelationService.save(resultList);
        }
    }
    /***
     * Description: 元空间的计算
     * @param resultList : 结果集
     * @param spaceCalDTOS : 空间数据
     * @param calculatingDTO : 请求参数
     * @param spaceMap:{floorId_classCode:空间对象}
     * @param calFlag : 是否计算的标记
     * @return : void
     * @author : lijie
     * @date :2021/10/11 22:06
     * Update By lijie 2021/10/11 22:06
     */
    private void ispaceCalculateRelationInternal(List<ObjectRelation> resultList, List<SpaceCalDTO> spaceCalDTOS,
                                                 CalculatingDTO calculatingDTO,
                                                 Map<String, List<SpaceCalDTO>> spaceMap,
                                                 Boolean calFlag) {
        if (!calFlag){
            return;
        }
        // 1.根据modelId批量查询门
        List<SpaceCalDTO> ispaces = objectDigitalMapper.queryIspacesByModelIds(spaceCalDTOS.stream()
                .map(SpaceCalDTO::getFloorModelId).collect(Collectors.toSet()),calculatingDTO);
        if (CollUtil.isEmpty(ispaces)){
            return;
        }
        // 1.1 获得modelId和[door,door]的映射
        Map<String, List<SpaceCalDTO>> ispaceMap = ispaces
                .stream().collect(Collectors.groupingBy(SpaceCalDTO::getModelId));
        // 2.根据空间分组及门的数据计算关系
        Set<Map.Entry<String, List<SpaceCalDTO>>> entries = spaceMap.entrySet();
        for (Map.Entry<String, List<SpaceCalDTO>> entry : entries) {
            List<SpaceCalDTO> spaces = entry.getValue();
            if (CollUtil.isEmpty(spaces) || spaces.size()<2){
                continue;
            }
            SpaceCalDTO spaceCal = spaces.get(0);
            if (!ispaceMap.containsKey(spaceCal.getFloorModelId())
                    || CollUtil.isEmpty(ispaceMap.get(spaceCal.getFloorModelId()))){
                continue;
            }
            List<SpaceCalDTO> subIspaces = ispaceMap.get(spaceCal.getFloorModelId());
            List<String> relIds = new ArrayList<>();
            for (SpaceCalDTO subIspace : subIspaces) {
                ArrayNode outLines = subIspace.getOutLines();
                for (SpaceCalDTO space : spaces) {
                    if (GeoToolsUtil.isPolyInPoly(space.getOutLines(),outLines)){
                        relIds.add(space.getSpaceId());
                    }
                }
                if (CollUtil.isNotEmpty(relIds)){
                    for (int i = 0; i < relIds.size(); i++) {
                        for (int j = 1; j < relIds.size(); j++) {
                            if (relIds.get(i).equals(relIds.get(j))){
                                continue;
                            }
                            resultList.add(createObjectRelation(calculatingDTO,spaceCal.getGroupCode(),
                                    spaceCal.getProjectId(),relIds.get(i),relIds.get(j),spaceCal.getClassCode()));
                        }
                    }
                }
            }
        }
    }

    /***
     * Description: 通过门计算空间连通性
     * @param resultList : 结果集
     * @param spaceCalDTOS : 空间数据
     * @param calculatingDTO : 请求参数
     * @param spaceMap:{floorId_classCode:空间对象}
     * @return : void
     * @author : lijie
     * @date :2021/10/11 17:44
     * Update By lijie 2021/10/11 17:44
     */
    private void calculateRelationInternal(List<ObjectRelation> resultList,
                                       List<SpaceCalDTO> spaceCalDTOS,
                                       CalculatingDTO calculatingDTO,
                                       Map<String, List<SpaceCalDTO>> spaceMap,
                                       String classCode,Boolean calFlag) {
        if (!calFlag){
            return;
        }
        // 1.根据modelId批量查询门
        List<SpaceCalDTO> doors = objectDigitalMapper.queryEquipmentsByModelIds(spaceCalDTOS.stream()
                .map(SpaceCalDTO::getFloorModelId).collect(Collectors.toSet()),calculatingDTO,classCode);
        if (CollUtil.isEmpty(doors)){
            return;
        }
        commonCalculateRelationInternal(doors,resultList,calculatingDTO,spaceMap);
    }
    /***
     * Description: 通用计算空间连通性
     * @param spaceCals : 空间数据
     * @param resultList : 结果集
     * @param calculatingDTO : 请求参数
     * @param spaceMap:{floorId_classCode:空间对象}
     * @return : void
     * @author : lijie
     * @date :2021/10/11 21:51
     * Update By lijie 2021/10/11 21:51
     */
    private void commonCalculateRelationInternal(List<SpaceCalDTO> spaceCals,
                                                 List<ObjectRelation> resultList,
                                                 CalculatingDTO calculatingDTO,
                                                 Map<String, List<SpaceCalDTO>> spaceMap) {
        // 1.1 获得modelId和[door,door]的映射
        Map<String, List<SpaceCalDTO>> doorMap = spaceCals
                .stream().collect(Collectors.groupingBy(SpaceCalDTO::getModelId));
        ObjectMapper objectMapper = SpringHelper.getBean(ObjectMapper.class);
        // 2.根据空间分组及门的数据计算关系
        Set<Map.Entry<String, List<SpaceCalDTO>>> entries = spaceMap.entrySet();
        for (Map.Entry<String, List<SpaceCalDTO>> entry : entries) {
            List<SpaceCalDTO> spaces = entry.getValue();
            if (CollUtil.isEmpty(spaces) || spaces.size()<2){
                continue;
            }
            SpaceCalDTO spaceCal = spaces.get(0);
            if (!doorMap.containsKey(spaceCal.getFloorModelId())
                    || CollUtil.isEmpty(doorMap.get(spaceCal.getFloorModelId()))){
                continue;
            }
            // 2.1 遍历相同下的门
            List<SpaceCalDTO> subDoors = doorMap.get(spaceCal.getFloorModelId());
            for (SpaceCalDTO subDoor : subDoors) {
                ObjectNode firstPoint = subDoor.getFirstPoint();
                ObjectNode secondPoint = subDoor.getSecondPoint();
                // 2.1.1 校验坐标点的值是否合法
                if (!checkPointIsValid(firstPoint,secondPoint)){
                    continue;
                }
                // 2.1.2 获得门打开后覆盖的坐标点
                List<OpenPointDTO> openPoints = getOpenPoint(firstPoint,secondPoint);
                if (CollUtil.isEmpty(openPoints)){
                    continue;
                }
                List<String> relIds = new ArrayList<>();
                // 2.1.3 通过门打开后的坐标点与空间关联性获得连通的空间数组
                for (SpaceCalDTO space : spaces) {
                    ArrayNode outLines = space.getOutLines();
                    for (OpenPointDTO openPoint : openPoints) {
                        ObjectNode pointNode = objectMapper
                                .createObjectNode()
                                .put("X", openPoint.getDestX()).put("Y", openPoint.getDestY());
                        if (GeoToolsUtil.isPointInPoly(pointNode,outLines)){
                            relIds.add(space.getSpaceId());
                            break;
                        }
                    }
                    if (relIds.size()==2){
                        break;
                    }
                }
                // 2.1.4 生成关系
                if (relIds.size()==2){
                    resultList.add(createObjectRelation(calculatingDTO,spaceCal.getGroupCode(),
                            spaceCal.getProjectId(),relIds.get(0),relIds.get(1),spaceCal.getClassCode()));
                }
            }
        }
    }

    /***
     * Description: 创建关系对象
     * @return : com.persagy.dmp.digital.entity.ObjectRelation
     * @author : lijie
     * @date :2021/10/11 21:39
     * Update By lijie 2021/10/11 21:39
     * @param calculatingDTO :请求参数
     * @param groupCode :集团编码
     * @param projectId :项目id
     * @param objFrom : objFrom
     * @param objTo : objTo
     * @param classCode: classCode
     */
    private ObjectRelation createObjectRelation(CalculatingDTO calculatingDTO, String groupCode,
                                                String projectId, String objFrom, String objTo, String classCode) {
        // 空间有相交的空间,说明两者竖井是连通的
        ObjectRelation objectRelation = new ObjectRelation();
        objectRelation.setGroupCode(groupCode);
        objectRelation.setProjectId(projectId);
        objectRelation.setGraphCode(calculatingDTO.getGraphCode());
        objectRelation.setRelCode(calculatingDTO.getRelCode());
        objectRelation.setRelValue(classCode);
        objectRelation.setObjFrom(objFrom);
        objectRelation.setObjTo(objTo);
        return objectRelation;
    }

    /***
     * Description: 获得门,窗,虚拟墙,元空间打开后两点的坐标信息
     * @param firstPoint : 第一个坐标点
     * @param secondPoint : 第二个坐标点
     * @return : com.persagy.dmp.rwd.digital.domain.OpenPointDTO
     * @author : lijie
     * @date :2021/10/11 20:19
     * Update By lijie 2021/10/11 20:19
     */
    private List<OpenPointDTO> getOpenPoint(ObjectNode firstPoint, ObjectNode secondPoint) {
        double deltaDistance = 200.0;
        double doorLength = 1500.0;
        double closedFirstX = NumberUtil.parseDouble(firstPoint.get("X").asText());
        double closedFirstY = NumberUtil.parseDouble(firstPoint.get("Y").asText());
        double closedSecondX = NumberUtil.parseDouble(secondPoint.get("X").asText());
        double closedSecondY = NumberUtil.parseDouble(secondPoint.get("Y").asText());
        // 1.计算两个坐标点的直线距离
        double distance = getSegmentDistance(closedFirstX, closedFirstY, closedSecondX, closedSecondY);
        // 2.计算该直线的斜率
        Double k=null;
        if(closedFirstX!=closedSecondX){
            k=(closedFirstY-closedSecondY)/(closedFirstX-closedSecondX);
        }
        // 2.1 获得垂直线的斜率
        Double verticalK = getVerticalK(k);
        // 2.2 获得当前直线的a
        Double closedA = getAByPointAndK(closedFirstX,closedFirstY,k);
        List<OpenPointDTO> result = new ArrayList<>();
        if (distance>deltaDistance){
            // 距离大于delta_distance, 则足够开两个门
            OpenPointDTO firstSegOpenPoint = getPointByDistanceOnSegment(closedFirstX,closedFirstY,
                    k,closedA,deltaDistance,closedSecondX,closedSecondY);
            OpenPointDTO secondSegOpenPoint = getPointByDistanceOnSegment(closedSecondX,closedSecondY,
                    k,closedA,deltaDistance,closedFirstX,closedFirstY);
            Double firstA = getAByPointAndK(firstSegOpenPoint.getDestX(), firstSegOpenPoint.getDestY(), verticalK);
            Double secondA = getAByPointAndK(secondSegOpenPoint.getDestX(), secondSegOpenPoint.getDestY(), verticalK);
            OpenPointDTO firstDistance = getPointByDistance(firstSegOpenPoint.getDestX(), firstSegOpenPoint.getDestY(), verticalK, firstA, doorLength);
            OpenPointDTO secondDistance = getPointByDistance(secondSegOpenPoint.getDestX(), secondSegOpenPoint.getDestY(), verticalK, secondA, doorLength);
            result.add(OpenPointDTO.builder().destX(firstDistance.getFirstX()).destY(firstDistance.getFirstY()).build());
            result.add(OpenPointDTO.builder().destX(firstDistance.getSecondX()).destY(firstDistance.getSecondY()).build());
            result.add(OpenPointDTO.builder().destX(secondDistance.getFirstX()).destY(secondDistance.getFirstY()).build());
            result.add(OpenPointDTO.builder().destX(secondDistance.getSecondX()).destY(secondDistance.getSecondY()).build());
            return result;
        }
        // 中间开门
        OpenPointDTO openPoint = getPointByDistanceOnSegment(closedFirstX, closedFirstY,
                k, closedA, distance / 2, closedSecondX, closedSecondY);
        Double verticalA = getAByPointAndK(openPoint.getDestX(), openPoint.getDestY(), verticalK);
        OpenPointDTO firstDistance = getPointByDistance(openPoint.getDestX(), openPoint.getDestY(), verticalK, verticalA, doorLength);
        result.add(OpenPointDTO.builder().destX(firstDistance.getFirstX()).destY(firstDistance.getFirstY()).build());
        result.add(OpenPointDTO.builder().destX(firstDistance.getSecondX()).destY(firstDistance.getSecondY()).build());
        return result;
    }
    /***
     * Description: 根据距离和坐标点计算打开后的坐标点
     * @param baseX : 第一个坐标点的Y值
     * @param baseY : 第一个坐标点的Y值
     * @param k : 斜率
     * @param a : a值
     * @param distance : 距离
     * @param vecX : 第二个坐标点的X值
     * @param vecY : 第二个坐标点的Y值
     * @return : com.persagy.dmp.rwd.digital.domain.OpenPointDTO
     * @author : lijie
     * @date :2021/10/11 20:54
     * Update By lijie 2021/10/11 20:54
     */
    private OpenPointDTO getPointByDistanceOnSegment(double baseX, double baseY,
                                                     Double k, Double a,
                                                     double distance,
                                                     double vecX,
                                                     double vecY) {
        OpenPointDTO openPoint = getPointByDistance(baseX,baseY,k,a,distance);
        Double distanceOne = getSegmentDistance(openPoint.getFirstX(), openPoint.getFirstY(), vecX, vecY);
        Double distanceTwo = getSegmentDistance(openPoint.getSecondX(), openPoint.getSecondY(), vecX, vecY);
        if (distanceOne>distanceTwo){
            return OpenPointDTO.builder().destX(openPoint.getSecondX()).destY(openPoint.getSecondY()).build();
        }
        return OpenPointDTO.builder().destX(openPoint.getFirstX()).destY(openPoint.getFirstY()).build();
    }
    /***
     * Description: 根据坐标和线段长度计算坐标点
     * @param baseX : x值
     * @param baseY : y值
     * @param k : 斜率
     * @param a : a值
     * @param distance : 距离
     * @return : com.persagy.dmp.rwd.digital.domain.OpenPointDTO
     * @author : lijie
     * @date :2021/10/11 20:58
     * Update By lijie 2021/10/11 20:58
     */
    private OpenPointDTO getPointByDistance(double baseX, double baseY, Double k, Double a, double distance) {
        if (null==k){
            return OpenPointDTO.builder()
                    .firstX(baseX)
                    .firstY(baseY+distance)
                    .secondX(baseX)
                    .secondY(baseY-distance).build();
        }
        double vectorX1 = Math.sqrt(distance*distance/(1+k*k));
        double vectorX2 = -vectorX1;
        double vectorY1 = k*vectorX1+a;
        double vectorY2 = k*vectorX2+a;
        return OpenPointDTO.builder()
                .firstX(baseX+vectorX1)
                .firstY(baseY+vectorY1)
                .secondX(baseX+vectorX2)
                .secondY(baseY+vectorY2).build();
    }

    /***
     * Description: 获得直线方程式中的a值(y=kx+a)
     * @param x : x坐标值
     * @param y : y坐标值
     * @param k : 斜率
     * @return : java.lang.Double
     * @author : lijie
     * @date :2021/10/11 20:34
     * Update By lijie 2021/10/11 20:34
     */
    private Double getAByPointAndK(double x, double y, Double k) {
        if (null==k){
            return 0.0;
        }
        return y-k*x;
    }

    /***
     * Description: 获得垂直线的斜率
     * @param k : 斜率
     * @return : java.lang.Double
     * @author : lijie
     * @date :2021/10/11 20:31
     * Update By lijie 2021/10/11 20:31
     */
    private Double getVerticalK(Double k) {
        if (null==k){
            return 0.0;
        }
        if (0.0==k){
            return null;
        }
        return 1/k;
    }

    /***
     * Description: 计算两个坐标点之间的距离
     * @param firstX : 第一个坐标点的x值
     * @param firstY : 第一个坐标点的Y值
     * @param secondX : 第二个坐标点的x值
     * @param secondY : 第二个坐标点的y值
     * @return : void
     * @author : lijie
     * @date :2021/10/11 20:24
     * Update By lijie 2021/10/11 20:24
     */
    private Double getSegmentDistance(Double firstX, Double firstY, Double secondX, Double secondY) {
        double xDistance = firstX - secondX;
        double yDistance = firstY - secondY;
        return Math.sqrt(xDistance*xDistance+yDistance*yDistance);
    }

    /***
     * Description: 校验两个坐标点是否合理
     * @param firstPoint : 第一个坐标点
     * @param secondPoint : 第二个坐标点
     * @return : boolean
     * @author : lijie
     * @date :2021/10/11 20:06
     * Update By lijie 2021/10/11 20:06
     */
    private Boolean checkPointIsValid(ObjectNode firstPoint, ObjectNode secondPoint) {
        // 1.校验是否为空
        if (null==firstPoint
                || null==secondPoint
                || !firstPoint.has("X")
                || !firstPoint.has("Y")
                || !secondPoint.has("X")
                || !secondPoint.has("Y")){
            return false;
        }
        // 2.校验是否为数字
        if (!NumberUtil.isNumber(firstPoint.get("X").asText())
                || !NumberUtil.isNumber(firstPoint.get("Y").asText())
                || !NumberUtil.isNumber(secondPoint.get("X").asText())
                || !NumberUtil.isNumber(secondPoint.get("Y").asText())){
            return false;
        }
        // 3.校验坐标点是否相等
        return firstPoint.get("X").equals(secondPoint.get("X")) && firstPoint.get("Y").equals(secondPoint.get("Y"));
    }

    /***
     * Description: 根据请求参数和id集合删除关系
     * @param calculatingDTO : 请求参数
     * @return : void
     * @author : lijie
     * @date :2021/9/11 16:13
     * Update By lijie 2021/9/11 16:13
     */
    private void deleteRelDataByCondition(CalculatingDTO calculatingDTO) {
        QueryWrapper<ObjectRelation> queryWrapper = ObjectDigitalCriteriaHelper.createCommonQueryWrapper(null,calculatingDTO);
        queryWrapper.eq(StrUtil.toUnderlineCase(ObjectRelation.GRAPH_CODE_HUM),calculatingDTO.getGraphCode());
        queryWrapper.eq(StrUtil.toUnderlineCase(ObjectRelation.REL_CODE_HUM),calculatingDTO.getRelCode());
        objectRelationService.delete(queryWrapper);
    }
}
