package jp.agentec.adf.net.http;

import org.json.adf.JSONException;
import org.json.adf.JSONObject;

import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.RandomAccessFile;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Observable;
import java.util.Observer;

import jp.agentec.abook.abv.bl.acms.client.json.AcmsMessageJSON;
import jp.agentec.abook.abv.bl.common.exception.ABVExceptionCode;
import jp.agentec.abook.abv.bl.common.exception.AcmsException;
import jp.agentec.abook.abv.bl.common.log.Logger;
import jp.agentec.abook.abv.bl.data.ABVDataCache;
import jp.agentec.adf.util.FileUtil;
import jp.agentec.adf.util.NumericUtil;

/**
 * HTTPを通してファイルをダウンロード
 * 
 * 注：旧ソースにあった分割DLは1並行（正確にはブロック数単位の最後の余りを分けて2並行）で、停止＆アプリ再起動後のレジュームは制御できていないことから削除
 * 
 */
public class HttpFileDownloader extends Observable {
	private static final String TAG = "HttpFileDownloader";

	//// HTTP Configuration ///
	private String method = "GET"; // HTTPメソッド（GET, POST）を示す文字列
	private URL url;
	private String encoding = HttpRequestSender.DefaultEncoding; // ダウンロード先との通信に利用する文字セット
	private HttpParameterObject param; // ダウンロード先に渡すパラメータ
	private HttpHeaderProperties properties; // HTTPヘッダーに設定するプロパーティ
	private int connectionTimeout = HttpRequestSender.DefaultConnectionTimeout; // ダウンロード先との接続までのタイムアウト時間（ミリ秒）
	private long startByte = 0; // 何バイト目からダウンロードを開始するか

	private boolean requireFileSizeFirst = true; // DL前にHEADメソッドでContent-Lengthよりファイル全体サイズを取得する必要があるか
	private HttpDownloadNotification notification;
	private boolean sync; // 同期DLか否か
	private boolean setFileTimestamp; // タイムスタンプをDateヘッダに合わせるか否か
	private long lastModifiedHeader; // レスポンスのlast-modifiedヘッダ

	private static final int BUFFER_SIZE = 4096;

	public HttpFileDownloader(String url, HttpParameterObject param, String outputFile) {
		this(url, param, null, outputFile, 0, null, null);
	}

	public HttpFileDownloader(String url, String outputFile, long startByte, Observer observer, Object customInformation) {
		this(url, null, null, outputFile, startByte, observer, customInformation);
	}

	/**
	 * コンストラクタ
	 * 
	 * @param url
	 * @param param
	 * @param properties
	 * @param observer
	 * @param startByte
	 * @param customInfo
	 * @param outputFile ダウンロードしたファイルの保存先のパスです。これはファイル名まで含める
	 * @param observer コンテンツの付加情報。この情報はObserverに通知
	 * trueに設定すると、ダウンロードのパーセンテージ（何パーセントまでダウンロードしたか）をobserverに通知
	 */
	public HttpFileDownloader(String url, HttpParameterObject param, HttpHeaderProperties properties, String outputFile, long startByte
			, Observer observer, Object customInfo) {

        if (url == null) {
            throw new IllegalArgumentException("url not allowed null.");
        } else {
            String adummy = ABVDataCache.getInstance().getAdummyKey();
            StringBuilder urlPath = new StringBuilder(url + "?");

            if (param != null) {
                urlPath.append(param.toHttpParameterString(encoding) + "&");
            }
            if (adummy != null) {
                urlPath.append("adummy=" + adummy);
            }
            try {
                this.url = new URL(urlPath.toString());
            } catch (MalformedURLException e) {
                throw new RuntimeException(e);
            }
        }

		if (properties == null) {
			this.properties = new HttpHeaderProperties();
		}
		else {
			this.properties = properties;
		}

		if (observer != null) {
			addObserver(observer);
		}

		notification = new HttpDownloadNotification(0, outputFile, null, customInfo);
		this.param = param;
		this.startByte = startByte >= 0 ? startByte : 0;
	}

	// Configuration

	public void setMethod(String method) {
		this.method = method;
	}

	public void setRequireFileSizeFirst(boolean requireFileSizeFirst) {
		this.requireFileSizeFirst = requireFileSizeFirst;
	}

	public void setFileTimestamp(boolean setFileTimestamp) {
		this.setFileTimestamp = setFileTimestamp;
	}
	
	public void setEncoding(String encoding) {
		this.encoding = encoding;
	}

	public void setConnectionTimeout(int connectionTimeout) {
		this.connectionTimeout = connectionTimeout;
	}

	// Notification委譲メソッド //

	public HttpDownloadNotification getNotification() {
		return notification;
	}

	/**
	 * ダウンロードしているファイルのサイズ（byte）を返します。
	 * @since 1.0.0
	 */
	public long getFileSize() {
		return notification.getFileSize();
	}

