package jp.sourceforge.nicoro;

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

import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.StatusLine;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.message.BasicLineFormatter;
import org.apache.http.protocol.HTTP;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;

import android.content.Context;
import android.content.SharedPreferences;
import android.os.SystemClock;
import android.text.TextUtils;
import android.util.Xml;

import java.io.IOException;
import java.io.InputStream;
import java.io.StringReader;
import java.io.UnsupportedEncodingException;
import java.util.LinkedList;
import java.util.concurrent.atomic.AtomicReference;

/**
 * コメント書き込みクラス
 */
public class MessageSender extends XmlLoader implements MessageSenderInterface {
    private static final boolean DEBUG_LOGD = Release.IS_DEBUG & true;
    private static final boolean DEBUG_LOGV_PARSE = Release.IS_DEBUG & false;
    private static final boolean DEBUG_LOGD_PARSE = Release.IS_DEBUG & true;

    /* ３秒以内連続送信は規制 */
    private static final int SEND_INTERVAL_TIME_MS = 3000;

    private AtomicReference<InputStream> mInDownload =
        new AtomicReference<InputStream>();
    private DefaultHttpClient mHttpClient;

    private long mLastSendTime;

    private String mUserAgent;
    private boolean mCommentSend184Always;

    private String mUrl;
    private String mThreadId;
    private String mNicosId;
    private String mCookieUserSession;
    private String mUserId;
//    private Context mContext;
    private String mTicket;
    private String mNicosTicket;
    private String mPostkey;
    private String mNicosPostkey;
    private String mYugi;
    private String mNicosYugi;
    private int mPremium;
    private String mThreadKey;
    private String mForce184;
    private String mNicosThreadKey;
    private String mNicosForce184;
    private boolean mGotThreadKey;
    private boolean mGotNicosThreadKey;

    private int mBlockNumber = -1;
    private int mNicosBlockNumber = -1;

    private static class ChatRequest {
        String threadId;
        String ticket;
        String postkey;
        String yugi;
        int vpos;
        String mail;
        String body;

        public void clear() {
            threadId = null;
            ticket = null;
            postkey = null;
            yugi = null;
            vpos = -1;
            mail = "";
            body = "";
        }
    }
    private ChatRequest mChatRequest;
    private LinkedList<ChatRequest> mChatRequestList = new LinkedList<MessageSender.ChatRequest>();

    private String mLastMail;
    private String mLastBody;

    private static class ChatResult {
        int status;
        int no;
        String thread;

        public void clear() {
            status = -1;
            no = -1;
            thread = null;
        }
    }
    private ChatResult mChatResult = new ChatResult();

    private MessageSenderInterface.EventListener mEventListener = null;

    public MessageSender(String url, String threadId, String nicosId,
            String cookie, String userId, int premium,
            Context context) {
        super();
        mUrl = url;
        mThreadId = threadId;
        mNicosId = nicosId;
        mCookieUserSession = cookie;
        mUserId = userId;
        mPremium = premium;
//        mContext = context;

        SharedPreferences sharedPreference =
            Util.getDefaultSharedPreferencesMultiProcess(context);
        mUserAgent = sharedPreference.getString(NicoroConfig.USER_AGENT, null);
        mCommentSend184Always = sharedPreference.getBoolean(
                context.getString(R.string.pref_key_comment_send_184_always),
                true);
    }

    @Override
    public void setTicket(String ticket, String nicosTicket) {
        mTicket = ticket;
        mNicosTicket = nicosTicket;
    }

//    public void setPostKey(String postkey, String nicosPostkey) {
//        mPostkey = postkey;
//        mNicosPostkey = nicosPostkey;
//    }

