/*
 * Copyright (C) 2017 Simon Marquis
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package fr.smarquis.trusk;


import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.MaskFilter;
import android.graphics.Typeface;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.support.annotation.ColorInt;
import android.support.annotation.DrawableRes;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.Px;
import android.support.annotation.StyleRes;
import android.text.Layout;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.style.AbsoluteSizeSpan;
import android.text.style.AlignmentSpan;
import android.text.style.BackgroundColorSpan;
import android.text.style.BulletSpan;
import android.text.style.ClickableSpan;
import android.text.style.DrawableMarginSpan;
import android.text.style.ForegroundColorSpan;
import android.text.style.IconMarginSpan;
import android.text.style.ImageSpan;
import android.text.style.LeadingMarginSpan;
import android.text.style.MaskFilterSpan;
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.view.View;

import java.lang.reflect.Array;

/**
 * Java wrapper around SpannableStringBuilder
 * Inspired by JakeWharton's Truss and Kotlin's kotlinx.html
 *
 * @see <a href="https://gist.github.com/JakeWharton/11274467">Truss</a>
 * @see <a href="https://github.com/Kotlin/kotlinx.html">kotlinx.html</a>
 */
@SuppressWarnings("WeakerAccess, unused")
public class Span {

    @Nullable
    private Object[] nodes;

    Span(@Nullable Object[] nodes) {
        this.nodes = nodes;
    }

    public static Span create(Object... nodes) {
        return new Span(nodes);
    }

    android.text.Spannable build() {
        return build(new SpannableStringBuilder());
    }

    android.text.Spannable build(SpannableStringBuilder builder) {
        if (nodes != null) {
            for (Object child : nodes) {
                if (child instanceof Span) {
                    ((Span) child).build(builder);
                } else {
                    builder.append(child.toString());
                }
            }
        }
        return builder;
    }

    public Span append(Object... nodes) {
        if (nodes == null || nodes.length == 0) {
            return this;
        } else if (this.nodes == null || this.nodes.length == 0) {
            this.nodes = nodes;
            return this;
        }
        final Object[] joinedArray = (Object[]) Array.newInstance(Object.class, this.nodes.length + nodes.length);
        System.arraycopy(this.nodes, 0, joinedArray, 0, this.nodes.length);
        System.arraycopy(nodes, 0, joinedArray, this.nodes.length, nodes.length);
        this.nodes = joinedArray;
        return this;
    }

    static class Node extends Span {

        Node(@Nullable Object[] nodes) {
            super(nodes);
        }

        public static Node span(Object span, Object... nodes) {
            return new SpanNode(span, nodes);
        }

        public static Span text(String content) {
            return new Leaf(content);
        }

        public static Node style(Integer style, Object... nodes) {
            return new SpanNode(new StyleSpan(style), nodes);
        }

        public static Node normal(Object... nodes) {
            return new SpanNode(new StyleSpan(Typeface.NORMAL), nodes);
        }

        public static Node bold(Object... nodes) {
            return new SpanNode(new StyleSpan(Typeface.BOLD), nodes);
        }

        public static Node italic(Object... nodes) {
            return new SpanNode(new StyleSpan(Typeface.ITALIC), nodes);
        }

        public static Node boldItalic(Object... nodes) {
            return new SpanNode(new StyleSpan(Typeface.BOLD_ITALIC), nodes);
        }

        public static Node underline(Object... nodes) {
            return new SpanNode(new UnderlineSpan(), nodes);
        }

        public static Node typeface(String family, Object... nodes) {
            return new SpanNode(new TypefaceSpan(family), nodes);
        }

        public static Node sansSerif(Object... nodes) {
            return typeface("sans-serif", nodes);
        }

        public static Node serif(Object... nodes) {
            return typeface("serif", nodes);
        }

        public static Node monospace(Object... nodes) {
            return typeface("monospace", nodes);
        }

        public static Node appearance(Context context, @StyleRes Integer appearance, Object... nodes) {
            return new SpanNode(new TextAppearanceSpan(context, appearance), nodes);
        }

        public static Node superscript(Object... nodes) {
            return new SpanNode(new SuperscriptSpan(), nodes);
        }

        public static Node subscript(Object... nodes) {
            return new SpanNode(new SubscriptSpan(), nodes);
        }

        public static Node strikethrough(Object... nodes) {
            return new SpanNode(new StrikethroughSpan(), nodes);
        }

        public static Node scaleX(Float proportion, Object... nodes) {
            return new SpanNode(new ScaleXSpan(proportion), nodes);
        }

        public static Node relativeSize(Float proportion, Object... nodes) {
            return new SpanNode(new RelativeSizeSpan(proportion), nodes);
        }

        public static Node absoluteSize(@Px Integer size, Object... nodes) {
            return new SpanNode(new AbsoluteSizeSpan(size), nodes);
        }

        public static Node absoluteSize(@Px Integer size, Boolean dip, Object... nodes) {
            return new SpanNode(new AbsoluteSizeSpan(size, dip), nodes);
        }

        public static Node quote(Object... nodes) {
            return new SpanNode(new QuoteSpan(), nodes);
        }

        public static Node quote(@ColorInt Integer color, Object... nodes) {
            return new SpanNode(new QuoteSpan(color), nodes);
        }

        public static Node mask(MaskFilter filter, Object... nodes) {
            return new SpanNode(new MaskFilterSpan(filter), nodes);
        }

        public static Node leadingMargin(Integer every, Object... nodes) {
            return new SpanNode(new LeadingMarginSpan.Standard(every), nodes);
        }

