package net.northking.cctp.upperComputer.driver.proxy;


import net.northking.cctp.upperComputer.driver.usbmuxd.AppleDevice;
import net.northking.cctp.upperComputer.driver.usbmuxd.UsbMuxd;
import net.northking.cctp.upperComputer.driver.usbmuxd.UsbMuxdConnectFailedException;
import net.northking.cctp.upperComputer.driver.usbmuxd.payload.usbmuxd.entity.DeviceConnectionDetail;
import net.northking.cctp.upperComputer.enums.FileBusinessTypeEnum;
import net.northking.cctp.upperComputer.entity.Attachment;
import net.northking.cctp.upperComputer.exception.ExecuteException;
import net.northking.cctp.upperComputer.utils.HttpUtils;
import org.bytedeco.ffmpeg.global.avcodec;
import org.bytedeco.ffmpeg.global.avutil;
import org.bytedeco.javacv.FFmpegFrameRecorder;
import org.bytedeco.javacv.Java2DFrameConverter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.imageio.*;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.stream.MemoryCacheImageOutputStream;
import java.awt.image.BufferedImage;
import java.io.*;
import java.math.BigDecimal;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

/**
 * 转么用于转发WebDriverAgent屏幕的逻辑<br>
 * 无论客户是否连接，此类都会从设备持续获取屏幕数据，并缓存最后画面<br>
 * 当无法获取到设备屏幕时，将向客户每0.5秒发送1张最后缓存的画面
 */
@SuppressWarnings("BusyWait")
public class WebDriverAgentScreenProxy {

    private Logger log = LoggerFactory.getLogger(WebDriverAgentScreenProxy.class);
    /**
     * WebDriverAgent 屏幕监听端口
     */
    private static final int WEB_DRIVER_AGENT_SCREEN_PORT = 9100;

    /**
     * 每个数据块的固定开头
     */
    private static final String BOUNDARY_STRING = "--BoundaryString--Content-type: image/jpg--=";

    private static float compressQuality = 0.75f;

    /**
     * 要转发的设备序列号
     */
    private final String udid;

    /**
     * 转发监听的端口号
     */
    private final int listenPort;

    /**
     * 输出header，请求方连接后首先发送，每次连接只发一次
     */
    private String header = null;

    /**
     * 最后有效图片头部块
     */
    private final byte[] lastChunkHeader = new byte[BOUNDARY_STRING.length() + 2];

    /**
     * 最后有效图片数据块
     */
    private byte[] lastChunk = new byte[0];


    /**
     * 最后一次有数据发送的时间
     */
    private long lastSendTime = 0L;

    private final ArrayList<SendChunkDataThread> sendChunkDataThreads = new ArrayList<>();

    private ChunkHandleThread chunkHandleThread = new ChunkHandleThread();

    private FetchScreenDataThread fetchScreenDataThread = new FetchScreenDataThread();

    private ScreenRecordThread screenRecordThread;

    private ServerSocket serverSocket;

    public WebDriverAgentScreenProxy(String udid, int listenPort) {
        this.udid = udid;
        this.listenPort = listenPort;
        chunkHandleThread.start();
        startFetch();
        startKeepSend();

    }

    public void startRecord(String videoName, int videoWidth, int videoHeight) {
        if (null == screenRecordThread) {
            this.screenRecordThread = new ScreenRecordThread(udid, videoName, videoWidth, videoHeight);
            this.screenRecordThread.start();
        } else {
            throw new ExecuteException("该设备已经处在录屏中!");
        }
    }

    public String stopRecordScreen(String uploadVideoAddress, String tenantId) {
        if (null != this.screenRecordThread) {
            String videoPath = this.screenRecordThread.endRecord(uploadVideoAddress, tenantId);
            this.screenRecordThread = null;
            return videoPath;
        } else {
            throw new ExecuteException("该设备没有开启录屏!");
        }
    }

    private void startFetch() {

        fetchScreenDataThread.start();
    }