    @Override
    public void run() {
        DefaultHttpClient httpClient = Util.createHttpClient();
        mHttpClient = httpClient;
        httpClient.getCookieStore().clear();

        try {
            MAIN_LOOP : while (true) {
                ChatRequest chatRequest = null;
                synchronized (mSync) {
                    if (mIsFinish) {
                        break MAIN_LOOP;
                    }
                    chatRequest = mChatRequestList.poll();
                    while (chatRequest == null) {
                        try {
                            mSync.wait();
                        } catch (InterruptedException e) {
                        }
                        if (mIsFinish) {
                            break MAIN_LOOP;
                        }
                        chatRequest = mChatRequestList.poll();
                    }
                }
                long sendIntervalTime = SystemClock.elapsedRealtime() - mLastSendTime;
                if (sendIntervalTime < SEND_INTERVAL_TIME_MS) {
                    // 前回送信処理より間を空ける
                    SystemClock.sleep(SEND_INTERVAL_TIME_MS - sendIntervalTime);

                    if (mIsFinish) {
                        break MAIN_LOOP;
                    }
                }

                String threadId = chatRequest.threadId;
                boolean toNicos = TextUtils.equals(threadId, mNicosId);
                String threadKey;
                String force184;
                if (toNicos) {
                    if (!mGotNicosThreadKey) {
                        NicoroAPIManager.ParseGetThreadKey parseGetThreadKey =
                            new NicoroAPIManager.ParseGetThreadKey();
                        try {
                            parseGetThreadKey.initialize(httpClient, threadId,
                                    mCookieUserSession, mUserAgent);
                            mNicosThreadKey = parseGetThreadKey.getThreadKey();
                            mNicosForce184 = parseGetThreadKey.getForce184();
                            mGotNicosThreadKey = true;
                        } catch (ClientProtocolException e) {
                            String errorMessage = e.toString();
                            dispatchOnOccurredError(errorMessage);
                            Log.d(LOG_TAG, errorMessage, e);
                            continue;
                        } catch (IOException e) {
                            String errorMessage = e.toString();
                            dispatchOnOccurredError(errorMessage);
                            Log.d(LOG_TAG, errorMessage, e);
                            continue;
                        }
                    }
                    threadKey = mNicosThreadKey;
                    force184 = mNicosForce184;
                } else {
                    if (!mGotThreadKey) {
                        NicoroAPIManager.ParseGetThreadKey parseGetThreadKey =
                            new NicoroAPIManager.ParseGetThreadKey();
                        try {
                            parseGetThreadKey.initialize(httpClient, threadId,
                                    mCookieUserSession, mUserAgent);
                            mThreadKey = parseGetThreadKey.getThreadKey();
                            mForce184 = parseGetThreadKey.getForce184();
                            mGotThreadKey = true;
                        } catch (ClientProtocolException e) {
                            String errorMessage = e.toString();
                            dispatchOnOccurredError(errorMessage);
                            Log.d(LOG_TAG, errorMessage, e);
                            continue;
                        } catch (IOException e) {
                            String errorMessage = e.toString();
                            dispatchOnOccurredError(errorMessage);
                            Log.d(LOG_TAG, errorMessage, e);
                            continue;
                        }
                    }
                    threadKey = mThreadKey;
                    force184 = mForce184;
                }

                InputStream inDownload = null;
                HttpEntity entityInput = null;

                int blockNumber = -1;
                try {
                    HttpPost httpRequest = createRequestThread(threadId,
                            threadKey, force184);
                    HttpResponse httpResponse = HttpManager.executeWrapper(
                            httpClient, httpRequest);
                    StatusLine statusLine = httpResponse.getStatusLine();
                    if (DEBUG_LOGD) {
                        Log.d(LOG_TAG, "MessageSender HTTP response>");
                        Log.d(LOG_TAG, BasicLineFormatter.formatStatusLine(statusLine, null));
                        Util.logHeaders(LOG_TAG, httpResponse.getAllHeaders());
                    }

                    int httpStatusCode = statusLine.getStatusCode();
                    if (httpStatusCode == HttpStatus.SC_OK) {
                        entityInput = httpResponse.getEntity();
                        inDownload = entityInput.getContent();
                        mInDownload.set(inDownload);

                        String xmlBody = readEntityAndDecode(inDownload);
                        int lastRes = getLastResFromXml(xmlBody);
                        if (lastRes < 0) {
                            dispatchOnOccurredError("get last_res failed");
                            continue;
                        }
                        if (DEBUG_LOGD) {
                            Log.d(LOG_TAG, Log.buf().append("MessageSender: last_res=")
                                    .append(lastRes).toString());
                        }
//                        blockNumber = lastRes / 100;
                        blockNumber = (lastRes + 1) / 100;
                    }
                } catch (ClientProtocolException e) {
                    String errorMessage = e.toString();
                    dispatchOnOccurredError(errorMessage);
                    Log.d(LOG_TAG, errorMessage, e);
                    continue;
                } catch (IOException e) {
                    String errorMessage = e.toString();
                    dispatchOnOccurredError(errorMessage);
                    Log.d(LOG_TAG, errorMessage, e);
                    continue;
                } finally {
                    if (entityInput != null) {
                        try {
                            entityInput.consumeContent();
                        } catch (IOException e) {
                            Log.e(LOG_TAG, e.toString(), e);
                        }
                    }
                    entityInput = null;
                    // 同時にcloseすると不具合出る可能性があるので一回のみに
                    if (inDownload != null && mInDownload.getAndSet(null) != null) {
                        try {
                            inDownload.close();
                        } catch (IOException e) {
                            Log.d(LOG_TAG, e.toString(), e);
                        }
                    }
                    inDownload = null;

                    mLastSendTime = SystemClock.elapsedRealtime();
                }

                assert blockNumber >= 0;

                if (mIsFinish) {
                    break MAIN_LOOP;
                }

                String postkey = null;
                String yugi = null;
                try {
                    if (toNicos) {
                        if (mNicosBlockNumber == blockNumber) {
                            postkey = mNicosPostkey;
                            yugi = mNicosYugi;
                        } else {
                            NicoroAPIManager.ParseGetPostKey parseGetPostKey =
                                getPostkey(httpClient, blockNumber, threadId);
                            postkey = parseGetPostKey.getPostKey();
                            if (postkey == null) {
                                dispatchOnOccurredError("get postkey failed");
                                continue;
                            }
                            yugi = parseGetPostKey.getYugi();
                            mNicosPostkey = postkey;
                            mNicosYugi = yugi;
                            mNicosBlockNumber = blockNumber;
                        }
                    } else {
                        assert TextUtils.equals(threadId, mThreadId);
                        if (mBlockNumber == blockNumber) {
                            postkey = mPostkey;
                            yugi = mYugi;
                        } else {
                            NicoroAPIManager.ParseGetPostKey parseGetPostKey =
                                getPostkey(httpClient, blockNumber, threadId);
                            postkey = parseGetPostKey.getPostKey();
                            if (postkey == null) {
                                dispatchOnOccurredError("get postkey failed");
                                continue;
                            }
                            yugi = parseGetPostKey.getYugi();
                            mPostkey = postkey;
                            mYugi = yugi;
                            mBlockNumber = blockNumber;
                        }
                    }
                } catch (ClientProtocolException e) {
                    String errorMessage = e.toString();
                    dispatchOnOccurredError(errorMessage);
                    Log.d(LOG_TAG, errorMessage, e);
                    continue;
                } catch (IOException e) {
                    String errorMessage = e.toString();
                    dispatchOnOccurredError(errorMessage);
                    Log.d(LOG_TAG, errorMessage, e);
                    continue;
                } finally {
                    mLastSendTime = SystemClock.elapsedRealtime();
                }

                assert postkey != null;
                if (DEBUG_LOGD) {
                    Log.d(LOG_TAG, Log.buf().append("MessageSender: postkey=")
                            .append(postkey).append(" yugi=").append(yugi)
                            .toString());
                }
                chatRequest.postkey = postkey;
                chatRequest.yugi = yugi;

                // 公式チャンネル動画は184をつけると投稿に失敗するためここで判定処理
                if (TextUtils.isEmpty(threadKey)) {
                    // 設定で常に184をつけるかどうか切替
                    if (mCommentSend184Always) {
                        chatRequest.mail = "184 " + chatRequest.mail;
                    }
                }

                if (mIsFinish) {
                    break MAIN_LOOP;
                }

                try {
                    HttpPost httpRequest = createRequest(chatRequest);
                    HttpResponse httpResponse = HttpManager.executeWrapper(httpClient,
                            httpRequest);
                    StatusLine statusLine = httpResponse.getStatusLine();
                    if (DEBUG_LOGD) {
                        Log.d(LOG_TAG, "MessageSender HTTP response>");
                        Log.d(LOG_TAG, BasicLineFormatter.formatStatusLine(statusLine, null));
                        Util.logHeaders(LOG_TAG, httpResponse.getAllHeaders());
                    }

                    int httpStatusCode = statusLine.getStatusCode();
                    if (httpStatusCode == HttpStatus.SC_OK) {
                        entityInput = httpResponse.getEntity();
                        inDownload = entityInput.getContent();
                        mInDownload.set(inDownload);

                        boolean result = readAndCreateData(inDownload);

                        if (result) {
                            if (mChatResult.status == 0) {
                                mChatRequest = chatRequest;
                                dispatchOnFinished();
                            } else {
                                dispatchOnOccurredError("chat_result status error");
                            }
                        } else {
                            dispatchOnOccurredError("chat_result XML parse failed");
                        }
                    } else {
                        // エラー
                        String errorMessage = "HTTP Status Code: " + httpStatusCode;
                        dispatchOnOccurredError(errorMessage);
                    }
                } catch (ClientProtocolException e) {
                    String errorMessage = e.toString();
                    dispatchOnOccurredError(errorMessage);
                    Log.d(LOG_TAG, errorMessage, e);
                } catch (IOException e) {
                    String errorMessage = e.toString();
                    dispatchOnOccurredError(errorMessage);
                    Log.d(LOG_TAG, errorMessage, e);
                } finally {
                    if (entityInput != null) {
                        try {
                            entityInput.consumeContent();
                        } catch (IOException e) {
                            Log.e(LOG_TAG, e.toString(), e);
                        }
                    }
                    // 同時にcloseすると不具合出る可能性があるので一回のみに
                    if (inDownload != null && mInDownload.getAndSet(null) != null) {
                        try {
                            inDownload.close();
                        } catch (IOException e) {
                            Log.d(LOG_TAG, e.toString(), e);
                        }
                    }

                    mLastSendTime = SystemClock.elapsedRealtime();
                }
            }
        } finally {
            mHttpClient.getConnectionManager().shutdown();
        }
    }

