package sgc.portal.base.storage.service;

import sgc.portal.SgcPortalProperties;
import sgc.portal.base.i18n.I18nMessage;
import sgc.portal.base.storage.model.FileBucket;
import sgc.portal.base.storage.model.ImageThumbnail;
import sgc.portal.base.exception.BusinessException;
import sgc.portal.base.storage.StorageException;
import sgc.portal.base.storage.Subpath;
import sgc.portal.base.storage.persistence.StorageMapper;
import sgc.portal.base.util.FileToHex;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.lang3.RandomStringUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.time.DurationFormatUtils;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import org.springframework.web.multipart.MultipartFile;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;

import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.image.BufferedImage;
import javax.imageio.ImageIO;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;

/**
 * 파일 업/다운로드 비즈니스 로직 인터페이스 구현체
 *
 * @author YongSang
 * @version 1.0.0
 * @since 2020. 9. 5.
 */
@Slf4j
@Service
@Transactional
public class FileSystemStorageService implements StorageService, InitializingBean {
    @Autowired
    private StorageMapper mapper;

    @Autowired
    private SgcPortalProperties props;

    private static List<String> allowedExtensions;

    /**
     * 첨부파일을 실제 물리 저장소에 이름을 변경(파일명 중복을 피하고 시스템 관리자가 파일명을 보더라도 어떤 파일인지 모르게 보안)하고
     * 파일 정보를 데이터베이스에 저장한다.
     *
     * @param id 파일 아이디가 null이 아닌경우 기존 파일 목록에 첨부파일을 추가 (첨부파일 수정 시 보통 사용)
     * @param multipartFiles 첨부파일
     * @param subpath 파일 업로드 패스
     * @return 저장된 첨부파일 정보
     */

    @Override
    public List<FileBucket> store(String id, List<MultipartFile> multipartFiles, Subpath subpath) {
        if (!CollectionUtils.isEmpty(multipartFiles)) {
            List<FileBucket> fileBuckets = new ArrayList<>(multipartFiles.size());
            if (StringUtils.isEmpty(id)) {
                id = RandomStringUtils.randomAlphanumeric(30);
            }

            for (MultipartFile multipartFile : multipartFiles) {
                if (multipartFile == null) {
                    continue;
                }

                String extension = FilenameUtils.getExtension(multipartFile.getOriginalFilename());
                if (!allowedExtensions.contains(Objects.requireNonNull(extension).toLowerCase())) {
                    throw new StorageException("exception.storage.not.allowd.file", multipartFile.getOriginalFilename());
                }

                FileBucket fileBucket = new FileBucket();
                fileBucket.setId(id);
                fileBucket.setBackNumber(Integer.parseInt(RandomStringUtils.randomNumeric(7)));
                fileBucket.setChangedFilename(fileBucket.getId() + fileBucket.getBackNumber());

                int index = multipartFile.getOriginalFilename().lastIndexOf("\\");
                String fileName = multipartFile.getOriginalFilename().substring(index+1);

                fileBucket.setOriginalFilename(fileName);
//                fileBucket.setOriginalFilename(multipartFile.getOriginalFilename());

                fileBucket.setFileSize(multipartFile.getSize());

                List<String> partial = subpath.resolve();
                partial.add(fileBucket.getChangedFilename());
                Path path = Paths.get(props.getStorage().getLocation(), partial.toArray(new String[partial.size()]));
                fileBucket.setLocation(path.getParent().toString());

                try {
                    Files.createDirectories(path.getParent());
                    Files.copy(multipartFile.getInputStream(), path);
                    fileBuckets.add(fileBucket);
                    mapper.insert(fileBucket);
                } catch (Exception e) {
                    for (FileBucket file : fileBuckets) {
                        Path filepath = Paths.get(file.getLocation(), file.getChangedFilename());
                        try {
                            Files.deleteIfExists(filepath);
                        } catch (IOException ioe) {
                            throw new StorageException("exception.storage", ioe.getMessage());
                        }
                    }
                    throw new StorageException("exception.storage", e.getMessage());
                }
            }
            return fileBuckets;
        }
        return Collections.emptyList();
    }

