package com.its.bis.service.worker;

import com.its.app.utils.Elapsed;
import com.its.app.utils.SysUtils;
import com.its.app.utils.TimeUtils;
import com.its.bis.api.dto.AgipObeLoc;
import com.its.bis.config.ServerConfig;
import com.its.bis.dto.*;
import com.its.bis.entity.TbBisLinkTrafClct;
import com.its.bis.entity.TbBisVehLoc;
import com.its.bis.process.DbmsData;
import com.its.bis.process.DbmsDataProcess;
import com.its.bis.process.DbmsDataType;
import com.its.bis.service.AppRepositoryService;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.MDC;

import java.util.Map;
import java.util.concurrent.LinkedBlockingQueue;

@Slf4j
public class BisAgipWorker implements Runnable {

    private long avgTime = 0;
    private int idx;
    private int qSize;

    private final ServerConfig config;
    private final AppRepositoryService repoService;
    private final DbmsDataProcess dbmsDataProcess;
    private final LinkedBlockingQueue<AgipObeLoc> DATA_QUEUE;

    public BisAgipWorker(int idx, int qSize, ServerConfig config, AppRepositoryService repoService, DbmsDataProcess dbmsDataProcess) {
        this.idx = idx;
        this.qSize = qSize;
        this.config = config;
        this.repoService = repoService;
        this.dbmsDataProcess = dbmsDataProcess;
        this.DATA_QUEUE = new LinkedBlockingQueue<>(qSize);
    }

    protected long calcProcessTime(long recvTime) {
        long jobTime = System.nanoTime() - recvTime;
        if (this.avgTime == 0) {
            this.avgTime = jobTime;
        }
        else {
            this.avgTime = (this.avgTime + jobTime) / 2L;
        }
        return this.avgTime;
    }

    /*
     *  작업큐에 데이터 추가
     */
    public boolean add(AgipObeLoc obeLoc) {
        boolean offer = false;
        try {
            //offer => full -> return
            //add   => full -> wait
            offer = this.DATA_QUEUE.offer(obeLoc);
            if (!offer) {
                MDC.put("id", obeLoc.getDeviceId());
                log.error("Packet Queue.offer: {}/{}, Queue Full: {} EA, {}, {}",
                        obeLoc.getDeviceId(), this.DATA_QUEUE.size(), this.qSize, TimeUtils.elapsedTime(obeLoc.getRcvNanoSeconds()), Thread.currentThread().getName());
                MDC.clear();
            }
        } catch (Exception e) {
            MDC.put("id", obeLoc.getDeviceId());
            log.error("Packet Queue.offer: Exception: {}, {}, {}", obeLoc.getDeviceId(), Thread.currentThread().getName(), e.getMessage());
            MDC.clear();
        }
        return offer;
    }