    @Override
    protected boolean createDataFromXml(String xmlBody) {
        parse(xmlBody);
        return true;
    }

    @Override
    protected void dispatchOnFinished() {
        if (mEventListener != null) {
            ChatRequest chatRequest = mChatRequest;
            mEventListener.onFinished(mChatResult.no, chatRequest.mail,
                    chatRequest.body, chatRequest.vpos);
        }
    }

    @Override
    protected void dispatchOnOccurredError(String errorMessage) {
        if (mEventListener != null) {
            mEventListener.onOccurredError(errorMessage);
        }
    }

    @Override
    protected boolean readAndCreateData(InputStream inDownload) throws IOException {
        String xmlBody = readEntityAndDecode(inDownload);
        return createDataFromXml(xmlBody);
    }

    @Override
    protected void shutdownNetwork() {
        if (mHttpClient != null) {
            mHttpClient.getConnectionManager().shutdown();
        }
    }

    @Override
    public boolean isNull() {
        return false;
    }

    @Override
    public boolean send(String mail, String body, int vpos, boolean toNicos) {
        if (toNicos && mNicosId == null) {
            Log.e(LOG_TAG, "MessageSender#send: nicos_id is null");
            return false;
        }
        if (TextUtils.isEmpty(body)) {
            // 空コメントは不可
            Log.w(LOG_TAG, "MessageSender#send: reject empty comment");
            return false;
        }
        if (TextUtils.equals(mail, mLastMail) && TextUtils.equals(body, mLastBody)) {
            // 全く同じコメントは連続送信不可
            Log.w(LOG_TAG, "MessageSender#send: reject duplicate comment");
            return false;
        }

        mLastMail = mail;
        mLastBody = body;

        ChatRequest chatRequest = new ChatRequest();
        chatRequest.clear();
        if (toNicos) {
            chatRequest.threadId = mNicosId;
            chatRequest.ticket = mNicosTicket;
        } else {
            chatRequest.threadId = mThreadId;
            chatRequest.ticket = mTicket;
        }

        chatRequest.vpos = vpos;
        if (mail != null) {
            chatRequest.mail = mail;
        }
        if (body != null) {
            chatRequest.body = body;
        }

        synchronized (mSync) {
            mChatRequestList.add(chatRequest);
            mSync.notifyAll();
        }
        return true;
    }

