/*
 * Copyright (C) 2007 The Android Open Source Project
 *
 * 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 com.fanfou.app.opensource.util;

import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import android.text.Spannable;
import android.text.SpannableString;
import android.text.Spanned;
import android.text.TextPaint;
import android.text.method.LinkMovementMethod;
import android.text.method.MovementMethod;
import android.text.style.URLSpan;
import android.webkit.WebView;
import android.widget.TextView;

/**
 *  Linkify take a piece of text and a regular expression and turns all of the
 *  regex matches in the text into clickable links.  This is particularly
 *  useful for matching things like email addresses, web urls, etc. and making
 *  them actionable.
 *
 *  Alone with the pattern that is to be matched, a url scheme prefix is also
 *  required.  Any pattern match that does not begin with the supplied scheme
 *  will have the scheme prepended to the matched text when the clickable url
 *  is created.  For instance, if you are matching web urls you would supply
 *  the scheme <code>http://</code>.  If the pattern matches example.com, which
 *  does not have a url scheme prefix, the supplied scheme will be prepended to
 *  create <code>http://example.com</code> when the clickable url link is
 *  created.
 */

/**
 * custom at 2011.11.17
 * 
 * @author mcxiaoke
 * @version 1.0 2011.11.17
 * 
 */
public class LinkifyCompat {
    /**
     * MatchFilter enables client code to have more control over what is allowed
     * to match and become a link, and what is not.
     * 
     * For example: when matching web urls you would like things like
     * http://www.example.com to match, as well as just example.com itelf.
     * However, you would not want to match against the domain in
     * [email protected]. So, when matching against a web url pattern you
     * might also include a MatchFilter that disallows the match if it is
     * immediately preceded by an at-sign (@).
     */
    public interface MatchFilter {
        /**
         * Examines the character span matched by the pattern and determines if
         * the match should be turned into an actionable link.
         * 
         * @param s
         *            The body of text against which the pattern was matched
         * @param start
         *            The index of the first character in s that was matched by
         *            the pattern - inclusive
         * @param end
         *            The index of the last character in s that was matched -
         *            exclusive
         * 
         * @return Whether this match should be turned into a link
         */
        boolean acceptMatch(CharSequence s, int start, int end);
    }

    /**
     * TransformFilter enables client code to have more control over how matched
     * patterns are represented as URLs.
     * 
     * For example: when converting a phone number such as (919) 555-1212 into a
     * tel: URL the parentheses, white space, and hyphen need to be removed to
     * produce tel:9195551212.
     */
    public interface TransformFilter {
        /**
         * Examines the matched text and either passes it through or uses the
         * data in the Matcher state to produce a replacement.
         * 
         * @param match
         *            The regex matcher state that found this URL text
         * @param url
         *            The text that was matched
         * 
         * @return The transformed form of the URL
         */
        String transformUrl(final Matcher match, String url);
    }

    public static class URLSpanNoUnderline extends URLSpan {
        public URLSpanNoUnderline(final String url) {
            super(url);
        }

        @Override
        public void updateDrawState(final TextPaint tp) {
            super.updateDrawState(tp);
            tp.setUnderlineText(false);
        }
    }

    /**
     * Bit field indicating that web URLs should be matched in methods that take
     * an options mask
     */
    public static final int WEB_URLS = 0x01;

    /**
     * Bit field indicating that email addresses should be matched in methods
     * that take an options mask
     */
    public static final int EMAIL_ADDRESSES = 0x02;

    /**
     * Bit field indicating that phone numbers should be matched in methods that
     * take an options mask
     */
    public static final int PHONE_NUMBERS = 0x04;

    /**
     * Bit field indicating that street addresses should be matched in methods
     * that take an options mask
     */
    public static final int MAP_ADDRESSES = 0x08;

    /**
     * Bit mask indicating that all available patterns should be matched in
     * methods that take an options mask
     */
    public static final int ALL = LinkifyCompat.WEB_URLS
            | LinkifyCompat.EMAIL_ADDRESSES | LinkifyCompat.PHONE_NUMBERS
            | LinkifyCompat.MAP_ADDRESSES;

