package ink.wgink.module.file.service.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.SaveException;
import ink.wgink.exceptions.base.SystemException;
import ink.wgink.module.file.dao.IFileDao;
import ink.wgink.module.file.enums.UploadTypeEnum;
import ink.wgink.module.file.pojo.dtos.FileDTO;
import ink.wgink.module.file.pojo.dtos.FileInfoDTO;
import ink.wgink.module.file.pojo.vos.FileVO;
import ink.wgink.module.file.service.IDefaultFileService;
import ink.wgink.module.file.service.IFileService;
import ink.wgink.pojo.ListPage;
import ink.wgink.pojo.pos.FilePO;
import ink.wgink.pojo.result.SuccessResultList;
import ink.wgink.properties.FileProperties;
import ink.wgink.util.UUIDUtil;
import ink.wgink.util.date.DateUtil;
import ink.wgink.util.map.HashMapUtil;
import ink.wgink.util.request.StaticResourceRequestUtil;
import net.coobird.thumbnailator.Thumbnails;
import org.apache.catalina.connector.ClientAbortException;
import org.apache.commons.lang3.StringUtils;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.net.URLEncoder;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.channels.WritableByteChannel;
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * @ClassName: DefaultFileServiceImpl
 * @Description: 默认文件处理
 * @Author: wanggeng
 * @Date: 2021/10/17 10:22 上午
 * @Version: 1.0
 */
@Service
public class DefaultFileServiceImpl extends DefaultBaseService implements IDefaultFileService {

    private static final char[] HEX_CODE = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'};

    @Autowired
    private IFileDao fileDao;
    @Autowired
    private FileProperties fileProperties;
    private String[] imageTypes;
    private String[] videoTypes;
    private String[] audioTypes;
    private String[] fileTypes;

    @Override
    public void saveFile(FileVO fileVO) {
        fileDao.save(HashMapUtil.beanToMap(fileVO));
    }

    @Override
    public String saveFileByUserId(String userId, FileVO fileVO) {
        String fileId;
        if (StringUtils.isBlank(fileVO.getFileId())) {
            fileId = UUIDUtil.getUUID();
            fileVO.setFileId(fileId);
        } else {
            fileId = fileVO.getFileId();
        }
        if (StringUtils.isBlank(fileVO.getGmtCreate())) {
            String dateTime = DateUtil.getTime();
            fileVO.setGmtCreate(dateTime);
            fileVO.setGmtModified(dateTime);
        }
        if (StringUtils.isBlank(fileVO.getCreator())) {
            fileVO.setCreator(userId);
            fileVO.setModifier(userId);
        }
        Map<String, Object> params = HashMapUtil.beanToMap(fileVO);
        fileDao.save(params);
        return fileId;
    }

    /**
     * 上传文件
     *
     * @param token
     * @param uploadFile
     * @param uploadTypeEnum
     * @param params
     * @throws SystemException
     */
    @Override
    public void uploadFile(String token, MultipartFile uploadFile, UploadTypeEnum uploadTypeEnum, Map<String, Object> params) throws SystemException {
        uploadFile(token, uploadFile, uploadTypeEnum, params, false);
    }

    @Override
    public void uploadFileByUserId(String userId, MultipartFile uploadFile, UploadTypeEnum uploadTypeEnum, Map<String, Object> params) {
        uploadFile(userId, uploadFile, uploadTypeEnum, params, true);
    }