    public void startListen() {
        try {
            serverSocket = new ServerSocket(listenPort);
            serverSocket.setReuseAddress(true);
            log.info("监听端口：" + listenPort + " 通向设备[" + udid + "] 端口：" + WEB_DRIVER_AGENT_SCREEN_PORT);

        } catch (IOException e) {
            log.error("监听端口[" + listenPort + "]发生错误", e);
            System.exit(-1);
        }
        while (!Thread.currentThread().isInterrupted()) {
            try {
                Socket socket = serverSocket.accept();
                log.info("新的客户：" + socket.getRemoteSocketAddress().toString());
                SendChunkDataThread sendThread = new SendChunkDataThread();
                sendThread.socket = socket;
                sendThread.inputStream = socket.getInputStream();
                sendThread.outputStream = socket.getOutputStream();
                sendThread.start();
                synchronized (sendChunkDataThreads) {
                    sendChunkDataThreads.add(sendThread);
                }
            } catch (Exception e) {
                break;
            }
        }

        try {
            serverSocket.close();
        } catch (IOException ignore) {
        }
    }

    private void startKeepSend() {
        KeepChunkSendThread thread = new KeepChunkSendThread();
        thread.start();
    }

    void notifyChunkReady() {
        lastSendTime = System.currentTimeMillis();
        synchronized (sendChunkDataThreads) {
            for (SendChunkDataThread sendThread : sendChunkDataThreads) {
                sendThread.dataDone();
            }
        }
        lastSendTime = System.currentTimeMillis();
    }

    public void stop9100Proxy() {
        //关闭socket
        if (null != this.serverSocket) {
            try {
                this.serverSocket.close();
            } catch (IOException e) {
                log.error("关闭服务连接失败，e",e);
            }
            this.serverSocket = null;
        }
        if (null != fetchScreenDataThread) {
            fetchScreenDataThread.interrupt();
        }
        if (null != chunkHandleThread) {
            chunkHandleThread.interrupt();
        }
    }

    /**
     * 从设备获取屏幕数据<br>
     * 程序只会有一条连接通道从设备读取屏幕数据，然后通知客户连入线程将数据发送给客户。<br>
     * 当苹果设备因为各种原因断开时，会尝试自动重连。
     */
    private class FetchScreenDataThread extends Thread {

        FetchScreenDataThread() {
            setName("FetchScreenDataThread");
        }


