/**
 * Copyright (c) 2011-2017, James Zhan 詹波 (inlet@126.com).
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.zzwtec.wechat.sdk.render;

import com.jfinal.kit.StrKit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.net.URLEncoder;

/**
 * FileRender.
 */
public class FileRender extends Render {

	private static final Logger logger = LoggerFactory.getLogger(FileRender.class);
	private static final String DEFAULT_CONTENT_TYPE = "application/octet-stream";
	
	private File file;
	private static String baseDownloadPath;
	private static ServletContext servletContext;
	private String downloadFileName = null;
	
	public FileRender(File file) {
		if (file == null) {
			throw new IllegalArgumentException("file can not be null.");
		}
		this.file = file;
	}
	
	public FileRender(File file, String downloadFileName) {
		this(file);
		
		if (StrKit.isBlank(downloadFileName)) {
			throw new IllegalArgumentException("downloadFileName can not be blank.");
		}
		this.downloadFileName = downloadFileName;
	}
	
	public FileRender(String fileName) {
		if (StrKit.isBlank(fileName)) {
			throw new IllegalArgumentException("fileName can not be blank.");
		}
		
		String fullFileName;
		fileName = fileName.trim();
		if (fileName.startsWith("/") || fileName.startsWith("\\")) {
			if (baseDownloadPath.equals("/")) {
				fullFileName = fileName;
			} else {
				fullFileName = baseDownloadPath + fileName;	
			}
		} else {
			fullFileName = baseDownloadPath + File.separator + fileName;
		}
		
		this.file = new File(fullFileName);
	}
	
	public FileRender(String fileName, String downloadFileName) {
		this(fileName);
		
		if (StrKit.isBlank(downloadFileName)) {
			throw new IllegalArgumentException("downloadFileName can not be blank.");
		}
		this.downloadFileName = downloadFileName;
	}
	
	static void init(String baseDownloadPath, ServletContext servletContext) {
		FileRender.baseDownloadPath = baseDownloadPath;
		FileRender.servletContext = servletContext;
	}
	
	public void render() {
		
		// ---------
		response.setHeader("Accept-Ranges", "bytes");
		String fn = downloadFileName == null ? file.getName() : downloadFileName;
		response.setHeader("Content-disposition", "attachment; " + encodeFileName(request, fn));
        String contentType = servletContext.getMimeType(file.getName());
        response.setContentType(contentType != null ? contentType : DEFAULT_CONTENT_TYPE);
        
        // ---------
        if (StrKit.isBlank(request.getHeader("Range"))) {
        	normalRender();
        } else {
        	rangeRender();
        }
	}
	
	protected String encodeFileName(String fileName) {
		try {
			// return new String(fileName.getBytes("GBK"), "ISO8859-1");
			return new String(fileName.getBytes(getEncoding()), "ISO8859-1");
		} catch (UnsupportedEncodingException e) {
			return fileName;
		}
	}
	
	/**
	 * 依据浏览器判断编码规则
	 */
	public String encodeFileName(HttpServletRequest request, String fileName) {
		String userAgent = request.getHeader("User-Agent");
		try {
			String encodedFileName = URLEncoder.encode(fileName, "UTF8");
			// 如果没有UA，则默认使用IE的方式进行编码
			if (userAgent == null) {
				return "filename=\"" + encodedFileName + "\"";
			}
			
			userAgent = userAgent.toLowerCase();
			// IE浏览器，只能采用URLEncoder编码
			if (userAgent.indexOf("msie") != -1) {
				return "filename=\"" + encodedFileName + "\"";
			}
			
			// Opera浏览器只能采用filename*
			if (userAgent.indexOf("opera") != -1) {
				return "filename*=UTF-8''" + encodedFileName;
			}
			
			// Safari浏览器，只能采用ISO编码的中文输出,Chrome浏览器，只能采用MimeUtility编码或ISO编码的中文输出
			if (userAgent.indexOf("safari") != -1 || userAgent.indexOf("applewebkit") != -1 || userAgent.indexOf("chrome") != -1) {
				return "filename=\"" + new String(fileName.getBytes("UTF-8"), "ISO8859-1") + "\"";
			}
			
			// FireFox浏览器，可以使用MimeUtility或filename*或ISO编码的中文输出
			if (userAgent.indexOf("mozilla") != -1) {
				return "filename*=UTF-8''" + encodedFileName;
			}
			
			return "filename=\"" + encodedFileName + "\"";
		} catch (UnsupportedEncodingException e) {
			throw new RuntimeException(e);
		}
	}
	