	/**
	 * ダウンロードの進行率のパーセンテージを返します。
	 * @since 1.0.0
	 */
	public float getDownloadRate() {
		return notification.getDownloadRate();
	}

	/**
	 * 現在のダウンロード状態を返します。
	 * @since 1.0.0
	 */
	public HttpDownloadState getState() {
		return notification.getDownloadState();
	}
	
	public boolean isDownloading() {
		return notification.isDownloading();
	}

	/**
	 * ダウンロードして、実際に保存されたフルパスを返します。
	 * @return ダウンロードして、実際に保存されたファイルのフルパスです。
	 * @since 1.0.0
	 */
	public String getOutputFileName() {
		return notification.getOutputFileName();
	}

	/**
	 * 最後に起こったエラーを返します。
	 * @return　エラーの{@link Exception}　インスタンスです。
	 * @since 1.0.0
	 */
	public Exception getError() {
		return notification.getError();
	}


	/// ダウンロード制御メソッド  ///

	/**
	 * ダウンロードを一時中止します。
	 * @since 1.0.0
	 */
	public void pause() {
		setState(HttpDownloadState.paused);
	}

	/**
	 * 一時中止しているダウンロードを再開します。
	 * @since 1.0.0
	 */
	public void resume() {
		setState(HttpDownloadState.downloading);
		downloadAsync();
	}

	/**
	 * ダウンロードをキャンセルします。ダウンロードしていたファイルは削除されません。削除する必要がある場合は、それを実装する必要があります。
	 * @since 1.0.0
	 */
	public void cancel() {
		setState(HttpDownloadState.canceled);
	}

	/**
	 * 現在のダウンロード状態を設定し、オブザーバへ通知
	 * 
	 * @param newState ダウンロード状態
	 * @since 1.0.0
	 */
	protected void setState(HttpDownloadState newState) {
		HttpDownloadState currentState = notification.getDownloadState();
		notification.setDownloadState(newState);
		if (newState != currentState || newState != HttpDownloadState.downloading) { // 状態変化もしくはDL以外
			setChanged();
			notifyObservers(notification);
		}
	}

	// Download処理 //

	/**
	 * ダウンロードを開始(同期型)
	 * ※この呼び出しが終わった後、stateをチェックすること（finishedかfailedのみ）
	 * 
	 * @throws InterruptedException 
	 */
	public void downloadSync() throws InterruptedException {
		sync = true;
		Thread t = downloadAsync();
		t.join();
		
		if (setFileTimestamp && lastModifiedHeader != 0) {
			new File(notification.getOutputFileName()).setLastModified(lastModifiedHeader);
		}
	}

	/**
	 * ダウンロードを開始(非同期型)
	 * 
	 * @return
	 */
	public Thread downloadAsync() {
		Thread thread = new Thread(new Runnable() {
			@Override
			public void run() {
				setState(HttpDownloadState.downloading);
				
				if (requireFileSizeFirst) { // レジュームがある場合は最初にContent-Lengthを取得して、全体のファイルサイズを取得する（Range指定するとRangeのサイズになってしまうのでRange指定なしで行う）
					if (getFileSize() == 0) { // TODO: 既存のファイルサイズとのチェック
						notification.setFileSize(getFileSizeByHeadRequest());
					}
				}

				executeDownload(); // ダウンロード実行

				if (notification.isDownloadCompleted() || (sync && notification.isDownloading())) { // 中断されていない場合は完了
					setState(HttpDownloadState.finished);
				} else {
					// ヘッダーのファイルサイズと実際のファイルサイズが異なる場合は、ステータスをfailedにセット
					if(notification.downloadFileName.contains("ContentInfo_") && notification.isDownloading()) {
						if(notification.fileSize != notification.getDownloadedSize()) {
							setState(HttpDownloadState.failed);
						}
					}
					Logger.d(TAG,  "HttpDownloadState Not finished downloadFileName :" + notification.downloadFileName + " HttpDownloadStatus : " + notification.getDownloadState());
				}
			}
		});
		
		thread.start();
		return thread;
	}


	/**
	 * HEADメソッドでContent-Lengthを取得
	 * 
	 * @return
	 */
	private long getFileSizeByHeadRequest() {
		HttpResponse response = new HttpResponse();
		HttpURLConnection conn = null;
		try {
			conn = HttpRequestSender.openConnection("HEAD", url, encoding, param, properties, connectionTimeout);
			response.httpResponseCode = conn.getResponseCode();
			response.httpResponseMessage = conn.getResponseMessage();
			response.contentLength = NumericUtil.parseLong(conn.getHeaderField("Content-Length"), 0);
			if (requireFileSizeFirst && response.contentLength < 1) {
				setError(new Exception("invalid content length in response header"));
			}

			if (response.httpResponseCode > 304) {
				handleErrorMessage(conn, response);
			}
		} catch (Exception e) {
			setError(e);
		} finally {
			if (conn != null) {
				conn.disconnect();
			}
		}
		return response.contentLength;
	}

