/*
 * Copyright (C) 2012 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.view;

import android.compat.annotation.UnsupportedAppUsage;
import android.content.res.Configuration;
import android.os.Build;

import java.text.BreakIterator;
import java.util.Locale;

/**
 * This class contains the implementation of text segment iterators
 * for accessibility support.
 *
 * Note: Such iterators are needed in the view package since we want
 * to be able to iterator over content description of any view.
 *
 * @hide
 */
public final class AccessibilityIterators {

    /**
     * @hide
     */
    public static interface TextSegmentIterator {
        public int[] following(int current);
        public int[] preceding(int current);
    }

    /**
     * @hide
     */
    public static abstract class AbstractTextSegmentIterator implements TextSegmentIterator {

        @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
        public AbstractTextSegmentIterator() {
        }

        @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
        protected String mText;

        private final int[] mSegment = new int[2];

        public void initialize(String text) {
            mText = text;
        }

        protected int[] getRange(int start, int end) {
            if (start < 0 || end < 0 || start ==  end) {
                return null;
            }
            mSegment[0] = start;
            mSegment[1] = end;
            return mSegment;
        }
    }

    static class CharacterTextSegmentIterator extends AbstractTextSegmentIterator
            implements ViewRootImpl.ConfigChangedCallback {
        private static CharacterTextSegmentIterator sInstance;

        private Locale mLocale;

        protected BreakIterator mImpl;

        public static CharacterTextSegmentIterator getInstance(Locale locale) {
            if (sInstance == null) {
                sInstance = new CharacterTextSegmentIterator(locale);
            }
            return sInstance;
        }

        private CharacterTextSegmentIterator(Locale locale) {
            mLocale = locale;
            onLocaleChanged(locale);
            ViewRootImpl.addConfigCallback(this);
        }

        @Override
        public void initialize(String text) {
            super.initialize(text);
            mImpl.setText(text);
        }

        @Override
        public int[] following(int offset) {
            final int textLegth = mText.length();
            if (textLegth <= 0) {
                return null;
            }
            if (offset >= textLegth) {
                return null;
            }
            int start = offset;
            if (start < 0) {
                start = 0;
            }
            while (!mImpl.isBoundary(start)) {
                start = mImpl.following(start);
                if (start == BreakIterator.DONE) {
                    return null;
                }
            }
            final int end = mImpl.following(start);
            if (end == BreakIterator.DONE) {
                return null;
            }
            return getRange(start, end);
        }

        @Override
        public int[] preceding(int offset) {
            final int textLegth = mText.length();
            if (textLegth <= 0) {
                return null;
            }
            if (offset <= 0) {
                return null;
            }
            int end = offset;
            if (end > textLegth) {
                end = textLegth;
            }
            while (!mImpl.isBoundary(end)) {
                end = mImpl.preceding(end);
                if (end == BreakIterator.DONE) {
                    return null;
                }
            }
            final int start = mImpl.preceding(end);
            if (start == BreakIterator.DONE) {
                return null;
            }
            return getRange(start, end);
        }

        @Override
        public void onConfigurationChanged(Configuration globalConfig) {
            final Locale locale = globalConfig.getLocales().get(0);
            if (locale == null) {
                return;
            }
            if (!mLocale.equals(locale)) {
                mLocale = locale;
                onLocaleChanged(locale);
            }
        }

        protected void onLocaleChanged(Locale locale) {
            mImpl = BreakIterator.getCharacterInstance(locale);
        }
    }

    static class WordTextSegmentIterator extends CharacterTextSegmentIterator {
        private static WordTextSegmentIterator sInstance;

        public static WordTextSegmentIterator getInstance(Locale locale) {
            if (sInstance == null) {
                sInstance = new WordTextSegmentIterator(locale);
            }
            return sInstance;
        }

        private WordTextSegmentIterator(Locale locale) {
           super(locale);
        }

        @Override
        protected void onLocaleChanged(Locale locale) {
            mImpl = BreakIterator.getWordInstance(locale);
        }

        @Override
        public int[] following(int offset) {
            final int textLegth = mText.length();
            if (textLegth <= 0) {
                return null;
            }
            if (offset >= mText.length()) {
                return null;
            }
            int start = offset;
            if (start < 0) {
                start = 0;
            }
            while (!isLetterOrDigit(start) && !isStartBoundary(start)) {
                start = mImpl.following(start);
                if (start == BreakIterator.DONE) {
                    return null;
                }
            }
            final int end = mImpl.following(start);
            if (end == BreakIterator.DONE || !isEndBoundary(end)) {
                return null;
            }
            return getRange(start, end);
        }

        @Override
        public int[] preceding(int offset) {
            final int textLegth = mText.length();
            if (textLegth <= 0) {
                return null;
            }
            if (offset <= 0) {
                return null;
            }
            int end = offset;
            if (end > textLegth) {
                end = textLegth;
            }
            while (end > 0 && !isLetterOrDigit(end - 1) && !isEndBoundary(end)) {
                end = mImpl.preceding(end);
                if (end == BreakIterator.DONE) {
                    return null;
                }
            }
            final int start = mImpl.preceding(end);
            if (start == BreakIterator.DONE || !isStartBoundary(start)) {
                return null;
            }
            return getRange(start, end);
        }

        private boolean isStartBoundary(int index) {
            return isLetterOrDigit(index)
                && (index == 0 || !isLetterOrDigit(index - 1));
        }

        private boolean isEndBoundary(int index) {
            return (index > 0 && isLetterOrDigit(index - 1))
                && (index == mText.length() || !isLetterOrDigit(index));
        }

        private boolean isLetterOrDigit(int index) {
            if (index >= 0 && index < mText.length()) {
                final int codePoint = mText.codePointAt(index);
                return Character.isLetterOrDigit(codePoint);
            }
            return false;
        }
    }

    static class ParagraphTextSegmentIterator extends AbstractTextSegmentIterator {
        private static ParagraphTextSegmentIterator sInstance;

        public static ParagraphTextSegmentIterator getInstance() {
            if (sInstance == null) {
                sInstance = new ParagraphTextSegmentIterator();
            }
            return sInstance;
        }

        @Override
        public int[] following(int offset) {
            final int textLength = mText.length();
            if (textLength <= 0) {
                return null;
            }
            if (offset >= textLength) {
                return null;
            }
            int start = offset;
            if (start < 0) {
                start = 0;
            }
            while (start < textLength && mText.charAt(start) == '\n'
                    && !isStartBoundary(start)) {
                start++;
            }
            if (start >= textLength) {
                return null;
            }
            int end = start + 1;
            while (end < textLength && !isEndBoundary(end)) {
                end++;
            }
            return getRange(start, end);
        }

        @Override
        public int[] preceding(int offset) {
            final int textLength = mText.length();
            if (textLength <= 0) {
                return null;
            }
            if (offset <= 0) {
                return null;
            }
            int end = offset;
            if (end > textLength) {
                end = textLength;
            }
            while(end > 0 && mText.charAt(end - 1) == '\n' && !isEndBoundary(end)) {
                end--;
            }
            if (end <= 0) {
                return null;
            }
            int start = end - 1;
            while (start > 0 && !isStartBoundary(start)) {
                start--;
            }
            return getRange(start, end);
        }

        private boolean isStartBoundary(int index) {
            return (mText.charAt(index) != '\n'
                && (index == 0 || mText.charAt(index - 1) == '\n'));
        }

        private boolean isEndBoundary(int index) {
            return (index > 0 && mText.charAt(index - 1) != '\n'
                && (index == mText.length() || mText.charAt(index) == '\n'));
        }
    }
}