    /**
     * 첨부파일 정보를 읽어온다 (단건)
     *
     * @param id 파일아이디
     * @return 파일정보
     */

    @Override
    public FileBucket load(String id) {
        return mapper.findById(id);
    }

    /**
     * 첨부파일 정보를 읽어온다. (여러건 중 한 개)
     *
     * @param id 파일아이디
     * @param backNumber 파일번호
     * @return 파일정보
     */

    @Override
    public FileBucket load(String id, Integer backNumber) {
        return mapper.findByPk(id, backNumber);
    }

    /**
     * 첨부파일 정보를 읽어온다. (여러건 중 다건)
     *
     * @param id 파일아이디
     * @param backNumbers 파일번호
     * @return 파일정보
     */

    @Override
    public List<FileBucket> load(String id, List<Integer> backNumbers){
        return mapper.findAllByIdAndBackNumberIn(id, backNumbers);
    }

    /**
     * 첨부파일 정보를 모두 읽어온다.
     *
     * @param id 파일아이디
     * @return 파일정보
     */

    @Override
    public List<FileBucket> loadAll(String id) {
        return mapper.findAllById(id);
    }

    /**
     * 첨부파일 정보를 모두 읽어온다.
     *
     * @param id 파일아이디
     * @param thumbType 섬네일 형태  100,200,300,400,500,auto100,auto200,auto300,auto400,auto500
     * @return 파일정보
     */

