package ink.wgink.module.file.media.service.video.impl;

import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import ink.wgink.common.base.DefaultBaseService;
import ink.wgink.exceptions.FileException;
import ink.wgink.exceptions.SearchException;
import ink.wgink.module.file.media.dao.video.IVideoDao;
import ink.wgink.module.file.media.enums.ConvertStatusEnum;
import ink.wgink.module.file.media.manager.MediaManager;
import ink.wgink.module.file.media.manager.domain.MusicMetaInfo;
import ink.wgink.module.file.media.manager.domain.VideoMetaInfo;
import ink.wgink.module.file.media.pojo.dtos.video.VideoDTO;
import ink.wgink.module.file.media.pojo.dtos.video.VideoUploadSuccessDTO;
import ink.wgink.module.file.media.pojo.vos.video.VideoVO;
import ink.wgink.module.file.media.service.IMediaService;
import ink.wgink.module.file.media.service.video.IVideoService;
import ink.wgink.module.file.media.task.transcoding.VideoConvertManager;
import ink.wgink.module.file.media.task.transcoding.runnable.VideoConvertRunnable;
import ink.wgink.pojo.ListPage;
import ink.wgink.pojo.result.SuccessResultList;
import ink.wgink.properties.media.MediaProperties;
import ink.wgink.util.UUIDUtil;
import ink.wgink.util.date.DateUtil;
import ink.wgink.util.map.HashMapUtil;
import org.apache.catalina.connector.ClientAbortException;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.io.RandomAccessFile;
import java.net.URLEncoder;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.channels.WritableByteChannel;
import java.util.List;
import java.util.Map;

/**
 * When you feel like quitting. Think about why you started
 * 当你想要放弃的时候，想想当初你为何开始
 *
 * @ClassName: VideoServiceImpl
 * @Description: 视频
 * @Author: WangGeng
 * @Date: 2021/6/8 22:12
 * @Version: 1.0
 **/
@Service
public class VideoServiceImpl extends DefaultBaseService implements IVideoService {

    @Autowired
    private IMediaService mediaService;
    @Autowired
    private MediaProperties mediaProperties;
    @Autowired
    private IVideoDao videoDao;

    @Override
    public VideoUploadSuccessDTO save(VideoVO videoVO) {
        return save(null, videoVO);
    }

    @Override
    public VideoUploadSuccessDTO save(String token, VideoVO videoVO) {
        String fileId = UUIDUtil.getUUID();
        Map<String, Object> params = HashMapUtil.beanToMap(videoVO);
        params.put("fileId", fileId);
        if (StringUtils.isBlank(token)) {
            setSaveInfo(params);
        } else {
            setAppSaveInfo(token, params);
        }
        videoDao.save(params);

        return new VideoUploadSuccessDTO(params.get("fileId").toString(), params.get("fileName").toString(), Long.parseLong(params.get("fileSize").toString()));
    }

    @Override
    public void remove(List<String> ids) {
        Map<String, Object> params = getHashMap(2);
        params.put("fileIds", ids);
        List<VideoDTO> videoDTOs = videoDao.list(params);
        checkConverting(videoDTOs);
        setUpdateInfo(params);
        videoDao.remove(params);
    }

    @Override
    public void delete(List<String> ids) {
        Map<String, Object> params = getHashMap(2);
        params.put("fileIds", ids);
        List<VideoDTO> videoDTOs = videoDao.list(params);
        checkConverting(videoDTOs);
        // 删除文件
        for (VideoDTO videoDTO : videoDTOs) {
            deleteSourceFile(videoDTO.getFileFullPath());
        }
        // 删除记录
        videoDao.delete(params);
    }

    @Override
    public void updateConvert(String fileId) {
        VideoDTO videoDTO = get(fileId);
        if (videoDTO == null) {
            throw new SearchException("视频不存在");
        }
        VideoConvertManager.getInstance().convert(new VideoConvertRunnable(fileId, videoDTO.getFileFullPath(), this));
        // 更新状态
        Map<String, Object> params = getHashMap(8);
        params.put("convertStatus", ConvertStatusEnum.CONVERTING.getValue());
        params.put("fileId", fileId);
        setUpdateInfo(params);
        videoDao.updateConvert(params);
    }

