package dareka.processor.util;

import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.Socket;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.GZIPInputStream;

import dareka.common.CloseUtil;
import dareka.common.FileUtil;
import dareka.common.Logger;
import dareka.processor.HttpHeader;
import dareka.processor.HttpRequestHeader;
import dareka.processor.Resource;
import dareka.processor.StringResource;
import dareka.processor.URLResource;
import dareka.processor.impl.Cache;
import dareka.processor.impl.NicoApiUtil;

public class CommentDownloader {
	
	private static void debug(String message) {
		Logger.debug("CommentDownloader: " + message);
	}
	
	// 該当するリクエストの際に呼ばれる
	public static Resource getResource(String smid,
			HttpRequestHeader requestHeader, Socket browser) throws IOException {
		return getResource(smid, "0", requestHeader, browser);
	}
	
	// 旧プレ用なのでそのまま残しておく
	public static Resource getResource(String smid, String fork,
			HttpRequestHeader requestHeader, Socket browser) throws IOException {
		String content = NicoApiUtil.getFlapiContent(
				"getflv?v=".concat(smid), requestHeader, null);
		Map<String, String> map = NicoApiUtil.query2map(content);
		String ms = map.get("ms");
		String threadId = map.get("thread_id");
		String userId = map.get("user_id");
		boolean isError = map.containsKey("error");
		
		if (isError || ms == null || threadId == null || userId == null)
			return StringResource.getNotFound();
		
		String extra = getExtraParams(map, requestHeader, false);
		String postData = "<thread no_compress=\"0\" fork=\"" + fork + "\" user_id=\"" + userId
				+ "\" " + "when=\"0\" waybackkey=\"0\" res_from=\"-1000\" "
				+ "version=\"20061206\" thread=\"" + threadId + "\"" + extra + " />";
		debug("postData=" + postData);
		
		URLResource r = NicoApiUtil.getURLResource(ms, requestHeader, postData);
		setContentFilename(r, ("1".equals(fork) ? "_" : "") + smid);
		return r;
	}
	
	private static void setContentFilename(URLResource r, String filename
			) throws IOException {
		// 既に受信済みのヘッダに対して設定する
		r.getResponseHeader(null, null).setMessageHeader("Content-Disposition",
				"attachment; filename=\"" + filename + ".xml");
	}
	
	/**
	 * 新プレ方式でコメントを取得するための URLResource を返す。
	 * 通常はIDを指定して内部で getflv を呼び出して情報を取得するが、
	 * 既に getflv の内容を取得している場合はそちらから情報を取得する。
	 * 
	 * @param target 取得対象のID(動画ID or スレッドID)、もしくは getflv の取得内容
	 * @param hasOwnerThread 投稿者コメントがあるか？
	 * @param requestHeader リクエストヘッダ
	 * @return コメントの URLResource (レスポンスヘッダ受信済み)、失敗した場合は null
	 * @throws IOException コメント取得中にエラーが発生した
	 * @see #store(File, byte[], String, int)
	 * @since NicoCache_nl+111111mod
	 */
	public static URLResource getURLResource(String target, boolean hasOwnerThread,
			HttpRequestHeader requestHeader) throws IOException {
		Map<String, String> map = getflv(target, requestHeader);
		if (map == null) {
			return null;
		}
		int max = 1000, end = 1000;
		try {
			int length = Integer.parseInt(map.get("l"));
			if (length < 60) {
				max = 100;
			} else if (length < 300) {
				max = 250;
			} else if (length < 600) {
				max = 500;
			}
			end =  length / 60 + 1;
		} catch (NumberFormatException e) {
			Logger.error(e);
		}
		// TODO optional_thread の扱いを検討
		
		String thread_id = map.get("thread_id");
		String user_id = map.get("user_id");
		String extra = getExtraParams(map, requestHeader, true);
		String postData = "<packet>" +
			"<thread thread=\"" + thread_id + "\" version=\"20090904\" user_id=\"" +
				user_id + "\"" + extra + "/>" +
			"<thread_leaves thread=\"" + thread_id + "\" user_id=\"" +
				user_id + "\"" + extra + ">0-" + end + ":100," + max + "</thread_leaves>" +
		(!hasOwnerThread || map.containsKey("optional_thread_id") ? "" :
			"<thread thread=\"" + thread_id + "\" version=\"20061206\" " +
				"res_from=\"-1000\" fork=\"1\" click_revision=\"-1\"/>"
		) + "</packet>";
		debug("postData=" + postData);
		
		URLResource r = NicoApiUtil.getURLResource(map.get("ms"), requestHeader, postData);
		setContentFilename(r, target.matches("\\w{2}\\d+") ? target : thread_id);
		return r;
	}
	
