/*
 * Copyright (C) 2006 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 android.text.method;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.icu.text.DecimalFormatSymbols;
import android.text.Editable;
import android.text.InputFilter;
import android.text.Selection;
import android.text.Spannable;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.format.DateFormat;
import android.view.KeyEvent;
import android.view.View;

import java.util.Collection;
import java.util.Locale;

/**
 * For numeric text entry
 * <p></p>
 * As for all implementations of {@link KeyListener}, this class is only concerned
 * with hardware keyboards.  Software input methods have no obligation to trigger
 * the methods in this class.
 */
public abstract class NumberKeyListener extends BaseKeyListener
    implements InputFilter
{
    /**
     * You can say which characters you can accept.
     */
    @NonNull
    protected abstract char[] getAcceptedChars();

    protected int lookup(KeyEvent event, Spannable content) {
        return event.getMatch(getAcceptedChars(), getMetaState(content, event));
    }

    public CharSequence filter(CharSequence source, int start, int end,
                               Spanned dest, int dstart, int dend) {
        char[] accept = getAcceptedChars();
        boolean filter = false;

        int i;
        for (i = start; i < end; i++) {
            if (!ok(accept, source.charAt(i))) {
                break;
            }
        }

        if (i == end) {
            // It was all OK.
            return null;
        }

        if (end - start == 1) {
            // It was not OK, and there is only one char, so nothing remains.
            return "";
        }

        SpannableStringBuilder filtered =
            new SpannableStringBuilder(source, start, end);
        i -= start;
        end -= start;

        int len = end - start;
        // Only count down to i because the chars before that were all OK.
        for (int j = end - 1; j >= i; j--) {
            if (!ok(accept, source.charAt(j))) {
                filtered.delete(j, j + 1);
            }
        }

        return filtered;
    }

    protected static boolean ok(char[] accept, char c) {
        for (int i = accept.length - 1; i >= 0; i--) {
            if (accept[i] == c) {
                return true;
            }
        }

        return false;
    }

    @Override
    public boolean onKeyDown(View view, Editable content,
                             int keyCode, KeyEvent event) {
        int selStart, selEnd;

        {
            int a = Selection.getSelectionStart(content);
            int b = Selection.getSelectionEnd(content);

            selStart = Math.min(a, b);
            selEnd = Math.max(a, b);
        }

        if (selStart < 0 || selEnd < 0) {
            selStart = selEnd = 0;
            Selection.setSelection(content, 0);
        }

        int i = event != null ? lookup(event, content) : 0;
        int repeatCount = event != null ? event.getRepeatCount() : 0;
        if (repeatCount == 0) {
            if (i != 0) {
                if (selStart != selEnd) {
                    Selection.setSelection(content, selEnd);
                }

                content.replace(selStart, selEnd, String.valueOf((char) i));

                adjustMetaAfterKeypress(content);
                return true;
            }
        } else if (i == '0' && repeatCount == 1) {
            // Pretty hackish, it replaces the 0 with the +

            if (selStart == selEnd && selEnd > 0 &&
                    content.charAt(selStart - 1) == '0') {
                content.replace(selStart - 1, selEnd, String.valueOf('+'));
                adjustMetaAfterKeypress(content);
                return true;
            }
        }

        adjustMetaAfterKeypress(content);
        return super.onKeyDown(view, content, keyCode, event);
    }

    /* package */
    @Nullable
    static boolean addDigits(@NonNull Collection<Character> collection, @Nullable Locale locale) {
        if (locale == null) {
            return false;
        }
        final String[] digits = DecimalFormatSymbols.getInstance(locale).getDigitStrings();
        for (int i = 0; i < 10; i++) {
            if (digits[i].length() > 1) { // multi-codeunit digits. Not supported.
                return false;
            }
            collection.add(Character.valueOf(digits[i].charAt(0)));
        }
        return true;
    }

    // From http://unicode.org/reports/tr35/tr35-dates.html#Date_Format_Patterns
    private static final String DATE_TIME_FORMAT_SYMBOLS =
            "GyYuUrQqMLlwWdDFgEecabBhHKkjJCmsSAzZOvVXx";
    private static final char SINGLE_QUOTE = '\'';

    /* package */
    static boolean addFormatCharsFromSkeleton(
            @NonNull Collection<Character> collection, @Nullable Locale locale,
            @NonNull String skeleton, @NonNull String symbolsToIgnore) {
        if (locale == null) {
            return false;
        }
        final String pattern = DateFormat.getBestDateTimePattern(locale, skeleton);
        boolean outsideQuotes = true;
        for (int i = 0; i < pattern.length(); i++) {
            final char ch = pattern.charAt(i);
            if (Character.isSurrogate(ch)) { // characters outside BMP are not supported.
                return false;
            } else if (ch == SINGLE_QUOTE) {
                outsideQuotes = !outsideQuotes;
                // Single quote characters should be considered if and only if they follow
                // another single quote.
                if (i == 0 || pattern.charAt(i - 1) != SINGLE_QUOTE) {
                    continue;
                }
            }

            if (outsideQuotes) {
                if (symbolsToIgnore.indexOf(ch) != -1) {
                    // Skip expected pattern characters.
                    continue;
                } else if (DATE_TIME_FORMAT_SYMBOLS.indexOf(ch) != -1) {
                    // An unexpected symbols is seen. We've failed.
                    return false;
                }
            }
            // If we are here, we are either inside quotes, or we have seen a non-pattern
            // character outside quotes. So ch is a valid character in a date.
            collection.add(Character.valueOf(ch));
        }
        return true;
    }

    /* package */
    static boolean addFormatCharsFromSkeletons(
            @NonNull Collection<Character> collection, @Nullable Locale locale,
            @NonNull String[] skeletons, @NonNull String symbolsToIgnore) {
        for (int i = 0; i < skeletons.length; i++) {
            final boolean success = addFormatCharsFromSkeleton(
                    collection, locale, skeletons[i], symbolsToIgnore);
            if (!success) {
                return false;
            }
        }
        return true;
    }


    /* package */
    static boolean addAmPmChars(@NonNull Collection<Character> collection,
                                @Nullable Locale locale) {
        if (locale == null) {
            return false;
        }
        final String[] amPm = DateFormat.getIcuDateFormatSymbols(locale).getAmPmStrings();
        for (int i = 0; i < amPm.length; i++) {
            for (int j = 0; j < amPm[i].length(); j++) {
                final char ch = amPm[i].charAt(j);
                if (Character.isBmpCodePoint(ch)) {
                    collection.add(Character.valueOf(ch));
                } else {  // We don't support non-BMP characters.
                    return false;
                }
            }
        }
        return true;
    }

    /* package */
    @NonNull
    static char[] collectionToArray(@NonNull Collection<Character> chars) {
        final char[] result = new char[chars.size()];
        int i = 0;
        for (Character ch : chars) {
            result[i++] = ch;
        }
        return result;
    }
}
