package com.tsi.sig.server.websocket.kafka;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.tsi.sig.server.config.KafkaConsumerConfig;
import com.tsi.sig.server.websocket.AllTopicCvimHandler;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.apache.kafka.clients.consumer.ConsumerRebalanceListener;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.apache.kafka.clients.consumer.ConsumerRecords;
import org.apache.kafka.clients.consumer.KafkaConsumer;
import org.apache.kafka.common.TopicPartition;
import org.apache.kafka.common.errors.WakeupException;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;

import java.nio.ByteBuffer;
import java.text.SimpleDateFormat;
import java.time.Duration;
import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Collectors;

@Slf4j
@Getter
@Setter
public class AllTopicConsumerThread implements Runnable {

    private AtomicBoolean stopFlag = new AtomicBoolean(true);

    private final Set<WebSocketSession> sessionSet = new HashSet<>();
    private final AllTopicCvimHandler websocketHandler;
    private final KafkaConsumerConfig kafkaConsumerConfig;

    private List<Map<String, Object>> signalData = new ArrayList<Map<String, Object>>();
    private KafkaConsumer<String, ByteBuffer> consumer;
    private String topicName;
    private ObjectMapper mapper;

    public AllTopicConsumerThread(AllTopicCvimHandler websocketHandler, KafkaConsumerConfig kafkaConsumerConfig) {
        this.websocketHandler = websocketHandler;
        this.kafkaConsumerConfig = kafkaConsumerConfig;
        this.consumer = new KafkaConsumer<>(this.kafkaConsumerConfig.getConsumerAllProperties());
        this.topicName = this.kafkaConsumerConfig.getAllTopic();
        this.mapper = new ObjectMapper();
    }

    private void testData() {
        Map<String, Object> header = new HashMap<>();

        header.put("nodeId", 110000001);
        header.put("oprTrans", 0);
        header.put("oprInd", 0);
        header.put("oprTurnoff", 0);
        header.put("oprBlink", 0);
        header.put("oprManual", 0);
        header.put("errScu", 0);
        header.put("errCenter", 0);
        header.put("errCont", 0);
        header.put("date", "2020-12-26 13:22:32");
        header.put("dataCount", 16);
        header.put("counter", 43);

        signalData.add(header);

        int[] light = {1, 2, 3, 7, 1, 2, 3, 7, 1, 2, 3, 7, 1, 2, 3, 7};        // 신호등정보 [직진,좌,보행,유턴]
        //int[]  light  	=  {1,2,3,1,2,3,1,2,3,1,2,3};		// 신호등정보 [직진,좌,보행]
        int[] dirAdd = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};            //연등지