        @Override
        public void run() {
            while (!isInterrupted()) {
                UsbMuxd usbmuxd = new UsbMuxd();
                UsbMuxd.ConnectOperator connectOperator = getDeviceConnectOperator(usbmuxd);
                if (connectOperator == null) {
                    log.warn("未能获取与设备[" + udid + "]的通信操作对象，稍后重试");
                    try {
                        sleep(500);
                    } catch (InterruptedException ignored) {
                    }
                    continue;
                }

                try {
                    connectOperator.outputStream.write("\n\n".getBytes(StandardCharsets.UTF_8));
                } catch (IOException e) {
                    log.warn("传递指令到设备[" + udid + "]时发生输入输出错误，稍后重试");
                    try {
                        sleep(500);
                    } catch (InterruptedException ignored) {
                    }
                    continue;
                }

                byte[] readCache = new byte[4096];
                ByteArrayOutputStream readBuffer = new ByteArrayOutputStream();
                ByteArrayOutputStream chunkBuffer = new ByteArrayOutputStream();
                byte[] chunkHeaderByteArray = BOUNDARY_STRING.getBytes();
                int chunkHeaderPosition = 0;
                int readBufferChunkEndIndex = 0;
                boolean readFirstChunk = false;
                int lastRawChunkSize = 0;
                boolean confirmSameFrame = false;

                int foundChunkHeaderAndNeedMoreBytes = 0;

                try {
                    while (!isInterrupted()) {
                        int count = connectOperator.inputStream.read(readCache);
                        if (count == -1) break;
                        if (count == 0) continue;
                        readBuffer.write(readCache, 0, count);

                        int i = 0;

                        while (i < count) {
                            if (foundChunkHeaderAndNeedMoreBytes > 0) {
                                foundChunkHeaderAndNeedMoreBytes--;
                                readBufferChunkEndIndex++;
                                if (foundChunkHeaderAndNeedMoreBytes == 0) {

                                    byte[] readArray = readBuffer.toByteArray();

                                    chunkBuffer.write(readArray, 0, readBufferChunkEndIndex - 2 - chunkHeaderByteArray.length);

                                    readBuffer.reset();
                                    int k = readBufferChunkEndIndex - 2 - chunkHeaderByteArray.length;
                                    for (int j = 0; j < BOUNDARY_STRING.length() + 2; j++, k++) {
                                        lastChunkHeader[j] = readArray[k];
                                    }
                                    readBuffer.write(readArray, readBufferChunkEndIndex, readArray.length - readBufferChunkEndIndex);

                                    readBufferChunkEndIndex = 0;
                                    if (!readFirstChunk) {
                                        readFirstChunk = true;
                                        header = chunkBuffer.toString();
                                        chunkBuffer.reset();
                                        notifyChunkReady();
                                        i++;
                                        continue;
                                    }
                                    byte[] rawImageFileByteArray = chunkBuffer.toByteArray();
                                    //这里是录屏
                                    if (null != screenRecordThread && screenRecordThread.isAlive()) {
                                        screenRecordThread.addImgToVideo(rawImageFileByteArray);
                                    }
                                    BigDecimal compressQualityBig = new BigDecimal(compressQuality);
                                    BigDecimal compareBig = new BigDecimal(1f);
                                    int compareFlag = compressQualityBig.compareTo(compareBig);
                                    if (compareFlag != 0) {
                                        if (lastRawChunkSize == rawImageFileByteArray.length) {
                                            if (confirmSameFrame) {
                                                chunkBuffer.reset();
                                                i++;
                                                continue;
                                            }
                                            confirmSameFrame = true;
                                        } else {
                                            confirmSameFrame = false;
                                            lastRawChunkSize = rawImageFileByteArray.length;
                                        }
                                        chunkHandleThread.putChunk(rawImageFileByteArray);
                                    } else {
                                        lastChunk = rawImageFileByteArray;
                                        notifyChunkReady();
                                    }

                                    chunkBuffer.reset();
                                }
                                i++;
                                continue;
                            }

                            if (chunkHeaderByteArray[chunkHeaderPosition] == readCache[i]) {
                                chunkHeaderPosition++;
                                if (chunkHeaderPosition == chunkHeaderByteArray.length) {
                                    foundChunkHeaderAndNeedMoreBytes = 2;
                                    readBufferChunkEndIndex++;
                                    chunkHeaderPosition = 0;
                                    i++;
                                    continue;
                                }
                            } else {
                                chunkHeaderPosition = 0;
                            }
                            readBufferChunkEndIndex++;
                            i++;
                        }
                    }
                } catch (IOException e) {
                    log.error("从设备读取数据失败，稍后重试", e);
                    try {
                        sleep(500);
                    } catch (InterruptedException ignored) {
                        log.error("线程中断异常，ignored",ignored);
                        Thread.currentThread().interrupt();
                    }
                }

            }
            log.warn("抓取屏幕线程退出。。。。。。。。。。。。");
        }

        private UsbMuxd.ConnectOperator getDeviceConnectOperator(UsbMuxd usbMuxd) {
            try {
                List<DeviceConnectionDetail> connectionDetailList = usbMuxd.listDevices();
                for (DeviceConnectionDetail detail : connectionDetailList) {
                    if (detail.serialNumber.equalsIgnoreCase(udid) && detail.connectStatus == DeviceConnectionDetail.ConnectStatus.ATTACHED) {
                        AppleDevice appleDevice = new AppleDevice(detail);
                        try {
                            return usbMuxd.connectDeviceLocalPort(appleDevice, WEB_DRIVER_AGENT_SCREEN_PORT);
                        } catch (IOException e) {
                            log.warn("设备已连接，但无法连接WebDriverAgent端口:" + WEB_DRIVER_AGENT_SCREEN_PORT);
                        }
                    }
                }
            } catch (UsbMuxdConnectFailedException e) {
                log.error("查找设备时无法连接到usbmuxd，是否至少连接了一个苹果设备？", e);
            } catch (IOException e) {
                log.error("查找设备时与usbmuxd通信发生错误", e);
            }
            return null;
        }
    }