    private HttpPost createPostRequest(String stringEntity) {
        HttpPost httpRequest = new HttpPost(mUrl);
        httpRequest.addHeader("Cookie", mCookieUserSession);
        if (mUserAgent != null) {
            httpRequest.setHeader("User-Agent", mUserAgent);
        }
        if (DEBUG_LOGD) {
            Log.d(LOG_TAG, "MessageSender HTTP request>");
            Util.logHeaders(LOG_TAG, httpRequest.getAllHeaders());
        }

        HttpEntity entity;
        try {
            if (DEBUG_LOGD) {
                Log.d(LOG_TAG, stringEntity);
            }
            entity = new StringEntity(stringEntity, HTTP.UTF_8);
        } catch (UnsupportedEncodingException e) {
            assert false;
            Log.e(LOG_TAG, e.toString(), e);
            return null;
        }
        httpRequest.setEntity(entity);

        return httpRequest;
    }

    private HttpPost createRequest(ChatRequest chatRequest) {
        String xmlForRequest = createXmlForRequest(chatRequest);
        return createPostRequest(xmlForRequest);
    }

    private String createXmlForRequest(ChatRequest chatRequest) {
        int length = chatRequest.body.length();
        StringBuilder chatBody = new StringBuilder(length);
        for (int i = 0; i < length; ++i) {
            char ch = chatRequest.body.charAt(i);
            if (ch == '&') {
                chatBody.append("&amp;");
            } else if (ch == '<') {
                chatBody.append("&lt;");
            } else if (ch == '>') {
                chatBody.append("&gt;");
            } else {
                chatBody.append(ch);
            }
        }

        StringBuilder builder = new StringBuilder(256);
        builder.append("<chat thread=\"").append(chatRequest.threadId)
            .append("\" vpos=\"").append(chatRequest.vpos)
            .append("\" mail=\"").append(chatRequest.mail)
            .append("\" ticket=\"").append(chatRequest.ticket)
            .append("\" user_id=\"").append(mUserId)
            .append("\" postkey=\"").append(chatRequest.postkey)
            .append("\" premium=\"").append(mPremium);
        if (chatRequest.yugi != null) {
            builder.append("\" yugi=\"").append(chatRequest.yugi);
        }
        builder.append("\">").append(chatBody)
            .append("</chat>");
        return builder.toString();
    }

