package com.its.dsrc.service;

import com.its.app.AppUtils;
import com.its.app.utils.SysUtils;
import com.its.dsrc.config.RunningConfig;
import com.its.dsrc.dao.mapper.RseSectMapper;
import com.its.dsrc.entity.TbRseSect;
import com.its.dsrc.entity.TbRseSectPassHs;
import com.its.dsrc.entity.TbRseSectTraf;
import com.its.dsrc.global.AppRepository;
import com.its.dsrc.process.DbmsData;
import com.its.dsrc.process.DbmsDataProcess;
import com.its.dsrc.process.DbmsDataType;
import com.its.dsrc.vo.voDsrcObuPassInfo;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.time.FastDateFormat;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.PostConstruct;
import java.text.ParseException;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

@Slf4j
@Getter
@RequiredArgsConstructor
@Service
@Transactional(rollbackFor = {Exception.class})
public class RseSectService {

    private RseSectMapper rseSectMapper;
    private final RunningConfig runningConfig;
    private final DbmsDataProcess dbmsDataProcess;

    // OBU DSRC 차량통과정보, { SECT_ID, [OBU_ID/passinfo, OBU_ID/passinfo, ...] }
    private Map<String, Map<String, voDsrcObuPassInfo>> dsrcObuPassMap = null;
    // OBU Travel Time History, { SECT_ID, [seconds/endpasstime, ...] }
    private Map<String, SortedMap<Integer, String>> travelTimeHistoryMap = null;

    @PostConstruct
    private void init() {
        log.info("RseSectService.init: Start.");
        this.rseSectMapper = (RseSectMapper) AppUtils.getBean(RseSectMapper.class);
        //this.dbmsJobProcess = (DbmsJobProcess) AppUtils.getBean(DbmsJobProcess.class);
        this.dsrcObuPassMap = Collections.synchronizedMap(new ConcurrentHashMap<>());
        this.travelTimeHistoryMap = Collections.synchronizedMap(new ConcurrentHashMap<>());
        //loadMaster();
        log.info("RseSectService.init: ..End.");
    }

    public void loadMaster() {
        try {
            List<TbRseSect> objList = this.rseSectMapper.selectRseSectAll();
            log.info("RseSectService.selectAll(), {} EA", objList.size());
            //RseRepository.getInstance().dsrcSectMap.clear();
            for (TbRseSect obj : objList) {
                AppRepository.getInstance().rseSectMap.put(obj.getRSE_SECT_ID(), obj);
            }
            setDsrcSectStartMap(objList);
            setDsrcSectEndMap(objList);
        }
        catch (Exception e) {
            log.error("dsrcCtlrMap.selectAll: {}", e);
        }

        // DSRC 통과 정보 저장 맵을 초기화한다.
        initDsrcObuPassMap();

        for (Map.Entry<String, TbRseSect> obj : AppRepository.getInstance().rseSectMap.entrySet()) {
            log.info(obj.toString());
        }
    }

    public void setDsrcSectStartMap(List<TbRseSect> dsrcSectList) {
        if (AppRepository.getInstance().rseSectStartMap == null) {
            log.error("setDsrcSectStartMap: DsrcRepository.getInstance().dsrcSectStartMap is NULL");
            return;
        }
        if (dsrcSectList == null) {
            log.error("setDsrcSectStartMap: dsrcSectList is NULL");
            return;
        }

        AppRepository.getInstance().rseSectStartMap.clear();
        for (TbRseSect vo : dsrcSectList) {
            String startDsrcId = vo.getSTRT_ID();
            String sectId = vo.getRSE_SECT_ID();
            AppRepository.getInstance().rseSectStartMap.put(startDsrcId, sectId);
        }
        log.info("setDsrcSectStartMap: {}", AppRepository.getInstance().rseSectStartMap.toString());
    }

    public void setDsrcSectEndMap(List<TbRseSect> dsrcSectList) {
        if (AppRepository.getInstance().rseSectEndMap == null) {
            log.error("setDsrcSectEndMap: DsrcRepository.getInstance().dsrcSectEndMap is NULL");
            return;
        }
        if (dsrcSectList == null) {
            log.error("setDsrcSectEndMap: dsrcSectList is NULL");
            return;
        }

        AppRepository.getInstance().rseSectEndMap.clear();
        for (TbRseSect vo : dsrcSectList) {
            String endDsrcId = vo.getEND_ID();
            String sectId = vo.getRSE_SECT_ID();
            AppRepository.getInstance().rseSectEndMap.put(endDsrcId, sectId);
        }
        log.info("setDsrcSectEndMap: {}", AppRepository.getInstance().rseSectEndMap.toString());
    }

