/*
 * Copyright (C) 2014 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.android.inputmethod.latin.utils;

import android.annotation.TargetApi;
import android.graphics.Matrix;
import android.graphics.Rect;
import android.inputmethodservice.ExtractEditText;
import android.inputmethodservice.InputMethodService;
import android.os.Build;
import android.text.Layout;
import android.text.Spannable;
import android.text.Spanned;
import android.view.View;
import android.view.ViewParent;
import android.view.inputmethod.CursorAnchorInfo;
import android.widget.TextView;

import com.android.inputmethod.compat.BuildCompatUtils;
import com.android.inputmethod.compat.CursorAnchorInfoCompatWrapper;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;

/**
 * This class allows input methods to extract {@link CursorAnchorInfo} directly from the given
 * {@link TextView}. This is useful and even necessary to support full-screen mode where the default
 * {@link InputMethodService#onUpdateCursorAnchorInfo(CursorAnchorInfo)} event callback must be
 * ignored because it reports the character locations of the target application rather than
 * characters on {@link ExtractEditText}.
 */
public final class CursorAnchorInfoUtils {
    private CursorAnchorInfoUtils() {
        // This helper class is not instantiable.
    }

    private static boolean isPositionVisible(final View view, final float positionX,
            final float positionY) {
        final float[] position = new float[] { positionX, positionY };
        View currentView = view;

        while (currentView != null) {
            if (currentView != view) {
                // Local scroll is already taken into account in positionX/Y
                position[0] -= currentView.getScrollX();
                position[1] -= currentView.getScrollY();
            }

            if (position[0] < 0 || position[1] < 0 ||
                    position[0] > currentView.getWidth() || position[1] > currentView.getHeight()) {
                return false;
            }

            if (!currentView.getMatrix().isIdentity()) {
                currentView.getMatrix().mapPoints(position);
            }

            position[0] += currentView.getLeft();
            position[1] += currentView.getTop();

            final ViewParent parent = currentView.getParent();
            if (parent instanceof View) {
                currentView = (View) parent;
            } else {
                // We've reached the ViewRoot, stop iterating
                currentView = null;
            }
        }

        // We've been able to walk up the view hierarchy and the position was never clipped
        return true;
    }

    /**
     * Extracts {@link CursorAnchorInfoCompatWrapper} from the given {@link TextView}.
     * @param textView the target text view from which {@link CursorAnchorInfoCompatWrapper} is to
     * be extracted.
     * @return the {@link CursorAnchorInfoCompatWrapper} object based on the current layout.
     * {@code null} if {@code Build.VERSION.SDK_INT} is 20 or prior or {@link TextView} is not
     * ready to provide layout information.
     */
    @Nullable
    public static CursorAnchorInfoCompatWrapper extractFromTextView(
            @Nonnull final TextView textView) {
        if (BuildCompatUtils.EFFECTIVE_SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
            return null;
        }
        return CursorAnchorInfoCompatWrapper.wrap(extractFromTextViewInternal(textView));
    }