    /**
     * Don't treat anything with fewer than this many digits as a phone number.
     */
    private static final int PHONE_NUMBER_MINIMUM_DIGITS = 5;

    /**
     * Filters out web URL matches that occur after an at-sign (@). This is to
     * prevent turning the domain name in an email address into a web link.
     */
    public static final MatchFilter sUrlMatchFilter = new MatchFilter() {
        @Override
        public final boolean acceptMatch(final CharSequence s, final int start,
                final int end) {
            if (start == 0) {
                return true;
            }

            if (s.charAt(start - 1) == '@') {
                return false;
            }

            return true;
        }
    };

    /**
     * Filters out URL matches that don't have enough digits to be a phone
     * number.
     */
    public static final MatchFilter sPhoneNumberMatchFilter = new MatchFilter() {
        @Override
        public final boolean acceptMatch(final CharSequence s, final int start,
                final int end) {
            int digitCount = 0;

            for (int i = start; i < end; i++) {
                if (Character.isDigit(s.charAt(i))) {
                    digitCount++;
                    if (digitCount >= LinkifyCompat.PHONE_NUMBER_MINIMUM_DIGITS) {
                        return true;
                    }
                }
            }
            return false;
        }
    };

    /**
     * Transforms matched phone number text into something suitable to be used
     * in a tel: URL. It does this by removing everything but the digits and
     * plus signs. For instance: &apos;+1 (919) 555-1212&apos; becomes
     * &apos;+19195551212&apos;
     */
    public static final TransformFilter sPhoneNumberTransformFilter = new TransformFilter() {
        @Override
        public final String transformUrl(final Matcher match, final String url) {
            return PatternsCompat.digitsAndPlusOnly(match);
        }
    };

    private static final void addLinkMovementMethod(final TextView t) {
        final MovementMethod m = t.getMovementMethod();

        if ((m == null) || !(m instanceof LinkMovementMethod)) {
            if (t.getLinksClickable()) {
                t.setMovementMethod(LinkMovementMethod.getInstance());
            }
        }
    }

    /**
     * Scans the text of the provided Spannable and turns all occurrences of the
     * link types indicated in the mask into clickable links. If the mask is
     * nonzero, it also removes any existing URLSpans attached to the Spannable,
     * to avoid problems if you call it repeatedly on the same text.
     */
    public static final boolean addLinks(final Spannable text, final int mask) {
        if (mask == 0) {
            return false;
        }

        final URLSpan[] old = text.getSpans(0, text.length(), URLSpan.class);

        for (int i = old.length - 1; i >= 0; i--) {
            text.removeSpan(old[i]);
        }

        final ArrayList<LinkSpec> links = new ArrayList<LinkSpec>();

        if ((mask & LinkifyCompat.WEB_URLS) != 0) {
            LinkifyCompat.gatherLinks(links, text, PatternsCompat.WEB_URL,
                    new String[] { "http://", "https://", "rtsp://" },
                    LinkifyCompat.sUrlMatchFilter, null);
        }

        if ((mask & LinkifyCompat.EMAIL_ADDRESSES) != 0) {
            LinkifyCompat.gatherLinks(links, text,
                    PatternsCompat.EMAIL_ADDRESS, new String[] { "mailto:" },
                    null, null);
        }

        if ((mask & LinkifyCompat.PHONE_NUMBERS) != 0) {
            LinkifyCompat.gatherLinks(links, text, PatternsCompat.PHONE,
                    new String[] { "tel:" },
                    LinkifyCompat.sPhoneNumberMatchFilter,
                    LinkifyCompat.sPhoneNumberTransformFilter);
        }

        if ((mask & LinkifyCompat.MAP_ADDRESSES) != 0) {
            LinkifyCompat.gatherMapLinks(links, text);
        }

        LinkifyCompat.pruneOverlaps(links);

        if (links.size() == 0) {
            return false;
        }

        for (final LinkSpec link : links) {
            LinkifyCompat.applyLink(link.url, link.start, link.end, text);
        }

        return true;
    }

