package com.its.safe.service;

import com.its.app.utils.Elapsed;
import com.its.app.utils.TimeUtils;
import com.its.safe.api.dto.AgipObeLoc;
import com.its.safe.config.ServerConfig;
import com.its.safe.dto.its.Location;
import com.its.safe.dto.its.NearNodeDto;
import com.its.safe.dto.its.TbNodeDto;
import com.its.safe.process.DbmsDataProcess;
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) {

    }

    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) / (float)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();
        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 < maxDistance) {
                maxDistance = fDistance;
                nearNode.setNodeId(node.getNodeId());
                nearNode.setDistance(fDistance);
            }
        }
        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());
            }
        }
    }
}