    public void initDsrcObuPassMap() {

        this.dsrcObuPassMap.clear();
        this.travelTimeHistoryMap.clear();
        String currTime = SysUtils.getSysTime();
        for (Map.Entry<String, TbRseSect> obj : AppRepository.getInstance().rseSectMap.entrySet()) {
            //<obuId, passInfo>
            Map<String, voDsrcObuPassInfo> temp = Collections.synchronizedMap(new ConcurrentHashMap<>());
            this.dsrcObuPassMap.put(obj.getValue().getRSE_SECT_ID(), temp);
            SortedMap<Integer, String> temp2 = Collections.synchronizedSortedMap(new TreeMap<>());
            temp2.put(Integer.valueOf(240), currTime);
            this.travelTimeHistoryMap.put(obj.getValue().getRSE_SECT_ID(), temp2);
        }
    }

     /**
     * DSRC 구간 소통정보 생성
     * 스케쥴러에 의해 실행됨, 매 5분 정주기
     */
    public void makeDsrcSectTraffic() {

        log.info("makeDsrcSectTraffic: Start...");

        int historyMinute = this.runningConfig.getFilteringHistoryMinute();
        float minSpeed = (float)this.runningConfig.getFilteringMinSpeed();
        float maxSpeed = (float)this.runningConfig.getFilteringMaxSpeed();
        String currentTm = SysUtils.getSysTime();

        // 필터링 초기화(, )OBU 통과 수집 이력 목록 초기화)
        for (Map.Entry<String, TbRseSect> obj : AppRepository.getInstance().rseSectMap.entrySet()) {
            String dsrcSectId = obj.getValue().getRSE_SECT_ID();
            log.debug("makeDsrcSectTraffic: initFiltering, SECT_ID: {}", dsrcSectId);

            initFiltering(dsrcSectId, historyMinute);
        }

        // OBU 구간 통행정보 목록
        List<TbRseSectPassHs> sectPassList = Collections.synchronizedList(new ArrayList<>());
        // 구간 소통정보 목록
        List<TbRseSectTraf> sectTrafList = Collections.synchronizedList(new ArrayList<>());

        // OBU 구간 통과 목록을 이용하여 교통정보 생성
        for (String dsrcSectId : this.dsrcObuPassMap.keySet()) {
            // 구간 ID로 구간정보 조회
            TbRseSect dsrcSect = AppRepository.getInstance().rseSectMap.get(dsrcSectId);
            if (dsrcSect == null) {
                log.error("makeDsrcSectTraffic: SECT_ID: {}, not found", dsrcSectId);
                continue;
            }

            log.debug("makeDsrcSectTraffic: SECT_ID: {}", dsrcSectId);

            int distance = 0;
            int totalCount = 0;
            int missMatchCount = 0;
            int totalSeconds = 0;
            int calcCount = 0;
            int avgSpeed = 0;

            // 구간 거리 체크
            distance = dsrcSect.getSECT_LNGT();
            if (distance <= 0) {
                log.error("makeDsrcSectTraffic: SECT_ID: {}, distance: {}, distance error", dsrcSectId, distance);
                continue;
            }

            // 구간 통과 차량 목록 조회
            Map<String, voDsrcObuPassInfo> obuPassInfoMap = this.dsrcObuPassMap.get(dsrcSectId);
            if (obuPassInfoMap == null) {
                log.warn("makeDsrcSectTraffic: SECT_ID: {}, not found", dsrcSectId);
                continue;
            }

            // 전체 통과 차량
            totalCount = obuPassInfoMap.size();
            if (totalCount <= 0) {
                log.error("makeDsrcSectTraffic: SECT_ID: {}, total pass volume zero", dsrcSectId);
                continue;
            }

            log.info("makeDsrcSectTraffic: SECT_ID: {}, pass obu: {} EA", dsrcSectId, totalCount);

            // 구간을 통과한 차량목록을 이용하여 구간 소통정보 생성
            for (String obuId : obuPassInfoMap.keySet()) {
                try {
                    voDsrcObuPassInfo dsrcObuPassInfo = obuPassInfoMap.get(obuId);
                    if (dsrcObuPassInfo == null) {
                        log.error("makeDsrcSectTraffic: SECT_ID: {}, obuId: {}, sect passing car get error", dsrcSectId, obuId);
                        missMatchCount++;
                        continue;
                    }

                    // 종점 DSRC 를 통과한 시각이 없으면 맷칭이 안된것임
                    if (dsrcObuPassInfo.getEndDsrcPassTm() == null) {
                        missMatchCount++;
                        if (dsrcObuPassInfo.getStartDsrcPassTm() != null) {
                            // 시점 통과시각과 현재시각이 기준값 이상 차이나면 리스트에서 삭제
                            if (gapTime(dsrcObuPassInfo.getStartDsrcPassTm(), currentTm, Calendar.MINUTE) > historyMinute) {
                                log.error("makeDsrcSectTraffic: Miss Matching, Old Pass Info delete: sect[{}], obu[{}]", dsrcSectId, dsrcObuPassInfo.toString());
                                this.dsrcObuPassMap.get(dsrcSectId).remove(obuId);
                            }
                        }
                        //log.error("makeDsrcSectTraffic: SECT_ID: {}, obuId: {}, end dsrc pass time null", dsrcSectId, obuId);
                        continue;   // 종료시각이 없으므로 사용하지 않음
                    }

                    // 시점 DSRC 를 통과한 시각이 없으면 DSRC 통과시 정보를 수집 못한것임(리스트에서 삭제)
                    if (dsrcObuPassInfo.getStartDsrcPassTm() == null) {
                        missMatchCount++;
                        this.dsrcObuPassMap.get(dsrcSectId).remove(obuId);
                        //log.error("makeDsrcSectTraffic: SECT_ID: {}, obuId: {}, start dsrc pass time null", dsrcSectId, obuId);
                        continue;   // 이상 수집정보로 사용하지 않음
                    }

                    // 시점 및 종점 통과시각이 존재하는 경우 소통정보에 사용
                    int passSeconds = gapTime(dsrcObuPassInfo.getStartDsrcPassTm(), dsrcObuPassInfo.getEndDsrcPassTm(), Calendar.SECOND);
                    if (passSeconds <= 0) {
                        // 구간 운행시각이 이상일 경우 목록에서 삭제
                        missMatchCount++;
                        this.dsrcObuPassMap.get(dsrcSectId).remove(obuId);
                        log.error("makeDsrcSectTraffic: SECT_ID: {}, obuId: {}, start/end passSeconds error: {} sec.", dsrcSectId, obuId, passSeconds);
                        continue;   // 사용하지 않음
                    }

                    // 통행속도 계산(최소/최대 속도값 확인)
                    float speed = (float)((distance * 3600) / (passSeconds * 1000L));
                    if (minSpeed > speed || maxSpeed < speed) {
                        missMatchCount++;
                        this.dsrcObuPassMap.get(dsrcSectId).remove(obuId);
                        log.error("makeDsrcSectTraffic: SECT_ID: {}, obuId: {}, calc speed error: spd: {}, min: {}, max: {}.", dsrcSectId, obuId, speed, minSpeed, maxSpeed);
                        continue;
                    }

                    if (!doFiltering(dsrcSectId, passSeconds, dsrcObuPassInfo.getEndDsrcPassTm(), obuId)) {
                        missMatchCount++;
                        this.dsrcObuPassMap.get(dsrcSectId).remove(obuId);
                        //log.error("makeDsrcSectTraffic: SECT_ID: {}, obuId: {}, doFiltering failed.", dsrcSectId, obuId);
                        continue;
                    }

                    totalSeconds = (totalSeconds + passSeconds);
                    calcCount++;

                    // 처리된 통과정보를 목록에서 삭제
                    this.dsrcObuPassMap.get(dsrcSectId).remove(obuId);

                    // OBU 구간 통행 정보를 저장
                    TbRseSectPassHs passVo = new TbRseSectPassHs();
                    passVo.setRSE_SECT_ID(dsrcSectId);
                    passVo.setCRTN_DT(currentTm);
                    passVo.setOBU_IDNT_NMBR(obuId);
                    passVo.setSTRT_SPOT_PASS_DT(dsrcObuPassInfo.getStartDsrcPassTm());
                    passVo.setEND_SPOT_PASS_DT(dsrcObuPassInfo.getEndDsrcPassTm());
                    sectPassList.add(passVo);
                    log.info("makeDsrcSectTraffic: SECT_ID: {}, obuId: {}, totalSeconds: {}, calcCount: {}, passSeconds: {}.", dsrcSectId, obuId, totalSeconds, calcCount, passSeconds);
                }
                catch(Exception e) {
                    missMatchCount++;
                    this.dsrcObuPassMap.get(dsrcSectId).remove(obuId);
                    log.error("makeDsrcSectTraffic: SECT_ID: {}, obuId: {}, Exception: {}.", dsrcSectId, obuId, e.toString());
                }
            } // 구간 통과 차량 목록

            if (totalCount <= missMatchCount) {
                log.error("makeDsrcSectTraffic: SECT_ID: {}, make traffic error, total: {} EA, missMatchCount: {} EA.", dsrcSectId, totalCount, missMatchCount);
                continue;
            }

            // 구간을 통과한 모든 OBU를 이용하여 소통정보 생성
            int matchedCount = totalCount - missMatchCount;
            int trvlSeconds = totalSeconds / matchedCount;
            if (matchedCount <= 0) {
                continue;   // 여기들어올일은 없음....
            }
            try {
                float fSpeed = (float)((distance*3600) / (trvlSeconds*1000L));
                avgSpeed = Math.round(fSpeed);
            }
            catch(Exception e) {
                log.error("makeDsrcSectTraffic: SECT_ID: {}, calc speed, Exception: {}.", dsrcSectId, e.toString());
                continue;
            }

            // 구간 소통정보 저장
            TbRseSectTraf sectTraf = new TbRseSectTraf();
            sectTraf.setRSE_SECT_ID(dsrcSectId);
            sectTraf.setPRCN_DT(currentTm);
            sectTraf.setTFVL(matchedCount);
            sectTraf.setTRVL_SPED(avgSpeed);
            sectTraf.setTRVL_HH(trvlSeconds);
            sectTrafList.add(sectTraf);

            log.info("makeDsrcSectTraffic: SECT_ID: {}, make traffic: {}", dsrcSectId, sectTraf.toString());
        } // 구간 목록

        // 구간소통정보 목록을 데이터베이스에 저장
        if (sectTrafList.size() > 0) {
            this.dbmsDataProcess.add(new DbmsData(DbmsDataType.DBMS_DATA_SECT_TRAF_HS, false, sectTrafList));
        }

        // OBU 구간 통행 정보를 데이터베이스에 저장
        if (sectPassList.size() > 0) {
            this.dbmsDataProcess.add(new DbmsData(DbmsDataType.DBMS_DATA_SECT_PASS_HS, false, sectPassList));
        }

        log.info("makeDsrcSectTraffic: End.....");
    }

