package com.ym.mec.biz.handler;

import be.tarsos.dsp.AudioDispatcher;
import be.tarsos.dsp.io.jvm.AudioDispatcherFactory;
import be.tarsos.dsp.pitch.PitchProcessor;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.ym.mec.biz.dal.dto.MusicPitchDetailDto;
import com.ym.mec.biz.dal.dto.SoundCompareHelper;
import com.ym.mec.biz.dal.dto.WavHeader;
import com.ym.mec.biz.dal.dto.WebSocketInfo;
import com.ym.mec.biz.service.SoundSocketService;
import com.ym.mec.biz.service.SysMusicCompareRecordService;
import com.ym.mec.common.constant.CommonConstants;
import org.apache.commons.io.FileUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import org.springframework.web.socket.*;
import org.springframework.web.socket.handler.AbstractWebSocketHandler;

import javax.sound.sampled.AudioFormat;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;

/**
 * @Author Joburgess
 * @Date 2021/6/9 0009
 */
@Service
public class WebSocketHandler extends AbstractWebSocketHandler {

    private static final Logger LOGGER = LoggerFactory.getLogger(WebSocketHandler.class);

    //存储客户端链接
    public static final Map<String, WebSocketSession> WS_CLIENTS = new ConcurrentHashMap<>();

    private final BigDecimal oneHundred = new BigDecimal(100);

    //检测偏移时长的最终时间
    private final int endCheckOffsetTime = 500;

    private final float simpleRate = 44100;
    private int simpleSize = 1024;

    private final AudioFormat audioFormat = new AudioFormat(simpleRate, 16, 1, true, false);
    private static final PitchProcessor.PitchEstimationAlgorithm algo = PitchProcessor.PitchEstimationAlgorithm.FFT_YIN;

    private static final String tmpDir = FileUtils.getTempDirectoryPath() + "/soundCompare/";

    //用户对应评分信息
    private Map<String, SoundCompareHelper> userSoundInfoMap = new ConcurrentHashMap<>();

    @Autowired
    private SysMusicCompareRecordService sysMusicCompareRecordService;

