package com.its.op.service.its.vds;

import com.its.op.config.VdsServerConfig;
import com.its.op.dto.its.vds.VdsControlDto;
import com.its.op.service.its.xprotocol.CenterProtocol;
import com.its.utils.SysUtils;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.Base64;
import java.util.concurrent.*;

@Slf4j
@RequiredArgsConstructor
@Service
public class VdsControlService {

    private final VdsServerConfig config;

    private static final int[] crc16_arc = {
            0x0000, 0xc0c1, 0xc181, 0x0140, 0xc301, 0x03c0, 0x0280, 0xc241,
            0xc601, 0x06c0, 0x0780, 0xc741, 0x0500, 0xc5c1, 0xc481, 0x0440,
            0xcc01, 0x0cc0, 0x0d80, 0xcd41, 0x0f00, 0xcfc1, 0xce81, 0x0e40,
            0x0a00, 0xcac1, 0xcb81, 0x0b40, 0xc901, 0x09c0, 0x0880, 0xc841,
            0xd801, 0x18c0, 0x1980, 0xd941, 0x1b00, 0xdbc1, 0xda81, 0x1a40,
            0x1e00, 0xdec1, 0xdf81, 0x1f40, 0xdd01, 0x1dc0, 0x1c80, 0xdc41,
            0x1400, 0xd4c1, 0xd581, 0x1540, 0xd701, 0x17c0, 0x1680, 0xd641,
            0xd201, 0x12c0, 0x1380, 0xd341, 0x1100, 0xd1c1, 0xd081, 0x1040,
            0xf001, 0x30c0, 0x3180, 0xf141, 0x3300, 0xf3c1, 0xf281, 0x3240,
            0x3600, 0xf6c1, 0xf781, 0x3740, 0xf501, 0x35c0, 0x3480, 0xf441,
            0x3c00, 0xfcc1, 0xfd81, 0x3d40, 0xff01, 0x3fc0, 0x3e80, 0xfe41,
            0xfa01, 0x3ac0, 0x3b80, 0xfb41, 0x3900, 0xf9c1, 0xf881, 0x3840,
            0x2800, 0xe8c1, 0xe981, 0x2940, 0xeb01, 0x2bc0, 0x2a80, 0xea41,
            0xee01, 0x2ec0, 0x2f80, 0xef41, 0x2d00, 0xedc1, 0xec81, 0x2c40,
            0xe401, 0x24c0, 0x2580, 0xe541, 0x2700, 0xe7c1, 0xe681, 0x2640,
            0x2200, 0xe2c1, 0xe381, 0x2340, 0xe101, 0x21c0, 0x2080, 0xe041,
            0xa001, 0x60c0, 0x6180, 0xa141, 0x6300, 0xa3c1, 0xa281, 0x6240,
            0x6600, 0xa6c1, 0xa781, 0x6740, 0xa501, 0x65c0, 0x6480, 0xa441,
            0x6c00, 0xacc1, 0xad81, 0x6d40, 0xaf01, 0x6fc0, 0x6e80, 0xae41,
            0xaa01, 0x6ac0, 0x6b80, 0xab41, 0x6900, 0xa9c1, 0xa881, 0x6840,
            0x7800, 0xb8c1, 0xb981, 0x7940, 0xbb01, 0x7bc0, 0x7a80, 0xba41,
            0xbe01, 0x7ec0, 0x7f80, 0xbf41, 0x7d00, 0xbdc1, 0xbc81, 0x7c40,
            0xb401, 0x74c0, 0x7580, 0xb541, 0x7700, 0xb7c1, 0xb681, 0x7640,
            0x7200, 0xb2c1, 0xb381, 0x7340, 0xb101, 0x71c0, 0x7080, 0xb041,
            0x5000, 0x90c1, 0x9181, 0x5140, 0x9301, 0x53c0, 0x5280, 0x9241,
            0x9601, 0x56c0, 0x5780, 0x9741, 0x5500, 0x95c1, 0x9481, 0x5440,
            0x9c01, 0x5cc0, 0x5d80, 0x9d41, 0x5f00, 0x9fc1, 0x9e81, 0x5e40,
            0x5a00, 0x9ac1, 0x9b81, 0x5b40, 0x9901, 0x59c0, 0x5880, 0x9841,
            0x8801, 0x48c0, 0x4980, 0x8941, 0x4b00, 0x8bc1, 0x8a81, 0x4a40,
            0x4e00, 0x8ec1, 0x8f81, 0x4f40, 0x8d01, 0x4dc0, 0x4c80, 0x8c41,
            0x4400, 0x84c1, 0x8581, 0x4540, 0x8701, 0x47c0, 0x4680, 0x8641,
            0x8201, 0x42c0, 0x4380, 0x8341, 0x4100, 0x81c1, 0x8081, 0x4040
    };

