package charactermanaj.util;

import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * ダウンロードをサポートする
 */
public final class DownloadUtils {

	/**
	 * ロガー
	 */
	private static final Logger logger = Logger.getLogger(DownloadUtils.class.getName());

	/**
	 * 偽装するユーザーエージェント名(nullまたは空文字の場合は偽装しない)
	 */
	private String impersonateUserAgent;

	/**
	 * 最大ホップ数
	 */
	private int maxHop = 10;

	/**
	 * ダウンロードファイルを終了時に削除するか？
	 */
	private boolean deleteDownloadFileOnExit = true;

	public void setImpersonateUserAgent(String impersonateUserAgent) {
		this.impersonateUserAgent = impersonateUserAgent;
	}

	public String getImpersonateUserAgent() {
		return impersonateUserAgent;
	}

	public void setMaxHop(int maxHop) {
		this.maxHop = maxHop;
	}

	public int getMaxHop() {
		return maxHop;
	}

	public void setDeleteDownloadFileOnExit(boolean deleteOnExit) {
		deleteDownloadFileOnExit = deleteOnExit;
	}

	public boolean isDeleteDownloadFileOnExit() {
		return deleteDownloadFileOnExit;
	}

	/**
	 * ヘッドレスポンス
	 */
	public static final class HeadResponse {

		String location;

		String contentType;

		String fileName;

		public String getLocation() {
			return location;
		}

		public String getContentType() {
			return contentType;
		}

		public String getFileName() {
			return fileName;
		}

		/**
		 * ファイルの拡張子、なければ空。
		 * 返される拡張子はドットを含む。
		 * @return ドットで始まる拡張子、もしくは空
		 */
		public String getDotExtension() {
			String name = fileName;
			int pos = name.lastIndexOf('/');
			if (pos >= 0) {
				name = name.substring(pos + 1);
			}
			pos = name.lastIndexOf('\\');
			if (pos >= 0) {
				name = name.substring(pos + 1);
			}

			int extPos = name.lastIndexOf(".");
			String ext = "";
			if (extPos > 0) {
				// ドットから始まる拡張子に切り取る
				ext = name.substring(extPos).toLowerCase();
			}
			return ext;
		}

		@Override
		public String toString() {
			return "location=" + location + ", contentType=" + contentType + ", fileName=" + fileName;
		}
	}

	/**
	 * 指定したURLのコンテンツをダウンロードする
	 * @param location URL
	 * @param os 出力先
	 * @throws IOException 失敗した場合
	 */
	public void loadContents(String location, OutputStream os) throws IOException {
		loadContents(getHead(location), os);
	}

	public void loadContents(HeadResponse headResponse, OutputStream os) throws IOException {
		String realLoction = headResponse.getLocation();

		URL url = new URL(realLoction);
		HttpURLConnection conn = (HttpURLConnection) url.openConnection();
		if (impersonateUserAgent != null && impersonateUserAgent.length() > 0) {
			conn.setRequestProperty("User-Agent", impersonateUserAgent);
		}
		conn.connect();
		try {
			int status = conn.getResponseCode();
			if (status == HttpURLConnection.HTTP_NOT_FOUND) { // 404
				throw new FileNotFoundException("Failed to load contents. status=" + status + ", url=" + url);
			}
			if (status < HttpURLConnection.HTTP_OK || status >= HttpURLConnection.HTTP_MULT_CHOICE) { // 200未満、300以上
				throw new IOException("Failed to load contents. status=" + status + ", url=" + url);
			}

			byte[] buf = new byte[4096];
			InputStream is = conn.getInputStream();
			try {
				for (;;) {
					int rd = is.read(buf);
					if (rd < 0) {
						break;
					}
					os.write(buf, 0, rd);
				}
				os.flush();
			} finally {
				is.close();
			}
		} finally {
			conn.disconnect();
		}
	}

	/**
	 * テンポラリディレクトりにコンテンツをダウンロードする
	 * @param headResponse
	 * @return テンポラリファイル
	 * @throws IOException
	 */
	public File downloadTemporary(HeadResponse headResponse) throws IOException {
		String ext = headResponse.getDotExtension();
		if (ext == null || ext.length() == 0) {
			ext = ".tmp";
		}
		File tmpFile = File.createTempFile("cmj-", ext);

		if (isDeleteDownloadFileOnExit()) {
			tmpFile.deleteOnExit(); // 終了時にファイルを消す。(気休め程度)
		}

		logger.log(Level.INFO, "Create temporary file: " + tmpFile);
		try {
			OutputStream bos = new BufferedOutputStream(new FileOutputStream(tmpFile));
			try {
				loadContents(headResponse, bos);

			} finally {
				bos.close();
			}

		} catch (RuntimeException ex) {
			tmpFile.delete();
			logger.log(Level.INFO, "Delete temporary file: " + tmpFile);
			throw ex;

		} catch (IOException ex) {
			logger.log(Level.INFO, "Delete temporary file: " + tmpFile);
			tmpFile.delete();
			throw ex;
		}
		return tmpFile;
	}