    /**
     * 上传文件
     *
     * @param tokenOrUserId
     * @param uploadFile
     * @param uploadTypeEnum
     * @param params
     * @param useUserId
     */
    private synchronized void uploadFile(String tokenOrUserId, MultipartFile uploadFile, UploadTypeEnum uploadTypeEnum, Map<String, Object> params, boolean useUserId) {
        String baseUploadPath = fileProperties.getUploadPath();
        if (StringUtils.isBlank(baseUploadPath)) {
            throw new SystemException("上传路径未配置");
        }
        String fileName = uploadFile.getOriginalFilename();
        // 文件大小
        long fileSize = uploadFile.getSize();
        // 文件类型
        String fileType = getFileType(fileName);
        // 文件保存路径
        String uploadPath = getUploadPath(baseUploadPath, uploadTypeEnum, fileType);
        // 文件保存名称
        String uploadFileName = getUploadFileName(fileType);
        String fileMd5 = uploadFile(uploadFile, uploadPath, uploadFileName);
        if (fileMd5 == null) {
            throw new SaveException("文件上传失败");
        }
        // 获取MD5相同的文件
        List<FileInfoDTO> fileInfoDTOs = fileDao.listByMd5(IFileService.FILE_MD5_PREFIX + fileMd5);
        if (fileInfoDTOs.size() > 0) {
            // 删除新增的文件
            File uploadedFile = new File(uploadPath + File.separator + uploadFileName);
            if (uploadedFile.exists()) {
                uploadedFile.delete();
            }
            // 保存记录，但文件信息都是原有的文件
            params.clear();
            FileInfoDTO fileInfoDTO = fileInfoDTOs.get(0);
            params.put("fileSummary", "REF:" + fileInfoDTO.getFileId());
            if (useUserId) {
                saveFileByUserId(tokenOrUserId, params, fileInfoDTO.getFileName(), fileInfoDTO.getFilePath(), fileInfoDTO.getFileUrl(), fileInfoDTO.getFileType(), fileInfoDTO.getFileSize());
            } else {
                saveFile(tokenOrUserId, params, fileInfoDTO.getFileName(), fileInfoDTO.getFilePath(), fileInfoDTO.getFileUrl(), fileInfoDTO.getFileType(), fileInfoDTO.getFileSize());
            }
            return;
        }
        params.put("fileSummary", "MD5:" + fileMd5);
        if (useUserId) {
            saveUploadFileInfoByUserId(tokenOrUserId, fileName, uploadPath, uploadFileName, fileType, fileSize, params);
        } else {
            saveUploadFileInfo(tokenOrUserId, fileName, uploadPath, uploadFileName, fileType, fileSize, params);
        }
    }

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

    @Override
    public void deleteFile(List<FilePO> filePOs) {
        Map<String, Object> fileParams = getHashMap(4);
        // 删除文件
        for (FilePO filePO : filePOs) {
            // 如果文件描述为空，可以直接删除源文件
            if (StringUtils.isBlank(filePO.getFileSummary())) {
                deleteSourceFile(filePO.getFilePath());
                continue;
            }
            // 文件描述不为空时，需要判断是否删除的是源文件，源文件在一个系统中只保留一份
            // 如果是引用文件的数据，不删除源文件
            if (filePO.getFileSummary().startsWith(IFileService.FILE_REF_PREFIX)) {
                continue;
            }
            // 如果不是MD5源文件，略过
            if (!filePO.getFileSummary().startsWith(IFileService.FILE_MD5_PREFIX)) {
                continue;
            }
            // 如果删除的是源文件，需要查询系统中是否还存在引用的数据
            List<FileInfoDTO> fileInfoDTOs = fileDao.listByMd5(IFileService.FILE_REF_PREFIX + filePO.getFileId());
            // 如果不存在对源文件引用的数据，则直接删除源文件
            if (fileInfoDTOs.size() == 0) {
                deleteSourceFile(filePO.getFilePath());
                continue;
            }
            fileParams.clear();
            // 如果存在引用数据，取出第一个修改为源文件，并将其他的引用更新为新的源文件ID
            FileInfoDTO firstFileInfoDTO = fileInfoDTOs.get(0);
            fileParams.put("fileSummary", firstFileInfoDTO.getFileSummary());
            fileParams.put("fileId", firstFileInfoDTO.getFileId());
            fileDao.updateSummary(fileParams);
            // 获取其他的ID列表，更新文件引用关系
            List<String> otherFileIds = new ArrayList<>();
            for (int i = 1; i < fileInfoDTOs.size(); i++) {
                otherFileIds.add(fileInfoDTOs.get(i).getFileId());
            }
            // 如果不存在其它的引用，略过
            if (otherFileIds.isEmpty()) {
                continue;
            }
            fileParams.remove("fileId");
            fileParams.put("fileSummary", IFileService.FILE_REF_PREFIX + firstFileInfoDTO.getFileId());
            fileParams.put("fileIds", otherFileIds);
            fileDao.updateSummary(fileParams);
        }
    }

    @Override
    public void delete(List<String> ids) {
        Map<String, Object> params = getHashMap(2);
        params.put("fileIds", ids);
        fileDao.delete(params);
    }

    /**
     * 删除源文件
     *
     * @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("文件删除失败");
            }
        }
    }

    /**
     * 保存文件
     *
     * @param token        token
     * @param params       参数
     * @param fileName     文件名
     * @param fileFullPath 文件全路径
     * @param fileUrl      文件相对地址
     * @param fileType     文件类型
     * @param fileSize     文件大小
     */
    private void saveFile(String token, Map<String, Object> params, String fileName, String fileFullPath, String fileUrl, String fileType, long fileSize) {
        setFileParams(params, fileName, fileFullPath, fileUrl, fileType, fileSize);
        if (StringUtils.isBlank(token)) {
            setSaveInfo(params);
        } else {
            setAppSaveInfo(token, params);
        }
        fileDao.save(params);
    }