    /**
     * 사분위편차를 활용하여 이상치를 제거한다.
     * 사분위편차 및 관리도 모형에 의한 GPS 수집기반 구간통행속도 데이터 이상치 제거방안 연구, 한국ITS학회논문지, 제7권, 제6호 (2008년 12월)
     *
     * 관측값을 순서대로 정렬한 후
     * 25% 위치한 값을 1사분위수(Q1)
     * 75% 위치한 값을 3사분위수(Q3)
     * 3사분위수와 1사분위수의 차이를 사분위편차(IQR)
     * 이상치제거: 1사분위수에서 사분위편차의 1.5배만큼을 뺀 값보다 작거나 3사분위수에서 사분위편차의 1.5배만큼을 더한 값보다 큰 값을 이상치로 판단함
     * 하한 기준값 : Q1 – 1.5ⅹIQR
     * 상한 기준값 : Q3 + 1.5ⅹIQR
     */
    boolean doFiltering(String dsrcSectId, int travelSeconds, String endDateTime, String obuId) {
        SortedMap<Integer, String> travelHistoryMap = this.travelTimeHistoryMap.get(dsrcSectId);
        List<Integer> list = Collections.synchronizedList(new ArrayList<>());

        Iterator<Integer> it = travelHistoryMap.keySet().iterator();
        while (it.hasNext()) {
            try {
                int travelTime = ((Integer) it.next()).intValue();
                if (travelTime > 0)
                {
                    list.add(travelTime);
                }
            }
            catch(Exception e) {
                log.error("doFiltering: dsrcSectId: {}, {}", dsrcSectId, e.toString());
            }
        }

        int size = list.size();
        if (size < 3) {
            this.travelTimeHistoryMap.get(dsrcSectId).put(travelSeconds, endDateTime);
            log.warn("doFiltering: failed, ==> list size small: {} EA. SECT_ID: {}, obuId: {}", size, dsrcSectId, obuId);
            return false;
        }

        int Q1Index = (int)(size * (1.0F / 4.0F));  // 1사분위 인덱스
        int Q3Index = (int)(size * (3.0F / 4.0F));  // 3사분위 인덱스

        int Q1 = list.get(Q1Index).intValue();      // 1사분위 값
        int Q3 = list.get(Q3Index).intValue();      // 3사분위 값
        int IQR = Math.abs(Q3 - Q1);
        double lowerBoundD = Q1 - 1.5D * IQR;
        double upperBoundD = Q3 + 1.5D * IQR;

        if (lowerBoundD <= 0.0D)
            lowerBoundD = 0;

        long lowerBound = Math.round(lowerBoundD);
        long upperBound = Math.round(upperBoundD);

        this.travelTimeHistoryMap.get(dsrcSectId).put(Integer.valueOf(travelSeconds), endDateTime);

        log.debug("doFiltering: Q1: {}, Q3: {}, IQR: {}, travelSeconds: {}, LOWER: {}, UPPER: {}",
                Integer.valueOf(Q1), Integer.valueOf(Q3), Integer.valueOf(IQR), Integer.valueOf(travelSeconds), Long.valueOf(lowerBound), Long.valueOf(upperBound));

        if (travelSeconds >= lowerBound && travelSeconds <= upperBound) {
            return true;
        }
        return false;
    }