    /**
     * VDS CRC16-ARC(https://crccalc.com/)
     * @param data : crc data array
     * @param start : start index
     * @param length : array length
     * @return : crc result
     */
    public int getCRC16ARC(byte[] data, int start, int length) {
        int crc = 0x0000;
        for (int ii = start; ii < (start+length); ii++) {
            //int b = data[ii];
            crc = (crc >> 8) ^ crc16_arc[(crc ^ data[ii]) & 0xff];
        }
        //log.error("{}", Integer.toHexString(crc));
        return crc & 0xFFFF;
    }

    /**
     * VDS Center Communication protocol buffer create
     * @param opCode : opcode
     * @param group : controller group no
     * @param address : controller address
     * @param frameNo : current frame no
     * @param cameraNo : camera no
     * @return : protocol buffer
     */
    public ByteBuffer getCommand(byte opCode, Integer group, Integer address, Integer frameNo, Integer cameraNo) {
        int length;
        if (opCode == 0x0C) {
            length = 11;
        } else if (opCode == 0x16) {
            length = 14;
        } else {
            log.error("Unknown VDS OPCode: {}", opCode);
            return null;
        }

        // Center packet
        byte[] center = CenterProtocol.getRequestHead(CenterProtocol.INT_ID_VDS_OPER, CenterProtocol.INT_ID_VDS_SERVER, opCode, length);

        // VDS  packet
        ByteBuffer vdsBuffer = ByteBuffer.allocate(length);
        vdsBuffer.order(ByteOrder.BIG_ENDIAN);
        vdsBuffer.put((byte)0x10); // DLE
        vdsBuffer.put((byte)0x02); // STX
        vdsBuffer.putShort((short)group.intValue());   // Address
        vdsBuffer.putShort((short)address.intValue()); // Address
        vdsBuffer.put(opCode);     // OP Code

        if (opCode == 0x16) {
            // 정지영상 요청
            vdsBuffer.putShort((short)frameNo.intValue()); // Frame NO
            vdsBuffer.put((byte)cameraNo.intValue()); // Camera No
        }
        vdsBuffer.put((byte)0x10); // DLE
        vdsBuffer.put((byte)0x03); // ETX
        int crc = getCRC16ARC(vdsBuffer.array(), 2, length - 6);
        byte crc1 = (byte)((crc >> 8) & 0xFF);
        byte crc2 = (byte)((crc     ) & 0xFF);
        vdsBuffer.put(crc1); // CRC1
        vdsBuffer.put(crc2); // CRC2
        // DLE Stuffing 는 하지 않는 것으로 패킷 분석결과 나타남.

        ByteBuffer cmdBuffer = ByteBuffer.allocate(center.length + length);
        cmdBuffer.order(ByteOrder.BIG_ENDIAN);
        cmdBuffer.put(center);
        cmdBuffer.put(vdsBuffer.array());
        log.info("VDS Request Buffer: {}", SysUtils.byteArrayToHex(cmdBuffer.array()));
        return cmdBuffer;
    }

    /**
     * VDS 제어기 리셋
     * @param req : request buffer
     * @return : reset result
     */
    public VdsControlDto.VdsControlRes controlReset(Long id, VdsControlDto.VdsControlResetReq req) {
        //TbVdsCtlr vds = this.requireOne(id);
        ByteBuffer cmdBuffer = getCommand((byte)0x0C, 0, Math.toIntExact(id), 0, 0);
        return requestVdsCommandReset(cmdBuffer);
    }

    /**
     * VDS 영상정보 요청
     * @param id
     * @param req
     * @return
     */
    public VdsControlDto.VdsControlRes requestStopImage(Long id, VdsControlDto.VdsControlStopImageReq req) {
        //TbVdsCtlr vds = this.requireOne(id);
        ByteBuffer cmdBuffer = getCommand((byte)0x16, 0, Math.toIntExact(id), req.getFrameNo(), req.getCameraNo());
        return requestVdsCommandStopImage(cmdBuffer);
    }

