package com.ym.mec.biz.service.impl;

import be.tarsos.dsp.AudioDispatcher;
import be.tarsos.dsp.AudioEvent;
import be.tarsos.dsp.AudioProcessor;
import be.tarsos.dsp.SilenceDetector;
import be.tarsos.dsp.beatroot.BeatRootOnsetEventHandler;
import be.tarsos.dsp.io.PipedAudioStream;
import be.tarsos.dsp.io.TarsosDSPAudioInputStream;
import be.tarsos.dsp.io.jvm.AudioDispatcherFactory;
import be.tarsos.dsp.onsets.ComplexOnsetDetector;
import be.tarsos.dsp.onsets.OnsetHandler;
import be.tarsos.dsp.pitch.PitchProcessor;
import com.alibaba.fastjson.JSON;
import com.ym.mec.biz.dal.dao.SysMusicScoreAccompanimentDao;
import com.ym.mec.biz.dal.dao.SysMusicScoreDao;
import com.ym.mec.biz.dal.dto.MusicPitchDetailDto;
import com.ym.mec.biz.dal.entity.SysMusicScore;
import com.ym.mec.biz.dal.entity.SysMusicScoreAccompaniment;
import com.ym.mec.biz.service.SoundService;
import com.ym.mec.common.constant.CommonConstants;
import com.ym.mec.common.controller.BaseController;
import com.ym.mec.common.entity.HttpResponseResult;
import com.ym.mec.common.exception.BizException;
import com.ym.mec.common.service.IdGeneratorService;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.UnsupportedAudioFileException;
import java.io.File;
import java.io.IOException;
import java.math.BigDecimal;
import java.net.URL;
import java.util.*;

/**
 * @Author Joburgess
 * @Date 2021/5/19 0019
 */
@Service
public class SoundServiceImpl implements SoundService {

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

    private float sampleRate = 44100;

    @Autowired
    private SysMusicScoreDao sysMusicScoreDao;
    @Autowired
    private IdGeneratorService idGeneratorService;

    /**
     * @describe 音频节拍信息提取
     * @author Joburgess
     * @date 2021/5/19 0019
     * @return
     */
    private List<Double> beatExtractor(byte[] bytes, String url) throws UnsupportedAudioFileException {
        List<Double> times = new ArrayList<>();
        int size = 512;
        int overlap = 256;
        AudioDispatcher dispatcher = StringUtils.isBlank(url)?getFromByteArray(bytes, size, overlap):getFromFile(url, size, overlap);

        ComplexOnsetDetector detector = new ComplexOnsetDetector(size,0.7,0.1);
        detector.setHandler((double time, double salience) -> times.add(time));
        dispatcher.addAudioProcessor(detector);

        dispatcher.run();
        return times;
    }

    /**
     * @describe 音频分贝信息提取
     * @author Joburgess
     * @date 2021/5/19 0019
     * @param bytes: 文件字节
     * @return java.util.List<java.lang.Double>
     */
    private List<Double> soundPressureLevelExtractor(byte[] bytes, String url) throws UnsupportedAudioFileException {
        List<Double> pitchs = new ArrayList<>();
        int size = 2048;
        int overlap = 0;
        final SilenceDetector silenceDetecor = new SilenceDetector();
        AudioDispatcher dispatcher = StringUtils.isBlank(url)?getFromByteArray(bytes, size, overlap):getFromFile(url, size, overlap);
        dispatcher.addAudioProcessor(silenceDetecor);
        dispatcher.addAudioProcessor(new AudioProcessor() {
            @Override
            public void processingFinished() {

            }

            @Override
            public boolean process(AudioEvent audioEvent) {
                pitchs.add(Double.isInfinite(silenceDetecor.currentSPL())?0:silenceDetecor.currentSPL());
                return true;
            }
        });
        dispatcher.run();
        return pitchs;
    }

    /**
     * @describe 音频时域信息提取
     * @author Joburgess
     * @date 2021/5/19 0019
     * @return java.util.List<java.lang.Double>
     */
    public List<MusicPitchDetailDto> pitchExtractor(File audioFile) throws IOException, UnsupportedAudioFileException {
        List<MusicPitchDetailDto> result = new ArrayList<>();
        PitchProcessor.PitchEstimationAlgorithm algo = PitchProcessor.PitchEstimationAlgorithm.FFT_YIN;

        float samplerate = AudioSystem.getAudioFileFormat(audioFile).getFormat().getSampleRate();
        int size = 256;
        int overlap = 128;
        AudioDispatcher dispatcher = AudioDispatcherFactory.fromFile(audioFile, size, overlap);
        dispatcher.addAudioProcessor(new PitchProcessor(algo, samplerate, size, (pitchDetectionResult, audioEvent) -> {
            int timeStamp = (int) (audioEvent.getTimeStamp()*1000);
            float pitch = pitchDetectionResult.getPitch();
            result.add(new MusicPitchDetailDto(timeStamp, pitch));
        }));
        dispatcher.run();
        return result;
    }

