package fr.gaulupeau.apps.Poche.tts;

import android.os.Handler;
import android.text.TextUtils;
import android.util.Log;
import android.webkit.WebView;

import org.json.JSONArray;
import org.json.JSONObject;

import java.util.ArrayList;
import java.util.List;

import fr.gaulupeau.apps.InThePoche.BuildConfig;
import fr.gaulupeau.apps.InThePoche.R;
import fr.gaulupeau.apps.Poche.data.StorageHelper;

/**
 * TextInterface to navigate in a WebView.
 */
class WebViewText implements TextInterface {

    private static final String TAG = WebViewText.class.getSimpleName();

    private static final String JS_PARSE_DOCUMENT_SCRIPT
            = StorageHelper.readRawString(R.raw.tts_parser);

    private final Handler handler = new Handler();

    private TtsConverter ttsConverter;

    private TtsHost ttsHost;

    private final List<GenericItem> textList = new ArrayList<>();

    private int current;

    private Integer storedScrollPosition;

    private Runnable readFinishedCallback;
    private Runnable parsingFinishedCallback;

    WebViewText(TtsConverter ttsConverter, TtsHost ttsHost) {
        this.ttsConverter = ttsConverter;
        this.ttsHost = ttsHost;
    }

    void setTtsHost(TtsHost ttsHost) {
        this.ttsHost = ttsHost;
    }

    void setReadFinishedCallback(Runnable readFinishedCallback) {
        this.readFinishedCallback = readFinishedCallback;
    }

    void parseWebViewDocument(Runnable callback) {
        Log.d(TAG, "parseWebViewDocument()");

        parsingFinishedCallback = callback;

        ttsHost.getJsTtsController().setWebViewText(this);
        ttsHost.getWebView().evaluateJavascript("javascript:" + JS_PARSE_DOCUMENT_SCRIPT
                + ";parseDocumentText();", null);
    }

    void onDocumentParseStart() {
        Log.d(TAG, "onDocumentParseStart()");
    }

    void onDocumentParseEnd() {
        Log.d(TAG, "onDocumentParseEnd()");

        if (parsingFinishedCallback != null) {
            parsingFinishedCallback.run();
        }
    }

    void onDocumentParseText(String text, float top, float bottom, String extras) {
        top = convertWebViewToScreenY(top);
        bottom = convertWebViewToScreenY(bottom);

        if (BuildConfig.DEBUG) {
            Log.v(TAG, String.format("onDocumentParseText(%s, %f, %f)", text, top, bottom));
        }

        addItem(new TextItem(text, top, bottom, parseTextItemExtras(extras)));
    }

    private List<TextItem.Extra> parseTextItemExtras(String extrasString) {
        if (TextUtils.isEmpty(extrasString)) return null;

        List<TextItem.Extra> result = new ArrayList<>();

        try {
            JSONArray jsonArray = new JSONArray(extrasString);

            for (int i = 0; i < jsonArray.length(); i++) {
                JSONObject jsonObject = jsonArray.getJSONObject(i);

                TextItem.Extra.Type type = TextItem.Extra.Type.getType(
                        jsonObject.getString("type"));

                TextItem.Extra extra = new TextItem.Extra(type,
                        jsonObject.getInt("start"), jsonObject.getInt("end"));

                result.add(extra);
            }
        } catch (Exception e) {
            Log.w(TAG, "parseExtras()", e);
        }

        return result;
    }

    void onDocumentParseImage(String altText, String title, String src, float top, float bottom) {
        top = convertWebViewToScreenY(top);
        bottom = convertWebViewToScreenY(bottom);

        if (BuildConfig.DEBUG) {
            Log.v(TAG, String.format("onDocumentParseImage(%s, %s, %s, %f, %f)",
                    altText, title, src, top, bottom));
        }

        addItem(new ImageItem(altText, title, src, top, bottom));
    }

    private void addItem(GenericItem item) {
        GenericItem prevItem = !textList.isEmpty() ? textList.get(textList.size() - 1) : null;

        item.timePosition = ttsConverter.approximateDuration(item)
                + (prevItem != null ? prevItem.timePosition : 0);

        textList.add(item);
    }

    @Override
    public synchronized int getCurrentIndex() {
        return current;
    }

    @Override
    public GenericItem getItem(int index) {
        if (index >= 0 && index < textList.size()) {
            return textList.get(index);
        } else {
            return null;
        }
    }

    /**
     * Go to the next text item.
     *
     * @return true if current item changed (not already the end).
     */
    @Override
    public synchronized boolean next() {
        //Log.d(TAG, "next, current=" + current);
        boolean result;
        if (current < textList.size() - 1) {
            current++;
            result = true;
        } else {
            handler.post(readFinishedCallback);
            result = false;
        }
        ensureTextRangeVisibleOnScreen(false);
        return result;
    }

