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 java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;

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

import org.apache.http.protocol.HTTP;
import org.xmlpull.v1.XmlPullParser;

import android.os.Handler;
import android.text.TextUtils;

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

    private static final long WAIT_FINISH_WITHOUT_SHUTDOWN_MS = 3000L;

    protected static final int NETWORK_IO_RETRY_COUNT = 10;
    private static final int WAIT_FOR_NETWORK_ERROR = 3000;

	private Thread mThread = null;
	private WeakReference<Handler> mRefHandler;
	protected boolean mIsStarted = false;
	protected volatile boolean mIsFinish = false;
    protected final Object mSync = new Object();

    protected int mNetworkIORetryCount;

	protected XmlLoader() {
	    mRefHandler = null;
	}
    protected XmlLoader(Handler handler) {
        mRefHandler = new WeakReference<Handler>(handler);
    }

    @Override
	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) {
		    String name = getClass().getSimpleName();
		    if (TextUtils.isEmpty(name)) {
	            mThread = new Thread(this);
		    } else {
	            mThread = new Thread(this, name);
		    }
    		mThread.start();
		} else {
		    handler.post(this);
		}
	}

	public boolean isStarted() {
	    return mIsStarted;
	}

    @Override
	public void finish() {
		setFinish();

		Thread thread = mThread;
        if (thread != null) {
            try {
                // まず強制終了なしで待機してみる
                thread.join(WAIT_FINISH_WITHOUT_SHUTDOWN_MS);
            } catch (InterruptedException e) {
                Log.d(LOG_TAG, e.toString(), e);
            }
            if (thread.isAlive()) {
                shutdownNetwork();
                while (true) {
                    try {
                        // 数秒で終わるという保証もないのでいっそタイムアウトなしに
                        thread.join();
                        break;
                    } catch (InterruptedException e) {
                        Log.d(LOG_TAG, e.toString(), e);
                    }
                }
            }
            mThread = null;
        }

		mIsStarted = false;
	}

    @Override
    public void finishAsync(ExecutorService executorService,
            final CallbackMessage<Void, Void> callback) {
        if (mIsFinish) {
            if (callback != null) {
                callback.sendMessageSuccess(null);
            }
        } else {
            setFinish();
            executorService.execute(new Runnable() {
                @Override
                public void run() {
                    finish();
                    if (callback != null) {
                        callback.sendMessageSuccess(null);
                    }
                }
            });
        }
    }

    @Override
    public void finishAsync(ExecutorService executorService,
            final CountDownLatch latch) {
        if (mIsFinish) {
            latch.countDown();
        } else {
            setFinish();
            executorService.execute(new Runnable() {
                @Override
                public void run() {
                    finish();
                    latch.countDown();
                }
            });
        }
    }

	/**
	 * 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.dLong(LOG_TAG, xmlBody);
//				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);
				}
			}
		}
	}

	@SuppressWarnings("unused")
    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 int b = byteBufferArray[end] & 0xff;
				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;
	}

    protected static void logStartTag(XmlPullParser pullParser, String name) {
        StringBuilder buf = Log.buf();
        Util.appendLogXmlIndent(buf, pullParser)
            .append("<").append(name).append(">");
        Util.appendLogXmlAttribute(buf, pullParser);
        Log.d(LOG_TAG, buf.toString());
    }
    protected static void logEndTag(XmlPullParser pullParser, String name) {
        Log.d(LOG_TAG, Util.appendLogXmlIndent(Log.buf(), pullParser)
                .append("</").append(name).append(">").toString());
    }
    protected static void logText(XmlPullParser pullParser) {
        String text = pullParser.getText();
        if (!TextUtils.isEmpty(text)) {
            Log.d(LOG_TAG, text);
        }
    }

    private void setFinish() {
        synchronized (mSync) {
            mIsFinish = true;
            mSync.notifyAll();
        }
    }

    protected void waitForNetworkError() {
        synchronized (mSync) {
            if (!mIsFinish) {
                try {
                    mSync.wait(WAIT_FOR_NETWORK_ERROR);
                } catch (InterruptedException e) {
                }
            }
        }
    }
}
