package com.ylcm.sys.aliyun;

import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;

import org.apache.log4j.Logger;
import org.springframework.web.multipart.MultipartFile;

import com.aliyun.oss.ClientException;
import com.aliyun.oss.OSSClient;
import com.aliyun.oss.OSSException;
import com.aliyun.oss.model.CompleteMultipartUploadRequest;
import com.aliyun.oss.model.InitiateMultipartUploadRequest;
import com.aliyun.oss.model.InitiateMultipartUploadResult;
import com.aliyun.oss.model.ListPartsRequest;
import com.aliyun.oss.model.PartETag;
import com.aliyun.oss.model.PartListing;
import com.aliyun.oss.model.UploadPartRequest;
import com.aliyun.oss.model.UploadPartResult;
import com.ylcm.sys.common.MyThreadPool;

/**
 * 阿里云分片断点续传
 * @author zhoucong
 *
 */
public class OssMultipartUtil {
	Logger logger = Logger.getLogger(OssMultipartUtil.class);

	private static final String ACCESS_ID = OssConfig.ACCESS_ID.val;
	private static final String ACCESS_KEY = OssConfig.ACCESS_KEY.val;
	private static final String OSS_ENDPOINT = OssConfig.OSS_ENDPOINT.val;
	private static final String BUCKET = OssConfig.BUCKET.val;
	
	//默认5个线程
    private static ExecutorService executorService = Executors.newFixedThreadPool(5);
	//如果上传完毕，uploadId则失效，这里将client设为null，则进度查询会显示完毕（不是必须）
    private static OSSClient client = null;
	//存储OSS返回来的partETags，是一个线程同步的arraylist，靠这个去查询
    private static List<PartETag> partETags = Collections.synchronizedList(new ArrayList<PartETag>());
    //默认一个静态的字段贮备，查询进度需要
    private String uploadId;
    //默认一个静态的字段储备文件分割的数量，进度查询需要
    private static Integer partCount;
    //标识符，false情况下说名还没有开始，true则前端可以开始查询进度（不是必须）
    private boolean progress = false;
    // 每片最少2MB，初始分片大小的基数
    private long partSize = 2 * 512 * 1024L;
    // 创建一个要存放的Object的名称，使用文件名怕重复，直接改名+存入数据库
    private String key;
    
