package com.imdroid.vermgr.service;

import com.imdroid.vermgr.entity.DeviceApp;
import com.imdroid.vermgr.utils.CRC16;
import io.netty.channel.Channel;
import lombok.extern.slf4j.Slf4j;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.sql.Timestamp;
import java.util.Arrays;
@Slf4j
public class UpgradeSession {
    public static final byte COM_OK         = 0;
    public static final byte COM_ERROR      = 1;
    public static final byte COM_ABORT      = 2;
    public static final byte COM_TIMEOUT    = 3;
    public static final byte COM_DATA       = 4;
    public static final byte COM_LIMIT      = 5;
    public static final byte COM_SAVE_ERR   = 6;
    public static final byte COM_FILE_ERR   = 7;

    public static final byte SOH = 0x01;  /* start of 128-byte data packet */
    public static final byte STX = 0x02;  /* start of 1024-byte data packet */
    public static final byte EOT = 0x04;  /* end of transmission */
    public static final byte ACK = 0x06;  /* acknowledge */
    public static final byte NAK = 0x15;  /* negative acknowledge */
    public static final byte CA  = 0x18; /* two of these in succession aborts transfer */
    public static final byte CC = 0x43;  /* 'C' == 0x43, request 16-bit CRC */

    public static final short PACKET_HEADER_SIZE = 3;
    public static final short PACKET_TRAILER_SIZE = 2;
    public static final short PACKET_SIZE = 128;
    public static final short PACKET_1K_SIZE = 1024;

    static private String version_path="firmware";

    private DeviceApp deviceApp;
    File firmware;
    FileInputStream reader;
    short tx_seq = 0;
    int tx_bytes = 0;
    byte nak_num = 0;

    byte[] header_buff;
    byte[] tailer_buff;
    byte[] soh_buff;
    byte[] stx_buff;
    boolean session_done = false;
    boolean file_done = false;
    boolean is_file_accepted = false;
    byte result = COM_OK;

    public UpgradeSession(DeviceApp app) {
        deviceApp = app;
        firmware = null;
        reader = null;
        header_buff = new byte[PACKET_HEADER_SIZE];
        tailer_buff = new byte[PACKET_TRAILER_SIZE];
        soh_buff = new byte[PACKET_SIZE];
        stx_buff = new byte[PACKET_1K_SIZE];
    }

    public byte getResult(){
        return result;
    }
    public DeviceApp getDeviceApp(){return deviceApp;}
    public long getSN(){return deviceApp.getDevice_sn();}
    public int getState() {return deviceApp.getUpgrade_state();}
    public int getTxPercentage() {
        if(firmware == null) return 0;
        return (int) (tx_bytes*100/firmware.length());
    }

    public void reset(){
        session_done = true;
        file_done = true;
        is_file_accepted = false;
        tx_seq = 0;
        nak_num = 0;
        tx_bytes = 0;

        try {
            if(reader!=null) reader.close();
        }
        catch (IOException e){
            log.error(firmware+" reader.close()",e);
        }
    }

    public boolean start(Channel channel) {
        session_done = false;
        file_done = false;
        is_file_accepted = false;
        tx_seq = 0;
        nak_num = 0;
        tx_bytes = 0;
        result = COM_FILE_ERR;

        String file_name = deviceApp.getFirmware();
        if(file_name != null){
            firmware = new File(version_path+"/" + file_name);
            if (firmware.exists()) {
                try {
                    if(reader!=null) reader.close();
                    reader = new FileInputStream(firmware);
                    result = COM_OK;
                }
                catch (FileNotFoundException e) {
                    log.error(firmware+" reader open",e);
                    reset();
                } catch (IOException e) {
                    log.error(firmware+" reader open",e);
                    reset();
                }
            }
        }

        // 发升级指示
        if(result != COM_OK){
            deviceApp.setUpgrade_state(DeviceApp.STATE_TX_FAILED);
            deviceApp.setUpgrade_result(result);
            return false;
        }
        else {
            long sn = deviceApp.getDevice_sn();
            byte[] ind_msg = new byte[5];
            ind_msg[0] = UpgradeManager.UPGRADE_IND;
            ind_msg[1] = (byte) (sn >> 24);
            ind_msg[2] = (byte) (sn >> 16);
            ind_msg[3] = (byte) (sn >> 8);
            ind_msg[4] = (byte) sn;
            byte tailer_buff[] = {0, 0};
            int crc = CRC16.calculate(ind_msg);
            tailer_buff[0] = (byte) (crc >> 8);
            tailer_buff[1] = (byte) crc;
            channel.write(ind_msg);
            channel.writeAndFlush(tailer_buff);
            log.info(deviceApp.getDevice_sn()+": send UPGRADE_IND");

            deviceApp.setUpgrade_state(DeviceApp.STATE_UPGRADING);
            return true;
        }
    }