    /**
     * VDS 통신서버로 RESET 메시지 전송
     * @param cmdBuffer
     * @return
     */
    public VdsControlDto.VdsControlRes requestVdsCommandReset(ByteBuffer cmdBuffer) {
        VdsControlDto.VdsControlRes result = new VdsControlDto.VdsControlRes(0, "success");
        if (cmdBuffer == null) {
            result.setResult(1, "VDS 통신 메시지 생성 중 오류가 발생하였습니다.");
            return result;
        }

        String ipAddress = this.config.getIpAddress();
        int port = this.config.getPort();
        int connTimeout = 3000; // milli-seconds
        int readTimeout = 2000; // milli-seconds

        Socket socket = null;
        try {
            socket = CenterProtocol.connectServer(ipAddress, port, connTimeout, readTimeout);
            if (CenterProtocol.sendData(socket, cmdBuffer.array())) {
                try {
                    while (true) {
                        byte[] head = CenterProtocol.receiveBytes(socket, 10);
                        if (head[0] == (byte) 0x24 && head[1] == (byte) 0x04 && head[5] == (byte) 0x0C) {
                            log.info("RECV RESPONSE HEAD: 10 Bytes. {}", SysUtils.byteArrayToHex(head));
                            if (head[4] == (byte) 0xFF) {
                                result.setResult(8, "VDS 서버에서 알수 없는 VDS 제어기 입니다.");
                            } else if (head[4] == (byte) 0xFE) {
                                result.setResult(9, "VDS 서버에서 VDS 제어기 통신 상태가 비정상 입니다.");
                            } else if (head[4] == (byte) 0xFD) {
                                result.setResult(7, "VDS 서버에서 VDS 제어기로 리셋 명령을 전송하지 못하였습니다.");
                            } else {
                                result.setResult(0, "VDS 서버에 제어기 리셋명령을 정상적으로 전송하였습니다.");
                            }
                        } else {
                            result.setResult(0, "VDS 서버에 제어기 리셋명령을 정상적으로 전송하였습니다.");
                        }
                        break;
                    }
                } catch (IOException e) {
                    // 이전 VDS 통신 서버에서는 응답을 주지 않기 때문에 타임아웃 걸린 경우 정상적으로 처리하도록 함
                    result.setResult(0, "VDS 서버에 제어기 리셋명령을 정상적으로 전송하였습니다.");
                }
            }
            else {
                result.setResult(3, "VDS 서버에 제어기 리셋명령을 정상적으로 전송하지 못하였습니다.");
            }
        } catch (IOException e) {
            log.error("Reset Request. {}, {}, IOException", ipAddress, port);
            String errMsg = "[VDS 서버: " + ipAddress + "." + port + "]";
            result.setResult(2, "VDS 서버와 통신이 실패하였습니다.\r\n" + errMsg);
            return result;
        }
        finally {
            try {
                if (socket != null) {
                    socket.close();
                }
            } catch (IOException e) {
                log.error("VdsControlService: requestVdsCommandReset, socket close IOException");
            }
        }
        return result;
    }