    /**
     * 上传文件
     * @param file			上传文件
     * @param key			文件存储路径
     * @param request		
     * @return
     */
    public String upload(MultipartFile file, String key, HttpServletRequest request){
    	this.key = key;
    	// 构造OSSClient
        client = new OSSClient(OSS_ENDPOINT, ACCESS_ID, ACCESS_KEY);
        
        HttpSession session = null;
        try {
        	session = request.getSession();
        	
        	// 1-获得uploadId
            uploadId = getUploadId(client, BUCKET, key);
            /*存储uploadId到session中*/
            session.setAttribute("uploadId", uploadId);
            
            // 2-断点续传分片
            partCount = getPartCount(file);
            
            // 3-开始配置子线程，开始上传多片文件到自己的bucket（oss的文件夹）中
            long startPos = 0l;
            long curPartSize = 0l;
            for (int i = 0; i < partCount; i++) {
                startPos = i * partSize;
                curPartSize = (i + 1 == partCount) ? (file.getSize() - startPos) : partSize;
                executorService.execute(new PartUploader(file, startPos, curPartSize, i + 1, uploadId, key));
            }
            
            // 4-将标识符设为true，前端开始可以访问到上传进度
            setProgress(true);
            //监控进度
            MyThreadPool.getInstance().execute(new progress(uploadId, key, session));
            
            // 5-等待所有的线程上传完毕后关闭线程
            executorService.shutdown();
            while (!executorService.isTerminated()) {
                try {
                    //如果有线程没有完毕，等待5秒
                    executorService.awaitTermination(5, TimeUnit.SECONDS);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            
            // 7-判断是否所有的分片都上传完毕（不是必须）
            // judgePartCount();

            // 8-必须：在将所有数据Part都上传完成后对整个文件的分片验证。
            verification();
            
//            // 9-成功，恢复初始值（恢复初始值是为了不让这次上传影响其他的文件上传）
//            restoreDefaultValues();
		} catch (OSSException oe) {
			logger.error("OSS错误原因：");
			logger.error("Error Message: " + oe.getErrorCode());
			logger.error("Error Code:       " + oe.getErrorCode());
			logger.error("Request ID:      " + oe.getRequestId());
			logger.error("Host ID:           " + oe.getHostId());
        } catch (ClientException ce) {
        	logger.error("OSSClient错误原因：");
        	logger.error("Error Message: " + ce.getMessage());
        } finally {
            //注意关闭资源
            if (client != null) {
                client.shutdown();
            }
            if (session != null && uploadId != null) {
            	session.setAttribute("mutipart:"+uploadId, 100);
			}
        }
    	return uploadId;
    }
	
    /**
     * 获得uploadId
     */
    private String getUploadId(OSSClient client, String bucketName, String key){
        InitiateMultipartUploadRequest request = new InitiateMultipartUploadRequest(bucketName, key);
        InitiateMultipartUploadResult result = client.initiateMultipartUpload(request);
        return result.getUploadId();
    }
    
    /**
     * 断点续传分片数量判断
     */
    public int getPartCount(MultipartFile file){
        long fileLength = file.getSize();
        int partCount = (int) (fileLength / partSize); // 分片个数，不能超过10000
        if (fileLength % partSize != 0) {
            partCount++;
        }
        if (partCount > 10000) {
        	//超过1万片，分片大小翻倍
        	partSize = partSize * 2;
        	return getPartCount(file);
        } else {
            return partCount;
        }
    }
    
    /**
     * 判断是否上传完毕，这里起始需要得到哪个错了，从哪个开始重新上传，获得part#，然后计算5M的大小上传
     * 这里只是单纯的记录一下，以后再完善
     * @return
     */
    public void judgePartCount(){
        if (partETags.size() != partCount) {
            System.out.println("一部分没有上传成功，需要从ListMultipartUploads里面查看上传数量");
            throw new IllegalStateException("Upload multiparts fail due to some parts are not finished yet");
        } else {
            System.out.println("上传成功，文件uploadId：" + uploadId + "\n");
        }
    }
    
    /**
     * 验证文件是否完全，OSS必须走这个方法
     * 最后返回CompleteMultipartUploadResult，这里不继续深化了
     * @return
     */
    public void verification(){
        // 修改顺序
        Collections.sort(partETags, new Comparator<PartETag>() {
            @Override
            public int compare(PartETag p1, PartETag p2) {
                return p1.getPartNumber() - p2.getPartNumber();
            }
        });
        //开始验证
        CompleteMultipartUploadRequest completeMultipartUploadRequest =
                new CompleteMultipartUploadRequest(BUCKET, key, uploadId, partETags);
        client.completeMultipartUpload(completeMultipartUploadRequest);
    }
    
    /**
     * 获取进度
     * @param uploadId
     * @param request
     * @return
     */
    public static Double getProgress(String uploadId, HttpServletRequest request){
    	Double progress = 0d;
    	try {
    		progress = (Double) request.getSession().getAttribute("mutipart:"+uploadId);
		} catch (Exception e) {
			progress = 0d;
		}
    	return progress;
    }
    
    /**
     * 恢复初始值
     * @return
     */
    public void restoreDefaultValues(){
        executorService = Executors.newFixedThreadPool(5);
        partETags = Collections.synchronizedList(new ArrayList<PartETag>());
        client = null;
        uploadId = null;
        partCount = 0;
    }
    
    public boolean isProgress() {
		return progress;
	}

	public void setProgress(boolean progress) {
		this.progress = progress;
	}

	/**
     * 子线程上传（多线程的上传效果提升明显，如果单线程则在for循环中一个个上传即可）
     */
    private static class PartUploader implements Runnable {
        private MultipartFile localFile;
        private long startPos;
        private long partSize;
        private int partNumber;
        private String uploadId;
        private String key;
        
        public PartUploader(MultipartFile localFile, long startPos, long partSize, int partNumber, String uploadId, String key) {
            this.localFile = localFile;
            this.startPos = startPos;
            this.partSize = partSize;
            this.partNumber = partNumber;
            this.uploadId = uploadId;
            this.key = key;
        }
        
        @Override
        public void run() {
            InputStream instream = null;
            try {
                instream = this.localFile.getInputStream();
                instream.skip(this.startPos);

                UploadPartRequest uploadPartRequest = new UploadPartRequest();
                uploadPartRequest.setBucketName(BUCKET);
                uploadPartRequest.setKey(key);
                uploadPartRequest.setUploadId(this.uploadId);
                uploadPartRequest.setInputStream(instream);
                uploadPartRequest.setPartSize(this.partSize);
                uploadPartRequest.setPartNumber(this.partNumber);

                UploadPartResult uploadPartResult = client.uploadPart(uploadPartRequest);
                synchronized (partETags) {
                    partETags.add(uploadPartResult.getPartETag());
                }
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                if (instream != null) {
                    try {
                        instream.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
    
    /**
     * 监控上传进度
     * @author Administrator
     *
     */
    private static class progress implements Runnable{
    	private String uploadId;
    	private String key;
    	private HttpSession session;
    	
    	public progress(String uploadId, String key, HttpSession session){
    		this.uploadId = uploadId;
    		this.key = key;
    		this.session = session;
    	}
    	
		@Override
		public void run() {
			while(true){
				//获得OSS上面的已经好了的分片和总分片相除获得进度
				ListPartsRequest listPartsRequest = new ListPartsRequest(BUCKET, key, uploadId);
				PartListing partListing;
				double progress = 0d;
				try {
					partListing = client.listParts(listPartsRequest);
					Integer nowPartCount = partListing.getParts().size();
					progress = (double)nowPartCount/partCount * 100;
					/*放入session中*/
					session.setAttribute("mutipart:"+uploadId, progress);
				}catch (Exception e){
					System.out.println("报错："+e);
					/*放入session中*/
					session.setAttribute("mutipart:"+uploadId, 100d);
					break;
				}
				try {
					/*0.5秒请求一次*/
					Thread.sleep(500);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
		}
    	
    }
}
