/*
 * Copyright (C) 2021 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 androidx.emoji2.bundled.util;

import static org.mockito.ArgumentMatchers.argThat;

import android.text.Spanned;
import android.text.TextUtils;

import androidx.emoji.text.EmojiSpan;

import org.hamcrest.Description;
import org.hamcrest.Matcher;
import org.hamcrest.TypeSafeMatcher;
import org.mockito.ArgumentMatcher;

/**
 * Utility class that includes matchers specific to emojis and EmojiSpans.
 */
public class EmojiMatcher {

    public static Matcher<CharSequence> hasEmojiAt(final int id, final int start,
            final int end) {
        return new EmojiResourceMatcher(id, start, end);
    }

    public static Matcher<CharSequence> hasEmojiAt(final Emoji.EmojiMapping emojiMapping,
            final int start, final int end) {
        return new EmojiResourceMatcher(emojiMapping.id(), start, end);
    }

    public static Matcher<CharSequence> hasEmojiAt(final int start, final int end) {
        return new EmojiResourceMatcher(-1, start, end);
    }

    public static Matcher<CharSequence> hasEmoji(final int id) {
        return new EmojiResourceMatcher(id, -1, -1);
    }

    public static Matcher<CharSequence> hasEmoji(final Emoji.EmojiMapping emojiMapping) {
        return new EmojiResourceMatcher(emojiMapping.id(), -1, -1);
    }

    public static Matcher<CharSequence> hasEmoji() {
        return new EmojiSpanMatcher();
    }

    public static Matcher<CharSequence> hasEmojiCount(final int count) {
        return new EmojiCountMatcher(count);
    }

    public static <T extends CharSequence> T sameCharSequence(final T expected) {
        return argThat(new ArgumentMatcher<T>() {
            @Override
            public boolean matches(T o) {
                if (o instanceof CharSequence) {
                    return TextUtils.equals(expected, o);
                }
                return false;
            }

            @Override
            public String toString() {
                return "doesn't match " + expected;
            }
        });
    }

    private static class EmojiSpanMatcher extends TypeSafeMatcher<CharSequence> {

        private EmojiSpan[] mSpans;

        EmojiSpanMatcher() {
        }

        @Override
        public void describeTo(Description description) {
            description.appendText("should have EmojiSpans");
        }

        @Override
        protected void describeMismatchSafely(final CharSequence charSequence,
                Description mismatchDescription) {
            mismatchDescription.appendText(" has no EmojiSpans");
        }

        @Override
        protected boolean matchesSafely(final CharSequence charSequence) {
            if (charSequence == null) return false;
            if (!(charSequence instanceof Spanned)) return false;
            mSpans = ((Spanned) charSequence).getSpans(0, charSequence.length(), EmojiSpan.class);
            return mSpans.length != 0;
        }
    }

    private static class EmojiCountMatcher extends TypeSafeMatcher<CharSequence> {

        private final int mCount;
        private EmojiSpan[] mSpans;

        EmojiCountMatcher(final int count) {
            mCount = count;
        }

        @Override
        public void describeTo(Description description) {
            description.appendText("should have ").appendValue(mCount).appendText(" EmojiSpans");
        }

        @Override
        protected void describeMismatchSafely(final CharSequence charSequence,
                Description mismatchDescription) {
            mismatchDescription.appendText(" has ");
            if (mSpans == null) {
                mismatchDescription.appendValue("no");
            } else {
                mismatchDescription.appendValue(mSpans.length);
            }

            mismatchDescription.appendText(" EmojiSpans");
        }

        @Override
        protected boolean matchesSafely(final CharSequence charSequence) {
            if (charSequence == null) return false;
            if (!(charSequence instanceof Spanned)) return false;
            mSpans = ((Spanned) charSequence).getSpans(0, charSequence.length(), EmojiSpan.class);
            return mSpans.length == mCount;
        }
    }

    private static class EmojiResourceMatcher extends TypeSafeMatcher<CharSequence> {
        private static final int ERR_NONE = 0;
        private static final int ERR_SPANNABLE_NULL = 1;
        private static final int ERR_NO_SPANS = 2;
        private static final int ERR_WRONG_INDEX = 3;
        private final int mResId;
        private final int mStart;
        private final int mEnd;
        private int mError = ERR_NONE;
        private int mActualStart = -1;
        private int mActualEnd = -1;

        EmojiResourceMatcher(int resId, int start, int end) {
            mResId = resId;
            mStart = start;
            mEnd = end;
        }

        @Override
        public void describeTo(final Description description) {
            if (mResId == -1) {
                description.appendText("should have EmojiSpan at ")
                        .appendValue("[" + mStart + "," + mEnd + "]");
            } else if (mStart == -1 && mEnd == -1) {
                description.appendText("should have EmojiSpan with resource id ")
                        .appendValue(Integer.toHexString(mResId));
            } else {
                description.appendText("should have EmojiSpan with resource id ")
                        .appendValue(Integer.toHexString(mResId))
                        .appendText(" at ")
                        .appendValue("[" + mStart + "," + mEnd + "]");
            }
        }

        @Override
        protected void describeMismatchSafely(final CharSequence charSequence,
                Description mismatchDescription) {
            int offset = 0;
            mismatchDescription.appendText("[");
            while (offset < charSequence.length()) {
                int codepoint = Character.codePointAt(charSequence, offset);
                mismatchDescription.appendText(Integer.toHexString(codepoint));
                offset += Character.charCount(codepoint);
                if (offset < charSequence.length()) {
                    mismatchDescription.appendText(",");
                }
            }
            mismatchDescription.appendText("]");

            switch (mError) {
                case ERR_NO_SPANS:
                    mismatchDescription.appendText(" had no spans");
                    break;
                case ERR_SPANNABLE_NULL:
                    mismatchDescription.appendText(" was null");
                    break;
                case ERR_WRONG_INDEX:
                    mismatchDescription.appendText(" had Emoji at ")
                            .appendValue("[" + mActualStart + "," + mActualEnd + "]");
                    break;
                default:
                    mismatchDescription.appendText(" does not have an EmojiSpan with given "
                            + "resource id ");
            }
        }

        @Override
        protected boolean matchesSafely(final CharSequence charSequence) {
            if (charSequence == null) {
                mError = ERR_SPANNABLE_NULL;
                return false;
            }

            if (!(charSequence instanceof Spanned)) {
                mError = ERR_NO_SPANS;
                return false;
            }

            Spanned spanned = (Spanned) charSequence;
            final EmojiSpan[] spans = spanned.getSpans(0, charSequence.length(), EmojiSpan.class);

            if (spans.length == 0) {
                mError = ERR_NO_SPANS;
                return false;
            }

            if (mStart == -1 && mEnd == -1) {
                for (int index = 0; index < spans.length; index++) {
                    if (mResId == spans[index].getId()) {
                        return true;
                    }
                }
                return false;
            } else {
                for (int index = 0; index < spans.length; index++) {
                    if (mResId == -1 || mResId == spans[index].getId()) {
                        mActualStart = spanned.getSpanStart(spans[index]);
                        mActualEnd = spanned.getSpanEnd(spans[index]);
                        if (mActualStart == mStart && mActualEnd == mEnd) {
                            return true;
                        }
                    }
                }

                if (mActualStart != -1 && mActualEnd != -1) {
                    mError = ERR_WRONG_INDEX;
                }

                return false;
            }
        }
    }
}