        int[] timeFlag = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};                  //시간정보신뢰성
        int[] walker = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};                    //보행자
        int[] unprotected = {0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};               //비보호 상태
        int[] stts = {3, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1};                      //신호등상태

        int[] dispTm = {30, 30, 20, 20, 20, 40, 40, 20, 10, 10, 01, 10, 10, 10, 10, 10};    //표출시간
        int[] remainTm = {0, 0, 0, 0, 0, 0, 0, 0, 20, 20, 5, 20, 0, 0, 0, 0};               //잔여시간
        int[] dirCode = {10, 10, 10, 10, 20, 20, 20, 20, 30, 30, 30, 30, 40, 40, 40, 40};   //방향코드

        for (int ii = 0; ii < 16; ii++) {
            Map<String, Object> temp = new HashMap<>();
            temp.put("light", light[ii]);
            temp.put("dirAdd", dirAdd[ii]);
            temp.put("timeFlag", timeFlag[ii]);
            temp.put("walker", walker[ii]);
            temp.put("unprotected", unprotected[ii]);
            temp.put("stts", stts[ii]);
            temp.put("dispTm", dispTm[ii]);
            temp.put("remainTm", remainTm[ii]);
            temp.put("dirCode", dirCode[ii]);
            signalData.add(temp);
        }
    }

    private void SetDataMapALL(String key, ByteBuffer value, int valueSize) {
        Map<String, Object> header = new HashMap<>();
        header.put("nodeId", key); //노드 ID

        if (value.get(22) == 0) { //통신OFF일때 [1이 정상 0에러]
            header.put("errorMsg", "통신 OFF");
            return;
        }

        int counter = value.get(39) & 0xff;
        int sttsCount = value.get(40) & 0x7F;           //신호정보개수
        int divFlag = (value.get(40) >> 7) & 0x01;      //분활flag

        int oprTrans = (value.get(37) >> 4) & 0x01;
        int oprInd = (value.get(37) >> 3) & 0x01;
        int oprTurnoff = (value.get(37) >> 2) & 0x01;
        int oprBlink = (value.get(37) >> 1) & 0x01;
        int oprManual = (value.get(37)) & 0x01;

        int errScu = (value.get(38) >> 2) & 0x01;
        int errCenter = (value.get(38) >> 1) & 0x01;
        int errCont = (value.get(38)) & 0x01;

        Date date = (Date) new Date(((long) value.getInt(41) & 0xffffffffL) * 1000L);
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        sdf.setTimeZone(TimeZone.getTimeZone("GMT+9"));
        String formattedDate = sdf.format(date);

        Date connDate = new Date();
        String formatConnDate = sdf.format(connDate);

        header.put("oprTrans", oprTrans);
        header.put("oprInd", oprInd);
        header.put("oprTurnoff", oprTurnoff);
        header.put("oprBlink", oprBlink);
        header.put("oprManual", oprManual);
        header.put("errScu", errScu);
        header.put("errCenter", errCenter);
        header.put("errCont", errCont);
        header.put("date", formattedDate);
        header.put("connDate",formatConnDate);
        header.put("dataCount", sttsCount);
        header.put("counter", counter);
        header.put("divFlag", divFlag);

        this.signalData.add(header);

        int idx = 0;
        for (int ii = 0; ii < sttsCount; ii++) {
            Map<String, Object> temp = new HashMap<>();
            idx = ii * 5;

            int light = (value.get(45 + idx) >> 4) & 0x0F;
            int dirAdd = (value.get(45 + idx)) & 0x0F;
            int timeFlag = (value.get(46 + idx) >> 7) & 0x01;
            int walker = (value.get(46 + idx) >> 6) & 0x01;
            int unprotected = (value.get(46 + idx) >> 3) & 0x01;
            int stts = (value.get(46 + idx)) & 0x07;
            int dispTm = value.get(47 + idx) & 0xff;
            int remainTm = value.get(48 + idx) & 0xff;
            int dirCode = value.get(49 + idx);

            temp.put("light", light);
            temp.put("dirAdd", dirAdd);
            temp.put("timeFlag", timeFlag);
            temp.put("walker", walker);
            temp.put("unprotected", unprotected);
            temp.put("stts", stts);
            temp.put("dispTm", dispTm);
            temp.put("remainTm", remainTm);
            temp.put("dirCode", dirCode);

            this.signalData.add(temp);
        }
    }

    public List<String> formatPartitions(Collection<TopicPartition> partitions) {
        return partitions.stream().map(topicPartition ->
                        String.format("\ntopic: %s, partition: %s", topicPartition.topic(), topicPartition.partition()))
                .collect(Collectors.toList());
    }

    @Override
    public void run() {

        log.info("AllTopicConsumerThread, Start consumer: {}", this.topicName);

        this.stopFlag.set(false);

        try {
            this.consumer.subscribe(Collections.singletonList(this.topicName),
                    new ConsumerRebalanceListener() {
                        @Override
                        public void onPartitionsRevoked(Collection<TopicPartition> partitions) {
                            log.info("AllTopicConsumerThread, onPartitionsRevoked - consumerName: {}, partitions: {}", topicName, formatPartitions(partitions));
                        }

                        @Override
                        public void onPartitionsAssigned(Collection<TopicPartition> partitions) {
                            log.info("AllTopicConsumerThread, onPartitionsAssigned - consumerName: {}, partitions: {}", topicName, formatPartitions(partitions));
                            consumer.seekToEnd(partitions);
                        }
                    });

            while (!this.stopFlag.get() && (!Thread.currentThread().isInterrupted())) {

                ConsumerRecords<String, ByteBuffer> records = this.consumer.poll(Duration.ofMillis(100));
                for (ConsumerRecord<String, ByteBuffer> record : records) {

                    SetDataMapALL(record.key().toString(), record.value(), record.serializedValueSize());
                    try {
                        String jsonInString = this.mapper.writeValueAsString(this.signalData);
                        synchronized (this.sessionSet) {
                            for (WebSocketSession session : this.sessionSet) {
                                try {
                                    this.websocketHandler.sendMessage(session, new TextMessage(jsonInString));
                                    //log.info("AllTopicConsumerThread, Send to: {}, {}, {} bytes.", session.getRemoteAddress().getAddress(), record.key(), jsonInString.length());
                                }
                                catch(Exception e) {
                                    log.error("AllTopicConsumerThread, Send Failed: {}, {}, {} bytes.", session, record.key(), jsonInString.length());
                                }
                            }
                        }
                    }
                    catch(JsonProcessingException e) {
                        log.error("AllTopicConsumerThread, ConsumerThread Json parsing Exception: {}, {}", this.topicName, e.getMessage());
                    }
                    this.signalData.clear();
                }
            }
            log.info("AllTopicConsumerThread, ConsumerThread: {}, stopped.", this.topicName);
        }
        catch(WakeupException e) {
            log.error("AllTopicConsumerThread, Consumer WakeupException: {}, {}", this.topicName, e);
            stop();
        }
        finally {
            //this.consumer.commitSync();
            stop();
            try {
                this.consumer.close();
            } catch(Exception e) {}
        }
    }

    public void addSession(WebSocketSession session) {
        synchronized (this.sessionSet) {
            this.sessionSet.add(session);
        }
    }

    public void removeSession(WebSocketSession session) {
        log.info("AllTopicConsumerThread, remove sessions: {}", this.sessionSet.toString());
        synchronized (this.sessionSet) {
            this.sessionSet.remove(session);
        }
    }

    public void stop() {
        this.stopFlag.set(true);
    }

    public void shutdown() {
        log.info("AllTopicConsumerThread, shutdown wakeup: {}", this.getClass().getSimpleName());
        this.consumer.wakeup();
    }
}