    /**
     * Applies a regex to a Spannable turning the matches into links.
     * 
     * @param text
     *            Spannable whose text is to be marked-up with links
     * @param pattern
     *            Regex pattern to be used for finding links
     * @param scheme
     *            Url scheme string (eg <code>http://</code> to be prepended to
     *            the url of links that do not have a scheme specified in the
     *            link text
     */
    public static final boolean addLinks(final Spannable text,
            final Pattern pattern, final String scheme) {
        return LinkifyCompat.addLinks(text, pattern, scheme, null, null);
    }

    /**
     * Applies a regex to a Spannable turning the matches into links.
     * 
     * @param s
     *            Spannable whose text is to be marked-up with links
     * @param p
     *            Regex pattern to be used for finding links
     * @param scheme
     *            Url scheme string (eg <code>http://</code> to be prepended to
     *            the url of links that do not have a scheme specified in the
     *            link text
     * @param matchFilter
     *            The filter that is used to allow the client code additional
     *            control over which pattern matches are to be converted into
     *            links.
     */
    public static final boolean addLinks(final Spannable s, final Pattern p,
            final String scheme, final MatchFilter matchFilter,
            final TransformFilter transformFilter) {
        boolean hasMatches = false;
        final String prefix = (scheme == null) ? "" : scheme.toLowerCase();
        final Matcher m = p.matcher(s);

        while (m.find()) {
            final int start = m.start();
            final int end = m.end();
            boolean allowed = true;

            if (matchFilter != null) {
                allowed = matchFilter.acceptMatch(s, start, end);
            }

            if (allowed) {
                final String url = LinkifyCompat.makeUrl(m.group(0),
                        new String[] { prefix }, m, transformFilter);

                LinkifyCompat.applyLink(url, start, end, s);
                hasMatches = true;
            }
        }

        return hasMatches;
    }

    /**
     * Scans the text of the provided TextView and turns all occurrences of the
     * link types indicated in the mask into clickable links. If matches are
     * found the movement method for the TextView is set to LinkMovementMethod.
     */
    public static final boolean addLinks(final TextView text, final int mask) {
        if (mask == 0) {
            return false;
        }

        final CharSequence t = text.getText();

        if (t instanceof Spannable) {
            if (LinkifyCompat.addLinks((Spannable) t, mask)) {
                LinkifyCompat.addLinkMovementMethod(text);
                return true;
            }

            return false;
        } else {
            final SpannableString s = SpannableString.valueOf(t);

            if (LinkifyCompat.addLinks(s, mask)) {
                LinkifyCompat.addLinkMovementMethod(text);
                text.setText(s);

                return true;
            }

            return false;
        }
    }

    /**
     * Applies a regex to the text of a TextView turning the matches into links.
     * If links are found then UrlSpans are applied to the link text match
     * areas, and the movement method for the text is changed to
     * LinkMovementMethod.
     * 
     * @param text
     *            TextView whose text is to be marked-up with links
     * @param pattern
     *            Regex pattern to be used for finding links
     * @param scheme
     *            Url scheme string (eg <code>http://</code> to be prepended to
     *            the url of links that do not have a scheme specified in the
     *            link text
     */
    public static final void addLinks(final TextView text,
            final Pattern pattern, final String scheme) {
        LinkifyCompat.addLinks(text, pattern, scheme, null, null);
    }

    /**
     * Applies a regex to the text of a TextView turning the matches into links.
     * If links are found then UrlSpans are applied to the link text match
     * areas, and the movement method for the text is changed to
     * LinkMovementMethod.
     * 
     * @param text
     *            TextView whose text is to be marked-up with links
     * @param p
     *            Regex pattern to be used for finding links
     * @param scheme
     *            Url scheme string (eg <code>http://</code> to be prepended to
     *            the url of links that do not have a scheme specified in the
     *            link text
     * @param matchFilter
     *            The filter that is used to allow the client code additional
     *            control over which pattern matches are to be converted into
     *            links.
     */
    public static final void addLinks(final TextView text, final Pattern p,
            final String scheme, final MatchFilter matchFilter,
            final TransformFilter transformFilter) {
        final SpannableString s = SpannableString.valueOf(text.getText());

        if (LinkifyCompat.addLinks(s, p, scheme, matchFilter, transformFilter)) {
            text.setText(s);
            LinkifyCompat.addLinkMovementMethod(text);
        }
    }