    private class SendChunkDataThread extends Thread {

        private Socket socket;

        private InputStream inputStream;

        private OutputStream outputStream;

        private boolean isHeaderSend = false;

        private boolean isActive = true;

        private boolean firstSend = false;

        private final Object waitDataLock = new Object();

        /**
         * 等待数据到来
         */
        private void waitingData() {
            synchronized (waitDataLock) {
                try {
                    waitDataLock.wait();
                } catch (InterruptedException e) {
                    log.error("线程中断异常，e",e);
                    Thread.currentThread().interrupt();
                }
            }
        }

        /**
         * 通知数据到来
         */
        public void dataDone() {
            synchronized (waitDataLock) {
                waitDataLock.notify();
            }
        }

        @Override
        public void run() {
            while (!isInterrupted() && !socket.isClosed()) {
                if (firstSend) {
                    waitingData();
                } else {
                    firstSend = true;
                    sendHeader();
                    sendChunk();
                    log.info("发送第一帧");
                }
                if (!sendHeader()) break;
                if (!sendChunk()) break;
            }

            synchronized (sendChunkDataThreads) {
                sendChunkDataThreads.remove(this);
            }
            isActive = false;
            if (!socket.isClosed()) {
                try {
                    socket.close();
                } catch (IOException ignored) {

                }
            }
            log.warn("发送屏幕的线程退出。。。。。。。。。。");
        }

        /**
         * 发送header<br>
         * 满足条件才会发送
         *
         * @return 是否操作成功
         */
        private boolean sendHeader() {
            if (isHeaderSend) return true;
            if (header != null) {
                try {
                    byte[] headerBytes = header.getBytes(StandardCharsets.UTF_8);
                    outputStream.write(headerBytes);
                    outputStream.flush();
                    isHeaderSend = true;
                    return true;
                } catch (IOException e) {
                    log.error("无法向客户发送header", e);
                }
            }
            return false;
        }

        /**
         * 发送画面数据
         *
         * @return 是否发送成功
         */
        private boolean sendChunk() {
            try {
                outputStream.write(lastChunkHeader);
                outputStream.write(lastChunk);
                outputStream.flush();
                return true;
            } catch (IOException ignore) {
            }
            return false;
        }
    }

    /**
     * 监测是否超过500毫秒设备都没有发送屏幕数据，若满足条件，则每0.5秒发送一次最后缓存的画面数据。<br>
     * 防止客户超时断开（socket hung up）
     */
    private class KeepChunkSendThread extends Thread {

        KeepChunkSendThread() {
            setName("KeepChunkSendThread");
        }

        @Override
        public void run() {
            while (!isInterrupted()) {
                synchronized (sendChunkDataThreads) {
                    if (sendChunkDataThreads.size() == 0) {
                        try {
                            sleep(500);
                        } catch (InterruptedException ignored) {
                            log.error("线程中断异常，ignored",ignored);
                            Thread.currentThread().interrupt();
                        }
                    }
                }

                if (header != null && lastChunk.length > 0 && System.currentTimeMillis() - lastSendTime > 500) {
                    synchronized (sendChunkDataThreads) {
                        if (sendChunkDataThreads.size() > 0) {
                            for (SendChunkDataThread thread : sendChunkDataThreads) {
                                thread.dataDone();
                            }
                        }
                    }
                }

                try {
                    sleep(500);
                } catch (InterruptedException ignored) {
                    log.error("线程中断异常，ignored",ignored);
                    Thread.currentThread().interrupt();
                }
            }
        }
    }

    private class ChunkHandleThread extends Thread {