	/**
	 * ダウンロードを実行する
	 */
	private void executeDownload() {
		HttpURLConnection conn = null;
		BufferedInputStream inputStream = null;
		RandomAccessFile file = null;
		long readBytes = 0;

		try {
			if (startByte == 0 && requireFileSizeFirst) { // headerを取らない場合304でDLしない場合があるので削除はしない
				FileUtil.delete(notification.getOutputFileName());
			}
			
			notification.setDownloadedSize(startByte);
			long endByte = notification.getFileSize();
			Logger.d(TAG, "Request Range bytes=%d-%d", startByte, endByte);
			
			if (startByte == 0 && endByte == 0) {
				// no Range
			}
			else {
				if (endByte > 0) {
					properties.addProperty(HttpHeaderProperties.PropertyKey.Range, String.format("bytes=%d-%d", startByte, endByte));
				}
				else {
					properties.addProperty(HttpHeaderProperties.PropertyKey.Range, String.format("bytes=%d-", startByte));
				}
			}

			conn = HttpRequestSender.openConnection(method, url, encoding, param, properties, connectionTimeout);
			HttpResponse response = new HttpResponse();
			response.httpResponseCode = conn.getResponseCode();
			response.httpResponseMessage = conn.getResponseMessage();
			lastModifiedHeader = conn.getLastModified();

			if (response.httpResponseCode == 304) {
				Logger.w(TAG, "Not modified.");
				notification.setDownloadRate(100);
			}
			else if (response.httpResponseCode < 200 || response.httpResponseCode >= 300) {
				handleErrorMessage(conn, response);
			}
			else {
				inputStream = new BufferedInputStream(conn.getInputStream(), BUFFER_SIZE);
				FileUtil.createParentDirectory(notification.getOutputFileName());
				file = new RandomAccessFile(notification.getOutputFileName(), "rw");
				file.seek(startByte);

				byte buffer[] = new byte[BUFFER_SIZE];
				int bufferedBytes;

				// 実際のダウンロード＆ファイル書き込み処理（最後まで読み込むか、notificationで停止・キャンセルになるまで続ける）
				while (notification.isDownloading() && (bufferedBytes = inputStream.read(buffer, 0, BUFFER_SIZE)) != -1) {
					if (startByte == 0 && bufferedBytes >= 2 && buffer[0] == 0x0d && buffer[1] == 0x0a) {
						//	最初にCRLFが入っていると無視する。
						file.write(buffer, 2, bufferedBytes - 2);
					} else {
						file.write(buffer, 0, bufferedBytes);
					}
					startByte += bufferedBytes;
					readBytes += bufferedBytes;
					notification.addDownloadedSize(bufferedBytes);
//					if (readBytes > 10000) throw new IOException("test"); // for unit test
				}
			}
		} catch (IOException e) {
			Logger.e("HttpFileDownloader", "run error", e);
			if (!sync && readBytes > 10000) { // 非同期で一定以上読んでエラーの場合はPauseとする（NW切れ等）
				setState(HttpDownloadState.paused);
			}
			else {
				setError(e);
			}
		} finally {
			if (file != null) {
				try {
					file.close();
                } catch (IOException e) {}
			}

			if (inputStream != null) {
				try {
					inputStream.close();
                } catch (IOException e) {}
			}

			if (conn != null) {
				try {
					conn.disconnect();
                } catch (Exception e2) {}
			}
		}
	}
	
	/**
	 * エラーの際Body部からjsonを読み込む
	 * 
	 * @param conn
	 * @param response
	 */
	private void handleErrorMessage(HttpURLConnection conn, HttpResponse response) {
		try {
			Logger.d(TAG, "handleErrorMessage.responseCode:" + response.httpResponseCode);
			InputStream es = conn.getErrorStream();
			if (es == null) {
				setError(new AcmsException(ABVExceptionCode.fromResponseCode(response.httpResponseCode), null));
				return;
			}
			BufferedReader reader = new BufferedReader(new InputStreamReader(es));
			StringBuilder sb = new StringBuilder();
			String line;
			while ((line = reader.readLine()) != null) {
				sb.append(line);
			}
			AcmsMessageJSON json = null;
			try {
                //noinspection ResultOfObjectAllocationIgnored
                new JSONObject(sb.toString()); // jsonフォーマットチェック
				json = new AcmsMessageJSON(sb.toString());
			} catch (JSONException e) {
				Logger.w(TAG, "Response not have json. " + e.toString());
			}
			setError(new AcmsException(ABVExceptionCode.fromResponseCode(response.httpResponseCode), json));
		} catch (Exception e) {
			Logger.e(TAG, "Response json error", e);
			setError(new Exception(response.toString()));
		}
	}

	/**
	 * 最後のエラーを設定します。
	 * @param error エラーの{@link Exception}　インスタンスです。
	 * @since 1.0.0
	 */
	private void setError(Exception error) {
		notification.setError(error);
		setState(HttpDownloadState.failed);
	}


}
