package jp.sourceforge.nicoro;

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.ref.WeakReference;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.CoderResult;
import java.nio.charset.CodingErrorAction;

import static jp.sourceforge.nicoro.Log.LOG_TAG;

import org.apache.http.protocol.HTTP;

import android.os.Handler;

/**
 * ネットワークからXMLをloadして解析する
 */
public abstract class XmlLoader implements Runnable {
	private static final boolean DEBUG_LOGD = Release.IS_DEBUG && false;

	private Thread mThread = null;
	private WeakReference<Handler> mRefHandler;
	protected boolean mIsStarted = false;
	protected volatile boolean mIsFinish = false;
	
	protected XmlLoader() {
	    mRefHandler = null;
	}
    protected XmlLoader(Handler handler) {
        mRefHandler = new WeakReference<Handler>(handler);
    }
	
	/**
	 * load開始する
	 */
	public void startLoad() {
		if (mIsStarted) {
			Log.d(LOG_TAG, "it has started");
			return;
		}
		mIsStarted = true;
		mIsFinish = false;
		
		Handler handler = (mRefHandler == null) ? null : mRefHandler.get();
		if (handler == null) {
            mThread = new Thread(this);
    		mThread.start();
		} else {
		    handler.post(this);
		}
	}

	/**
	 * 動作中のloadを終了させる
	 */
	public void finish() {
		mIsFinish = true;
		shutdownNetwork();
		if (mThread != null) {
    		try {
    			mThread.join(2000L);
    		} catch (InterruptedException e) {
    			Log.d(LOG_TAG, e.toString(), e);
    		}
    		mThread = null;
		}
		mIsStarted = false;
	}
	
	/**
	 * XML文字列を解析して必要なデータを作成する
	 * @param xmlBody
	 * @return データ作成に成功／失敗
	 */
	protected abstract boolean createDataFromXml(String xmlBody);
	/**
	 * データ作成が完了したことを通知する
	 */
	protected abstract void dispatchOnFinished();
	/**
	 * データ作成中にエラーが発生したことを通知する
	 * @param errorMessage エラーメッセージ
	 */
	protected abstract void dispatchOnOccurredError(String errorMessage);
	
	/**
	 * XMLを読み取って必要なデータを作成する
	 * @param inDownload XMLの入力元
	 * @return データ作成に成功／失敗
	 * @throws IOException
	 */
	protected abstract boolean readAndCreateData(InputStream inDownload)
	throws IOException;
	
	/**
	 * ネットワーク接続を強制切断する
	 */
	protected abstract void shutdownNetwork();

	/**
	 * Entityを読み取ってUTF-8からUTF-16にデコードする
	 * @param inDownload 読み取り元
	 * @return デコードした文字列
	 * @throws IOException
	 */
	protected String readEntityAndDecode(InputStream inDownload)
	throws IOException {
		String xmlBody = readEntityAndDecodeDefault(inDownload);
//		String xmlBody = readEntityAndDecodeSpecial(inDownload);
		return xmlBody;
	}
	
	private String readEntityAndDecodeDefault(InputStream inDownload)
	throws IOException {
		InputStreamReader inReader = null;
		try {
			inReader = new InputStreamReader(inDownload, HTTP.UTF_8);
			
			StringBuilder stringBody = new StringBuilder(1024 * 4);
			char[] buffer = new char[1024*4];
			while (!mIsFinish) {
				int read = inReader.read(buffer, 0, buffer.length);
				if (read < 0) {
					break;
				}
				stringBody.append(buffer, 0, read);
			}
			
			String xmlBody = stringBody.toString();
			if (DEBUG_LOGD) {
				Log.d(LOG_TAG, "===== read xml log start =====");
				
//				Log.d(LOG_TAG, Log.buf().append("read xml=").append(xmlBody).toString());
				int start = 0;
				int end = 0;
				int xmlBodyLength = xmlBody.length();
				while (end < xmlBodyLength) {
					end = start + 200;
					if (end > xmlBodyLength) {
						end = xmlBodyLength;
					}
					Log.d(LOG_TAG, xmlBody.substring(start, end));
					start = end;
				}
				
				Log.d(LOG_TAG, "===== read xml log end =====");
			}
			return xmlBody;
		} finally {
			if (inReader != null) {
				try {
					inReader.close();
				} catch (IOException e) {
					Log.d(LOG_TAG, e.toString(), e);
				}
			}
		}
	}

	private String readEntityAndDecodeSpecial(InputStream inDownload)
	throws IOException {
		// 文字化け対策→効果無し？
		final CharsetDecoder decoder = Charset.forName(HTTP.UTF_8).newDecoder()
		.onMalformedInput(CodingErrorAction.REPLACE)
		.onUnmappableCharacter(CodingErrorAction.REPLACE);
//		.onMalformedInput(CodingErrorAction.REPORT)
//		.onUnmappableCharacter(CodingErrorAction.REPORT);
		
		final StringBuilder stringBody = new StringBuilder(1024 * 4);
		final CharBuffer charBuffer = CharBuffer.allocate(1024 * 4);
		final ByteBuffer byteBuffer = ByteBuffer.allocate(1024 * 8);
		final char[] charBufferArray = charBuffer.array();
		final byte[] byteBufferArray = byteBuffer.array();
		int lastByteOffset = 0;
		while (!mIsFinish) {
			final int readByte = inDownload.read(
					byteBufferArray,
					lastByteOffset,
					byteBufferArray.length - lastByteOffset);
			if (readByte < 0) {
				byteBuffer.position(0).limit(lastByteOffset);
				charBuffer.position(0);
				final CoderResult result = decoder.decode(byteBuffer, charBuffer, true);
				stringBody.append(charBufferArray, 0, charBuffer.position());
				break;
			}
			
			final int byteBufferSize = lastByteOffset + readByte; 
			
			int end;
			for (end = byteBufferSize - 1; end > 0; --end) {
				final byte b = byteBufferArray[end];
				if (b < 0x80 || b > 0xbf) {
					break;
				}
			}
			
			byteBuffer.position(0).limit(end);
			charBuffer.position(0);
			final CoderResult result = decoder.decode(byteBuffer, charBuffer, false);
			stringBody.append(charBufferArray, 0, charBuffer.position());
			final int byteBufferPositionAfterDecode = byteBuffer.position();
			if (byteBufferPositionAfterDecode != byteBufferSize) {
				lastByteOffset = byteBufferSize - byteBufferPositionAfterDecode;
				assert lastByteOffset <= byteBufferPositionAfterDecode : "lastByteOffset=" + lastByteOffset + " byteBufferPositionAfterDecode=" + byteBufferPositionAfterDecode;
				System.arraycopy(byteBufferArray, byteBufferPositionAfterDecode,
						byteBufferArray, 0,
						lastByteOffset);
			}
		}
		
		String xmlBody = stringBody.toString();
		if (DEBUG_LOGD) {
			Log.d(LOG_TAG, Log.buf().append("read xml=").append(xmlBody).toString());
		}
		return xmlBody;
	}
}