    private AudioDispatcher getFromByteArray(byte[] bytes, int size, int overlap) throws UnsupportedAudioFileException {
        AudioFormat audioFormat = new AudioFormat(sampleRate, 16, 1, true, false);
        AudioDispatcher dispatcher = AudioDispatcherFactory.fromByteArray(bytes, audioFormat, size, overlap);
        return dispatcher;
    }

    private AudioDispatcher getFromFile(String url, int size, int overlap) {
        PipedAudioStream file = new PipedAudioStream(url);
        TarsosDSPAudioInputStream stream = file.getMonoStream(44100,0);

        AudioDispatcher dispatcher = new AudioDispatcher(stream, size, overlap);
        return dispatcher;
    }

    @Override
    public HttpResponseResult compare(MultipartFile record, Integer musicScoreId) {
        if(record.getSize()<1024){
            throw new BizException("录音时间太短");
        }
        SysMusicScore sysMusicScore = sysMusicScoreDao.get(musicScoreId);
        if(Objects.isNull(sysMusicScore)|| StringUtils.isBlank(sysMusicScore.getUrl())){
            throw new BizException("伴奏信息错误");
        }
        Map<String, BigDecimal> result = new HashMap<>();

        LOGGER.info("录音文件大小：{}MB，伴奏编号：{}，源文件：{}", record.getSize()/1048576f, musicScoreId, sysMusicScore.getUrl());
        String tmpDir = FileUtils.getTempDirectoryPath() + "/";

        BigDecimal oneHandred = new BigDecimal(100);
        //总分
        BigDecimal score = BigDecimal.ZERO;
        //相似度
        BigDecimal intonation = BigDecimal.ZERO;
        //节奏
        BigDecimal cadence = BigDecimal.ZERO;
        //完整度
        BigDecimal integrity = BigDecimal.ZERO;
        File f = null;
        File f_r = null;
        try {
            //存储录音文件
            String filePath_r =  tmpDir + idGeneratorService.generatorId("sound") + "_r.wav";
            f_r = new File(filePath_r);
            FileUtils.copyToFile(record.getInputStream(), f_r);

            //下载原音文件
            URL url = new URL(sysMusicScore.getUrl());
            String filePath = tmpDir + idGeneratorService.generatorId("sound") + "_s.mp3";
            f = new File(filePath);
            FileUtils.copyURLToFile(url, f);

            //相似度
            List<Double> pitchs_s = soundPressureLevelExtractor(null, filePath);
            List<Double> pitchs_r = soundPressureLevelExtractor(null, filePath_r);

            int maxLength = pitchs_s.size();
            if(pitchs_r.size()<maxLength){
                maxLength = pitchs_r.size();
            }

            double pitchSize = 0;
            double allPitchGap = 0;
            for(int i=0;i<maxLength;i++){
                double pitch1 = Math.abs(pitchs_s.get(i));
                double pitch2 = Math.abs(pitchs_r.get(i));
                double pitchGap = Math.abs(pitch1-pitch2);
                if(pitchGap>pitch1||pitchGap>15){
                    pitchGap = pitch1;
                }else if(pitchGap<5){
                    pitchGap = 0;
                }
                allPitchGap+=pitchGap;
                pitchSize+=pitch1;
            }
            double intonation_d = 0;
            if(pitchSize>0){
                intonation_d = 1-(allPitchGap/pitchSize);
            }
            intonation = new BigDecimal(intonation_d).multiply(oneHandred).setScale(0, BigDecimal.ROUND_HALF_UP);

            //节奏
            List<Double> times_s = beatExtractor(null, filePath);
            List<Double> times_r = beatExtractor(null, filePath_r);

            float sameTimes = 0;
            for (Double time1 : times_s) {
                for (Double time2 : times_r) {
                    if(Math.abs(time2-time1)<1.3){
                        sameTimes++;
                        break;
                    }
                }
            }
            double cadence_d = 0;
            sameTimes = sameTimes>times_r.size()?times_r.size():sameTimes;
            int allTimes = times_s.size();
            if (times_r.size()>0){
                cadence_d = sameTimes/allTimes;
            }
            cadence = new BigDecimal(cadence_d).multiply(oneHandred).setScale(0, BigDecimal.ROUND_HALF_UP);

            //完整度
            BigDecimal time_s = new BigDecimal(times_s.size());
            BigDecimal time_r = new BigDecimal(times_r.size());
            if(time_s.compareTo(time_r)<0){
                time_r = time_s;
            }
            integrity = time_r.divide(time_s, CommonConstants.DECIMAL_PLACE, BigDecimal.ROUND_HALF_UP).multiply(oneHandred).setScale(0, BigDecimal.ROUND_HALF_UP);

            score = intonation.multiply(new BigDecimal(0.4)).add(cadence.multiply(new BigDecimal(0.4))).add(integrity.multiply(new BigDecimal(0.2))).setScale(0, BigDecimal.ROUND_HALF_UP);


        } catch (UnsupportedAudioFileException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            if(f!=null){
                f.delete();
            }
//            if(f_r!=null){
//                f_r.deleteOnExit();
//            }
        }

        result.put("score", score);
        result.put("intonation", intonation);
        result.put("cadence", cadence);
        result.put("integrity", integrity);
        return BaseController.succeed(result);
    }