	private void normalRender() {
		response.setHeader("Content-Length", String.valueOf(file.length()));
		InputStream inputStream = null;
        OutputStream outputStream = null;
        try {
            inputStream = new BufferedInputStream(new FileInputStream(file));
            outputStream = response.getOutputStream();
            byte[] buffer = new byte[1024];
            for (int len = -1; (len = inputStream.read(buffer)) != -1;) {
                outputStream.write(buffer, 0, len);
            }
            outputStream.flush();
        } catch (IOException e) {
        	if (getDevMode()) {
        		throw new RenderException(e);
        	}
        } catch (Exception e) {
        	throw new RenderException(e);
        } finally {
            if (inputStream != null)
                try {inputStream.close();} catch (IOException e) {logger.error(e.getMessage(), e);}
            if (outputStream != null)
            	try {outputStream.close();} catch (IOException e) {logger.error(e.getMessage(), e);}
        }
	}
	
	private void rangeRender() {
		Long[] range = {null, null};
		processRange(range);
		
		String contentLength = String.valueOf(range[1].longValue() - range[0].longValue() + 1);
		response.setHeader("Content-Length", contentLength);
		response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);	// status = 206
		
		// Content-Range: bytes 0-499/10000
		StringBuilder contentRange = new StringBuilder("bytes ").append(String.valueOf(range[0])).append("-").append(String.valueOf(range[1])).append("/").append(String.valueOf(file.length()));
		response.setHeader("Content-Range", contentRange.toString());
		
		InputStream inputStream = null;
		OutputStream outputStream = null;
        try {
        	long start = range[0];
        	long end = range[1];
            inputStream = new BufferedInputStream(new FileInputStream(file));
            if (inputStream.skip(start) != start)
                	throw new RuntimeException("File skip error");
            outputStream = response.getOutputStream();
            byte[] buffer = new byte[1024];
            long position = start;
            for (int len; position <= end && (len = inputStream.read(buffer)) != -1;) {
            	if (position + len <= end) {
            		outputStream.write(buffer, 0, len);
            		position += len;
            	}
            	else {
            		for (int i=0; i<len && position <= end; i++) {
            			outputStream.write(buffer[i]);
                    	position++;
            		}
            	}
            }
            outputStream.flush();
        }
        catch (IOException e) {
        	if (getDevMode())	throw new RenderException(e);
        }
        catch (Exception e) {
        	throw new RenderException(e);
        }
        finally {
            if (inputStream != null)
                try {inputStream.close();} catch (IOException e) {logger.error(e.getMessage(), e);}
            if (outputStream != null)
            	try {outputStream.close();} catch (IOException e) {logger.error(e.getMessage(), e);}
        }
	}
	
	/**
	 * Examples of byte-ranges-specifier values (assuming an entity-body of length 10000):
	 * The first 500 bytes (byte offsets 0-499, inclusive): bytes=0-499
	 * The second 500 bytes (byte offsets 500-999, inclusive): bytes=500-999
	 * The final 500 bytes (byte offsets 9500-9999, inclusive): bytes=-500
	 * 															Or bytes=9500-
	 */
	private void processRange(Long[] range) {
		String rangeStr = request.getHeader("Range");
		int index = rangeStr.indexOf(',');
		if (index != -1)
			rangeStr = rangeStr.substring(0, index);
		rangeStr = rangeStr.replace("bytes=", "");
		
		String[] arr = rangeStr.split("-", 2);
		if (arr.length < 2)
			throw new RuntimeException("Range error");
		
		long fileLength = file.length();
		for (int i=0; i<range.length; i++) {
			if (StrKit.notBlank(arr[i])) {
				range[i] = Long.parseLong(arr[i].trim());
				if (range[i] >= fileLength)
					range[i] = fileLength - 1;
			}
		}
		
		// Range format like: 9500-
		if (range[0] != null && range[1] == null) {
			range[1] = fileLength - 1;
		}
		// Range format like: -500
		else if (range[0] == null && range[1] != null) {
			range[0] = fileLength - range[1];
			range[1] = fileLength - 1;
		}
		
		// check final range
		if (range[0] == null || range[1] == null || range[0].longValue() > range[1].longValue())
			throw new RuntimeException("Range error");
	}
}