    /**
     * 필터링(이상치제거)를 수행하기 전에 이상치제거 초기화 작업을 수행한다.
     * OBU 통행 이력 맵에서 가공시각 이전 수집된 정보를 모두 삭제한다.
     */
    void initFiltering(String dsrcSectId, int historyMinute) {
        SortedMap<Integer, String> travelHistoryMap = this.travelTimeHistoryMap.get(dsrcSectId);
        SortedMap<Integer, String> tempMap = Collections.synchronizedSortedMap(new TreeMap<>());
        tempMap.putAll(travelHistoryMap);

        String currentTime = SysUtils.getSysTime();
        Iterator<Integer> it = tempMap.keySet().iterator();
        while (it.hasNext()) {
            try {
                int travelTime = ((Integer) it.next()).intValue();
                if (travelTime == 0)
                    continue;

                String endDateTime = tempMap.get(Integer.valueOf(travelTime));
                if (endDateTime == null || endDateTime.equals(""))
                    continue;

                if (gapTime(endDateTime, currentTime, Calendar.MINUTE) > historyMinute) {
                    // 기준시간 이상 된 이력정보를 리스트에서 삭제
                    this.travelTimeHistoryMap.get(dsrcSectId).remove(Integer.valueOf(travelTime));
                }
            }
            catch(Exception e) {
                log.error("initFiltering: dsrcSectId: {}, {}", dsrcSectId, e.toString());
            }
        }
    }