    private static final void applyLink(final String url, final int start,
            final int end, final Spannable text) {
        final URLSpanNoUnderline span = new URLSpanNoUnderline(url);

        text.setSpan(span, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
    }

    private static final void gatherLinks(final ArrayList<LinkSpec> links,
            final Spannable s, final Pattern pattern, final String[] schemes,
            final MatchFilter matchFilter, final TransformFilter transformFilter) {
        final Matcher m = pattern.matcher(s);

        while (m.find()) {
            final int start = m.start();
            final int end = m.end();

            if ((matchFilter == null) || matchFilter.acceptMatch(s, start, end)) {
                final LinkSpec spec = new LinkSpec();
                final String url = LinkifyCompat.makeUrl(m.group(0), schemes,
                        m, transformFilter);

                spec.url = url;
                spec.start = start;
                spec.end = end;

                links.add(spec);
            }
        }
    }

    private static final void gatherMapLinks(final ArrayList<LinkSpec> links,
            final Spannable s) {
        String string = s.toString();
        String address;
        int base = 0;

        while ((address = WebView.findAddress(string)) != null) {
            final int start = string.indexOf(address);

            if (start < 0) {
                break;
            }

            final LinkSpec spec = new LinkSpec();
            final int length = address.length();
            final int end = start + length;

            spec.start = base + start;
            spec.end = base + end;
            string = string.substring(end);
            base += end;

            String encodedAddress = null;

            try {
                encodedAddress = URLEncoder.encode(address, "UTF-8");
            } catch (final UnsupportedEncodingException e) {
                continue;
            }

            spec.url = "geo:0,0?q=" + encodedAddress;
            links.add(spec);
        }
    }

    private static final String makeUrl(String url, final String[] prefixes,
            final Matcher m, final TransformFilter filter) {
        if (filter != null) {
            url = filter.transformUrl(m, url);
        }

        boolean hasPrefix = false;

        for (int i = 0; i < prefixes.length; i++) {
            if (url.regionMatches(true, 0, prefixes[i], 0, prefixes[i].length())) {
                hasPrefix = true;

                // Fix capitalization if necessary
                if (!url.regionMatches(false, 0, prefixes[i], 0,
                        prefixes[i].length())) {
                    url = prefixes[i] + url.substring(prefixes[i].length());
                }

                break;
            }
        }

        if (!hasPrefix) {
            url = prefixes[0] + url;
        }

        return url;
    }

    private static final void pruneOverlaps(final ArrayList<LinkSpec> links) {
        final Comparator<LinkSpec> c = new Comparator<LinkSpec>() {
            @Override
            public final int compare(final LinkSpec a, final LinkSpec b) {
                if (a.start < b.start) {
                    return -1;
                }

                if (a.start > b.start) {
                    return 1;
                }

                if (a.end < b.end) {
                    return 1;
                }

                if (a.end > b.end) {
                    return -1;
                }

                return 0;
            }

            @Override
            public final boolean equals(final Object o) {
                return false;
            }
        };

        Collections.sort(links, c);

        int len = links.size();
        int i = 0;

        while (i < (len - 1)) {
            final LinkSpec a = links.get(i);
            final LinkSpec b = links.get(i + 1);
            int remove = -1;

            if ((a.start <= b.start) && (a.end > b.start)) {
                if (b.end <= a.end) {
                    remove = i + 1;
                } else if ((a.end - a.start) > (b.end - b.start)) {
                    remove = i + 1;
                } else if ((a.end - a.start) < (b.end - b.start)) {
                    remove = i;
                }

                if (remove != -1) {
                    links.remove(remove);
                    len--;
                    continue;
                }

            }

            i++;
        }
    }
}

class LinkSpec {
    String url;
    int start;
    int end;
}