    /**
     * VDS 통신서버로 요청 메시지 전송
     * @param cmdBuffer
     * @return
     */
    public VdsControlDto.VdsControlRes requestVdsCommandStopImage(ByteBuffer cmdBuffer) {

        VdsControlDto.VdsControlRes result = new VdsControlDto.VdsControlRes(0, "success");
        if (cmdBuffer == null) {
            result.setResult(1, "VDS 통신 메시지 생성 중 오류가 발생하였습니다.");
            return result;
        }

        String ipAddress = this.config.getIpAddress();
        int port = this.config.getPort();
        int taskTimeout = 6000; // milli-seconds
        int connectTimeout = 3000; // milli-seconds
        int readTimeout = 4000; // milli-seconds

        ExecutorService executorService = Executors.newSingleThreadExecutor();
        Callable<Integer> task = new Callable<Integer>() {
            @Override
            public Integer call() {
                int taskResult = 0;
                SocketAddress socketAddress = new InetSocketAddress(ipAddress, port);
                Socket socket = null;

//                String saveDir = ItsUtils.createUserDir("/image/vds/");
//                //String saveFileName = saveDir + ItsUtils.getSysTime() + ".jpg";
//                String saveFileName = saveDir + "vdsImage" + ".jpg";
//                //log.error("{}", saveFileName);
//                byte[] fileContent = new byte[0];
//                try {
//                    fileContent = FileUtils.readFileToByteArray(new File(saveFileName));
//                } catch (IOException e) {
//                    log.error("{}", e.getMessage());
//                }
//                String image = Base64.getEncoder().encodeToString(fileContent);
//                result.setImage(image);

                try {
                    socket = new Socket();
                    socket.setSoTimeout(readTimeout);			    /* InputStream 에서 데이터읽을때의 timeout */
                    socket.connect(socketAddress, connectTimeout);  /* socket 연결 자체에대한 timeout */

                    if (!CenterProtocol.sendData(socket, cmdBuffer.array())) {
                        taskResult = 3;
                        result.setResult(taskResult, "VDS 서버에 정지영상 요청 명령을 정상적으로 전송하지 못하였습니다.");
                    }
                    if (taskResult != 0) {
                        return taskResult;
                    }

                    // 24 04 01 01 00 70 00 00 00 0d ==> ACK 메시지
                    // OpCode: 5
                    // Length: 6 7 8 9
                    // 00 00 00 00 00 16 00 00 00 00 ==> 이미지정보 수신 헤더.....+ N
                    // 24 04 01 01 00 16 00 00 00 00 ==> 이미지정보 수신후 마지막 패킷

                    // Read Frame Head
                    while (true) {
                        // 이미지 헤더를 읽을 때 까지 헤더 크기 만큼 데이터 수신
                        // 상태 정보(ACK) 등이 수신될수 있다.
                        byte[] head = CenterProtocol.receiveBytes(socket, 10);
                        if (head[0] == (byte)0x24 && head[1] == (byte)0x04 && head[5] == (byte)0x16) {
                            log.info("RECV RESPONSE HEAD: 10 Bytes. {}", SysUtils.byteArrayToHex(head));
                            if (head[4] == (byte)0xFF) {
                                result.setResult(8, "VDS 서버에서 알수 없는 VDS 제어기 입니다.");
                            } else if (head[4] == (byte)0xFE) {
                                result.setResult(9, "VDS 서버에서 VDS 제어기 통신 상태가 비정상 입니다.");
                            }
                            break;
                        }

                        if (head[0] == (byte)0x00 && head[1] == (byte)0x00 && head[5] == (byte)0x16) {
                            log.info("RECV IMAGE HEAD  FIRST: 10 Bytes. {}", SysUtils.byteArrayToHex(head));

                            int imageHeaderRemainLength = 172;  // 이미지 헤더 사이즈 만큼 더 읽어야 함
                            byte[] imageHead = CenterProtocol.receiveBytes(socket, imageHeaderRemainLength);
                            int imageSize = SysUtils.bytesToInt(imageHead, 159, ByteOrder.BIG_ENDIAN);
                            log.info("RECV IMAGE HEAD SECOND: 172 Bytes. Image Size: {} Bytes.", imageSize);
                            if (imageSize > 0) {
                                // 이미지 사이즈 만큼 이미지 데이터를 읽어온다.
                                byte[] imageArr = CenterProtocol.receiveBytes(socket, imageSize);
                                String imageString = Base64.getEncoder().encodeToString(imageArr);
                                result.setImage(imageString);
                                result.setResult(0, "VDS 서버로 부터 정지영상 이미지를 정상적으로 수신하였습니다.");
                            } else {
                                result.setResult(99, "VDS 제어기의 현재 통신 상태가 정상적이지 않습니다.");
                            }
                            break;
                        } else {
                            int dataSize = SysUtils.bytesToInt(head, 6, ByteOrder.BIG_ENDIAN);
                            log.info("RECV HEAD: 10 Bytes, Data {} Bytes Remain. {}", dataSize, SysUtils.byteArrayToHex(head));
                            if (dataSize > 0) {
                                byte[] data = CenterProtocol.receiveBytes(socket, dataSize);
                                log.info("RECV DATA: {} Data Bytes. {}", dataSize, SysUtils.byteArrayToHex(data));
                            }
                        }
                    }
                } catch (IOException e) {
                    log.error("Image Request. {}, {}, IOException", ipAddress, port);
                    String errMsg = "[VDS 서버: " + ipAddress + "." + port + "]";
                    taskResult = 2;
                    result.setResult(taskResult, "VDS 서버와 통신이 실패하였습니다.\r\n" + errMsg);
                } finally {
                    try {
                        if (socket != null) {
                            socket.close();
                        }
                    } catch (IOException e) {
                        log.error("VdsControlService: requestVdsCommandReset, socket close IOException");
                    }
                }
                return taskResult;
            }
        };

        Future<Integer> future = executorService.submit(task);
        try {
            Integer taskResult = future.get(taskTimeout, TimeUnit.MILLISECONDS);
            if (taskResult == 0) {
                log.info("VDS Stop Image Request Task Future Result: {}", taskResult);
            }
        } catch (InterruptedException e) {
            result.setResult(5, "VDS 정지영상 요청 작업 중 인터럽트가 발생하여 작업이 실패하였습니다.");
        } catch (ExecutionException e) {
            result.setResult(6, "VDS 정지영상 요청 작업 스레드를 생성하지 못하여 작업이 실패하였습니다.");
        } catch (TimeoutException e) {
            // timeout 발생시 진입하는 catch 문
            result.setResult(7, "VDS 정지영상 요청 작업이 시간초과 되어 작업이 실패하였습니다.");
        } finally {
            executorService.shutdown();
        }
        log.info("requestVdsCommandStopImage: {}, {}", result.getError(), result.getMessage());
        return result;
    }

}