    @Override
    public void updateConvert(String fileId, File convertFile) {
        VideoDTO videoDTO = get(fileId);
        if (videoDTO == null) {
            throw new SearchException("视频不存在");
        }
        Map<String, Object> params = getHashMap(10);
        params.put("fileSize", convertFile.length());
        params.put("fileType", "mp4");
        params.put("fileName", convertFile.getName());
        params.put("fileFullPath", convertFile.getAbsolutePath());

        VideoMetaInfo videoMetaInfo = MediaManager.getInstance().getVideoMetaInfo(convertFile);
        System.out.println(videoMetaInfo);
        params.put("videoDuration", videoMetaInfo.getDuration());
        params.put("videoWidth", videoMetaInfo.getWidth());
        params.put("videoHeight", videoMetaInfo.getHeight());
        params.put("videoBitRate", videoMetaInfo.getBitRate());
        params.put("videoEncoder", videoMetaInfo.getEncoder());
        params.put("videoFrameRate", videoMetaInfo.getFrameRate());
        params.put("convertStatus", ConvertStatusEnum.CONVERTED.getValue());

        MusicMetaInfo musicMetaInfo = videoMetaInfo.getMusicMetaInfo();
        if (musicMetaInfo != null) {
            params.put("audioDuration", musicMetaInfo.getDuration());
            params.put("audioBitRate", musicMetaInfo.getBitRate());
            params.put("audioSampleRate", musicMetaInfo.getSampleRate());
        }
        params.put("fileId", fileId);
        setUpdateInfoByUserId(params, "1");
        videoDao.update(params);
        // 删除源文件
        deleteSourceFile(videoDTO.getFileFullPath());
    }

    @Override
    public VideoUploadSuccessDTO upload(MultipartFile video) {
        return upload(null, video);
    }

    @Override
    public VideoUploadSuccessDTO upload(String token, MultipartFile video) {
        String filePath = VIDEO_PATH + File.separator + DateUtil.getDays();
        String uploadFolderPath = mediaProperties.getUploadPath() + File.separator + filePath;
        String fileName = video.getOriginalFilename();
        long fileSize = video.getSize();
        String fileType = mediaService.getFileType(fileName);
        String uuidFileName = UUIDUtil.getUUID() + "." + fileType;
        String fileMd5 = mediaService.upload(video, uploadFolderPath, uuidFileName);
        // 判断文件是否存在，已经存在的不再新增，直接返回结果，删除临时文件
        VideoDTO videoDTO = getByMd5(fileMd5);
        String fileFullPath = uploadFolderPath + File.separator + uuidFileName;
        File uploadFile = new File(fileFullPath);
        if (videoDTO != null) {
            LOG.error("上传的【{}】视频已经存在，原文件为【{}】，返回原文件ID", videoDTO.getFileName(), videoDTO.getFileName());
            uploadFile.delete();
            return new VideoUploadSuccessDTO(videoDTO.getFileId());
        }

        // 构建视频内容
        VideoVO videoVO = new VideoVO();
        videoVO.setFileName(fileName.split("\\.")[0]);
        videoVO.setFileFullPath(fileFullPath);
        videoVO.setFilePath(filePath);
        videoVO.setFileSize(fileSize);
        videoVO.setFileType(fileType);
        videoVO.setFileMd5(fileMd5);
        videoVO.setIsBack(0);

        VideoMetaInfo videoMetaInfo = MediaManager.getInstance().getVideoMetaInfo(uploadFile);
        if (videoMetaInfo == null) {
            throw new FileException("上传失败");
        }
        videoVO.setVideoDuration(videoMetaInfo.getDuration());
        videoVO.setVideoWidth(videoMetaInfo.getWidth());
        videoVO.setVideoHeight(videoMetaInfo.getHeight());
        videoVO.setVideoBitRate(videoMetaInfo.getBitRate());
        videoVO.setVideoEncoder(videoMetaInfo.getEncoder());
        videoVO.setVideoFrameRate(videoMetaInfo.getFrameRate());
        videoVO.setConvertStatus(ConvertStatusEnum.UN_CONVERTED.getValue());

        MusicMetaInfo musicMetaInfo = videoMetaInfo.getMusicMetaInfo();
        if (musicMetaInfo != null) {
            videoVO.setAudioDuration(musicMetaInfo.getDuration());
            videoVO.setAudioBitRate(musicMetaInfo.getBitRate());
            videoVO.setAudioSampleRate(musicMetaInfo.getSampleRate());
        }
        return save(token, videoVO);
    }

    @Override
    public SseEmitter getConvertProgress() {
        final String sseEmitterId = UUIDUtil.getUUID();
        SseEmitter sseEmitter = new SseEmitter(0L);
        sseEmitter.onTimeout(() -> {
            VideoConvertManager.getInstance().removeConvertProgressSseEmitter(sseEmitterId);
            LOG.debug("连接超时，移出队列");
        });
        sseEmitter.onCompletion(() -> {
            VideoConvertManager.getInstance().removeConvertProgressSseEmitter(sseEmitterId);
            LOG.debug("连接结束，移出队列");
        });
        VideoConvertManager.getInstance().addConvertProgressSseEmitter(sseEmitterId, sseEmitter);
        return sseEmitter;
    }

    @Override
    public VideoDTO get(Map<String, Object> params) {
        return videoDao.get(params);
    }