    @Override
    public HttpResponseResult measureCompare(List<MusicPitchDetailDto> musicXmlInfos, MultipartFile record) {
        Map<String, BigDecimal> result = new HashMap<>();

        String tmpDir = FileUtils.getTempDirectoryPath() + "/";
        //存储录音文件
        String filePath_r =  tmpDir + idGeneratorService.generatorId("sound") + "_r.wav";

        LOGGER.info("录音文件大小：{}MB, 保存路径：{}, musicXml信息：{}", record.getSize()/1048576f, filePath_r, JSON.toJSONString(musicXmlInfos));

        BigDecimal oneHundred = new BigDecimal(100);
        //总分
        BigDecimal score = BigDecimal.ZERO;
        //相似度
        BigDecimal intonation = BigDecimal.ZERO;
        //节奏
        BigDecimal cadence = BigDecimal.ZERO;
        //完整度
        BigDecimal integrity = BigDecimal.ZERO;
        File f_r = null;
        try {
            f_r = new File(filePath_r);
            FileUtils.copyToFile(record.getInputStream(), f_r);

            List<MusicPitchDetailDto> recordInfos = pitchExtractor(f_r);

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

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

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

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

            for (MusicPitchDetailDto musicXmlInfo : musicXmlInfos) {
                int startTimeStamp = musicXmlInfo.getTimeStamp();
                int endTimeStamp = musicXmlInfo.getTimeStamp()+musicXmlInfo.getDuration();

                //时间范围内有效音准数量
                float recordValidIntonationNum = 0;
                //时间范围内有效节奏数量
                float cadenceValidNum = 0;
                //时间范围内有效音频数量
                float recordValidNum = 0;
                //时间范围内匹配次数
                float compareNum = 0;
                for (MusicPitchDetailDto recordInfo : recordInfos) {
                    //如果在时间范围之外直接跳过
                    if(recordInfo.getTimeStamp()<startTimeStamp||recordInfo.getTimeStamp()>endTimeStamp){
                        continue;
                    }
                    compareNum++;
                    //如果在最低有效频率以下则跳过
                    if(recordInfo.getFrequency()<minValidFrequency){
                        continue;
                    }
                    recordValidNum++;
                    //如果频率差值在节奏误差范围内
                    if(recordInfo.getFrequency()-musicXmlInfo.getFrequency()<=cadenceErrRange){
                        cadenceValidNum++;
                    }
                    //如果频率差值在音准误差范围内
                    if(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(musicXmlInfos.size()), CommonConstants.DECIMAL_PLACE, BigDecimal.ROUND_DOWN).multiply(oneHundred).setScale(0, BigDecimal.ROUND_HALF_UP);
            cadence = new BigDecimal(cadenceNum).divide(new BigDecimal(musicXmlInfos.size()), CommonConstants.DECIMAL_PLACE, BigDecimal.ROUND_DOWN).multiply(oneHundred).setScale(0, BigDecimal.ROUND_HALF_UP);
            integrity = new BigDecimal(validNum).divide(new BigDecimal(musicXmlInfos.size()), CommonConstants.DECIMAL_PLACE, BigDecimal.ROUND_DOWN).multiply(oneHundred).setScale(0, BigDecimal.ROUND_HALF_UP);

            score = intonation.multiply(new BigDecimal(0.4)).add(cadence.multiply(new BigDecimal(0.4))).add(integrity.multiply(new BigDecimal(0.2))).setScale(0, BigDecimal.ROUND_HALF_UP);
        } catch (UnsupportedAudioFileException e) {
            LOGGER.error("文件类型不支持：{}", FilenameUtils.getExtension(filePath_r));
        } catch (IOException e) {
            LOGGER.error("IO异常：{}", e);
        } catch (ArithmeticException e){
            LOGGER.info("无musicXml信息");
        }finally {
            if(f_r!=null){
                f_r.deleteOnExit();
            }
        }

        result.put("score", score);
        result.put("intonation", intonation);
        result.put("cadence", cadence);
        result.put("integrity", integrity);
        return BaseController.succeed(result);
    }
}