	/**
	 * コメントを指定ディレクトリに保存する。
	 * 最も新しい保存済みコメントから minInterval 秒以内であれば保存しない。
	 * 
	 * @param dir 保存先ディレクトリ、null を指定した場合は cacheCommentExtension
	 * 互換モードで保存 (cacheFolder 直下の "#comment" ディレクトリ)
	 * @param bcontent 保存するコメント内容
	 * @param theBeginning コメント先頭文字列、事前に取得していない場合は null
	 * @param minInterval 最小の保存間隔(秒)、0なら常に保存する
	 * @return 保存したコメントファイル、失敗した場合は null
	 * @see #getURLResource(String, boolean, HttpRequestHeader)
	 * @see #getTheBeginning(byte[])
	 * @since NicoCache_nl+111111mod
	 */
	public static File store(File dir, byte[] bcontent, String theBeginning,
			int minInterval) {
		boolean ccCompat = false;
		if (dir == null) {
			dir = getCacheCommentDir();
			ccCompat = true;
		}
		if (theBeginning == null) {
			theBeginning = getTheBeginning(bcontent);
		}
		String baseName = getBaseName(dir, theBeginning, minInterval, ccCompat);
		if (baseName != null) {
			if (!dir.exists()) dir.mkdirs();
			OutputStream out = null;
			try {
				File commFile = new File(dir, baseName +
						(isGzipped(bcontent) ? ".xml.gz" : ".xml"));
				out = new FileOutputStream(commFile);
				out.write(bcontent);
				return commFile;
			} catch (IOException e) {
				Logger.error(e);
			} finally {
				CloseUtil.close(out);
			}
		}
		return null;
	}
	
	private static File getCacheCommentDir() {
		return new File(Cache.getCacheDir(), "#comment");
	}
	
	/**
	 * 対象のコメントを取得して指定ディレクトリに無条件で保存する。
	 * 
	 * このメソッドは簡易取得のためのヘルパーメソッドなので、条件付き保存したい場合は
	 * {@link #getURLResource(String, boolean, HttpRequestHeader) getURLResource}
	 * を使って取得してから条件判断した後に、
	 * {@link #store(File, byte[], String, int) store}
	 * を使って保存する。
	 * 
	 * @param dir 保存先ディレクトリ、null なら cacheCommentExtension 互換
	 * @param target 取得対象のID(動画ID or スレッドID)、もしくは getflv の取得内容
	 * @param hasOwnerThread 投稿者コメントがあるか？
	 * @param requestHeader リクエストヘッダ
	 * @return 保存したコメントファイル、失敗した場合は null
	 * @since NicoCache_nl+111111mod
	 */
	public static File download(File dir, String target, boolean hasOwnerThread,
			HttpRequestHeader requestHeader) {
		try {
			requestHeader.setMessageHeader(HttpHeader.ACCEPT_ENCODING,
					HttpHeader.CONTENT_ENCODING_GZIP);
			URLResource r = getURLResource(target, hasOwnerThread, requestHeader);
			if (r != null) {
				return store(dir, r.getResponseBody(dir == null), null, 0);
			}
		} catch (IOException e) {
			Logger.error(e);
		}
		return null;
	}
	
	/**
	 * コメントの主要な情報が含まれている、最初の chat タグ直前までの文字列を展開して返す。
	 * gzip 圧縮されている場合は、chat タグが見つかるまで順次展開しながら検索するので、
	 * コメントの大部分を占める chat タグ部分の gzip 展開は行わない。
	 * 
	 * @param bcontent メッセージ鯖から返されたコンテンツ本体
	 * @return 最初の chat タグ直前までの文字列(chat タグが無い場合は全ての文字列)、
	 * 何らかのエラーが発生した場合は null
	 */
	public static String getTheBeginning(byte[] bcontent) {
		try {
			InputStream in = new ByteArrayInputStream(bcontent);
			if (isGzipped(bcontent)) {
				in = new GZIPInputStream(in);
			}
			return readHeader(in);
		} catch (IOException e) {
			Logger.error(e);
		}
		return null;
	}
	
