package allen.town.podcast.core.util.playback;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Color;
import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import android.text.TextUtils;
import android.util.Log;
import android.util.TypedValue;

import androidx.annotation.Nullable;
import org.apache.commons.io.IOUtils;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;

import java.io.IOException;
import java.io.InputStream;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import code.name.monkey.appthemehelper.ThemeStore;
import allen.town.podcast.core.R;
import allen.town.podcast.core.util.Converter;

/**
 * Connects chapter information and shownotes of a shownotesProvider, for example by making it possible to use the
 * shownotes to navigate to another position in the podcast or by highlighting certain parts of the shownotesProvider's
 * shownotes.
 * <p/>
 * A timeline object needs a shownotesProvider from which the chapter information
 * is retrieved and shownotes are generated.
 */
public class Timeline {
    private static final String TAG = "Timeline";

    private static final Pattern TIMECODE_LINK_REGEX = Pattern.compile("allentown://focuspodcast-timecode/(\\d+)");
    private static final String TIMECODE_LINK = "<a class=\"timecode\" href=\"allentown://focuspodcast-timecode/%d\">%s</a>";
    private static final Pattern TIMECODE_REGEX = Pattern.compile("\\b((\\d+):)?(\\d+):(\\d{2})\\b");
    private static final Pattern LINE_BREAK_REGEX = Pattern.compile("<br */?>");

    private final String rawShownotes;
    private final String noShownotesLabel;
    private final int playableDuration;
    private final String webviewStyle;

    public Timeline(Context context, @Nullable String rawShownotes, int playableDuration) {
        this.rawShownotes = rawShownotes;

        noShownotesLabel = context.getString(R.string.no_shownotes_label);
        this.playableDuration = playableDuration;
        final String colorPrimary = colorAttrToHtml(context, android.R.attr.textColorPrimary);
        final String colorAccent = colorIntToHtml(context, ThemeStore.accentColor(context));
        final int margin = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 8,
                context.getResources().getDisplayMetrics());
        String styleString = "";
        try {
            InputStream templateStream = context.getAssets().open("shownotesStyle.css");
            styleString = IOUtils.toString(templateStream, "UTF-8");
        } catch (IOException e) {
            e.printStackTrace();
        }
        webviewStyle = String.format(Locale.US, styleString, colorPrimary, colorAccent,colorAccent,
                margin, margin, margin, margin);
    }

    private String colorAttrToHtml(Context context, int colorAttr) {
        TypedArray res = context.getTheme().obtainStyledAttributes(new int[]{colorAttr});
        @ColorInt int col = res.getColor(0, 0);
        final String color = colorToHtml(context,col);
        res.recycle();
        return color;
    }

    private String colorIntToHtml(Context context, @ColorInt int colorInt) {
        return colorToHtml(context, colorInt);
    }

    private String colorToHtml(Context context, @ColorInt int colorInt) {
        @ColorInt int col = colorInt;
        final String color = "rgba(" + Color.red(col) + "," + Color.green(col) + ","
                + Color.blue(col) + "," + (Color.alpha(col) / 255.0) + ")";
        return color;
    }

    /**
     * Applies an app-specific CSS stylesheet and adds timecode links (optional).
     * <p/>
     * This method does NOT change the original shownotes string of the shownotesProvider object and it should
     * also not be changed by the caller.
     *
     * @return The processed HTML string.
     */
    @NonNull
    public String processShownotes() {
        String shownotes = rawShownotes;

        //加br是因为在pixel 4l Tiramisu上面显示不全
        if (TextUtils.isEmpty(shownotes)) {
            Log.d(TAG, "no shownotes");
            shownotes = "<html><head></head><body><p id='apNoShownotes'>" + noShownotesLabel + "</p><br/></body></html>";
        }

        // replace ASCII line breaks with HTML ones if shownotes don't contain HTML line breaks already
        if (!LINE_BREAK_REGEX.matcher(shownotes).find() && !shownotes.contains("<p>")) {
            shownotes = shownotes.replace("\n", "<br />");
        }

        Document document = Jsoup.parse(shownotes);
        document.head().appendElement("style").attr("type", "text/css").text(webviewStyle);

        // apply timecode links
        addTimecodes(document);
        return document.toString();
    }

    /**
     * Returns true if the given link is a timecode link.
     */
    public static boolean isTimecodeLink(String link) {
        return link != null && link.matches(TIMECODE_LINK_REGEX.pattern());
    }

    /**
     * Returns the time in milliseconds that is attached to this link or -1
     * if the link is no valid timecode link.
     */
    public static int getTimecodeLinkTime(String link) {
        if (isTimecodeLink(link)) {
            Matcher m = TIMECODE_LINK_REGEX.matcher(link);

            try {
                if (m.find()) {
                    return Integer.parseInt(m.group(1));
                }
            } catch (NumberFormatException e) {
                e.printStackTrace();
            }
        }
        return -1;
    }

    private void addTimecodes(Document document) {
        Elements elementsWithTimeCodes = document.body().getElementsMatchingOwnText(TIMECODE_REGEX);
        Log.d(TAG, "find " + elementsWithTimeCodes.size() + " timecodes");

        if (elementsWithTimeCodes.size() == 0) {
            // No elements with timecodes
            return;
        }
        boolean useHourFormat = true;

        if (playableDuration != Integer.MAX_VALUE) {

            // We need to decide if we are going to treat short timecodes as HH:MM or MM:SS. To do
            // so we will parse all the short timecodes and see if they fit in the duration. If one
            // does not we will use MM:SS, otherwise all will be parsed as HH:MM.
            for (Element element : elementsWithTimeCodes) {
                Matcher matcherForElement = TIMECODE_REGEX.matcher(element.html());
                while (matcherForElement.find()) {

                    // We only want short timecodes right now.
                    if (matcherForElement.group(1) == null) {
                        int time = Converter.durationStringShortToMs(matcherForElement.group(0), true);

                        // If the parsed timecode is greater then the duration then we know we need to
                        // use the minute format so we are done.
                        if (time > playableDuration) {
                            useHourFormat = false;
                            break;
                        }
                    }
                }

                if (!useHourFormat) {
                    break;
                }
            }
        }

        for (Element element : elementsWithTimeCodes) {

            Matcher matcherForElement = TIMECODE_REGEX.matcher(element.html());
            StringBuffer buffer = new StringBuffer();

            while (matcherForElement.find()) {
                String group = matcherForElement.group(0);

                int time = matcherForElement.group(1) != null
                                        ? Converter.durationStringLongToMs(group)
                                        : Converter.durationStringShortToMs(group, useHourFormat);

                String replacementText = group;
                if (time < playableDuration) {
                    replacementText = String.format(Locale.US, TIMECODE_LINK, time, group);
                }

                matcherForElement.appendReplacement(buffer, replacementText);
            }

            matcherForElement.appendTail(buffer);
            element.html(buffer.toString());
        }
    }
}