    public void process(AgipObeLoc obeLoc) {
        // 샘플데이터: latitude=36.56392018, longitude=128.74352336
        Location currLocation = Location.builder()
                .mDistance(0)
                .mInitialBearing(0)
                .mFinalBearing(0)
                .mLatitude(obeLoc.getGnssInfo().getLongitude())
                .mLongitude(obeLoc.getGnssInfo().getLatitude())
//                .mLatitude(obeLoc.getGnssInfo().getLatitude())
//                .mLongitude(obeLoc.getGnssInfo().getLongitude())
                .build();

        BisObe bisObe = this.repoService.getObeMap(obeLoc.getDeviceId());
        if (bisObe == null) {
            bisObe = BisObe.builder()
                    .deviceId(obeLoc.getDeviceId())
                    .carId(obeLoc.getCarId())
                    .carNumber(obeLoc.getCarNumber())
                    .location(currLocation)
                    .moveDist(0)
                    .stNode(NodeRunDto.builder().nodeId(0L).inTime(0L).outTime(0L).waitTm(0).build())
                    .edNode(NodeRunDto.builder().nodeId(0L).inTime(0L).outTime(0L).waitTm(0).build())
                    .runNode(NodeRunDto.builder().nodeId(0L).inTime(0L).outTime(0L).waitTm(0).build())
                    .runLink(LinkRunDto.builder().linkId(0L).fNodeId(0L).tNodeId(0L).trvlHh(0).build())
                    .build();
            this.repoService.putObeMap(bisObe);
        }
        else {
            bisObe.setLocation(currLocation);
        }

        bisObe.setHeight(obeLoc.getGnssInfo().getHeight());
        bisObe.setSpeed(obeLoc.getGnssInfo().getSpeed());
        bisObe.setAngle(obeLoc.getGnssInfo().getAngle());

        MDC.put("id", bisObe.getDeviceId());

        /**************************************************************************************************
            노드진입1 - 노드1운행 - 노드진출1 ---- 구간 운행중 ---- 노드진입2 - 노드2운행 - 노드진출2
            BIS 구간 운행시간 = 노드진입2 - 노드진출1
        ***************************************************************************************************/

        /**
         * 최근접 통과노드정보를 구한다.
         */
        long currSysTime = System.currentTimeMillis();
        NearNodeDto passNode = getNearNode(currLocation);
        Long stNodeId = bisObe.getStNode().getNodeId();
        Long edNodeId = bisObe.getEdNode().getNodeId();
        Long runNodeId = bisObe.getRunNode().getNodeId();
        Long passNodeId = passNode.getNodeId();
        log.info("R1.DEVICE ID: {}, 시작노드: {}, 종료노드: {}, 현재노드: {}", bisObe.getDeviceId(), stNodeId, edNodeId, passNodeId);
        if (passNodeId != 0L) {
            // 노드 진입/운행
            if (runNodeId == 0L) {
                // 운행노드 정보가 없는 경우
                bisObe.getRunNode().setNodeId(passNodeId);
                bisObe.getRunNode().setInTime(currSysTime);
                log.info("R2.DEVICE ID: {}, 운행노드 도착: {}", bisObe.getDeviceId(), passNodeId);
            }
            bisObe.getRunNode().setOutTime(currSysTime);

            if (stNodeId == 0L) {
                // 운행중인 시작노드가 없는 경우 - 진입 노드정보 설정
                bisObe.getStNode().setNodeId(passNodeId);
                bisObe.getStNode().setInTime(currSysTime);
                bisObe.getStNode().setOutTime(currSysTime);
                log.info("S1.DEVICE ID: {}, 시작노드 도착: {}", bisObe.getDeviceId(), passNodeId);
            }
            else {
                // 운행중인 노선의 시작노드가 존해하는 경우
                if (stNodeId.equals(passNodeId)) {
                    // 시작노드와 현재노드가 같은경우 - 운행중 노드의 진출시각을 업데이트
                    bisObe.getStNode().setOutTime(currSysTime);
                    log.info("S2.DEVICE ID: {}, 시작노드 운행중: {}", bisObe.getDeviceId(), passNodeId);
                } else {
                    if (edNodeId == 0L) {
                        // 운행중인 종료노드가 없는 경우 - 진입 노드정보 설정
                        bisObe.getEdNode().setNodeId(passNodeId);
                        bisObe.getEdNode().setInTime(currSysTime);
                        bisObe.getEdNode().setOutTime(currSysTime);
                        log.info("E1.DEVICE ID: {}, 종료노드 도착: {}", bisObe.getDeviceId(), passNodeId);
                        // 시작노드 진출시각과 종료노드 도착시각을 이용하여 구간운행정보를 생성
                        calcLinkTraffic(bisObe);
                    } else {
                        if (edNodeId.equals(passNodeId)) {
                            // 시작노드와 동일한 노드를 운행중
                            bisObe.getEdNode().setOutTime(currSysTime);
                            log.info("E2.DEVICE ID: {}, 종료노드 운행중: {}", bisObe.getDeviceId(), passNodeId);
                        } else {
                            log.info("X1.DEVICE ID: {}, 시작노드: {}, 종료노드: {}, 현재노드: {}, 구간노드정보 불일치.....", bisObe.getDeviceId(), stNodeId, edNodeId, passNodeId);
                        }
                    }
                }
            }
        }
        else {
            // 근접한 노드를 찾을 수 없을 때...즉 구간을 운행중일때
            if (runNodeId != 0L) {
                // 운행노드 정보가 존해하면 노드운행을 종료(진출)
                bisObe.getRunNode().setOutTime(currSysTime);
                bisObe.getRunNode().calWaitTm();    // 노드 대기시간 계산
                log.info("R3.DEVICE ID: {}, 운행노드 진출: {}, 대기시간: {} sec.", bisObe.getDeviceId(), runNodeId, bisObe.getRunNode().getWaitTm());

                bisObe.getRunNode().init(); // 운행중 노드 초기화

                if (stNodeId.equals(runNodeId)) {
                    bisObe.getStNode().setOutTime(currSysTime);
                    bisObe.getStNode().calWaitTm();    // 노드 대기시간 계산
                    log.info("S3.DEVICE ID: {}, 시작노드 진출: {}, 대기시간: {} sec.", bisObe.getDeviceId(), stNodeId, bisObe.getStNode().getWaitTm());
                }
                else if (edNodeId.equals(runNodeId)) {
                    bisObe.getEdNode().setOutTime(currSysTime);
                    bisObe.getEdNode().calWaitTm();    // 노드 대기시간 계산
                    log.info("E3.DEVICE ID: {}, 종료노드 진출: {}, 대기시간: {} sec.", bisObe.getDeviceId(), edNodeId, bisObe.getEdNode().getWaitTm());

                    // 종료노드를 빠져 나왔기 때문에 종료노드를 시작노드로 변경
                    bisObe.getStNode().copy(bisObe.getEdNode());
                    bisObe.getEdNode().init();
                }
                else {
                    // 시작노드 종료노드
                    log.info("X2.DEVICE ID: {}, 시작노드: {}, 종료노드: {}, 노선운행 노드: {}, 시작/종료/운행 노드 불일치.", bisObe.getDeviceId(), stNodeId, edNodeId, runNodeId);
                }
            }
            else {
                log.info("R3.DEVICE ID: {}, 시작노드: {}, 종료노드: {}, 노선운행 중.", bisObe.getDeviceId(), stNodeId, edNodeId);
            }
        }
        MDC.remove(bisObe.getDeviceId());
        MDC.clear();

        TbBisVehLoc bisVehLoc = TbBisVehLoc.builder()
                .DEVICE_ID(bisObe.getDeviceId())
                .CLCT_DT(SysUtils.getSysTime())
                .LOC_TYPE(obeLoc.getGnssInfo().getType())
                .SPEED(obeLoc.getGnssInfo().getSpeed())
                .HEGT(obeLoc.getGnssInfo().getHeight())
                .ANGLE(obeLoc.getGnssInfo().getAngle())
                .LNG(obeLoc.getGnssInfo().getLongitude())
                .LAT(obeLoc.getGnssInfo().getLatitude())
                .PASS_NODE_ID(passNodeId)
                .build();

        // 버스 현재위치 정보를 업데이트 한다.
        this.dbmsDataProcess.add(new DbmsData(DbmsDataType.DBMS_DATA_UPD_BIS_VEH_LOC, false, bisVehLoc));
    }