    public boolean restart(Channel channel) {
        session_done = false;
        file_done = false;
        is_file_accepted = false;
        tx_seq = 0;
        nak_num = 0;
        result = COM_FILE_ERR;

        try {
            if(reader!=null) reader.close();
            reader = new FileInputStream(firmware);
            result = COM_OK;
        }
        catch (FileNotFoundException e) {
            log.error(firmware+" reader open failed: ",e);
            reset();
        } catch (IOException e) {
            log.error(firmware+" reader open failed: ",e);
            reset();
        }

        return (result == COM_OK);
    }

    public void reject(){
        result = COM_ERROR;
        deviceApp.setUpgrade_state(DeviceApp.STATE_TX_FAILED);
        deviceApp.setUpgrade_result(result);
        deviceApp.setUpgrade_time(new Timestamp(System.currentTimeMillis()));
    }

    public void onTimeout(){
        result = COM_TIMEOUT;
        deviceApp.setUpgrade_state(DeviceApp.STATE_TX_FAILED);
        deviceApp.setUpgrade_result(result);
        deviceApp.setUpgrade_time(new Timestamp(System.currentTimeMillis()));
    }

    public boolean procYmodem(Channel channel, byte[] rx_data){
        if(session_done) return true;

        try {
            switch (rx_data[0]) {
                case CC:
                    log.info("C");
                    if(tx_seq!=0){
                        //这种情况说明上次接收端意外中断了，需要重新开始
                        restart(channel);
                    }
                    if (!is_file_accepted) {
                        sendFileInfo(channel);
                    } else {
                        tx_seq++;
                        sendPacket(channel);
                        //sendSmallPacket(channel);
                    }
                    break;
                case ACK:
                    if(rx_data.length == 4)
                        log.info("ACK "+rx_data[1]+" "+(rx_data[2]<<8|((short)rx_data[3]&0xFF)));
                    else log.info("ACK");
                    nak_num = 0;
                    if (!is_file_accepted) {
                        is_file_accepted = true;
                    }
                    else if(!file_done){
                        tx_seq++;
                        sendPacket(channel);
                        //sendSmallPacket(channel);
                    }
                    if(file_done) {
                        is_file_accepted=false;
                        tx_seq=0;
                    }
                    break;
                case NAK:
                    if(rx_data.length == 4)
                        log.info("NAK "+rx_data[1]+" "+(rx_data[2]<<8|((short)rx_data[3]&0xFF)));
                    else log.info("NAK");
                    nak_num++;
                    if(nak_num>=3){
                        log.info("too much NAK, cancel transmit");
                        result = COM_ERROR;
                        reset();
                    }
                    else resendPacket(channel);
                    break;
                case CA:
                    log.info("CA");
                    result = rx_data[1];
                    reset();
                    break;
                default:
                    log.info("unknown ind received: "+rx_data[0]+" "+rx_data[1]);
                    break;
            }
        }
        catch (IOException e){
            reset();
            log.error(firmware+" reader error: ",e);
            result = COM_FILE_ERR;
        }

        if(result == COM_ERROR || result==COM_FILE_ERR){
            header_buff[0] = CA;
            channel.writeAndFlush(header_buff);
        }

        if(session_done){
            if(result == COM_OK) {
                deviceApp.setUpgrade_state(DeviceApp.STATE_TX_COMPLETED);
            }
            else{
                deviceApp.setUpgrade_state(DeviceApp.STATE_TX_FAILED);
            }
            deviceApp.setUpgrade_result(result);
            deviceApp.setUpgrade_time(new Timestamp(System.currentTimeMillis()));
        }
        return session_done;
    }