    @Override
    public VideoDTO get(String fileId) {
        Map<String, Object> params = getHashMap(2);
        params.put("fileId", fileId);
        return get(params);
    }

    @Override
    public VideoDTO getByMd5(String fileMd5) {
        Map<String, Object> params = getHashMap(2);
        params.put("fileMd5", fileMd5);
        return get(params);
    }

    @Override
    public List<VideoDTO> list(Map<String, Object> params) {
        return videoDao.list(params);
    }

    @Override
    public SuccessResultList<List<VideoDTO>> listPage(ListPage page) {
        PageHelper.startPage(page.getPage(), page.getRows());
        List<VideoDTO> videoDTOs = list(page.getParams());
        PageInfo<VideoDTO> pageInfo = new PageInfo<>(videoDTOs);
        return new SuccessResultList<>(videoDTOs, pageInfo.getPageNum(), pageInfo.getTotal());
    }

    @Override
    public void downLoad(HttpServletRequest request, HttpServletResponse response, Boolean isOpen, String fileId) {
        VideoDTO videoDTO = get(fileId);
        if (videoDTO == null) {
            response.setStatus(HttpStatus.MOVED_PERMANENTLY.value());
            try {
                response.sendRedirect("404.html");
            } catch (IOException e) {
                throw new FileException("文件输出异常", e);
            }
            return;
        }
        try (
                RandomAccessFile randomAccessFile = new RandomAccessFile(videoDTO.getFileFullPath(), "r");
                FileChannel fileChannel = randomAccessFile.getChannel();
                OutputStream outputStream = response.getOutputStream();
                WritableByteChannel writableByteChannel = Channels.newChannel(outputStream);
        ) {
            response.setHeader("Content-Length", String.valueOf(videoDTO.getFileSize()));
            response.setContentType(mediaService.getContentType(videoDTO.getFileType()));
            if (!isOpen) {
                response.setHeader("Content-Disposition", "attachment;fileName=" + URLEncoder.encode(videoDTO.getFileName(), "UTF-8"));
            } else {
                response.setHeader("Content-Disposition", "inline;fileName=" + URLEncoder.encode(videoDTO.getFileName(), "UTF-8"));
            }
            String rangeString = request.getHeader("Range");
            LOG.debug("range: {}", rangeString);
            long contentLength = Long.valueOf(videoDTO.getFileSize());
            long startRange = 0;
            long endRange = contentLength - 1;
            if (!StringUtils.isBlank(rangeString)) {
                if (!isOpen) {
                    response.setContentType("multipart/byteranges");
                }
                String[] rangeArray = rangeString.substring(rangeString.indexOf("=") + 1).split("-");
                startRange = Long.valueOf(rangeArray[0]);
                if (rangeArray.length > 1) {
                    endRange = Long.valueOf(rangeArray[1]);
                }
                setRangeHeader(startRange, endRange, response, videoDTO.getFileId(), contentLength);
                randomAccessFile.seek(startRange);
            }
            LOG.debug("startRange: {}, endRange: {}", startRange, endRange);
            long totalOutputLength = endRange - startRange + 1;
            fileChannel.transferTo(startRange, totalOutputLength, writableByteChannel);
            outputStream.flush();
        } catch (Exception e) {
            if (e instanceof ClientAbortException) {
                LOG.debug("客户端断开连接");
            } else {
                throw new FileException("文件输出异常", e);
            }
        }
    }

    /**
     * 处理视频流问题
     *
     * @param startRange
     * @param endRange
     * @param response
     * @param fileId
     * @param contentLength
     */
    private void setRangeHeader(long startRange, long endRange, HttpServletResponse response, String fileId, long contentLength) {
        // 这里不设置，会出现第一次加载很慢的情况
        response.setHeader("Content-Length", String.valueOf(endRange - startRange + 1));
        response.setHeader("Content-Range", String.format("bytes %d-%d/%d", startRange, endRange, contentLength));
        response.setHeader("Accept-Ranges", "bytes");
        response.setHeader("Etag", fileId);
        response.setStatus(206);
    }

    /**
     * 检查转换中状态
     */
    private void checkConverting(List<VideoDTO> videoDTOs) {
        for (VideoDTO videoDTO : videoDTOs) {
            if (StringUtils.equals(ConvertStatusEnum.CONVERTING.getValue(), videoDTO.getConvertStatus())) {
                throw new SearchException("存在转码中的视频，删除失败");
            }
        }
    }

    /**
     * 删除源文件
     *
     * @param sourceFilePath 源文件路径
     */
    private void deleteSourceFile(String sourceFilePath) {
        File file = new File(sourceFilePath);
        if (file.exists()) {
            boolean isDelete = file.delete();
            if (isDelete) {
                LOG.debug("源文件删除成功");
            } else {
                LOG.debug("源文件删除失败");
            }
        }
    }

}