    private void calcLinkTraffic(BisObe bisObe) {
        // 시작노드 진출시각과 종료노드 진입시각을 이용하여 구간 소통정보를 생성
        Long stNodeId = bisObe.getStNode().getNodeId();
        Long edNodeId = bisObe.getEdNode().getNodeId();
        long stTime = bisObe.getStNode().getOutTime();  // 시작노드 진출시각
        long edTime = bisObe.getEdNode().getInTime();   // 종료노드 도착시각

        int travelSec = (int) ((edTime - stTime) / 1000);   // 여행시간(초)
        if (travelSec > (this.config.getMaxTrvlMin() * 60)) {
            // 구간 통과 시간이 설정한 값보다 크기 때문에 통행시간을 계산하지 않는다. 지연 또는 정류장대기 등의 이유일것....
            // 현재 통과 노드를 시작노드로 설정한다.
            log.warn("T3.DEVICE ID: {}, 시작노드: {}, 종료노드: {}, 여행시간 오류: {} sec.", bisObe.getDeviceId(), stNodeId, edNodeId, travelSec);
        }
        else {
            TbLinkDto link = this.repoService.getSectMap(stNodeId, edNodeId);
            if (link == null) {
                // 구간을 찾지 못하였음... 중간에 노드를 건너 띈 경우
                log.warn("T2.DEVICE ID: {}, 시작노드: {}, 종료노드: {}, 구간 맷칭 오류...", bisObe.getDeviceId(), stNodeId, edNodeId);
            }
            else {
                // 구간을 통과하였으므로 구간교통정보를 생성한다.
                int speed = calcSpeed(link.getLinkLeng(), travelSec);
                int prcsSpeed = Math.min(100, speed);
                log.info("T1.DEVICE ID: {}, 시작노드: {}, 종료노드: {}, 운행구간: {}, {} m, {}/{} km/h, {} seconds.",
                        bisObe.getDeviceId(), stNodeId, edNodeId, link.getLinkId(), link.getLinkLeng(), speed, prcsSpeed, travelSec);

                TbBisLinkTrafClct bisLinkTrafClct = TbBisLinkTrafClct.builder()
                        .CLCT_DT(SysUtils.getSysTime())
                        .LINK_ID(link.getLinkId())
                        .DEVICE_ID(bisObe.getDeviceId())
                        .SPED(prcsSpeed)
                        .TRVL_HH(travelSec)
                        .ST_NODE_ARR_DT(TimeUtils.millisToString(stTime))
                        .ED_NODE_ARR_DT(TimeUtils.millisToString(edTime))
                        .build();

                // 구간 소통정보 이력으로 저장
                this.dbmsDataProcess.add(new DbmsData(DbmsDataType.DBMS_DATA_CRT_BIS_LINK_TRAF_CLCT, false, bisLinkTrafClct));
            }
        }
    }