        private boolean enableScale = true;
        private final ImageReader imageReader;
        private final ImageWriter imageWriter;
        private final ImageWriteParam imageWriteParam;
        private byte[] inChunk = new byte[0];

        private final Object lock = new Object();
        private final ByteArrayOutputStream chunkOutputStream = new ByteArrayOutputStream();

        public ChunkHandleThread() {
            setName("ChunkHandleThread");
            Iterator<ImageReader> imageReaders = ImageIO.getImageReadersByFormatName("jpg");
            if (!imageReaders.hasNext()) {
                throw new IllegalStateException(("不支持jpg编码"));
            }
            imageReader = imageReaders.next();
            Iterator<ImageWriter> imageWriters = ImageIO.getImageWritersByFormatName("jpg");
            if (!imageWriters.hasNext()) {
                throw new IllegalStateException("不支持jpg编码");
            }
            imageWriter = imageWriters.next();
            imageWriteParam = imageWriter.getDefaultWriteParam();
            imageWriteParam.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
            imageWriteParam.setCompressionQuality(compressQuality);
        }

        public void updateQuality(float quality) {
            imageWriteParam.setCompressionQuality(quality);
        }

        public void putChunk(byte[] chunk) {
            synchronized (lock) {
                inChunk = chunk;
                lock.notify();
            }
        }

        @Override
        public void run() {
            while (!isInterrupted()) {
                byte[] currentChunk;
                if (inChunk.length == 0) {
                    synchronized (lock) {
                        try {
                            lock.wait();
                        } catch (InterruptedException e) {
                            break;
                        }
                    }
                }
                synchronized (lock) {
                    currentChunk = inChunk;
                    inChunk = new byte[0];
                }

                try {
                    imageReader.setInput(ImageIO.createImageInputStream(new ByteArrayInputStream(currentChunk)), true);
                    IIOMetadata meta = imageReader.getImageMetadata(0);

                    BufferedImage bufferedImage = ImageIO.read(new ByteArrayInputStream(currentChunk));
                    IIOImage iIOImage;
                    if (enableScale) {
                        int scaleWidth = (int) (bufferedImage.getWidth() * 0.5);
                        int scaleHeight = (int) (bufferedImage.getHeight() * 0.5);
                        BufferedImage scaledBufferImage = new BufferedImage(scaleWidth, scaleHeight, BufferedImage.TYPE_INT_RGB);

                        for (int x = 0; x < scaleWidth; x++) {
                            for (int y = 0; y < scaleHeight; y++) {
                                scaledBufferImage.setRGB(x, y, bufferedImage.getRGB(x * 2, y * 2));
                            }
                        }
                        iIOImage = new IIOImage(scaledBufferImage, null, meta);
                    } else {
                        iIOImage = new IIOImage(bufferedImage, null, meta);
                    }


                    chunkOutputStream.reset();
                    imageWriter.setOutput(new MemoryCacheImageOutputStream(chunkOutputStream));
                    imageWriter.write(null, iIOImage, imageWriteParam);
                    lastChunk = chunkOutputStream.toByteArray();
                    notifyChunkReady();

                } catch (IOException e) {
                    log.error("执行io异常，e",e);
                }

            }
        }
    }

    class DynamicValue extends Thread {

        public DynamicValue() {
            setName("DynamicValue");
        }

        @Override
        public void run() {
            BufferedReader bufferReader = new BufferedReader(new InputStreamReader(System.in));

            do {
                String line;
                try {
                    line = bufferReader.readLine();
                } catch (IOException e) {
                    break;
                }
                if (line == null) {
                    break;
                }
                if (line.contains("=")) {
                    String[] inputData = line.split("=");
                    if (inputData.length < 2) {
                        showHelp();
                        continue;
                    }
                    String commandName = inputData[0];
                    if (commandName.equals("quality")) {
                        float value = 0.75f;
                        try {
                            value = Float.parseFloat(inputData[1]);
                        } catch (NumberFormatException e) {
                            System.out.println("压缩质量格式错误");
                            continue;
                        }
                        if (value < 0 || value > 1.0f) {
                            System.out.println("压缩质量范围错误，范围：0~1.0");
                            continue;
                        }
                        compressQuality = value;
                        chunkHandleThread.updateQuality(compressQuality);
                        System.out.println("压缩画质已变更：" + compressQuality);
                    } else if (commandName.equals("scale")) {
                        chunkHandleThread.enableScale = Boolean.parseBoolean(inputData[1]);
                        if (chunkHandleThread.enableScale) {
                            System.out.println("已启用半分辨率");
                        } else {
                            System.out.println("已使用原始分辨率");
                        }
                    }
                } else {
                    showHelp();
                }
            } while (!isInterrupted());
        }
    }