    public WebSocketHandler() {
        super();
        File soundDir = new File(tmpDir);
        if(!soundDir.exists()){
            soundDir.mkdir();
        }
    }

    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        String phone = session.getPrincipal().getName().split(":")[1];
        LOGGER.info("{}上线", phone);
        WS_CLIENTS.put(phone, session);
        super.afterConnectionEstablished(session);
    }

    @Override
    public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception {
        super.handleMessage(session, message);
    }

    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        String phone = session.getPrincipal().getName().split(":")[1];
        LOGGER.info("{}: {}", phone, message.getPayload());
        WebSocketInfo webSocketInfo = JSON.parseObject(message.getPayload(), WebSocketInfo.class);
        JSONObject bodyObject = (JSONObject) webSocketInfo.getBody();

        String commond = "";
        if(webSocketInfo.getHeader().containsKey(SoundSocketService.COMMOND)){
            commond = webSocketInfo.getHeader().get(SoundSocketService.COMMOND);
        }
        switch (commond){
            case SoundSocketService.MUSIC_XML:
                userSoundInfoMap.put(phone, new SoundCompareHelper());
                List<MusicPitchDetailDto> musicXmlInfos = JSON.parseArray(bodyObject.getString("musicXmlInfos"), MusicPitchDetailDto.class);
                userSoundInfoMap.get(phone).setMusicScoreId(bodyObject.getInteger("id"));
                userSoundInfoMap.get(phone).setMeasureXmlInfoMap(musicXmlInfos.stream().collect(Collectors.groupingBy(MusicPitchDetailDto::getMeasureIndex)));
                for (Map.Entry<Integer, List<MusicPitchDetailDto>> userMeasureXmlInfoEntry : userSoundInfoMap.get(phone).getMeasureXmlInfoMap().entrySet()) {
                    MusicPitchDetailDto musicPitchDetailDto = userMeasureXmlInfoEntry.getValue().stream().max(Comparator.comparing(MusicPitchDetailDto::getTimeStamp)).get();
                    userSoundInfoMap.get(phone).getMeasureEndTime().put(userMeasureXmlInfoEntry.getKey(), musicPitchDetailDto.getTimeStamp()+musicPitchDetailDto.getDuration());
                }
                break;
            case SoundSocketService.RECORD_START:
                if(!userSoundInfoMap.containsKey(phone)){
                    break;
                }
                File file = new File(tmpDir+phone + "_"+ userSoundInfoMap.get(phone).getMusicScoreId() +"_"+ LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss")) +".wav");
                userSoundInfoMap.get(phone).setAccessFile(new RandomAccessFile(file, "rw"));
                break;
            case SoundSocketService.RECORD_END:
                if(!userSoundInfoMap.containsKey(phone)){
                    break;
                }
                if(!CollectionUtils.isEmpty(userSoundInfoMap.get(phone).getMeasureEndTime())){
                    Integer lastMeasureIndex = userSoundInfoMap.get(phone).getMeasureEndTime().keySet().stream().min(Integer::compareTo).get();
                    double recordTime = userSoundInfoMap.get(phone).getAccessFile().length()/(audioFormat.getFrameSize()*audioFormat.getFrameRate())*1000;
                    //如果结束时时长大于某小节，则此小节需要评分
                    if(recordTime>userSoundInfoMap.get(phone).getMeasureEndTime().get(lastMeasureIndex)){
                        measureCompare(phone, lastMeasureIndex);
                        userSoundInfoMap.get(phone).getMeasureEndTime().remove(lastMeasureIndex);
                    }
                }
                calTotalScore(phone);
                createHeader(phone);
                break;
            case SoundSocketService.RECORD_CANCEL:
                createHeader(phone);
                break;
            default:
                break;
        }
    }

    @Override
    protected void handleBinaryMessage(WebSocketSession session, BinaryMessage message) throws Exception {
        String phone = session.getPrincipal().getName().split(":")[1];
        if(!userSoundInfoMap.containsKey(phone)){
            return;
        }
        if(Objects.nonNull(userSoundInfoMap.get(phone).getAccessFile())){
            userSoundInfoMap.get(phone).getAccessFile().write(message.getPayload().array());
        }
        List<MusicPitchDetailDto> recordInfo = new ArrayList<>();
        AudioDispatcher dispatcher = AudioDispatcherFactory.fromByteArray(message.getPayload().array(), audioFormat, simpleSize, 128);
        dispatcher.addAudioProcessor(new PitchProcessor(algo, simpleRate, simpleSize, (pitchDetectionResult, audioEvent) -> {
            int timeStamp = (int) (userSoundInfoMap.get(phone).getMeasureStartTime() + audioEvent.getTimeStamp()*1000);
            float pitch = pitchDetectionResult.getPitch();
//            LOGGER.info("频率：{}, {}", timeStamp, pitch);
            recordInfo.add(new MusicPitchDetailDto(timeStamp, pitch));
        }));
        dispatcher.run();
        if(Objects.isNull(userSoundInfoMap.get(phone).getAccessFile())){
            return;
        }

        double recordTime = userSoundInfoMap.get(phone).getAccessFile().length()/(audioFormat.getFrameSize()*audioFormat.getFrameRate())*1000;

        userSoundInfoMap.get(phone).setMeasureStartTime(recordTime);
        userSoundInfoMap.get(phone).getRecordMeasurePithInfo().addAll(recordInfo);

        //如果是第一小节，需要计算出录音与播放不同步导致的空白时间偏移量
        if(userSoundInfoMap.get(phone).getOffsetTime()<=0&&recordTime<endCheckOffsetTime){
            int hasPitchNum = 0;
            for (MusicPitchDetailDto ri : userSoundInfoMap.get(phone).getRecordMeasurePithInfo()) {
                if(hasPitchNum<=0){
                    userSoundInfoMap.get(phone).setOffsetTime(ri.getTimeStamp());
                }
                if(ri.getFrequency()>0){
                    hasPitchNum++;
                }else{
                    hasPitchNum=0;
                }
                if(hasPitchNum>=3){
                    LOGGER.info("偏移时间：{}", userSoundInfoMap.get(phone).getOffsetTime());
                    break;
                }
            }
            if(hasPitchNum<3){
                userSoundInfoMap.get(phone).setOffsetTime(0);
            }
        }

        if(userSoundInfoMap.get(phone).getOffsetTime()<=0&&recordTime<endCheckOffsetTime){
            return;
        }
        for (Map.Entry<Integer, Integer> userMeasureEndTimeMapEntry : userSoundInfoMap.get(phone).getMeasureEndTime().entrySet()) {
            if((recordTime - userSoundInfoMap.get(phone).getOffsetTime())>userMeasureEndTimeMapEntry.getValue()){
                measureCompare(phone, userMeasureEndTimeMapEntry.getKey());
                userSoundInfoMap.get(phone).getMeasureEndTime().remove(userMeasureEndTimeMapEntry.getKey());
                break;
            }
        }
    }

    @Override
    protected void handlePongMessage(WebSocketSession session, PongMessage message) throws Exception {
        super.handlePongMessage(session, message);
        LOGGER.info("心跳信息：{}", new String(message.getPayload().array(), "utf-8"));
    }

    @Override
    public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
        String phone = session.getPrincipal().getName().split(":")[1];
        session.close();
        if(!WS_CLIENTS.containsKey(phone)){
            return;
        }
        exception.printStackTrace();
        LOGGER.info("发生了错误,移除客户端: {}", phone);
        WS_CLIENTS.remove(phone);
        createHeader(phone);
    }

    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
        super.afterConnectionClosed(session, status);
        String phone = session.getPrincipal().getName().split(":")[1];
        LOGGER.info("{}离线", phone);
        WS_CLIENTS.remove(phone);
        createHeader(phone);
    }

    @Override
    public boolean supportsPartialMessages() {
        return super.supportsPartialMessages();
    }

    /**
     * @describe 保存录音数据，并生成wav头信息
     * @author Joburgess
     * @date 2021/6/25 0025
     * @param phone:
     * @return void
     */
    private void createHeader(String phone) throws IOException {
        if(!userSoundInfoMap.containsKey(phone)){
            return;
        }
        if(Objects.nonNull(userSoundInfoMap.get(phone).getAccessFile())){
            RandomAccessFile randomAccessFile = userSoundInfoMap.get(phone).getAccessFile();
            LOGGER.info("音频时长：{}", randomAccessFile.length()/(audioFormat.getFrameSize()*audioFormat.getFrameRate())*1000);
            randomAccessFile.seek(0);
            randomAccessFile.write(WavHeader.getWaveHeader(randomAccessFile.length(), (long) audioFormat.getFrameRate(), audioFormat.getSampleSizeInBits()));
            randomAccessFile.close();
            userSoundInfoMap.get(phone).setAccessFile(null);
        }
//        userSoundInfoMap.get(phone).setRecordMeasurePithInfo(null);
        LOGGER.info("评分数据：{}", JSON.toJSONString(userSoundInfoMap.get(phone)));
        userSoundInfoMap.remove(phone);
    }

    /**
     * @describe 数据比对，生成分数
     * @author Joburgess
     * @date 2021/6/25 0025
     * @param phone:
     * @param measureIndex:
     * @return void
     */
    private void measureCompare(String phone, int measureIndex) throws IOException {
        //相似度
        BigDecimal intonation = BigDecimal.ZERO;
        //节奏
        BigDecimal cadence = BigDecimal.ZERO;
        //完整度
        BigDecimal integrity = BigDecimal.ZERO;

        try {
            //最低有效频率
            float minValidFrequency = 20;

            //有效音频数量
            float validNum = 0;
            //音频有效阈值
            float validDuty = 0.5f;

            //音准匹配数量
            float intonationNum = 0;
            //音准匹配误差范围
            float intonationErrRange = 15;
            //音准有效阈值
            float intonationValidDuty = 0.7f;

            //节奏匹配数量
            float cadenceNum = 0;
            //节奏匹配误差范围
            float cadenceErrRange = 30;
            //节奏有效阈值
            float cadenceValidDuty = 0.6f;

            int totalCompareNum = userSoundInfoMap.get(phone).getMeasureXmlInfoMap().get(measureIndex).size();

            for (MusicPitchDetailDto musicXmlInfo : userSoundInfoMap.get(phone).getMeasureXmlInfoMap().get(measureIndex)) {
                int startTimeStamp = musicXmlInfo.getTimeStamp() + userSoundInfoMap.get(phone).getOffsetTime();
                int endTimeStamp = musicXmlInfo.getTimeStamp()+musicXmlInfo.getDuration() + userSoundInfoMap.get(phone).getOffsetTime();

                //时间范围内有效音准数量
                float recordValidIntonationNum = 0;
                //时间范围内有效节奏数量
                float cadenceValidNum = 0;
                //时间范围内有效音频数量
                float recordValidNum = 0;
                //时间范围内匹配次数
                float compareNum = 0;
                for (MusicPitchDetailDto recordInfo : userSoundInfoMap.get(phone).getRecordMeasurePithInfo()) {
                    //如果在时间范围之外直接跳过
                    if(recordInfo.getTimeStamp()<startTimeStamp||recordInfo.getTimeStamp()>endTimeStamp){
                        continue;
                    }
//                    LOGGER.info("{}频率({}-{})：{}, {}", recordInfo.getTimeStamp(), startTimeStamp, endTimeStamp, musicXmlInfo.getFrequency(), recordInfo.getFrequency());
                    compareNum++;
                    //如果在最低有效频率以下则跳过
                    if(recordInfo.getFrequency()<minValidFrequency&&musicXmlInfo.getFrequency()!=-1){
                        continue;
                    }
                    recordValidNum++;
                    //如果频率差值在节奏误差范围内
                    if(Math.abs(recordInfo.getFrequency()-musicXmlInfo.getFrequency())<=cadenceErrRange){
                        cadenceValidNum++;
                    }
                    //如果频率差值在音准误差范围内
                    if(Math.abs(recordInfo.getFrequency()-musicXmlInfo.getFrequency())<=intonationErrRange){
                        recordValidIntonationNum++;
                    }
                }
                //有效音频占比
                float recordValidDuty = recordValidNum/compareNum;
                if(recordValidDuty<validDuty){
                    continue;
                }
                validNum++;
                //有效音高占比
                float intonationDuty = recordValidIntonationNum/compareNum;
                //有效节奏占比
                float cadenceDuty = cadenceValidNum/compareNum;
                if(intonationDuty>=intonationValidDuty){
                    intonationNum++;
                }
                if(cadenceDuty>=cadenceValidDuty){
                    cadenceNum++;
                }
            }

            intonation = new BigDecimal(intonationNum).divide(new BigDecimal(totalCompareNum), CommonConstants.DECIMAL_PLACE, BigDecimal.ROUND_DOWN).multiply(oneHundred).setScale(0, BigDecimal.ROUND_HALF_UP);
            cadence = new BigDecimal(cadenceNum).divide(new BigDecimal(totalCompareNum), CommonConstants.DECIMAL_PLACE, BigDecimal.ROUND_DOWN).multiply(oneHundred).setScale(0, BigDecimal.ROUND_HALF_UP);
            integrity = new BigDecimal(validNum).divide(new BigDecimal(totalCompareNum), CommonConstants.DECIMAL_PLACE, BigDecimal.ROUND_DOWN).multiply(oneHundred).setScale(0, BigDecimal.ROUND_HALF_UP);
        } catch (ArithmeticException e){
            LOGGER.info("无musicXml信息");
        }

        if(userSoundInfoMap.get(phone).getUserScoreMap().containsKey("intonation")){
            userSoundInfoMap.get(phone).getUserScoreMap().put("intonation", intonation.add(userSoundInfoMap.get(phone).getUserScoreMap().get("intonation")));
        }else{
            userSoundInfoMap.get(phone).getUserScoreMap().put("intonation", intonation);
        }

        if(userSoundInfoMap.get(phone).getUserScoreMap().containsKey("cadence")){
            userSoundInfoMap.get(phone).getUserScoreMap().put("cadence", cadence.add(userSoundInfoMap.get(phone).getUserScoreMap().get("cadence")));
        }else{
            userSoundInfoMap.get(phone).getUserScoreMap().put("cadence", cadence);
        }

        if(userSoundInfoMap.get(phone).getUserScoreMap().containsKey("integrity")){
            userSoundInfoMap.get(phone).getUserScoreMap().put("integrity", integrity.add(userSoundInfoMap.get(phone).getUserScoreMap().get("integrity")));
        }else{
            userSoundInfoMap.get(phone).getUserScoreMap().put("integrity", integrity);
        }

        Map<String, BigDecimal> scoreData = new HashMap<>();
        scoreData.put("intonation", intonation);
        scoreData.put("cadence", cadence);
        scoreData.put("integrity", integrity);

        userSoundInfoMap.get(phone).getUserMeasureScoreMap().put(measureIndex, scoreData);

        WS_CLIENTS.get(phone).sendMessage(new TextMessage(JSON.toJSONString(createPushInfo("measureScore", measureIndex, intonation, cadence, integrity))));
    }

    /**
     * @describe 计算最终评分
     * @author Joburgess
     * @date 2021/6/25 0025
     * @param phone:
     * @return void
     */
    private void calTotalScore(String phone) throws IOException {
        int totalCompareNum = userSoundInfoMap.get(phone).getMeasureXmlInfoMap().keySet().size();
        int currentCompareNum = totalCompareNum-userSoundInfoMap.get(phone).getMeasureEndTime().keySet().size();
        BigDecimal intonation = BigDecimal.ZERO;
        BigDecimal cadence = BigDecimal.ZERO;
        BigDecimal integrity = BigDecimal.ZERO;

        if(currentCompareNum>0){
            intonation = userSoundInfoMap.get(phone).getUserScoreMap().get("intonation").divide(new BigDecimal(currentCompareNum), 0, BigDecimal.ROUND_DOWN);
            cadence = userSoundInfoMap.get(phone).getUserScoreMap().get("cadence").divide(new BigDecimal(currentCompareNum), 0, BigDecimal.ROUND_DOWN);
            integrity = new BigDecimal(currentCompareNum).divide(new BigDecimal(totalCompareNum), CommonConstants.DECIMAL_PLACE, BigDecimal.ROUND_DOWN).multiply(oneHundred).setScale(0, BigDecimal.ROUND_DOWN);
        }

        WS_CLIENTS.get(phone).sendMessage(new TextMessage(JSON.toJSONString(createPushInfo("overall", -1, intonation, cadence, integrity))));

        //存储评分数据
        sysMusicCompareRecordService.saveMusicCompareData(phone, userSoundInfoMap.get(phone).getMusicScoreId(), userSoundInfoMap.get(phone).getUserMeasureScoreMap());
    }

    /**
     * @describe 生成评分结果
     * @author Joburgess
     * @date 2021/6/25 0025
     * @param command:
     * @param measureIndex:
     * @param intonation:
     * @param cadence:
     * @param integrity:
     * @return com.ym.mec.biz.dal.dto.WebSocketInfo
     */
    private WebSocketInfo createPushInfo(String command, Integer measureIndex,
                                         BigDecimal intonation, BigDecimal cadence, BigDecimal integrity){
        WebSocketInfo webSocketInfo = new WebSocketInfo();
        HashMap<String, String> header = new HashMap<>();
        header.put("commond", command);
        webSocketInfo.setHeader(header);
        Map<String, Object> result = new HashMap<>();
        BigDecimal score = intonation.multiply(new BigDecimal(0.5)).add(cadence.multiply(new BigDecimal(0.5))).setScale(0, BigDecimal.ROUND_HALF_UP);
//        BigDecimal score = cadence.setScale(0, BigDecimal.ROUND_HALF_UP);
        result.put("score", score);
        result.put("intonation", intonation);
        result.put("cadence", cadence);
        result.put("integrity", integrity);
        result.put("measureIndex", measureIndex);
        webSocketInfo.setBody(result);
        LOGGER.info("小节频分：{}", JSON.toJSONString(webSocketInfo));
        return webSocketInfo;
    }
}