    public void report() {
        long avgTime = 0;
        log.info("Packet: Remain Q: {}, Average: {}, {}", this.DATA_QUEUE.size(), TimeUtils.elapsedTimeStr(avgTime), Thread.currentThread().getName());
    }

    public int calcSpeed(int distance, int seconds) {
        int speed;
        if (distance <= 0 || seconds <= 0) {
            return 2;
        }
        speed = (int)(((distance * 3.6) / seconds) + 0.5);
        return speed;
    }

    public NearNodeDto getNearNode(Location loc) {
        Elapsed elapsed = new Elapsed();
        //Location(mDistance=0.0, mInitialBearing=0.0, mFinalBearing=0.0, mLatitude=128.73881251, mLongitude=36.56128759)
        double maxDistance = this.config.getMaxDistance()+1.;
        NearNodeDto nearNode = NearNodeDto.builder().nodeId(0L).distance(maxDistance).build();
        for (Map.Entry<Long, TbNodeDto> e : this.repoService.getEntrySetNode()) {
            TbNodeDto node = e.getValue();
            double fDistance = (float) getDistance(loc.getMLatitude(), loc.getMLongitude(), node.getXCrdn(), node.getYCrdn());
            if (fDistance < node.getBisRadius()) {
                // 노드영역 안에 위치하고 있음....
                if (maxDistance > fDistance) {
                    // 혹시 모를 최근접 노드를 확인....
                    maxDistance = fDistance;
                    nearNode.setNodeId(node.getNodeId());
                    nearNode.setDistance(fDistance);
                }
            }
        }
        if (elapsed.milliSeconds() > 200) {
            log.info("getNearNode: {} ms. {} m, {}", elapsed.milliSeconds(), maxDistance, nearNode);
        }
        return nearNode;
    }

    public double getDistance(double lat1, double lon1, double lat2, double lon2) {
        double EARTH_RADIUS = 6371.0;
        double dLat = Math.toRadians(lat2 - lat1);
        double dLon = Math.toRadians(lon2 - lon1);

        double a = Math.sin(dLat/2)* Math.sin(dLat/2)+ Math.cos(Math.toRadians(lat1))* Math.cos(Math.toRadians(lat2))* Math.sin(dLon/2)* Math.sin(dLon/2);
        double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
        return (EARTH_RADIUS * c * 1000);    // Distance in m
    }

    @Override
    public void run() {
        log.info("{} Start. Q_SIZE: {}", Thread.currentThread().getName(), this.qSize);
        while (true) {
            try {
                AgipObeLoc obeLoc = this.DATA_QUEUE.take();
                if (obeLoc != null) {
                    process(obeLoc);
                }
                else {
                    Thread.yield();
                }
            }
            catch (Exception e) {
                log.error("Exception: {}", e.getMessage());
            }
        }
    }
}
