package com.bunk3r.spanez;

import android.content.Context;
import android.content.res.Resources;
import android.graphics.Typeface;
import android.support.annotation.ColorRes;
import android.support.annotation.FloatRange;
import android.support.annotation.IntRange;
import android.support.annotation.NonNull;
import android.support.annotation.StringRes;
import android.support.annotation.StyleRes;
import android.support.annotation.UiThread;
import android.support.annotation.VisibleForTesting;
import android.support.v4.content.ContextCompat;
import android.text.Layout;
import android.text.SpannableString;
import android.text.Spanned;
import android.text.method.LinkMovementMethod;
import android.text.style.AbsoluteSizeSpan;
import android.text.style.AlignmentSpan;
import android.text.style.BackgroundColorSpan;
import android.text.style.ClickableSpan;
import android.text.style.ForegroundColorSpan;
import android.text.style.LocaleSpan;
import android.text.style.QuoteSpan;
import android.text.style.RelativeSizeSpan;
import android.text.style.ScaleXSpan;
import android.text.style.StrikethroughSpan;
import android.text.style.StyleSpan;
import android.text.style.SubscriptSpan;
import android.text.style.SuperscriptSpan;
import android.text.style.TextAppearanceSpan;
import android.text.style.TypefaceSpan;
import android.text.style.URLSpan;
import android.text.style.UnderlineSpan;
import android.widget.TextView;

import com.bunk3r.spanez.api.ContentEZ;
import com.bunk3r.spanez.api.EZ;
import com.bunk3r.spanez.api.Locator;
import com.bunk3r.spanez.api.StyleEZ;
import com.bunk3r.spanez.callbacks.OnSpanClickListener;
import com.bunk3r.spanez.models.ParagraphLocator;
import com.bunk3r.spanez.models.TargetRange;
import com.bunk3r.spanez.spans.ClickableSpanEZ;

import java.util.Locale;

import static com.bunk3r.spanez.api.EZ.BOLD;
import static com.bunk3r.spanez.api.EZ.ITALIC;
import static com.bunk3r.spanez.api.EZ.STRIKETHROUGH;
import static com.bunk3r.spanez.api.EZ.SUBSCRIPT;
import static com.bunk3r.spanez.api.EZ.SUPERSCRIPT;
import static com.bunk3r.spanez.api.EZ.UNDERLINE;

/**
 * Part of SpanEZ
 * Created by joragu on 1/12/2017.
 */

public class SpanEZ implements ContentEZ, StyleEZ {
    private static final int RESOURCE_NOT_SET = -1;

    private final TextView target;
    private final Context context;
    private final Resources resources;

    private int spanFlags = Spanned.SPAN_INCLUSIVE_INCLUSIVE;
    private String content;
    private SpannableString spannableContent;

    /**
     * This is the starting place, once the target has been selected you can apply all the changes
     * to that target.
     *
     * @param target The View were the string is located
     * @return an SpanEZ instance were you can apply all the different span styles
     */
    public static ContentEZ withTextView(TextView target) {
        return new SpanEZ(target);
    }

    private SpanEZ(@NonNull TextView targetView) {
        target = targetView;
        context = target.getContext();
        resources = target.getResources();
    }

    @Override
    public StyleEZ withCurrentContent() {
        CharSequence currentText = target.getText();
        if (currentText instanceof SpannableString) {
            spannableContent = (SpannableString) currentText;
            content = spannableContent.toString();
        } else {
            content = currentText.toString();
            spannableContent = new SpannableString(content);
        }
        return this;
    }

    @Override
    public StyleEZ withContent(String text) {
        content = text;
        spannableContent = new SpannableString(content);
        return this;
    }

    @Override
    public StyleEZ withContent(@StringRes int textResId) {
        String text = resources.getString(textResId);
        return withContent(text);
    }

    @Override
    public StyleEZ withFormattedContent(@StringRes int textResId, @NonNull Object... args) {
        String text = resources.getString(textResId, args);
        return withContent(text);
    }

    @Override
    public StyleEZ inclusive() {
        spanFlags = Spanned.SPAN_INCLUSIVE_INCLUSIVE;
        return this;
    }

    @Override
    public StyleEZ inclusiveExclusive() {
        spanFlags = Spanned.SPAN_INCLUSIVE_EXCLUSIVE;
        return this;
    }

    @Override
    public StyleEZ exclusiveInclusive() {
        spanFlags = Spanned.SPAN_EXCLUSIVE_INCLUSIVE;
        return this;
    }

    @Override
    public StyleEZ exclusive() {
        spanFlags = Spanned.SPAN_EXCLUSIVE_EXCLUSIVE;
        return this;
    }