    @Override
    public List<FileBucket> loadAllWithThumbnail(String id, String thumbType) {
    	List<FileBucket> fileList = this.loadAll(id);
    	int width = 0;
    	int height = 0;
    	String thumbKey = "";
    	String imageType = "png";
    	
    	switch(thumbType) {
    	case "100" :
    	case "100x100" :
    		thumbKey = "THUMB100";
    		height = width = 100;
    		break;
    	case "200" :
    	case "200x200" :
    		thumbKey = "THUMB200";
    		height = width = 200;
    		break;
     	case "300" :
    	case "300x300" :
    		thumbKey = "THUMB300";
    		height = width = 300;
    		break;
    	case "400" :
    	case "400x400" :
    		thumbKey = "THUMB300";
    		height = width = 400;
    		break;
    	case "500" :
    	case "500x500" :
    		thumbKey = "THUMB500";
    		height = width = 500;
    		break;
       	case "auto100" :
    		thumbKey = "THUMBA100";
    		height = 100;
    		width = 0;
    		break;
       	case "auto200" :
    		thumbKey = "THUMBA200";
    		height = 200;
    		width = 0;
    		break;
       	case "auto300" :
    		thumbKey = "THUMBA300";
    		height = 300;
    		width = 0;
    		break;
       	case "auto400" :
    		thumbKey = "THUMBA400";
    		height = 400;
    		width = 0;
    		break;
       	case "auto500" :
    		thumbKey = "THUMBA500";
    		height = 500;
    		width = 0;
    		break;
    	}
    	if (!"".equals(thumbKey) && !"".equals(imageType) && (width > 0 || height > 0)) {
    		ObjectMapper mapper = new ObjectMapper();
    		for(FileBucket fileBucket : fileList) {
    			ImageThumbnail thumbNail = new ImageThumbnail();
    			thumbNail.setWidth(width);
    			thumbNail.setHeight(height);
    			thumbNail.setImageType(imageType);
    			thumbNail.setImageSrc("");
    			Path imagePath = Paths.get(fileBucket.getLocation(), fileBucket.getChangedFilename());
    			if (imagePath.toFile().exists()) {
    				try {
    					Path jsonPath = Paths.get(fileBucket.getLocation(), fileBucket.getChangedFilename() + "_thumb.json");
    					Map<String,ImageThumbnail> jsonData = new HashMap<>();
    	    			if (jsonPath.toFile().exists()) {
    	    				jsonData = mapper.readValue(jsonPath.toFile(), new TypeReference<Map<String,ImageThumbnail>>(){});
    	    				if (jsonData.containsKey(thumbKey)) {
    	    					ImageThumbnail savedThumb = jsonData.get(thumbKey);
    	    					if (savedThumb != null && !"".equals(savedThumb.getImageSrc())) {
    	    						thumbNail.setImageSrc(savedThumb.getImageSrc());
    	    						thumbNail.setWidth(savedThumb.getWidth());
    	    		    			thumbNail.setHeight(savedThumb.getHeight());
    	    		    		}
    	    				}
    	    			}
    	    			if ("".equals(thumbNail.getImageSrc())) {
    	    				String originalFilename = fileBucket.getOriginalFilename().toLowerCase();
    	    				if (originalFilename.endsWith(".png") || originalFilename.endsWith(".jpg") || originalFilename.endsWith(".jpeg") || originalFilename.endsWith(".gif")) {
    	    					BufferedImage orgImage = ImageIO.read(imagePath.toFile());
    	    					int orgWidth = orgImage.getWidth();
    	    					int orgHeight = orgImage.getHeight();
    	    					int imgWidth = width;
    	    					int imgHeight = height;
    	    					Double orgRate = (double) orgWidth / (double) orgHeight;
    	    					if (imgHeight == 0) {
    	    						imgHeight = (int) ((double) imgWidth / orgRate);
    	    					} else if (imgWidth == 0) {
    	    						imgWidth = (int) ((double) imgHeight * orgRate);
    	    					}
    	    					Double thumbRate = (double) imgWidth / (double) imgHeight;
    	    		    		int resizeWidth = 0;
    	    					int resizeHeight = 0;
    	    					
    	    					if (thumbRate > orgRate) {
    	    						resizeWidth = (int) (imgHeight * orgRate);
    	    						resizeHeight = imgHeight;
    	    					} else {
    	    						resizeWidth = imgWidth;
    	    						resizeHeight = (int) (imgWidth / orgRate);
    	    					}
    	    					Image resizedImage = orgImage.getScaledInstance(resizeWidth, resizeHeight, Image.SCALE_AREA_AVERAGING);
    	    					orgImage = new BufferedImage(resizeWidth, resizeHeight, BufferedImage.TYPE_INT_ARGB);
    	    					Graphics g = orgImage.getGraphics();
    	    					g.drawImage(resizedImage, 0, 0, null);
    	    					orgWidth = orgImage.getWidth();
    	    					orgHeight = orgImage.getHeight();
    	    					BufferedImage thumbImage = new BufferedImage(imgWidth, imgHeight, BufferedImage.TYPE_INT_ARGB);
    	    					Graphics2D thumbGraphic = thumbImage.createGraphics();
    	    					int startX = 0;
    	    					int startY = 0;
    	    					int endX = 0;
    	    					int endY = 0;
    	    					if (thumbRate > orgRate) {
    	    						startX = 0;
    	    						endX = orgWidth;
    	    						int reSize = (int) (orgWidth / thumbRate);
    	    						startY = (orgHeight - reSize) / 2;
    	    						endY = startY + reSize;
    	    					} else {
    	    						startY = 0;
    	    						endY = orgHeight;
    	    						int reSize = (int) (orgHeight * thumbRate);
    	    						startX = (orgWidth - reSize) / 2;
    	    						endX = startX + reSize;
    	    					}
    	    					thumbGraphic.drawImage(orgImage, 0, 0, imgWidth, imgHeight, startX, startY, endX, endY, null);
    	    					ByteArrayOutputStream thumbBaos = new ByteArrayOutputStream();
    	    					ImageIO.write(thumbImage, imageType, thumbBaos);
    	    					log.info("gen image : " + originalFilename);
    	    					thumbNail.setWidth(imgWidth);
	    		    			thumbNail.setHeight(imgHeight);
	    		    			thumbNail.setImageSrc("data:image/"+imageType+";base64," + new String(Base64.encodeBase64(thumbBaos.toByteArray())));
    	    				} else {
    	    					thumbNail.setImageSrc(originalFilename);
        	    			}
    	    				jsonData.put(thumbKey, thumbNail);
    	    				mapper.writeValue(jsonPath.toFile(), jsonData);
    	    			}
					} catch (IOException e) {
						log.info("Thumbnail Generate Error : " + e.toString());
						thumbNail.setImageSrc("");
		    		}
    			} else {
    				thumbNail.setImageSrc("");
		    	}
    			if (thumbNail.getImageSrc() == null || "".equals(thumbNail.getImageSrc()) || !thumbNail.getImageSrc().startsWith("data:image")) {
    				thumbNail.setWidth(128);
    				thumbNail.setHeight(128);
    				thumbNail.setImageType("png");
    				thumbNail.setImageSrc("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAYAAADDPmHLAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAABUdJREFUeJztnVuoVVUUhmd2MbqfHroRnFMEpRZFQYYPgVmYQZSYRgRFEQYVFIIRlQ9BvniMikIoo0OWGYGlRg9h9JRYb90gqJQkijA1MioNuszB2tJinjHmup+1O/P74H/Zjn/Ocdb697rvpXMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACQIrO9tnkd8PoHdSpZxlu8ZpVaM1PAmNd+1/+CSU37vEaLV0/3POH6Xxipam2J9dM5r7v+F0Sq2lRi/XSOFoCVXktRq1qpLOehDcBlvXY0PZFlSgAShgAkDgFIHAKQOAQgcQhA4hCAxCEAiUMAEocAJA4BSBwCkDgEIHEIQOIQgMQhAIlDABKHACQOAUgcApA4BCBxCEDinOYmPyp+Va8dDSAAiUMAEqfvAKzwejPQbTXGkU3s7V5Pe73msv3rs173ep3VSqeTOd5rwk3u/4KO5uuEPgNwtddfyvyrK4xxkte412FlnCOSOTZ4ndFW4wPWGvNd2fI8ndJXAE722q3MXSUA8s3+0hhD049ec1rq3wovASjJi8q8VQIg3/wvImPEQjDasHeZe1dkDgJQwA1efyvzVgnA8xF/kbY37P+FgvEJQITTvb5X5qwSgDGvPw3vW14LveZ7rXN20BbU7H9RZEwCUIJNynxVA/Co4dvsdVRQ+5RRu6FG72XCSwAiLFbmqhOATwzfJUrtiNchpfagy07jqlD2bSoEQOFsl70YqWkARg3P1xHPVsOzsEL/t5TsnQAYvKPM87nyWVEAbjQ82yKeJw3PwyV7l9PNnyr0TwAC7lHmkAW6QPm8KACPGJ7xiOcuwzNRsv/NilcC97gxLgHIMeb1izLHMq+Llc+LAvCK4Xkg4plveD4u0b8WHtmVyVaBABQww+sDZfw3Bv9eJwDvGZ67I565hmdPQf/nev2s+I7cqyAABTykjL3X/XdNvk4Adhie2A2kSw3P/ohHTie1sG3J1RCACBd5/a6MvSRXUycAnxqexQW9aJ7DEc+DSr0ct5yZqyEABse4bP8ajrsxqKsTgG8Mz6KIZ8zwiI5T6i/0+k2pXRrUEQCDVcqYP7jsSlqeNgNwfcRznuERHRvUSng/Uuq05/cIgIJ4tXvz2ia6zV3AzRHPLMNzSKl9TKnLH7fkIQABM70+U8abMOrrBGCn4bk14rnc8OwL6uRgUQvvEqdDAAJWK2P96rILPlcoWqbUi14O6kZyc2w3PHdG+ppneL7N1Rzt9K3Lh0bvonXGuHcEdeENqqGizQBYK6ep8gdfrxo190X6utbw7MjVnNhR76LwOGOo+L8FQDvAFK2J9LXc8KzP1RCAnIY5ANZu4+1IX+OGZ0WuhgDkNMwBsC7q7Ir09a7huSZXQwByqhsA+RYeqCDtJpHoj6DupmCerwzfXKWnkcF4Ya2cAeRXzAkVexdpVzpFB4O6ZAJQlTqngcIawydboBlB7TNG7XrXHE4DG1I3APJsv/VM/vsuO06QC0MbjRrRvBb6JwANqRsA4SXDW0ZbW+qfADSkSQDkl0XaNfsifed1Tkv9E4CGNAmAID8ItZ4P0CQPgLT5w00C0JCmARDkEq48eLLXGEskN32e8zq1rcYHEICGyL348xWFt43LILdxr3PZ3TxZ2fLzLflvceUm0SltNKsgWyCt/5kdzdcJfb8fAHqGACQOAUgcApA4BCBxCEDilP3JcxtaNUV/E1SgyatWCMA0QJ6XK3rlCQGY5tzvsqd3CUDCyOVX2RqELzRuU7On7K8BAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAYTv4FzdbENLYlKywAAAAASUVORK5CYII=");
    			}	
    			fileBucket.setThumbnail(thumbNail);
    		}
    	}
    	return fileList;
    }
   