    private void showHelp() {
        System.out.println("" +
                "屏幕串流模式，使用以下参数动态修改效果：" +
                "quality=[float] 修改画面压缩质量，0~1.0，越高画面质量越好，1.0时将同时禁用缩放" +
                "scale=[boolean] 是否启用半分辨率缩放");
    }

    private class ScreenRecordThread extends Thread {

        private Logger logger = LoggerFactory.getLogger(ScreenRecordThread.class);

        private FFmpegFrameRecorder recorder;

        private String filePath = System.getProperty("user.dir") + "/temp/";

        private Java2DFrameConverter converter = new Java2DFrameConverter();

        private final int FRAME_RATE = 8;

        private String udid;

        private String videoName;

        public ScreenRecordThread(String udid, String videoName, int videoWidth, int videoHeight) {
            this.udid = udid;
            this.videoName = videoName;
            File path = new File(filePath);
            if (!path.exists()) {
                path.mkdirs();
            }
            //todo:这里注意改正linux的配置参数，写文件
            recorder = new FFmpegFrameRecorder(filePath + videoName, videoWidth, videoHeight);
            recorder.setVideoCodec(avcodec.AV_CODEC_ID_H264);
            recorder.setFrameRate(FRAME_RATE);
            recorder.setPixelFormat(avutil.AV_PIX_FMT_YUV420P);
        }

        @Override
        public void run() {
            try {
                recorder.start();
            } catch (FFmpegFrameRecorder.Exception e) {
                logger.warn("录屏启动失败。。。。。。。", e);
            }
            while (!isInterrupted()) {
                logger.info("ios设备【{}】录屏中。。。。。。。。。。。。。", udid);
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    break;
                }
            }
            logger.info("结束ios设备【{}】任务【{}】的录屏。。。。。。。", udid, videoName);
        }

        private void addImgToVideo(byte[] imgData) {
            try {
                BufferedImage read = ImageIO.read(new ByteArrayInputStream(imgData));
                recorder.record(converter.getFrame(read));
            } catch (IOException e) {
                logger.warn("添加图片文件出现错误。。。。。");
            }
        }

        public String endRecord(String uploadAddress, String tenantId) {
            try {
                recorder.stop();
            } catch (FFmpegFrameRecorder.Exception e) {
                logger.warn("结束录屏失败。。。。。。", e);
            }
            try {
                recorder.release();
            } catch (FFmpegFrameRecorder.Exception e) {
                logger.warn("释放录屏失败。。。。。。", e);
            }
            this.interrupt();
            String result = uploadVideoToServer(uploadAddress, tenantId);
            logger.warn("停止录屏并上传完成。。。。。。。。。。。");
            return result;
        }

        private String uploadVideoToServer(String uploadAddress, String tenantId) {
            File file = new File(filePath + videoName);
            String result = null;
            try {
                logger.info("上传视频文件");
                Attachment upload = HttpUtils.upload(uploadAddress, file.getAbsolutePath(), tenantId, null, FileBusinessTypeEnum.MOBILE_TASK_VIDEO.getCode());
                result = upload.getUrlPath();
                logger.info("上传视频文件结果：{}", result);
            } catch (Exception e) {
                logger.warn("上传视频文件失败。。。。。。。。", e);
            }
            //todo:上传完之后要删除
            return result;
        }

    }
}