    @Override
    public StyleEZ style(@NonNull Locator locator, @EZ.STYLE int styles) {
        if (isFlagSet(styles, BOLD)) {
            bold(locator);
        }

        if (isFlagSet(styles, ITALIC)) {
            italic(locator);
        }

        if (isFlagSet(styles, UNDERLINE)) {
            underline(locator);
        }

        if (isFlagSet(styles, STRIKETHROUGH)) {
            strikethrough(locator);
        }

        if (isFlagSet(styles, SUBSCRIPT)) {
            subscript(locator);
        }

        if (isFlagSet(styles, SUPERSCRIPT)) {
            superscript(locator);
        }
        return this;
    }

    /**
     * Applies a {@code Bold} style to all the {@code Character} within the given range. Or throws
     * and {@code IndexOutOfBoundsException} if the range provided is outside the content bounds.
     *
     * @param locator range were the span will be applied to
     */
    private void bold(@NonNull Locator locator) {
        StyleSpan bold = new StyleSpan(Typeface.BOLD);
        addMultipleSpan(locator, bold);
    }

    /**
     * Applies an {@code Italic} style to all the {@code Character} within the given range. Or throws
     * and {@code IndexOutOfBoundsException} if the range provided is outside the content bounds.
     *
     * @param locator range were the span will be applied to
     */
    private void italic(@NonNull Locator locator) {
        StyleSpan italic = new StyleSpan(Typeface.ITALIC);
        addMultipleSpan(locator, italic);
    }

    /**
     * Applies a {@code Underline} style to all the {@code Character} within the given range. Or throws
     * and {@code IndexOutOfBoundsException} if the range provided is outside the content bounds.
     *
     * @param locator range were the span will be applied to
     */
    private void underline(@NonNull Locator locator) {
        UnderlineSpan underline = new UnderlineSpan();
        addMultipleSpan(locator, underline);
    }

    /**
     * Applies an {@code Strikethrough} style (Meaning that it crosses all text with a horizontal line)
     * to all the {@code Character} within the given range. Or throws and {@code IndexOutOfBoundsException}
     * if the range provided is outside the content bounds.
     *
     * @param locator range were the span will be applied to
     */
    private void strikethrough(@NonNull Locator locator) {
        StrikethroughSpan strikethroughSpan = new StrikethroughSpan();
        addMultipleSpan(locator, strikethroughSpan);
    }

    /**
     * Applies a {@code Subscript} style to all the {@code Character} within the given range. Or throws
     * and {@code IndexOutOfBoundsException} if the range provided is outside the content bounds.
     *
     * @param locator range were the span will be applied to
     */
    private void subscript(@NonNull Locator locator) {
        SubscriptSpan subscriptSpan = new SubscriptSpan();
        addMultipleSpan(locator, subscriptSpan);
    }

    /**
     * Applies a {@code Superscript} style to all the {@code Character} within the given range. Or throws
     * and {@code IndexOutOfBoundsException} if the range provided is outside the content bounds.
     *
     * @param locator range were the span will be applied to
     */
    private void superscript(@NonNull Locator locator) {
        SuperscriptSpan superscriptSpan = new SuperscriptSpan();
        addMultipleSpan(locator, superscriptSpan);
    }

    @Override
    public StyleEZ clickable(@NonNull Locator locator, @NonNull OnSpanClickListener spanClick) {
        for (TargetRange targetRange : locator.locate(content)) {
            ClickableSpan clickSpan = ClickableSpanEZ.from(spanClick,
                                                           content.substring(targetRange.getStart(),
                                                                             targetRange
                                                                                     .getEnd() + 1));
            target.setMovementMethod(new LinkMovementMethod());
            addSpan(targetRange, clickSpan);
        }
        return this;
    }

    @Override
    public StyleEZ foregroundColor(@NonNull Locator locator, @ColorRes int fgColorResId) {
        final int fgColor = ContextCompat.getColor(context, fgColorResId);
        ForegroundColorSpan fgColorSpan = new ForegroundColorSpan(fgColor);
        addMultipleSpan(locator, fgColorSpan);
        return this;
    }

    @Override
    public StyleEZ backgroundColor(@NonNull Locator locator, @ColorRes int bgColorResId) {
        final int bgColor = ContextCompat.getColor(context, bgColorResId);
        BackgroundColorSpan bgColorSpan = new BackgroundColorSpan(bgColor);
        addMultipleSpan(locator, bgColorSpan);
        return this;
    }

    @Override
    public StyleEZ quote(@NonNull ParagraphLocator paragraph) {
        return quote(paragraph, RESOURCE_NOT_SET);
    }