        public static Node leadingMargin(Integer first, Integer rest, Object... nodes) {
            return new SpanNode(new LeadingMarginSpan.Standard(first, rest), nodes);
        }

        public static Node foregroundColor(@ColorInt Integer color, Object... nodes) {
            return new SpanNode(new ForegroundColorSpan(color), nodes);
        }

        public static Node backgroundColor(@ColorInt Integer color, Object... nodes) {
            return new SpanNode(new BackgroundColorSpan(color), nodes);
        }

        public static Node bullet(Object... nodes) {
            return new SpanNode(new BulletSpan(), nodes);
        }

        public static Node bullet(@Px Integer gapWidth, Object... nodes) {
            return new SpanNode(new BulletSpan(gapWidth), nodes);
        }

        public static Node bullet(Integer gapWidth, @ColorInt Integer color, Object... nodes) {
            return new SpanNode(new BulletSpan(gapWidth, color), nodes);
        }

        public static Node align(Layout.Alignment align, Object... nodes) {
            return new SpanNode(new AlignmentSpan.Standard(align), nodes);
        }

        public static Node drawableMargin(Drawable drawable, Object... nodes) {
            return new SpanNode(new DrawableMarginSpan(drawable), nodes);
        }

        public static Node drawableMargin(Drawable drawable, @Px Integer padding, Object... nodes) {
            return new SpanNode(new DrawableMarginSpan(drawable, padding), nodes);
        }

        public static Node iconMargin(Bitmap bitmap, Object... nodes) {
            return new SpanNode(new IconMarginSpan(bitmap), nodes);
        }

        public static Node iconMargin(Bitmap bitmap, @Px Integer padding, Object... nodes) {
            return new SpanNode(new IconMarginSpan(bitmap, padding), nodes);
        }

        public static Node image(Context context, Bitmap bitmap, Object... nodes) {
            return new SpanNode(new ImageSpan(context, bitmap), nodes);
        }

        public static Node image(Context context, Bitmap bitmap, Integer verticalAlignment, Object... nodes) {
            return new SpanNode(new ImageSpan(context, bitmap, verticalAlignment), nodes);
        }

        public static Node image(Drawable drawable, Object... nodes) {
            return new SpanNode(new ImageSpan(drawable), nodes);
        }

        public static Node image(Drawable drawable, Integer verticalAlignment, Object... nodes) {
            return new SpanNode(new ImageSpan(drawable, verticalAlignment), nodes);
        }

        public static Node image(Context context, Uri uri, Object... nodes) {
            return new SpanNode(new ImageSpan(context, uri), nodes);
        }

        public static Node image(Context context, Uri uri, Integer verticalAlignment, Object... nodes) {
            return new SpanNode(new ImageSpan(context, uri, verticalAlignment), nodes);
        }

        public static Node image(Context context, @DrawableRes Integer resourceId, Object... nodes) {
            return new SpanNode(new ImageSpan(context, resourceId), nodes);
        }

        public static Node image(Context context, @DrawableRes Integer resourceId, Integer verticalAlignment, Object... nodes) {
            return new SpanNode(new ImageSpan(context, resourceId, verticalAlignment), nodes);
        }

        public static ClickableSpanNode clickable(Object... nodes) {
            return new ClickableSpanNode(nodes);
        }

        static class ClickableSpanNode extends SpanNode {

            private static class InternalClickableSpan extends ClickableSpan {

                @Nullable
                private OnClickCallback callback;

                @Override
                public void onClick(View widget) {
                    if (callback != null) {
                        callback.onClick(this);
                    }
                }
            }

            interface OnClickCallback {
                void onClick(ClickableSpan span);
            }

            ClickableSpanNode(Object... nodes) {
                super(new InternalClickableSpan(), nodes);
            }

            public ClickableSpanNode onClick(OnClickCallback callback) {
                if (span instanceof InternalClickableSpan) {
                    ((InternalClickableSpan) span).callback = callback;
                }
                return this;
            }
        }


        public static UrlSpanNode url(String url, Object... nodes) {
            return new UrlSpanNode(url, nodes);
        }

        static class UrlSpanNode extends SpanNode {

            private static class InternalUrlSpan extends URLSpan {

                @Nullable
                private OnClickCallback callback;

                InternalUrlSpan(String url) {
                    super(url);
                }

                @Override
                public void onClick(View widget) {
                    if (callback == null || callback.onClick(this)) {
                        super.onClick(widget);
                    }
                }
            }

            interface OnClickCallback {
                boolean onClick(URLSpan span);
            }

            UrlSpanNode(String url, Object... nodes) {
                super(new InternalUrlSpan(url), nodes);
            }

            public UrlSpanNode onClick(OnClickCallback callback) {
                if (span instanceof InternalUrlSpan) {
                    ((InternalUrlSpan) span).callback = callback;
                }
                return this;
            }
        }

        static class SpanNode extends Node {

            @NonNull
            final protected Object span;

            SpanNode(@NonNull Object span, Object... nodes) {
                super(nodes);
                this.span = span;
            }

            android.text.Spannable build(SpannableStringBuilder builder) {
                int start = builder.length();
                super.build(builder);
                builder.setSpan(span, start, builder.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
                return builder;
            }
        }

        static class Leaf extends Span {

            @Nullable
            final private Object content;

            Leaf(@Nullable Object content) {
                super(null);
                this.content = content;
            }

            android.text.Spannable build(SpannableStringBuilder builder) {
                if (content != null) {
                    builder.append(content.toString());
                }
                return builder;
            }
        }

    }
}