    /**
     * 첨부파일 정보를 삭제한다. (단건)
     *
     * @param id 파일아이디
     */

    @Override
    public void remove(String id) {
        removeInternal(Collections.singletonList(load(id)));
    }

    /**
     * 첨부파일 정보를 삭제한다. (여러건 중 다건)
     *
     * @param id 파일아이디
     * @param backNumbers 파일번호
     */

    @Override
    public void remove(String id, List<Integer> backNumbers) {
        List<FileBucket> fileBuckets = mapper.findAllByIdAndBackNumberIn(id, backNumbers);
        removeInternal(fileBuckets);
    }

    /**
     * 첨부파일 정보를 모두 삭제한다.
     *
     * @param id 파일아이디
     */

    @Override
    public void removeAll(String id) {
        removeInternal(loadAll(id));
    }


    @Override
    public void deleteAll(String date) {
        List<FileBucket> fileBuckets = mapper.findAllByDeletedAndCreateDateLessThan(date);
        try {
            for (FileBucket fileBucket : fileBuckets) {
            	//db데이터 삭제
                mapper.delete(fileBucket.getId(), fileBucket.getBackNumber());
                //물리파일삭제
                Path path = Paths.get(fileBucket.getLocation(), fileBucket.getChangedFilename());
                Files.deleteIfExists(path);
                //물리파일(thumb)삭제
                path = Paths.get(fileBucket.getLocation(), fileBucket.getChangedFilename() + "_thumb.json");
                Files.deleteIfExists(path);
                log.info("The file '{}' has been deleted.", path);
            }

            /*// 임시 디렉토리 삭제 (압축파일을 생성하기 위해 사용되는 디렉토리), 24시간 지난 것만 삭제
            int period = 24;
            Path tempDir = Paths.get(props.getStorage().getTempLocation());
            log.info("Starts deleting temporary files from this path. {}", tempDir);
            DirectoryStream<Path> stream = Files.newDirectoryStream(tempDir, new DirectoryStream.Filter<Path>() {
                private final Date now = new Date();

                @Override
                public boolean accept(Path entry) throws IOException {
                    int deletePeriod = Integer.parseInt(DurationFormatUtils.formatPeriod(
                            Files.getLastModifiedTime(entry).toMillis(), now.getTime(), "HH"));
                    return deletePeriod > period;
                }
            });

            int count = 0;
            for (Path entry : stream) {
                log.info("The file '{}' has been deleted.", entry);
                Files.deleteIfExists(entry);
                count++;
            }
            log.info("All {} files have been deleted.", count);*/
        } catch (IOException e) {
            throw new StorageException("exception.storage.delete", e.getMessage());
        }
    }