    @Override
    public synchronized boolean rewind(long desiredTimeToRewind,
                                       int currentIndex, long progressInCurrentItem) {
        int startIndex = current;
        if (currentIndex != startIndex) progressInCurrentItem = 0;

/*
        if (startIndex == 0 && progressInCurrentItem < desiredTimeToRewind / 10) {
            Log.d(TAG, "rewind() no time to rewind");
            return false;
        }
*/

        int index = startIndex;
        long timeToRewind = desiredTimeToRewind - progressInCurrentItem;

        while (timeToRewind > 0 && index > 0) {
            int newIndex = index - 1;
            long newTimeToRewind = timeToRewind - itemDuration(newIndex);
            long alreadyRewound = desiredTimeToRewind - timeToRewind;

            if (newTimeToRewind > 0 || alreadyRewound < desiredTimeToRewind / 2) {
                index = newIndex;
                timeToRewind = newTimeToRewind;
            } else {
                break;
            }
        }

        Log.d(TAG, "rewind() " + startIndex + " => " + index);
        current = index;

        ensureTextRangeVisibleOnScreen(true);

        return true;
    }

    @Override
    public synchronized boolean fastForward(long desiredTimeToSkip,
                                            int currentIndex, long progressInCurrentItem) {
        int startIndex = current;
        if (currentIndex != startIndex) progressInCurrentItem = 0;

        if (startIndex == textList.size() - 1) {
            Log.d(TAG, "fastForward() no time to skip");
            return false;
        }

        int index = startIndex + 1;
        long timeToSkip = desiredTimeToSkip - (itemDuration(startIndex) - progressInCurrentItem);

        while (timeToSkip > 0 && index < textList.size() - 1) {
            int newIndex = index + 1;
            long newTimeToSkip = timeToSkip - itemDuration(index);
            long alreadySkipped = desiredTimeToSkip - timeToSkip;

            if (newTimeToSkip > 0 || alreadySkipped < desiredTimeToSkip / 3) {
                index = newIndex;
                timeToSkip = newTimeToSkip;
            } else {
                break;
            }
        }

        Log.d(TAG, "fastForward() " + startIndex + " => " + index);
        current = index;

        ensureTextRangeVisibleOnScreen(true);

        return true;
    }

    private long itemDuration(int index) {
        return textList.get(index).timePosition
                - (index > 0 ? textList.get(index - 1).timePosition : 0);
    }

    @Override
    public boolean skipToNext() {
        return ttsHost != null && ttsHost.nextArticle();
    }

    @Override
    public boolean skipToPrevious() {
        return ttsHost != null && ttsHost.previousArticle();
    }

    @Override
    public synchronized void restoreFromStart() {
        Log.d(TAG, "restoreFromStart -> current = 0");
        current = 0;
    }

    @Override
    public void storeCurrent() {
        storedScrollPosition = ttsHost != null ? ttsHost.getScrollY() : null;
    }

    @Override
    public synchronized void restoreCurrent() {
        if (ttsHost == null) return;

        if (storedScrollPosition != null) {
            int position = storedScrollPosition;
            storedScrollPosition = null;

            if (position == ttsHost.getScrollY()) {
                // no scrolling has been done since pause, don't restore anything
                return;
            }
        }

        float currentTop = ttsHost.getScrollY();
        float currentBottom = currentTop + ttsHost.getViewHeight();
        int result = Math.min(current, textList.size() - 1);
        GenericItem textItem = textList.get(result);
        if (textItem.bottom <= currentTop || textItem.top >= currentBottom) {
            // current not displayed on screen, switch to the first text visible:
            result = textList.size() - 1;
            for (int i = 0; i < textList.size(); i++) {
                if (textList.get(i).top > currentTop) {
                    result = i;
                    break;
                }
            }
        }
        current = result;
        Log.d(TAG, "restoreCurrent -> current = " + current);
    }

    @Override
    public synchronized long getTime() {
        long result = -1;
        if (current > 0) {
            result = textList.get(current - 1).timePosition;
        }
        return result;
    }

    @Override
    public long getTotalDuration() {
        long result = -1;
        if (textList.size() > 0) {
            result = textList.get(textList.size() - 1).timePosition;
        }
        return result;
    }

    private void ensureTextRangeVisibleOnScreen(boolean canMoveBackward) {
        if (ttsHost == null) return;

        GenericItem item = textList.get(current);
        if (item.bottom > ttsHost.getScrollY() + ttsHost.getViewHeight()
                || canMoveBackward && item.top < ttsHost.getScrollY()) {
            // TODO: check: call directly?
            handler.post(() -> ttsHost.scrollTo((int) item.top));
        }
    }

    private float convertWebViewToScreenY(float y) {
        if (ttsHost == null) {
            Log.w(TAG, "convertWebViewToScreenY() ttsHost is null");
            return 0;
        }

        WebView webView = ttsHost.getWebView();
        return y * webView.getHeight() / webView.getContentHeight();
    }

}