package com.zekunyan.linktextview;

import android.content.Context;
import android.graphics.Color;
import android.text.Layout;
import android.text.Selection;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.Spanned;
import android.text.TextPaint;
import android.text.method.LinkMovementMethod;
import android.text.style.ClickableSpan;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.widget.TextView;

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

/**
 * Created by zekunyan on 14-10-21.
 * Email: [email protected]
 */
public class LinkTextView extends TextView {
    //Map for linkID to holder.
    private HashMap<Integer, LinkTextHolder> mLinkIDMap = new HashMap<Integer, LinkTextHolder>();
    //Base id for each link.
    private int mBaseID = 1;
    //The plain content text.
    private String mOriginText = "";
    //Spannable string for adding link
    private SpannableString mSpannableString = null;
    //If the view has been attached to window
    private boolean mHasShown = false;

    private static class LinkTextHolder {
        private String mText = "";
        private int mLinkID = -1;
        private int mBeginIndex = 0;
        private int mEndIndex = 0;
        //Color config.
        private LinkTextConfig mLinkTextConfig;
        //On click callback
        private OnClickInLinkText mOnClickInLinkText = null;
        //Attachment data
        private Object mAttachment = null;
    }

    public static class LinkTextConfig {
        public boolean mIsLinkUnderLine = false;
        public int mTextNormalColor = Color.BLACK;
        public int mTextPressedColor = Color.BLUE;
        public int mBackgroundNormalColor = Color.WHITE;
        public int mBackgroundPressedColor = Color.WHITE;
    }

    public interface OnClickInLinkText {
        /**
         * Called when click the link.
         *
         * @param clickText  Link text.
         * @param linkID     Link ID.
         * @param attachment Data attached to the link.
         */
        public void onLinkTextClick(String clickText, int linkID, Object attachment);
    }

    //Constructors
    public LinkTextView(Context context) {
        super(context);
    }

    public LinkTextView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    //Set text and reset to default config

    /**
     * Set the text you want to show. This should be called before addClick.
     *
     * @param text The text you want to show.
     */
    public void setClickableText(String text) {
        this.mHasShown = false;
        this.mOriginText = text;
        this.mLinkIDMap.clear();
        this.mSpannableString = new SpannableString(mOriginText);
    }

    /**
     * Add a new clickable link.
     *
     * @param start      Begin index for link.
     * @param end        End index for link.
     * @param click      Callback.
     * @param attachment Data attached to the link.
     * @return The ID for this clickable link.
     */
    public int addClick(int start, int end, OnClickInLinkText click, Object attachment) {
        if (start < 0 || end < 0 || start >= mOriginText.length() || end >= mOriginText.length()) {
            return -1;
        }

        final LinkTextHolder linkTextHolder = new LinkTextHolder();
        linkTextHolder.mLinkTextConfig = new LinkTextConfig();
        linkTextHolder.mAttachment = attachment;
        linkTextHolder.mBeginIndex = start;
        linkTextHolder.mEndIndex = end;
        linkTextHolder.mLinkID = mBaseID;
        linkTextHolder.mOnClickInLinkText = click;
        linkTextHolder.mText = mOriginText.substring(start, end);
        mLinkIDMap.put(mBaseID++, linkTextHolder);

        mSpannableString.setSpan(new TouchableSpan(linkTextHolder) {
            @Override
            public void onClick(View view) {
                if (linkTextHolder.mOnClickInLinkText != null) {
                    linkTextHolder.mOnClickInLinkText.onLinkTextClick(linkTextHolder.mText,
                            linkTextHolder.mLinkID, linkTextHolder.mAttachment);
                }
            }
        }, start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);

        updateLinks();

        return linkTextHolder.mLinkID;
    }

