/*
 * 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 android.view;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.graphics.Rect;

import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.Preconditions;

import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.function.Function;

/**
 * Abstract class to track a collection of rects reported by the views under the same
 * {@link ViewRootImpl}.
 * @hide
 */
@VisibleForTesting
public class ViewRootRectTracker {
    private final Function<View, List<Rect>> mRectCollector;
    private boolean mViewsChanged = false;
    private boolean mRootRectsChanged = false;
    private List<Rect> mRootRects = Collections.emptyList();
    private List<ViewInfo> mViewInfos = new ArrayList<>();
    private List<Rect> mRects = Collections.emptyList();

    // Keeps track of whether updateRectsForView has been called but there was no subsequent call
    // on computeChanges yet. Since updateRectsForView is called when sending a message and
    // computeChanges when it is received, this tracks whether such message is in the queue already
    private boolean mWaitingForComputeChanges = false;

    /**
     * @param rectCollector given a view returns a list of the rects of interest for this
     *                      ViewRootRectTracker
     * @hide
     */
    @VisibleForTesting
    public ViewRootRectTracker(Function<View, List<Rect>> rectCollector) {
        mRectCollector = rectCollector;
    }

    public void updateRectsForView(@NonNull View view) {
        boolean found = false;
        final Iterator<ViewInfo> i = mViewInfos.iterator();
        while (i.hasNext()) {
            final ViewInfo info = i.next();
            final View v = info.getView();
            if (v == null || !v.isAttachedToWindow() || !v.isAggregatedVisible()) {
                mViewsChanged = true;
                i.remove();
                continue;
            }
            if (v == view) {
                found = true;
                info.mDirty = true;
                break;
            }
        }
        if (!found && view.isAttachedToWindow()) {
            mViewInfos.add(new ViewInfo(view));
            mViewsChanged = true;
        }
        mWaitingForComputeChanges = true;
    }

    /**
     * @return all Rects from all visible Views in the global (root) coordinate system,
     * or {@code null} if Rects are unchanged since the last call to this method.
     */
    @Nullable
    public List<Rect> computeChangedRects() {
        if (computeChanges()) {
            return mRects;
        }
        return null;
    }

    /**
     * Computes changes to all Rects from all Views.
     * After calling this method, the updated list of Rects can be retrieved
     * with {@link #getLastComputedRects()}.
     *
     * @return {@code true} if there were changes, {@code false} otherwise.
     */
    public boolean computeChanges() {
        mWaitingForComputeChanges = false;
        boolean changed = mRootRectsChanged;
        final Iterator<ViewInfo> i = mViewInfos.iterator();
        final List<Rect> rects = new ArrayList<>(mRootRects);
        while (i.hasNext()) {
            final ViewInfo info = i.next();
            switch (info.update()) {
                case ViewInfo.CHANGED:
                    changed = true;
                    // Deliberate fall-through
                case ViewInfo.UNCHANGED:
                    rects.addAll(info.mRects);
                    break;
                case ViewInfo.GONE:
                    mViewsChanged = true;
                    i.remove();
                    break;
            }
        }
        if (changed || mViewsChanged) {
            mViewsChanged = false;
            mRootRectsChanged = false;
            if (!mRects.equals(rects)) {
                mRects = rects;
                return true;
            }
        }
        return false;
    }

    public boolean isWaitingForComputeChanges() {
        return mWaitingForComputeChanges;
    }

    /**
     * Returns a List of all Rects from all visible Views in the global (root) coordinate system.
     * This list is only updated when calling {@link #computeChanges()} or
     * {@link #computeChangedRects()}.
     *
     * @return all Rects from all visible Views in the global (root) coordinate system
     */
    @NonNull
    public List<Rect> getLastComputedRects() {
        return mRects;
    }

    /**
     * Sets rects defined in the global (root) coordinate system, i.e. not for a specific view.
     */
    public void setRootRects(@NonNull List<Rect> rects) {
        Preconditions.checkNotNull(rects, "rects must not be null");
        mRootRects = rects;
        mRootRectsChanged = true;
        mWaitingForComputeChanges = true;
    }

    @NonNull
    public List<Rect> getRootRects() {
        return mRootRects;
    }

    @NonNull
    private List<Rect> getTrackedRectsForView(@NonNull View v) {
        final List<Rect> rects = mRectCollector.apply(v);
        return rects == null ? Collections.emptyList() : rects;
    }

    private class ViewInfo {
        public static final int CHANGED = 0;
        public static final int UNCHANGED = 1;
        public static final int GONE = 2;

        private final WeakReference<View> mView;
        boolean mDirty = true;
        List<Rect> mRects = Collections.emptyList();

        ViewInfo(View view) {
            mView = new WeakReference<>(view);
        }

        public View getView() {
            return mView.get();
        }

        public int update() {
            final View view = getView();
            if (view == null || !view.isAttachedToWindow()
                    || !view.isAggregatedVisible()) return GONE;
            final List<Rect> localRects = getTrackedRectsForView(view);
            final List<Rect> newRects = new ArrayList<>(localRects.size());
            for (Rect src : localRects) {
                Rect mappedRect = new Rect(src);
                ViewParent p = view.getParent();
                if (p != null && p.getChildVisibleRect(view, mappedRect, null)) {
                    newRects.add(mappedRect);
                }
            }

            if (mRects.equals(localRects)) return UNCHANGED;
            mRects = newRects;
            return CHANGED;
        }
    }
}
