package jp.agentec.adf.net.http;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

import jp.agentec.abook.abv.bl.common.log.Logger;
import jp.agentec.adf.net.http.HttpHeaderProperties.PropertyKey;
import jp.agentec.adf.util.FileUtil;
import jp.agentec.adf.util.StringUtil;

public class HttpWebPageDownloader {
	private String mBaseDir;
    private String mBaseUrl;
    private List<String> finishedFileList = new ArrayList<String>();

    private static final String EXPIRES_REGEX = "meta\\shttp-equiv=\"Expires\"\\scontent=\"(\\d+)\"";
    private static final String[] LINK_REGX = new String[]{"url\\([\"']?(.*?)[\"']?\\)", "src=[\"'](.*?)[\"']",  "href=[\"'](.*?)[\"']"};
    private static final String[] RECURSIVE_TARGET = new String[]{"/", ".html", ".css"};

	public HttpWebPageDownloader(String baseDir, String baseUrl) {
        mBaseDir = baseDir;
        mBaseUrl = baseUrl;
	}

	/**
	 * htmlUrlを指定して、そこからリンクされているファイルも取得する。
	 * css/htmlに限り、再帰的に取得する。
	 * ファイルのタイムスタンプを見て、If-Modified-Sinceヘッダを付加。
	 * 
	 * HTMLの条件：
	 * ① HTML/CSSからリンクされているファイルは、同じドメインでかつ、そのHTMLと同じディレクトリかサブディレクトリに置かれていること。
	 * ② JSからのリンクは取得しない。
	 * ③ HTML/CSSからのリンクは、リンクは、href="xxx" src="xxx" href='xxx' src='xxx' url(xxx)の形式のみ取得する。
	 * ④ 内部のURLは相対パスのみOKとする。
     *
	 * @param url
	 * @param dir 保存ディレクトリ
	 * @return expiresの値(但しhtmlが変更されていない場合は空を返す)
	 * @throws Exception 取得エラーは.htmlのURLについてのエラーの場合は例外をスローする。それ以外は無視。
	 * 
	 */
	public static String getUrl(String url, String dir) throws Exception {
		FileUtil.createNewDirectory(dir);
		
		String savedPath = dir + "/" + FileUtil.getFileName(url);
		if (savedPath.endsWith("/")) {
			savedPath += "index.html";
		}
		long htmlTime = FileUtil.exists(savedPath)? new File(savedPath).lastModified(): 0;
		
		HttpWebPageDownloader downloader = new HttpWebPageDownloader(dir, getParentPath(url));
		downloader.fetch(url, savedPath, url);
		
		return new File(savedPath).lastModified() != htmlTime? downloader.extractExpires(savedPath): null;
	}

	/**
	 * ファイルを取得して所定ディレクトリに保存する。
	 * html,cssの場合、再帰的にリンクを取得する。
	 * 
	 * @param url
	 * @param savedPath
	 * @param orgUrl
	 * @throws Exception
	 */
	private void fetch(String url, String savedPath, String orgUrl) throws Exception {
		if (finishedFileList.contains(savedPath)) {
			return;
		}
		finishedFileList.add(savedPath);
		fetchFile(url, savedPath);
		if (StringUtil.endsWithAny(url, RECURSIVE_TARGET)) {
			for (String linkedUrl : extractLink(url, savedPath)) {
				Logger.d("url: " + linkedUrl);
				String linkedSavedPath = getSavedPath(linkedUrl, url, savedPath);
				if (!linkedSavedPath.startsWith(mBaseDir)) {
					Logger.w("saving above mBaseDir is not allowed. path: " + linkedSavedPath);
                    continue;
				}
				fetch(linkedUrl, linkedSavedPath, url);
//				if (!linkedUrl.startsWith(getParentPath(orgUrl))) { // 絶対指定・外部リンクはなしにするのでいったんコメントアウト
//					replaceFile(savedPath, linkedUrl);
//				}
			}
		}
	}