    /**
     * 객체가 초기화 될 때 파일업로드의 루트정보, 업로드 허용 확장자 변수를 초기화
     */

    @Override
    public void afterPropertiesSet() {
        this.allowedExtensions = props.getStorage().getAllowedExtensions();
    }

    private void removeInternal(List<FileBucket> fileBuckets) {
        for (FileBucket fileBucket : fileBuckets) {
            fileBucket.setDeleted("Y");
            mapper.update(fileBucket);
        }
    }


    @Override
    public String imgStreamInternal(String id) throws IOException{

        FileBucket fileBucket = mapper.findById(id);

        if (fileBucket == null) {
            return null;
        }

        String oriFileNm = fileBucket.getOriginalFilename();
        String retString;

        int pos = oriFileNm.lastIndexOf( "." );
        String ext = oriFileNm.substring( pos + 1 );

        if(!imgInclusion(ext)){
            throw new BusinessException("-1", new I18nMessage("storage.img.extension", ext));
        }

        Path path = Paths.get(fileBucket.getLocation(), fileBucket.getChangedFilename());

        byte[] fileContent = Files.readAllBytes(path);

        String changeString =  new String( Base64.encodeBase64( fileContent ) );
        retString = "data:image/" + ext + ";base64, " + changeString;

        return retString;
    }