	/**
	 * URLを指定してリダイレクトがある場合はリダイレクトでなくなるまで探索した最後のURLを返す。
	 * @param location 開始するURL
	 * @return 探索されたURL
	 * @throws IOException 読み込みに失敗した場合、もしくは最大ホップ数を超えた場合
	 */
	public HeadResponse getHead(String location) throws IOException {
		String initLocation = location;
		int hopCount = 0;
		for (;;) {
			logger.log(Level.INFO, "Connect to " + location);
			URL url = new URL(location);
			HttpURLConnection conn = (HttpURLConnection) url.openConnection();

			int status;
			conn.setRequestMethod("HEAD");
			conn.setInstanceFollowRedirects(false); // 自動リダイレクトはしない
			if (impersonateUserAgent != null && impersonateUserAgent.length() > 0) {
				conn.setRequestProperty("User-Agent", impersonateUserAgent);
			}
			conn.connect();
			try {
				status = conn.getResponseCode();

			} finally {
				conn.disconnect();
			}

			if (status == HttpURLConnection.HTTP_MOVED_TEMP || // 302
					status == HttpURLConnection.HTTP_MOVED_PERM || // 301
					status == HttpURLConnection.HTTP_SEE_OTHER) { // 303
				if (hopCount > maxHop) {
					// 転送回数が多すぎる!
					throw new IOException("too many hops! " + hopCount);
				}
				location = conn.getHeaderField("Location");
				if (location == null || location.isEmpty()) {
					// locationヘッダがない
					throw new IOException("bad response. location not found.");
				}
				hopCount++;
				logger.log(Level.INFO, "Location to " + location);
				continue;
			}

			if (status >= HttpURLConnection.HTTP_OK && status < HttpURLConnection.HTTP_MULT_CHOICE) { // 200以上 300未満
				HeadResponse response = new HeadResponse();
				response.location = location;
				response.contentType = conn.getContentType();

				String contentDisposition = conn.getHeaderField("Content-Disposition");
				String fileName = null;
				if (contentDisposition != null && contentDisposition.length() > 0) {
					fileName = parseAttachmentFileName(contentDisposition);
				}
				if (fileName == null || fileName.length() == 0) {
					fileName = initLocation; // ファイル名の指定がない場合は最初のロケーション名を使用する
				}
				response.fileName = fileName;
				logger.log(Level.INFO, "response success. " + response);
				return response;
			}

			if (status == HttpURLConnection.HTTP_NOT_FOUND) { // 404
				// ファイルが見つからない場合
				throw new FileNotFoundException("Failed to load contents. status=" + status + ", url=" + url);
			}

			// 何らかのエラー
			logger.log(Level.WARNING, "response failed. status=" + status);
			throw new IOException("response failed. status=" + status);
		}
	}

	/**
	 * セミコロンで行を区切る。
	 * (ダブルクォートがある場合は、閉じられるまではセミコロンは無視する。)
	 * @param line
	 * @return
	 */
	private static List<String> splitSemicolon(String line) {
		List<String> lines = new ArrayList<String>();
		StringBuilder buf = new StringBuilder();
		int mode = 0;
		for (char ch : line.toCharArray()) {
			if (mode == 0) {
				if (ch == '"') {
					// ダブルクォートがある場合は閉じるまでセミコロンを無視する
					buf.append((char) ch);
					mode = 1;

				} else if (ch == ';') {
					lines.add(buf.toString());
					buf.setLength(0);

				} else {
					buf.append((char) ch);
				}
			} else if (mode == 1) {
				if (ch == '"') {
					buf.append((char) ch);
					mode = 0;

				} else {
					buf.append((char) ch);
				}
			}
		}
		if (buf.length() > 0) {
			lines.add(buf.toString());
		}
		return lines;
	}

	/**
	 * key=value形式の文字列のリストからマップを生成する。
	 * valueがダブルクォートで囲まれている場合はダブルクォートを除去する。
	 * @param lines
	 * @return
	 */
	private static Map<String, String> parseKeyValuePair(List<String> lines) {
		Map<String, String> keyValueMap = new TreeMap<String, String>(String.CASE_INSENSITIVE_ORDER);
		for (String line : lines) {
			line = line.trim();
			if (!line.isEmpty()) {
				int pos = line.indexOf("=");
				String key, value;
				if (pos >= 0) {
					key = line.substring(0, pos);
					value = line.substring(pos + 1);
					value = value.trim();
					if (value.startsWith("\"") && value.endsWith("\"")) {
						// ダブルクォートで囲まれている場合は外す
						value = value.substring(1, value.length() - 1);
					}
				} else {
					key = line;
					value = "";
				}
				keyValueMap.put(key, value);
			}
		}
		return keyValueMap;
	}

	/**
	 * Content-Dispositionのヘッダーパラメータからファイル名を取得する。
	 * @param contentDisposition
	 * @return
	 */
	private static String parseAttachmentFileName(String contentDisposition) {
		List<String> lines = splitSemicolon(contentDisposition);
		logger.log(Level.FINE, "content-dispotion: " + lines);
		Map<String, String> kv = parseKeyValuePair(lines);

		String fileName = null;

		// 文字コードつきファイル名パラメータがあれば、それを解析・取得する
		String encodedFileName = kv.get("filename*");
		if (encodedFileName != null && encodedFileName.length() > 0) {
			// 文字コードの取得(空の場合もありえる)
			int pos = encodedFileName.indexOf('\'');
			String encoding = encodedFileName.substring(0, pos);
			if (encoding.isEmpty()) {
				encoding = "utf-8"; // UTF-8をデフォルトとみなす
			}

			// 言語の取得(空の場合もありえる)
			int pos2 = encodedFileName.indexOf('\'', pos + 1);
			String language = encodedFileName.substring(pos + 1, pos2);

			// ファイル名
			try {
				fileName = URLDecoder.decode(encodedFileName.substring(pos2 + 1), encoding);

			} catch (UnsupportedEncodingException ex) {
				logger.log(Level.WARNING, "url encoding error: " + encodedFileName, ex);
				fileName = null;
			}
			logger.log(Level.INFO, "attachment filename*=" + encoding + "," + language + "," + fileName);
		}

		// 文字コードつきファイル名がなければ、文字コードなしファイル名を取得する
		if (fileName == null || fileName.length() == 0) {
			fileName = kv.get("filename");
			logger.log(Level.INFO, "attachment filename=" + fileName);
		}

		return fileName;
	}
}