    /**
     * 保存文件
     *
     * @param userId
     * @param params
     * @param fileName
     * @param fileFullPath
     * @param fileUrl
     * @param fileType
     * @param fileSize
     */
    private void saveFileByUserId(String userId, Map<String, Object> params, String fileName, String fileFullPath, String fileUrl, String fileType, long fileSize) {
        setFileParams(params, fileName, fileFullPath, fileUrl, fileType, fileSize);
        setSaveInfoByUserId(params, userId);
        fileDao.save(params);
    }

    private void setFileParams(Map<String, Object> params, String fileName, String fileFullPath, String fileUrl, String fileType, long fileSize) {
        params.put("fileId", UUIDUtil.getUUID());
        params.put("fileName", fileName);
        params.put("filePath", fileFullPath);
        params.put("fileUrl", fileUrl);
        params.put("fileType", fileType);
        params.put("fileSize", fileSize);
        params.put("isBack", 0);
    }

    @Override
    public void saveUploadFileInfo(String token, String fileName, String uploadPath, String uploadFileName, String fileType, long fileSize, Map<String, Object> params) {
        saveUploadFileInfo(token, fileName, uploadPath, uploadFileName, fileType, fileSize, params, false);
    }

    @Override
    public void saveUploadFileInfoByUserId(String userId, String fileName, String uploadPath, String uploadFileName, String fileType, long fileSize, Map<String, Object> params) {
        saveUploadFileInfo(userId, fileName, uploadPath, uploadFileName, fileType, fileSize, params, true);
    }

    private void saveUploadFileInfo(String tokenOrUserId, String fileName, String uploadPath, String uploadFileName, String fileType, long fileSize, Map<String, Object> params, boolean useUserId) {
        String fixPath = uploadPath.replace(fileProperties.getUploadPath(), "");
        if ("\\".equals(File.separator)) {
            fixPath = fixPath.replace("\\", "/");
        }
        if (fixPath.startsWith("/")) {
            fixPath = fixPath.substring(1, fixPath.length() - 1);
        }
        String fileFullPath = String.format("%s%s%s", uploadPath, File.separator, uploadFileName);
        // 压缩图片
        if (isImageFile(fileType)) {
            compressImage(fileFullPath);
            File photo = new File(fileFullPath);
            fileSize = photo.length();
        }
        if (useUserId) {
            saveFileByUserId(tokenOrUserId, params, fileName, fileFullPath, String.format("files/%s/%s", fixPath, uploadFileName), fileType, fileSize);
        } else {
            saveFile(tokenOrUserId, params, fileName, fileFullPath, String.format("files/%s/%s", fixPath, uploadFileName), fileType, fileSize);
        }
    }

    @Override
    public boolean downLoadFile(HttpServletRequest request, HttpServletResponse response, Map<String, Object> params, boolean canRange) {
        FilePO filePO = fileDao.getPO(params);
        if (null == filePO) {
            throw new FileException("文件不存在");
        }
        boolean isOpen = Boolean.valueOf(params.get("isOpen").toString());
        return downLoadFile(request, response, filePO, isOpen, canRange);
    }