    @Override
    public StyleEZ quote(@NonNull ParagraphLocator paragraph, @ColorRes int quoteColorResId) {
        final QuoteSpan quoteSpan;
        if (quoteColorResId == RESOURCE_NOT_SET) {
            quoteSpan = new QuoteSpan();
        } else {
            final int quoteColor = ContextCompat.getColor(context, quoteColorResId);
            quoteSpan = new QuoteSpan(quoteColor);
        }

        addMultipleSpan(paragraph, quoteSpan);
        return this;
    }

    @Override
    public StyleEZ alignCenter(@NonNull ParagraphLocator paragraph) {
        AlignmentSpan centerSpan = new AlignmentSpan.Standard(Layout.Alignment.ALIGN_CENTER);
        addMultipleSpan(paragraph, centerSpan);
        return this;
    }

    @Override
    public StyleEZ alignEnd(@NonNull ParagraphLocator paragraph) {
        AlignmentSpan oppositeSpan = new AlignmentSpan.Standard(Layout.Alignment.ALIGN_OPPOSITE);
        addMultipleSpan(paragraph, oppositeSpan);
        return this;
    }

    @Override
    public StyleEZ alignStart(@NonNull ParagraphLocator paragraph) {
        AlignmentSpan normalSpan = new AlignmentSpan.Standard(Layout.Alignment.ALIGN_NORMAL);
        addMultipleSpan(paragraph, normalSpan);
        return this;
    }

    @Override
    public StyleEZ link(@NonNull Locator locator, @NonNull String url) {
        URLSpan urlSpan = new URLSpan(url);
        addMultipleSpan(locator, urlSpan);
        return this;
    }

    @Override
    public StyleEZ font(@NonNull Locator locator, @NonNull @EZ.FontFamily String fontFamily) {
        TypefaceSpan typefaceSpan = new TypefaceSpan(fontFamily);
        addMultipleSpan(locator, typefaceSpan);
        return this;
    }

    @Override
    public StyleEZ appearance(@NonNull Locator locator, @StyleRes int appearance) {
        TextAppearanceSpan textAppearanceSpan = new TextAppearanceSpan(context, appearance);
        addMultipleSpan(locator, textAppearanceSpan);
        return this;
    }

    @Override
    public StyleEZ locale(@NonNull Locator locator, @NonNull Locale locale) {
        LocaleSpan localeSpan = new LocaleSpan(locale);
        addMultipleSpan(locator, localeSpan);
        return this;
    }

    @Override
    public StyleEZ scaleX(@NonNull Locator locator, @FloatRange(from = 0.f) float proportion) {
        ScaleXSpan scaleXSpan = new ScaleXSpan(proportion);
        addMultipleSpan(locator, scaleXSpan);
        return this;
    }

    @Override
    public StyleEZ relativeSize(@NonNull Locator locator, @FloatRange(from = 0.f) float proportion) {
        RelativeSizeSpan relativeSizeSpan = new RelativeSizeSpan(proportion);
        addMultipleSpan(locator, relativeSizeSpan);
        return this;
    }

    @Override
    public StyleEZ absoluteSize(@NonNull Locator locator, @IntRange(from = 1) int pixels) {
        AbsoluteSizeSpan absoluteSizeSpan = new AbsoluteSizeSpan(pixels);
        addMultipleSpan(locator, absoluteSizeSpan);
        return this;
    }

    @Override
    public StyleEZ absoluteSizeDP(@NonNull Locator locator, @IntRange(from = 1) int dips) {
        AbsoluteSizeSpan absoluteSizeSpan = new AbsoluteSizeSpan(dips, true);
        addMultipleSpan(locator, absoluteSizeSpan);
        return this;
    }

    @Override
    @UiThread
    public void apply() {
        target.setText(spannableContent);
    }

    /**
     * Applies the given {@code span} to the specified ranges
     *
     * @param locator the locator to be used to decide were to apply this span
     * @param span    the span to be applied
     */
    private void addMultipleSpan(@NonNull Locator locator, @NonNull Object span) {
        for (TargetRange targetRange : locator.locate(content)) {
            addSpan(targetRange, span);
        }
    }

    /**
     * Applies the given {@code span} to the specified range from
     *
     * @param targetRange the range were the span will be applied to
     * @param span        the span to be applied
     */
    @SuppressWarnings("WeakerAccess")
    @VisibleForTesting
    protected void addSpan(@NonNull TargetRange targetRange, @NonNull Object span) {
        final int start = targetRange.getStart();
        final int end = targetRange.getEnd();
        // Added 1 to the span, because it seems that internally it does exclusive range
        spannableContent.setSpan(span, start, end + 1, spanFlags);
    }


    private boolean isFlagSet(@EZ.STYLE int flag, int flagToVerify) {
        return (flag & flagToVerify) == flagToVerify;
    }

}