	private String getSavedPath(String linkedUrl, String orgUrl, String orgSavedPath) {
		String ret;
        String parentPath = getParentPath(orgUrl);
		if (linkedUrl.startsWith(mBaseUrl)) {
			ret = mBaseDir + linkedUrl.substring(mBaseUrl.length());
		}
		else if (parentPath != null && parentPath.startsWith(mBaseUrl)) {
			ret = mBaseDir + parentPath.substring(mBaseUrl.length()) + "/" + FileUtil.getFileName(linkedUrl);
		}
		else {
			ret = getParentPath(orgSavedPath) + "/" + FileUtil.getFileName(linkedUrl);
		}
		
		if (ret.endsWith("/")) {
			ret += "index.html";
		}
		while (ret.contains("../")) {
			ret = ret.replaceAll("/[^/]*?/../", "/");
		}
		while (ret.contains("//")) {
			ret = ret.replace("//", "/");
		}
		return ret;
	}

	// 今回は使用しない。
//	private void replaceFile(String path, String targetUrl) throws IOException {
//		String content = FileUtil.readTextFile(path);
//		String replaced = FileUtil.getFileName(targetUrl);
//		FileUtil.createFile(path, content.replace(targetUrl, "./" + replaced));
//	}

	private String extractExpires(String htmlPath) throws IOException {
		return StringUtil.extractRegexString(FileUtil.readTextFile(htmlPath), EXPIRES_REGEX, 1);
	}

	private String convertUrl(String parentUrl, String url) {
		String baseUrl = getParentPath(parentUrl);
		String targetUrl;
		if (url.startsWith("http")) {
			targetUrl = url;
		}
		else if (url.startsWith("./")) {
			targetUrl = baseUrl + "/" + url.substring(2);
		}
		else if (url.startsWith("../")) {
			targetUrl = getParentPath(baseUrl) + "/" + url.substring(2);
		}
		else {
			targetUrl = baseUrl + "/" + url;
		}
		
		targetUrl = targetUrl.replaceAll("[\"']", "");

		return targetUrl;
	}
	private List<String> extractLink(String htmlUrl, String path) throws IOException {
		String html = FileUtil.readTextFile(path);
		List<String> urlList = new ArrayList<String>();
		if (html != null) {
			for (String regx : LINK_REGX) {
				extractUrl(html, regx, htmlUrl, urlList);
			}
		}
		return urlList;
	}

	private void extractUrl(String html, String regex, String htmlUrl, List<String> urlList) {
		List<List<String>> extractlist = StringUtil.extractRegexString(html, regex, new int[]{1});
		for (List<String> list : extractlist) {
			if (StringUtil.isNullOrEmpty(list.get(0))) {
				continue;
			}
			String targetUrl = convertUrl(htmlUrl, list.get(0));
			if (!StringUtil.isNullOrEmpty(targetUrl) && !urlList.contains(targetUrl)) {
                if (targetUrl.startsWith(mBaseUrl)) {
                    urlList.add(targetUrl);
                } else {
                    Logger.w("Other than mBaseUrl is not allowed. " + targetUrl);
                }
			}
		}
	}


	/**
	 * Webから取得する
	 * 
	 * @param url 取得先
	 * @param path 保存先
	 * @throws Exception
	 */
	public void fetchFile(String url, String path) throws Exception {
		Logger.d("fetchFile: " + url + " to " + path);
		if (path.contains("?")) {
			path = path.substring(0, path.lastIndexOf("?"));
		}
		
		HttpHeaderProperties prop = null;
		if (FileUtil.exists(path)) {
			prop = new HttpHeaderProperties();
			prop.addProperty(PropertyKey.IfModifiedSince, "" + new File(path).lastModified());
		}
		
		HttpFileDownloader downloader = new HttpFileDownloader(url, null, prop, path, 0, null, null);
		downloader.setRequireFileSizeFirst(false);
		downloader.setFileTimestamp(true);
		downloader.downloadSync();
		
		if (downloader.getError() != null) {
			Logger.w(downloader.getError().toString());
            throw downloader.getError();
		}
	}

	private static String getParentPath(String name) {
		if (StringUtil.isNullOrEmpty(name)) {
            return name;
        }

		int index = name.lastIndexOf('/');
		if (index > 0) {
			return name.substring(0, index);
		} else if (index == 0) {
			return "/";
		}
		return null;
	}

}