    /**
     * Add a new clickable link with additional configurations.
     * Color is 0xARGB.
     *
     * @param start            Begin index for link.
     * @param end              End index for link.
     * @param click            Callback.
     * @param attachment       Data attached to the link.
     * @param isUnderLine      Whether to show the under line.
     * @param textNormalColor  Normal color for text.
     * @param textPressedColor Pressed color for text.
     * @param bgNormalColor    Normal color for background.
     * @param bgPressedColor   Pressed color for background.
     * @return The ID for this clickable link.
     */
    public int addClick(int start, int end, OnClickInLinkText click, Object attachment, boolean isUnderLine,
                        int textNormalColor, int textPressedColor,
                        int bgNormalColor, int bgPressedColor) {
        int id = this.addClick(start, end, click, attachment);
        if (id == -1) {
            return id;
        }

        LinkTextHolder linkTextHolder = mLinkIDMap.get(id);
        //Set custom color
        linkTextHolder.mLinkTextConfig.mIsLinkUnderLine = isUnderLine;
        linkTextHolder.mLinkTextConfig.mTextNormalColor = textNormalColor;
        linkTextHolder.mLinkTextConfig.mTextPressedColor = textPressedColor;
        linkTextHolder.mLinkTextConfig.mBackgroundNormalColor = bgNormalColor;
        linkTextHolder.mLinkTextConfig.mBackgroundPressedColor = bgPressedColor;

        updateLinks();

        return id;
    }