    /**
     * Returns {@link CursorAnchorInfo} from the given {@link TextView}.
     * @param textView the target text view from which {@link CursorAnchorInfo} is to be extracted.
     * @return the {@link CursorAnchorInfo} object based on the current layout. {@code null} if it
     * is not feasible.
     */
    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    @Nullable
    private static CursorAnchorInfo extractFromTextViewInternal(@Nonnull final TextView textView) {
        final Layout layout = textView.getLayout();
        if (layout == null) {
            return null;
        }

        final CursorAnchorInfo.Builder builder = new CursorAnchorInfo.Builder();

        final int selectionStart = textView.getSelectionStart();
        builder.setSelectionRange(selectionStart, textView.getSelectionEnd());

        // Construct transformation matrix from view local coordinates to screen coordinates.
        final Matrix viewToScreenMatrix = new Matrix(textView.getMatrix());
        final int[] viewOriginInScreen = new int[2];
        textView.getLocationOnScreen(viewOriginInScreen);
        viewToScreenMatrix.postTranslate(viewOriginInScreen[0], viewOriginInScreen[1]);
        builder.setMatrix(viewToScreenMatrix);

        if (layout.getLineCount() == 0) {
            return null;
        }
        final Rect lineBoundsWithoutOffset = new Rect();
        final Rect lineBoundsWithOffset = new Rect();
        layout.getLineBounds(0, lineBoundsWithoutOffset);
        textView.getLineBounds(0, lineBoundsWithOffset);
        final float viewportToContentHorizontalOffset = lineBoundsWithOffset.left
                - lineBoundsWithoutOffset.left - textView.getScrollX();
        final float viewportToContentVerticalOffset = lineBoundsWithOffset.top
                - lineBoundsWithoutOffset.top - textView.getScrollY();

        final CharSequence text = textView.getText();
        if (text instanceof Spannable) {
            // Here we assume that the composing text is marked as SPAN_COMPOSING flag. This is not
            // necessarily true, but basically works.
            int composingTextStart = text.length();
            int composingTextEnd = 0;
            final Spannable spannable = (Spannable) text;
            final Object[] spans = spannable.getSpans(0, text.length(), Object.class);
            for (Object span : spans) {
                final int spanFlag = spannable.getSpanFlags(span);
                if ((spanFlag & Spanned.SPAN_COMPOSING) != 0) {
                    composingTextStart = Math.min(composingTextStart,
                            spannable.getSpanStart(span));
                    composingTextEnd = Math.max(composingTextEnd, spannable.getSpanEnd(span));
                }
            }

            final boolean hasComposingText =
                    (0 <= composingTextStart) && (composingTextStart < composingTextEnd);
            if (hasComposingText) {
                final CharSequence composingText = text.subSequence(composingTextStart,
                        composingTextEnd);
                builder.setComposingText(composingTextStart, composingText);

                final int minLine = layout.getLineForOffset(composingTextStart);
                final int maxLine = layout.getLineForOffset(composingTextEnd - 1);
                for (int line = minLine; line <= maxLine; ++line) {
                    final int lineStart = layout.getLineStart(line);
                    final int lineEnd = layout.getLineEnd(line);
                    final int offsetStart = Math.max(lineStart, composingTextStart);
                    final int offsetEnd = Math.min(lineEnd, composingTextEnd);
                    final boolean ltrLine =
                            layout.getParagraphDirection(line) == Layout.DIR_LEFT_TO_RIGHT;
                    final float[] widths = new float[offsetEnd - offsetStart];
                    layout.getPaint().getTextWidths(text, offsetStart, offsetEnd, widths);
                    final float top = layout.getLineTop(line);
                    final float bottom = layout.getLineBottom(line);
                    for (int offset = offsetStart; offset < offsetEnd; ++offset) {
                        final float charWidth = widths[offset - offsetStart];
                        final boolean isRtl = layout.isRtlCharAt(offset);
                        final float primary = layout.getPrimaryHorizontal(offset);
                        final float secondary = layout.getSecondaryHorizontal(offset);
                        // TODO: This doesn't work perfectly for text with custom styles and TAB
                        // chars.
                        final float left;
                        final float right;
                        if (ltrLine) {
                            if (isRtl) {
                                left = secondary - charWidth;
                                right = secondary;
                            } else {
                                left = primary;
                                right = primary + charWidth;
                            }
                        } else {
                            if (!isRtl) {
                                left = secondary;
                                right = secondary + charWidth;
                            } else {
                                left = primary - charWidth;
                                right = primary;
                            }
                        }
                        // TODO: Check top-right and bottom-left as well.
                        final float localLeft = left + viewportToContentHorizontalOffset;
                        final float localRight = right + viewportToContentHorizontalOffset;
                        final float localTop = top + viewportToContentVerticalOffset;
                        final float localBottom = bottom + viewportToContentVerticalOffset;
                        final boolean isTopLeftVisible = isPositionVisible(textView,
                                localLeft, localTop);
                        final boolean isBottomRightVisible =
                                isPositionVisible(textView, localRight, localBottom);
                        int characterBoundsFlags = 0;
                        if (isTopLeftVisible || isBottomRightVisible) {
                            characterBoundsFlags |= CursorAnchorInfo.FLAG_HAS_VISIBLE_REGION;
                        }
                        if (!isTopLeftVisible || !isBottomRightVisible) {
                            characterBoundsFlags |= CursorAnchorInfo.FLAG_HAS_INVISIBLE_REGION;
                        }
                        if (isRtl) {
                            characterBoundsFlags |= CursorAnchorInfo.FLAG_IS_RTL;
                        }
                        // Here offset is the index in Java chars.
                        builder.addCharacterBounds(offset, localLeft, localTop, localRight,
                                localBottom, characterBoundsFlags);
                    }
                }
            }
        }

        // Treat selectionStart as the insertion point.
        if (0 <= selectionStart) {
            final int offset = selectionStart;
            final int line = layout.getLineForOffset(offset);
            final float insertionMarkerX = layout.getPrimaryHorizontal(offset)
                    + viewportToContentHorizontalOffset;
            final float insertionMarkerTop = layout.getLineTop(line)
                    + viewportToContentVerticalOffset;
            final float insertionMarkerBaseline = layout.getLineBaseline(line)
                    + viewportToContentVerticalOffset;
            final float insertionMarkerBottom = layout.getLineBottom(line)
                    + viewportToContentVerticalOffset;
            final boolean isTopVisible =
                    isPositionVisible(textView, insertionMarkerX, insertionMarkerTop);
            final boolean isBottomVisible =
                    isPositionVisible(textView, insertionMarkerX, insertionMarkerBottom);
            int insertionMarkerFlags = 0;
            if (isTopVisible || isBottomVisible) {
                insertionMarkerFlags |= CursorAnchorInfo.FLAG_HAS_VISIBLE_REGION;
            }
            if (!isTopVisible || !isBottomVisible) {
                insertionMarkerFlags |= CursorAnchorInfo.FLAG_HAS_INVISIBLE_REGION;
            }
            if (layout.isRtlCharAt(offset)) {
                insertionMarkerFlags |= CursorAnchorInfo.FLAG_IS_RTL;
            }
            builder.setInsertionMarkerLocation(insertionMarkerX, insertionMarkerTop,
                    insertionMarkerBaseline, insertionMarkerBottom, insertionMarkerFlags);
        }
        return builder.build();
    }
}