    void sendFileInfo(Channel channel){
        header_buff[0] = SOH;
        header_buff[1] = (byte) tx_seq;
        header_buff[2] = (byte) ~tx_seq;

        Arrays.fill(soh_buff, (byte) 0);
        if(file_done) {
            reset();//end of session
        }
        else{
            int pos = 0;
            byte[] file_name = firmware.getName().getBytes();
            for (int i = 0; i < file_name.length; i++) soh_buff[pos++] = file_name[i];
            soh_buff[pos++] = 0;
            String strFileSize = "" + firmware.length();
            byte[] file_size = strFileSize.getBytes();
            for (int i = 0; i < file_size.length; i++) soh_buff[pos++] = file_size[i];
            soh_buff[pos++] = 0;
        }

        int crc = CRC16.calculate(soh_buff);
        tailer_buff[0] = (byte) (crc>>8);
        tailer_buff[1] = (byte) crc;
        channel.write(header_buff);
        channel.write(soh_buff);
        channel.writeAndFlush(tailer_buff);
        log.info(deviceApp.getDevice_sn()+": send file info");
    }

    void sendPacket(Channel channel) throws IOException {
        if(tx_seq==0) return;
        if(file_done){
            header_buff[0] = EOT;
            channel.writeAndFlush(header_buff);
            log.info(deviceApp.getDevice_sn()+": send end of tx, seq="+tx_seq);
            return;
        }

        header_buff[0] = STX;
        header_buff[1] = (byte) tx_seq;
        header_buff[2] = (byte) ~tx_seq;

        int read_bytes = reader.read(stx_buff);
        if(read_bytes != -1) tx_bytes+=read_bytes;

        if(read_bytes == -1 || read_bytes==0){
            // send EOT
            header_buff[0] = EOT;
            channel.writeAndFlush(header_buff);
            file_done = true;
            log.info(deviceApp.getDevice_sn()+": send end of tx, seq="+tx_seq);
            return;
        }
        else if(read_bytes>=PACKET_1K_SIZE){

        }
        else if(read_bytes<PACKET_1K_SIZE && read_bytes>PACKET_SIZE){
            for(int i=read_bytes; i<PACKET_1K_SIZE; i++) stx_buff[i]=0x1A;
        }
        else{
            header_buff[0] = SOH;
            Arrays.fill(soh_buff, (byte) 0x1A);
            for(int i=0; i<read_bytes; i++) soh_buff[i] = stx_buff[i];
        }

        int crc = CRC16.calculate((header_buff[0] == STX)?stx_buff:soh_buff);
        tailer_buff[0] = (byte) (crc>>8);
        tailer_buff[1] = (byte) crc;
        channel.write(header_buff);
        channel.write((header_buff[0] == STX)?stx_buff:soh_buff);
        channel.writeAndFlush(tailer_buff);
        log.info(deviceApp.getDevice_sn()+": send packet "+tx_seq);
    }
    void sendSmallPacket(Channel channel) throws IOException {
        if(tx_seq==0) return;
        if(file_done){
            header_buff[0] = EOT;
            channel.writeAndFlush(header_buff);
            log.info(deviceApp.getDevice_sn()+": send end of tx, seq="+tx_seq);
            return;
        }

        header_buff[0] = SOH;
        header_buff[1] = (byte) tx_seq;
        header_buff[2] = (byte) ~tx_seq;

        int read_bytes = reader.read(soh_buff);
        if(read_bytes != -1) tx_bytes+=read_bytes;

        if(read_bytes == -1 || read_bytes==0){
            // send EOT
            header_buff[0] = EOT;
            channel.writeAndFlush(header_buff);
            file_done = true;
            log.info(deviceApp.getDevice_sn()+": send end of tx, seq="+tx_seq);
            return;
        }
        else if(read_bytes>=PACKET_SIZE){

        }
        else{
            for(int i=read_bytes; i<PACKET_SIZE; i++) soh_buff[i]=0x1A;
        }

        int crc = CRC16.calculate(soh_buff);
        tailer_buff[0] = (byte) (crc>>8);
        tailer_buff[1] = (byte) crc;
        channel.write(header_buff);
        channel.write(soh_buff);
        channel.writeAndFlush(tailer_buff);
        log.info(deviceApp.getDevice_sn()+": send packet "+tx_seq);
    }

    void resendPacket(Channel channel){
        if(file_done){
            header_buff[0] = EOT;
            channel.writeAndFlush(header_buff);
        }
        else {
            channel.write(header_buff);
            channel.write((header_buff[0] == STX) ? stx_buff : soh_buff);
            channel.writeAndFlush(tailer_buff);
        }
        log.info(deviceApp.getDevice_sn()+": resend packet "+header_buff[1]);
    }
}