    private void parse(String body) {
        XmlPullParser pullParser = Xml.newPullParser();
        ChatResult chatResult = mChatResult;
        chatResult.clear();
        try {
            pullParser.setInput(new StringReader(body));

            int next;
            String name = null;

            while ((next = pullParser.next()) != XmlPullParser.END_DOCUMENT) {
                if (DEBUG_LOGV_PARSE) {
                    Log.v(LOG_TAG, Log.buf().append("next=").append(next).toString());
                }

                boolean loggedParse = false;
                switch (next) {
                case XmlPullParser.START_TAG:
                    name = pullParser.getName();
                    if (DEBUG_LOGD_PARSE) {
                        XmlLoader.logStartTag(pullParser, name);
                        loggedParse = true;
                    }
                    if ("chat_result".equals(name)) {
                        chatResult.status = Util.parseInt(
                                pullParser.getAttributeValue(null, "status"), -1);
                        chatResult.no = Util.parseInt(
                                pullParser.getAttributeValue(null, "no"), -1);
                        chatResult.thread =
                            pullParser.getAttributeValue(null, "thread");
                    } else {
                        // その他のタグはとりあえず無視
                        if (DEBUG_LOGD && !loggedParse) {
                            XmlLoader.logStartTag(pullParser, name);
                        }
                    }
                    break;
                case XmlPullParser.TEXT:
                    if (DEBUG_LOGD_PARSE) {
                        XmlLoader.logText(pullParser);
                        loggedParse = true;
                    }
//                    String text = pullParser.getText();
                    if (name != null) {
                        // とりあえず無視
                        if (DEBUG_LOGD && !loggedParse) {
                            XmlLoader.logText(pullParser);
                        }
                    }
                    break;
                case XmlPullParser.END_TAG:
                    name = pullParser.getName();
                    if (DEBUG_LOGD_PARSE) {
                        XmlLoader.logEndTag(pullParser, name);
                        loggedParse = true;
                    }
                    if ("chat_result".equals(name)) {
                    } else {
                        // その他のタグはとりあえず無視
                        if (DEBUG_LOGD && !loggedParse) {
                            XmlLoader.logEndTag(pullParser, name);
                        }
                    }
                    name = null;
                    break;
                default:
                    break;
                }
            }
        } catch (XmlPullParserException e) {
            Log.d(LOG_TAG, e.toString(), e);
        } catch (IOException e) {
            Log.d(LOG_TAG, e.toString(), e);
        }

        if (DEBUG_LOGD) {
            Log.d(LOG_TAG, Log.buf().append("chat_result status=")
                    .append(chatResult.status).append(" no=")
                    .append(chatResult.no).append(" thread=")
                    .append(chatResult.thread).toString());
        }
    }