    public int gapTime(String startTm, String endTm, int calcType) {

        if (startTm == null || startTm.equals("") || startTm.length() != 14) {
            return -1;
        }
        if (endTm == null || endTm.equals("") || endTm.length() != 14) {
            return -1;
        }

        FastDateFormat fastDateFormat = FastDateFormat.getInstance("yyyyMMddHHmmss", new Locale("ko_KR")); //Locale.getDefault());

        Date startDateTime, endDateTime;
        try {
            startDateTime = fastDateFormat.parse(startTm);
            endDateTime = fastDateFormat.parse(endTm);
        } catch (ParseException e) {
            e.printStackTrace();
            return -1;
        }

        GregorianCalendar gcStartDateTime = new GregorianCalendar();
        GregorianCalendar gcEndDateTime = new GregorianCalendar();
        gcStartDateTime.setTime(startDateTime);
        gcEndDateTime.setTime(endDateTime);

        long gap = gcEndDateTime.getTimeInMillis() - gcStartDateTime.getTimeInMillis();
        long hour = gap / 1000L / 60L / 60L;
        long min = gap / 1000L / 60L;
        long second = gap / 1000L;
        if (Calendar.HOUR == calcType)
            return (int)hour;
        if (Calendar.MINUTE == calcType)
            return (int)min;
        if (Calendar.SECOND == calcType)
            return (int)second;
        return -1;
    }

}