    /**
     * Remove link by link ID
     *
     * @param linkID
     */
    public void removeLink(int linkID) {
        if (!mLinkIDMap.containsKey(linkID)) {
            return;
        }

        try {
            mLinkIDMap.remove(linkID);
        } catch (Exception e) {
            //TODO: Log...
        }

        //Reset all links
        mSpannableString = new SpannableString(mOriginText);
        for (Integer id : mLinkIDMap.keySet()) {
            final LinkTextHolder linkTextHolder = mLinkIDMap.get(id);
            mSpannableString.setSpan(new TouchableSpan(linkTextHolder) {
                @Override
                public void onClick(View view) {
                    if (linkTextHolder.mOnClickInLinkText != null) {
                        linkTextHolder.mOnClickInLinkText.onLinkTextClick(linkTextHolder.mText,
                                linkTextHolder.mLinkID, linkTextHolder.mAttachment);
                    }
                }
            }, linkTextHolder.mBeginIndex, linkTextHolder.mEndIndex, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
        }
        updateLinks();
    }

    /**
     * Remove all clickable links.
     */
    public void removeAllLink() {
        mLinkIDMap.clear();
        mSpannableString = new SpannableString(mOriginText);
        updateLinks();
    }

    /**
     * Set new config for specific link.
     *
     * @param linkID Clickable link id.
     * @param config Config.
     */
    public void setNewConfig(int linkID, LinkTextConfig config) {
        if (!mLinkIDMap.containsKey(linkID)) {
            //TODO: Log...
            return;
        }

        LinkTextHolder linkTextHolder = mLinkIDMap.get(linkID);

        if (linkTextHolder == null) {
            //TODO: Log...
            return;
        }

        linkTextHolder.mLinkTextConfig = config;
        invalidate();
    }

    /**
     * Get specific link config by linkID.
     *
     * @param linkID Clickable link id.
     * @return Config.
     */
    public LinkTextConfig getConfig(int linkID) {
        if (!mLinkIDMap.containsKey(linkID)) {
            return null;
        }
        return mLinkIDMap.get(linkID).mLinkTextConfig;
    }

    /**
     * Set specific link text normal color.
     *
     * @param linkID Clickable link id.
     * @param color  Text normal color
     */
    public void setTextNormalColor(int linkID, int color) {
        if (!mLinkIDMap.containsKey(linkID)) {
            return;
        }
        LinkTextConfig config = getConfig(linkID);
        config.mTextNormalColor = color;
        invalidate();
    }

    /**
     * Set specific link text pressed color
     *
     * @param linkID Clickable link id.
     * @param color  Text pressed color
     */
    public void setTextPressedColor(int linkID, int color) {
        if (!mLinkIDMap.containsKey(linkID)) {
            return;
        }
        LinkTextConfig config = getConfig(linkID);
        config.mTextPressedColor = color;
        invalidate();
    }

    /**
     * Set specific link background normal color
     *
     * @param linkID Clickable link id.
     * @param color  Background normal color
     */
    public void setBackgroundNormalColor(int linkID, int color) {
        if (!mLinkIDMap.containsKey(linkID)) {
            return;
        }
        LinkTextConfig config = getConfig(linkID);
        config.mBackgroundNormalColor = color;
        invalidate();
    }

    /**
     * Set specific link background pressed color.
     *
     * @param linkID Clickable link id.
     * @param color  Background pressed color
     */
    public void setBackgroundPressedColor(int linkID, int color) {
        if (!mLinkIDMap.containsKey(linkID)) {
            return;
        }
        LinkTextConfig config = getConfig(linkID);
        config.mBackgroundPressedColor = color;
        invalidate();
    }

    /**
     * Get IDs for a specific link text.
     *
     * @param linkText Link text.
     * @return IDs
     */
    public List<Integer> getSpecificLinkIDsByText(String linkText) {
        ArrayList<Integer> ids = new ArrayList<Integer>();

        for (Integer id : mLinkIDMap.keySet()) {
            final LinkTextHolder linkTextHolder = mLinkIDMap.get(id);

            if (linkTextHolder.mText.equals(linkText)) {
                ids.add(linkTextHolder.mLinkID);
            }
        }

        return ids;
    }

    //Update
    private void updateLinks() {
        //Refresh view
        if (mHasShown) {
            this.setText(mSpannableString);
            this.setMovementMethod(LinkTouchMovementMethod.getInstance());
            invalidate();
        }
    }

    @Override
    protected void onAttachedToWindow() {
        super.onAttachedToWindow();

        //Set all clickable spans
        if (!mHasShown) {
            mHasShown = true;
            this.setText(mSpannableString);
            this.setMovementMethod(LinkTouchMovementMethod.getInstance());
        }
    }

    //Inner class
    private static abstract class TouchableSpan extends ClickableSpan {
        private boolean mIsPressed;
        private LinkTextHolder linkTextHolder;

        public TouchableSpan(LinkTextHolder linkTextHolder) {
            this.linkTextHolder = linkTextHolder;
        }

        //For LinkTouchMovementMethod call
        public void setPressed(boolean isSelected) {
            mIsPressed = isSelected;
        }

        @Override
        public void updateDrawState(TextPaint textPaint) {
            super.updateDrawState(textPaint);

            textPaint.setColor(mIsPressed ? linkTextHolder.mLinkTextConfig.mTextPressedColor :
                    linkTextHolder.mLinkTextConfig.mTextNormalColor);
            textPaint.bgColor = mIsPressed ? linkTextHolder.mLinkTextConfig.mBackgroundPressedColor :
                    linkTextHolder.mLinkTextConfig.mBackgroundNormalColor;
            textPaint.setUnderlineText(linkTextHolder.mLinkTextConfig.mIsLinkUnderLine);
        }
    }

    private static class LinkTouchMovementMethod extends LinkMovementMethod {
        private TouchableSpan mPressedSpan;
        private static LinkTouchMovementMethod mInstance;

        //Override static method
        static public LinkTouchMovementMethod getInstance() {
            if (mInstance == null) {
                mInstance = new LinkTouchMovementMethod();
            }
            return mInstance;
        }

        @Override
        public boolean onTouchEvent(TextView textView, Spannable spannable, MotionEvent event) {
            switch (event.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    mPressedSpan = getPressedSpan(textView, spannable, event);
                    if (mPressedSpan != null) {
                        mPressedSpan.setPressed(true);
                        Selection.setSelection(spannable, spannable.getSpanStart(mPressedSpan),
                                spannable.getSpanEnd(mPressedSpan));
                    }
                    break;

                case MotionEvent.ACTION_MOVE:
                    TouchableSpan touchedSpan = getPressedSpan(textView, spannable, event);
                    if (mPressedSpan != null && touchedSpan != mPressedSpan) {
                        mPressedSpan.setPressed(false);
                        mPressedSpan = null;
                        Selection.removeSelection(spannable);
                    }
                    break;

                default:
                    if (mPressedSpan != null) {
                        mPressedSpan.setPressed(false);
                        super.onTouchEvent(textView, spannable, event);
                    }
                    mPressedSpan = null;
                    Selection.removeSelection(spannable);
                    break;
            }

            return true;
        }

        private TouchableSpan getPressedSpan(TextView textView, Spannable spannable, MotionEvent event) {

            int x = (int) event.getX();
            int y = (int) event.getY();

            x -= textView.getTotalPaddingLeft();
            y -= textView.getTotalPaddingTop();

            x += textView.getScrollX();
            y += textView.getScrollY();

            Layout layout = textView.getLayout();
            int line = layout.getLineForVertical(y);
            int off = layout.getOffsetForHorizontal(line, x);

            TouchableSpan[] link = spannable.getSpans(off, off, TouchableSpan.class);
            TouchableSpan touchedSpan = null;
            if (link.length > 0) {
                touchedSpan = link[0];
            }
            return touchedSpan;
        }

    }
}