	/**
	 * @param commentFile 取得対象のコメントファイル(gzip判定は拡張子のみ)
	 * @see #getTheBeginning(byte[])
	 * @since NicoCache_nl+111123mod
	 */
	public static String getTheBeginning(File commentFile) {
		InputStream in = null;
		try {
			return readHeader(in = getInputStream(commentFile));
		} catch (IOException e) {
			Logger.error(e);
		} finally {
			CloseUtil.close(in);
		}
		return null;
	}
	
	private static boolean isGzipped(byte[] buf) {
		return buf != null && buf.length > 1 &&
			   buf[0] == (byte) 0x1f && buf[1] == (byte) 0x8b;
	}
	
	private static InputStream getInputStream(File file) throws IOException {
		InputStream in =  new BufferedInputStream(new FileInputStream(file));
		if (file.getName().endsWith(".gz")) {
			in = new GZIPInputStream(in);
		}
		return in;
	}
	
	private static String readHeader(InputStream in) {
		try {
			StringBuilder sb = new StringBuilder();
			InputStreamReader r = new InputStreamReader(in, "UTF-8");
			char[] breaker = "<chat ".toCharArray();
			int c, n = 0;
			while ((c = r.read()) != -1) {
				sb.append((char) c);
				if (breaker[n] != c) {
					n = 0;
				} else if (++n == breaker.length) {
					sb.setLength(sb.length() - breaker.length);
					break;
				}
			}
			return sb.toString();
		} catch (IOException e) {
			Logger.error(e);
		}
		return null;
	}
	
	//公式動画(1000コメ)
	//<packet><thread thread="1313637197" version="20090904" user_id="1" threadkey="1314382908.dLa_KxOOAjv9yWas5BRCzjo0dQ0" force_184="1"/>
	//<thread_leaves thread="1313637197" user_id="1" threadkey="1314382908.dLa_KxOOAjv9yWas5BRCzjo0dQ0" force_184="1">0-25:100,1000</thread_leaves></packet>
	//一般動画(250コメ) 投稿者コメントあり
	//<packet><thread thread="1197027961" version="20090904" user_id="1"/>
	//<thread_leaves thread="1197027961" user_id="1">0-5:100,250</thread_leaves>
	//<thread thread="1197027961" version="20061206" res_from="-1000" fork="1" click_revision="-1"/></packet>
	
	private static Map<String, String> getflv(String target,
			HttpRequestHeader requestHeader) {
		String content = target;
		if (target.matches("\\w{2}\\d+")) {
			content = NicoApiUtil.getFlapiContent(
					"getflv", requestHeader, "v=" + target);
			if (content == null) {
				debug("no getflv: v=" + target);
				return null;
			}
		}
		Map<String, String> map = NicoApiUtil.query2map(content);
		if (map.containsKey("error") || !map.containsKey("ms") ||
				!map.containsKey("thread_id") || !map.containsKey("user_id")) {
			debug("getflv error: " + content);
			return null;
		}
		return map;
	}
	
	private static String getExtraParams(Map<String, String> getflv,
			HttpRequestHeader requestHeader, boolean scores) {
		String extra = "";
		String thread_id = getflv.get("thread_id");
		if (thread_id != null && getflv.containsKey("needs_key")) {
			String content = NicoApiUtil.getFlapiContent(
					"getthreadkey?thread=" + thread_id, requestHeader, null);
			if (content != null) {
				Map<String, String> getthreadkey = NicoApiUtil.query2map(content);
				String threadKey = getthreadkey.get("threadkey");
				if (threadKey != null) {
					extra += " threadkey=\"" + threadKey + "\"";
					if (getthreadkey.containsKey("force_184")) {
						extra += " force_184=\"1\"";
					}
				}
			}
		}
		if (scores) {
			extra += " scores=\"1\""; // NG共有機能対応(111005)
		}
		return extra;
	}
	
	private static final Pattern VIDEO_ID_PATTERN = Pattern.compile(
			"<view_counter [^>]*id=\"([a-z]{2}\\d+)\"");
	private static final Pattern SERVER_TIME_PATTERN = Pattern.compile(
			"<thread [^>]*server_time=\"(\\d{10})\"");
	private static final Pattern COMMENT_FILENAME_PATTERN = Pattern.compile(
			"([a-z]{2}\\d+)_(\\d{1,10}|\\d{12}z)(\\.xml(?:|\\.gz))");
	