    @Override
    public String imgStreamInternal(String id, Integer backNumber) throws IOException{

        FileBucket fileBucket = mapper.findByPk(id, backNumber);

        if (fileBucket == null) {
            return null;
        }

        String oriFileNm = fileBucket.getOriginalFilename();
        String retString;

        int pos = oriFileNm.lastIndexOf( "." );
        String ext = oriFileNm.substring( pos + 1 );

        if(!imgInclusion(ext)){
            throw new BusinessException("-1", new I18nMessage("storage.img.extension", ext));
        }

        Path path = Paths.get(fileBucket.getLocation(), fileBucket.getChangedFilename());

        byte[] fileContent = Files.readAllBytes(path);

        String changeString =  new String( Base64.encodeBase64( fileContent ) );
        retString = "data:image/" + ext + ";base64, " + changeString;

        return retString;
    }

    @Override
    public String imgToHexInternal(String id) throws IOException{

        FileBucket fileBucket = mapper.findById(id);

        if (fileBucket == null) {
            return null;
        }

        String oriFileNm = fileBucket.getOriginalFilename();
        String retString;

        int pos = oriFileNm.lastIndexOf( "." );
        String ext = oriFileNm.substring( pos + 1 );

        if(!imgInclusion(ext)){
            throw new BusinessException("-1", new I18nMessage("storage.img.extension", ext));
        }

        Path path = Paths.get(fileBucket.getLocation(), fileBucket.getChangedFilename());
        File imgFile = path.toFile();


        return FileToHex.convertToHex(imgFile);
    }

    @Override
    public String imgToHexInternal(String id, Integer backNumber) throws IOException{

        FileBucket fileBucket = mapper.findByPk(id, backNumber);

        if (fileBucket == null) {
            return null;
        }

        String oriFileNm = fileBucket.getOriginalFilename();
        String retString;

        int pos = oriFileNm.lastIndexOf( "." );
        String ext = oriFileNm.substring( pos + 1 );

        if(!imgInclusion(ext)){
            throw new BusinessException("-1", new I18nMessage("storage.img.extension", ext));
        }

        Path path = Paths.get(fileBucket.getLocation(), fileBucket.getChangedFilename());
        File imgFile = path.toFile();


        return FileToHex.convertToHex(imgFile);
    }

    /**
     * 이미지 파일 확장자 확인 한다.
     *
     * @param ext 확장자
     * @return 이미지 확장자 여부(true:포함, false:미포함)
     * @see SgcPortalProperties
     */
    private boolean imgInclusion(String ext){
        List<String> extensions = props.getStorage().getImgAllowedExtensions();
        for (String extension:extensions) {
            if(ext.equalsIgnoreCase(extension)) {
                return true;
            }
        }
        return false;
    }

}