    @Override
    public boolean downLoadFile(HttpServletRequest request, HttpServletResponse response, FilePO filePO, boolean isOpen, boolean canRange) {
        boolean isDownloadComplete = false;
        try (
                RandomAccessFile randomAccessFile = new RandomAccessFile(filePO.getFilePath(), "r");
                FileChannel fileChannel = randomAccessFile.getChannel();
                OutputStream outputStream = response.getOutputStream();
                WritableByteChannel writableByteChannel = Channels.newChannel(outputStream);
        ) {
            response.setHeader("Content-Length", filePO.getFileSize().toString());
            response.setContentType(StaticResourceRequestUtil.getContentType(filePO.getFileType()));
            if (!isOpen) {
                // 下载
                response.setHeader("Content-Disposition", "attachment;fileName=" + URLEncoder.encode(filePO.getFileName(), "UTF-8"));
            } else {
                // 直接打开
                response.setHeader("Content-Disposition", "inline;fileName=" + URLEncoder.encode(filePO.getFileName(), "UTF-8"));
                // 如果是图片资源，开启缓存
                if (isImageFile(filePO.getFileType())) {
                    // 如果存在校验修改时间且未做修改，返回304，使用本地资源
                    String ifModifiedSince = request.getHeader("If-Modified-Since");
                    if (StringUtils.isNotBlank(ifModifiedSince) && StringUtils.equalsIgnoreCase(ifModifiedSince, filePO.getGmtModified())) {
                        response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
                        return true;
                    }
                    // 缓存有效时间为7天
                    response.setHeader("Expires", DateTime.now().plusDays(7).toDateTime(DateTimeZone.forID("GMT")).toString());
                    // 一小时之内不发送新请求
                    response.setHeader("max-age", "3600");
                    response.setHeader("Last-Modified", filePO.getGmtModified());
                }
            }
            String rangeString = null;
            if (canRange && request != null) {
                rangeString = request.getHeader("Range");
                LOG.debug("range: {}", rangeString);
            }
            long contentLength = Long.valueOf(filePO.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, filePO.getFileId(), contentLength);
                randomAccessFile.seek(startRange);
            }
            LOG.debug("startRange: {}, endRange: {}", startRange, endRange);
            long totalOutputLength = endRange - startRange + 1;
            fileChannel.transferTo(startRange, totalOutputLength, writableByteChannel);
            outputStream.flush();
            if (endRange == contentLength - 1) {
                isDownloadComplete = true;
            }
        } catch (Exception e) {
            e.printStackTrace();
            if (e instanceof ClientAbortException) {
                LOG.debug("客户端断开连接");
            } else {
                throw new FileException("文件输出异常", e);
            }
        }
        return isDownloadComplete;
    }


    @Override
    public String getUploadPath(String baseUploadPath, UploadTypeEnum uploadTypeEnum, String fileType) throws FileException {
        StringBuilder filePath = new StringBuilder();
        if (!baseUploadPath.endsWith(File.separator)) {
            filePath.append(baseUploadPath).append(File.separator);
        } else {
            filePath.append(baseUploadPath);
        }
        boolean hasFileType = !StringUtils.isBlank(fileType);
        if (uploadTypeEnum.getValue() == UploadTypeEnum.IMAGE.getValue()) {
            if (hasFileType && !isTypeCorrect(getImageTypes(), fileType)) {
                throw new FileException("图片格式不支持上传");
            }
            filePath.append("images");
        } else if (uploadTypeEnum.getValue() == UploadTypeEnum.VIDEO.getValue()) {
            if (hasFileType && !isTypeCorrect(getVideoTypes(), fileType)) {
                throw new FileException("视频格式不支持上传");
            }
            filePath.append("videos");
        } else if (uploadTypeEnum.getValue() == UploadTypeEnum.AUDIO.getValue()) {
            if (hasFileType && !isTypeCorrect(getAudioTypes(), fileType)) {
                throw new FileException("音频格式不支持上传");
            }
            filePath.append("audios");
        } else if (uploadTypeEnum.getValue() == UploadTypeEnum.ERROR_EXCEL.getValue()) {
            filePath.append("errorexcels");
        } else {
            if (hasFileType && !isTypeCorrect(getFileTypes(), fileType)) {
                throw new FileException("文件格式不支持上传");
            }
            filePath.append("files");
        }
        filePath.append(File.separator).append(DateUtil.getDays());
        return filePath.toString();
    }

    /**
     * 获取上传文件名称
     *
     * @param fileType
     * @return
     */
    private String getUploadFileName(String fileType) {
        String uploadFileName = UUIDUtil.get32UUID();
        if (!StringUtils.isEmpty(fileType)) {
            uploadFileName += "." + fileType;
        }
        return uploadFileName;
    }

    @Override
    public SuccessResultList<List<FileInfoDTO>> listPageInfo(ListPage page) {
        PageHelper.startPage(page.getPage(), page.getRows());
        List<FileInfoDTO> fileInfoDTOs = fileDao.listInfo(page.getParams());
        PageInfo<FileInfoDTO> pageInfo = new PageInfo<>(fileInfoDTOs);
        return new SuccessResultList<>(fileInfoDTOs, pageInfo.getPageNum(), pageInfo.getTotal());
    }

    @Override
    public List<FileDTO> list(List<String> idList) {
        Map<String, Object> params = getHashMap(1);
        params.put("ids", idList);
        return fileDao.list(params);
    }

    @Override
    public List<FilePO> listPO(List<String> idList) {
        Map<String, Object> params = getHashMap(2);
        params.put("fileIds", idList);
        return fileDao.listPO(params);
    }

    @Override
    public FilePO getPO(String fileId) {
        Map<String, Object> params = new HashMap<>();
        params.put("fileId", fileId);
        return fileDao.getPO(params);
    }

    @Override
    public List<FileDTO> listByUrl(String fileUrl) {
        Map<String, Object> params = getHashMap(2);
        params.put("fileUrl", fileUrl);
        return fileDao.list(params);
    }

    /**
     * 处理视频流问题
     *
     * @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);
    }

    /**
     * 是否是图片文件, GIF图片不压缩
     *
     * @param fileType
     * @return
     */
    private boolean isImageFile(String fileType) {
        if (StringUtils.equalsIgnoreCase("gif", fileType)) {
            return false;
        }
        String imageTypes = fileProperties.getImageTypes();
        for (String imageType : imageTypes.split(",")) {
            if (StringUtils.equalsIgnoreCase(fileType, imageType)) {
                return true;
            }
        }
        return false;
    }

    /**
     * 压缩图片
     *
     * @param fileFullPath
     */
    private void compressImage(String fileFullPath) {
        if (fileFullPath.endsWith(".blob")) {
            return;
        }
        try {
            Thumbnails.of(fileFullPath).scale(1.0f).outputQuality(fileProperties.getImageOutputQuality()).toFile(fileFullPath);
        } catch (IOException e) {
            LOG.error(e.getMessage(), e);
        }
    }

    /**
     * 获取文件类型
     *
     * @param fileName
     * @return
     */
    private String getFileType(String fileName) {
        String[] names = fileName.split("\\.");
        if (names != null) {
            return names[names.length - 1].toLowerCase();
        }
        return "";
    }

    public String[] getImageTypes() {
        return imageTypes == null ? fileProperties.getImageTypes().split(",") : imageTypes;
    }

    public void setImageTypes(String[] imageTypes) {
        this.imageTypes = imageTypes;
    }

    public String[] getVideoTypes() {
        return videoTypes == null ? fileProperties.getVideoTypes().split(",") : videoTypes;
    }

    public void setVideoTypes(String[] videoTypes) {
        this.videoTypes = videoTypes;
    }

    public String[] getAudioTypes() {
        return audioTypes == null ? fileProperties.getAudioTypes().split(",") : audioTypes;
    }

    public void setAudioTypes(String[] audioTypes) {
        this.audioTypes = audioTypes;
    }

    public String[] getFileTypes() {
        return fileTypes == null ? fileProperties.getFileTypes().split(",") : fileTypes;
    }

    public void setFileTypes(String[] fileTypes) {
        this.fileTypes = fileTypes;
    }


    /**
     * 校验类型
     *
     * @param types
     * @param fileType
     * @return
     */
    private boolean isTypeCorrect(String[] types, String fileType) {
        for (String type : types) {
            if (StringUtils.equalsIgnoreCase(fileType, type)) {
                return true;
            }
        }
        return false;
    }

    /**
     * 保存文件到本地
     *
     * @param uploadFile
     * @param filePath
     * @param uploadFileName
     * @return
     */
    private String uploadFile(MultipartFile uploadFile, String filePath, String uploadFileName) throws FileException {
        String fileMd5;
        File uploadFolder = new File(filePath);
        if (!uploadFolder.exists()) {
            uploadFolder.mkdirs();
        }
        InputStream uploadFileInputStream = null;
        FileOutputStream uploadFileOutputStream = null;
        try {
            uploadFileInputStream = uploadFile.getInputStream();
            uploadFileOutputStream = new FileOutputStream(new File(uploadFolder + "/" + uploadFileName));
            MessageDigest messageDigest = MessageDigest.getInstance("MD5");
            int readLength;
            for (byte[] buf = new byte[IFileService.INPUT_STREAM_SIZE]; (readLength = uploadFileInputStream.read(buf)) > -1; ) {
                uploadFileOutputStream.write(buf, 0, readLength);
                messageDigest.update(buf, 0, readLength);
            }
            uploadFileOutputStream.flush();
            // 计算文件的MD5
            byte[] data = messageDigest.digest();
            StringBuilder fileMd5SB = new StringBuilder(data.length * 2);
            for (byte b : data) {
                fileMd5SB.append(HEX_CODE[(b >> 4) & 0xF]);
                fileMd5SB.append(HEX_CODE[(b & 0xF)]);
            }
            fileMd5 = fileMd5SB.toString();
        } catch (Exception e) {
            LOG.error(e.getMessage(), e);
            throw new FileException("文件上传失败");
        } finally {
            try {
                if (null != uploadFileOutputStream) {
                    uploadFileOutputStream.close();
                }
                if (null != uploadFileInputStream) {
                    uploadFileInputStream.close();
                }
            } catch (Exception e1) {
                LOG.error(e1.getMessage());
                throw new FileException("文件上传失败");
            }
        }
        return fileMd5;
    }


}