	private static String getBaseName(File dir, String theBeginning,
			int minInterval, boolean ccCompat) {
		if (dir == null || theBeginning == null) {
			return null;
		}
		Matcher m;
		if (!(m = VIDEO_ID_PATTERN.matcher(theBeginning)).find()) {
			return null;
		}
		String videoId = m.group(1);
		long time;
		if ((m = SERVER_TIME_PATTERN.matcher(theBeginning)).find()) {
			time = Long.parseLong(m.group(1));
		} else {
			time = System.currentTimeMillis() / 1000L;
		}
		if (dir.isDirectory() && minInterval > 0) {
			long latest = getLatestCommentFile(dir, videoId, null).time;
			if (latest + minInterval >= time) {
				debug("restInterval=" + (latest + minInterval - time));
				return null;
			}
		}
		if (ccCompat) {
			return videoId + "_" + new SimpleDateFormat(
					"yyMMddHHmmss").format(new Date(time * 1000)) + "z";
		}
		return videoId + "_" + time;
	}
	
	/**
	 * 指定ディレクトリからコメントファイルを検索する。
	 * 
	 * @param dir 検索するディレクトリ、null なら cacheCommentExtension 互換
	 * @param smid 検索するコメントの smid
	 * @param suffix 一致するコメントの接尾辞、null なら最も新しいコメント
	 * @return 見つかったコメントファイル、見つからなかった場合は null
	 * @since NicoCache_nl+111111mod
	 */
	public static File find(File dir, String smid, String suffix) {
		return getLatestCommentFile(dir, smid, suffix).file;
	}
	
	/**
	 * コメントファイルを読み込み文字列で返す。指定のコメントファイルが
	 * cacheCommentExtension 形式の場合、投稿者コメントがあれば末尾に追加する。
	 * 
	 * @param commentFile コメントファイル
	 * @return 読み込んだコメントファイルの文字列
	 * @since NicoCache_nl+111111mod
	 */
	public static String load(File commentFile) {
		InputStream in = null;
		try {
			in = getInputStream(commentFile);
			ByteArrayOutputStream bout = new ByteArrayOutputStream();
			if (FileUtil.copy(in, bout) <= 0) {
				return null;
			}
			String content = bout.toString("UTF-8");
			
			// cacheCommentExtension形式の投稿者コメントがあるか？
			String name = commentFile.getName();
			if (name.endsWith(".gz")) {
				name = name.substring(0, name.length() - 3);
			}
			if (name.endsWith("z.xml")) {
				File ownerCommentFile = new File(commentFile.getParentFile(),
						name.replace("z.xml", "s.xml"));
				if (ownerCommentFile.isFile()) {
					bout.reset();
					if (FileUtil.copy(ownerCommentFile, bout) > 0) {
						// 投稿者コメントを通常コメント末尾に追加(新プレ仕様)
						String ownerContent = bout.toString("UTF-8");
						int pos = ownerContent.indexOf("<thread ");
						if (pos > 0) {
							content = content.replace("</packet>",
									ownerContent.substring(pos));
						}
					}
				}
			}
			return content;
		} catch (IOException e) {
			Logger.error(e);
		} finally {
			CloseUtil.close(in);
		}
		return null;
	}
	
	private static class CommentFile {
		File file; long time;
	}
	
	private static CommentFile getLatestCommentFile(
			File dir, final String videoId, final String suffix) {
		if (dir == null) {
			dir = getCacheCommentDir();
		}
		CommentFile latest = new CommentFile();
		File[] found = dir.listFiles(new FilenameFilter() {
			public boolean accept(File dir, String name) {
				Matcher m = COMMENT_FILENAME_PATTERN.matcher(name);
				return m.matches() && videoId.equals(m.group(1)) &&
						(suffix == null || suffix.equals(m.group(2)));
			}
		});
		if (found == null || found.length == 0) {
			return latest;
		}
		for (File f : found) {
			Matcher m = COMMENT_FILENAME_PATTERN.matcher(f.getName());
			if (m.matches()) {
				String s = m.group(2); long t = 0L;
				if (s.length() > 10) {
					try {
						t = new SimpleDateFormat(
								"yyMMddHHmmss'z'").parse(s).getTime() / 1000L;
					} catch (ParseException e) {
						Logger.error(e);
					}
				} else {
					t = Long.parseLong(s);
				}
				if (latest.time < t) {
					latest.time = t; latest.file = f;
				}
			}
		}
		return latest;
	}
	
}