    private HttpPost createRequestThread(String threadId,
            String threadKey, String force184) {
        String xmlForRequest = createXmlForRequestThread(threadId, threadKey, force184);
        return createPostRequest(xmlForRequest);
    }

    private String createXmlForRequestThread(String threadId,
            String threadKey, String force184) {
        StringBuilder builder = new StringBuilder(128);
        builder.append("<packet>")
            .append("<thread version=\"20090904\" thread=\"")
            .append(threadId)
            .append("\" user_id=\"")
            .append(mUserId);
        if (threadKey != null) {
            builder.append("\" threadkey=\"").append(threadKey);
        }
        if (force184 != null) {
            builder.append("\" force_184=\"").append(force184);
        }
        builder.append("\" />")
            .append("</packet>");
        return builder.toString();
    }

    private int getLastResFromXml(String body) {
        if (DEBUG_LOGD) {
            Log.dLong(LOG_TAG, body);
        }

        XmlPullParser pullParser = Xml.newPullParser();
        try {
            pullParser.setInput(new StringReader(body));

            int next;
            while ((next = pullParser.next()) != XmlPullParser.END_DOCUMENT) {
                if (next == XmlPullParser.START_TAG) {
                    String name = pullParser.getName();
                    if ("thread".equals(name)) {
                        return Util.parseInt(
                                pullParser.getAttributeValue(null, "last_res"), -1);
                    }
                }
            }
        } catch (XmlPullParserException e) {
            Log.d(LOG_TAG, e.toString(), e);
        } catch (IOException e) {
            Log.d(LOG_TAG, e.toString(), e);
        }
        return -1;
    }

    private NicoroAPIManager.ParseGetPostKey getPostkey(DefaultHttpClient httpClient, int blockNumber,
            String threadId) throws ClientProtocolException, IOException {
        String userSession = mCookieUserSession;
        String userAgent = mUserAgent;

        NicoroAPIManager.ParseGetPostKey parseGetPostKey = new NicoroAPIManager.ParseGetPostKey();
        parseGetPostKey.initialize(httpClient, blockNumber, threadId, userSession, userAgent);
        return parseGetPostKey;
    }

    @Override
    public void setEventListener(MessageSenderInterface.EventListener eventListener) {
        mEventListener = eventListener;
    }
}
