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

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.os.AsyncTask;
import android.os.Parcel;
import android.support.test.InstrumentationRegistry;
import android.support.test.filters.SmallTest;
import android.support.test.runner.AndroidJUnit4;
import android.view.View;
import android.view.ViewGroup;

import com.android.frameworks.coretests.R;

import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.runner.RunWith;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.concurrent.CountDownLatch;

/**
 * Tests for RemoteViews.
 */
@RunWith(AndroidJUnit4.class)
@SmallTest
public class RemoteViewsTest {

    // This can point to any other package which exists on the device.
    private static final String OTHER_PACKAGE = "com.android.systemui";

    @Rule
    public final ExpectedException exception = ExpectedException.none();

    private Context mContext;
    private String mPackage;
    private LinearLayout mContainer;

    @Before
    public void setup() {
        mContext = InstrumentationRegistry.getContext();
        mPackage = mContext.getPackageName();
        mContainer = new LinearLayout(mContext);
    }

    @Test
    public void clone_doesNotCopyBitmap() {
        RemoteViews original = new RemoteViews(mPackage, R.layout.remote_views_test);
        Bitmap bitmap = Bitmap.createBitmap(10, 10, Bitmap.Config.ARGB_8888);

        original.setImageViewBitmap(R.id.image, bitmap);
        RemoteViews clone = original.clone();
        View inflated = clone.apply(mContext, mContainer);

        Drawable drawable = ((ImageView) inflated.findViewById(R.id.image)).getDrawable();
        assertSame(bitmap, ((BitmapDrawable)drawable).getBitmap());
    }

    @Test
    public void clone_originalCanStillBeApplied() {
        RemoteViews original = new RemoteViews(mPackage, R.layout.remote_views_test);

        RemoteViews clone = original.clone();

        clone.apply(mContext, mContainer);
    }

    @Test
    public void clone_clones() {
        RemoteViews original = new RemoteViews(mPackage, R.layout.remote_views_test);

        RemoteViews clone = original.clone();
        original.setTextViewText(R.id.text, "test");
        View inflated = clone.apply(mContext, mContainer);

        TextView textView = (TextView) inflated.findViewById(R.id.text);
        assertEquals("", textView.getText());
    }

    @Test
    public void clone_child_fails() {
        RemoteViews original = new RemoteViews(mPackage, R.layout.remote_views_test);
        RemoteViews child = new RemoteViews(mPackage, R.layout.remote_views_test);

        original.addView(R.id.layout, child);

        exception.expect(IllegalStateException.class);
        RemoteViews clone = child.clone();
    }

    @Test
    public void clone_repeatedly() {
        RemoteViews original = new RemoteViews(mPackage, R.layout.remote_views_test);

        original.clone();
        original.clone();

        original.apply(mContext, mContainer);
    }

    @Test
    public void clone_chained() {
        RemoteViews original = new RemoteViews(mPackage, R.layout.remote_views_test);

        RemoteViews clone = original.clone().clone();

        clone.apply(mContext, mContainer);
    }

    @Test
    public void parcelSize_nestedViews() {
        RemoteViews original = new RemoteViews(mPackage, R.layout.remote_views_test);
        // We don't care about the actual layout id.
        RemoteViews child = new RemoteViews(mPackage, 33);
        int expectedSize = getParcelSize(original) + getParcelSize(child);
        original.addView(R.id.layout, child);

        // The application info will get written only once.
        assertTrue(getParcelSize(original) < expectedSize);
        assertEquals(getParcelSize(original), getParcelSize(original.clone()));

        original = new RemoteViews(mPackage, R.layout.remote_views_test);
        child = new RemoteViews(OTHER_PACKAGE, 33);
        expectedSize = getParcelSize(original) + getParcelSize(child);
        original.addView(R.id.layout, child);

        // Both the views will get written completely along with an additional view operation
        assertTrue(getParcelSize(original) > expectedSize);
        assertEquals(getParcelSize(original), getParcelSize(original.clone()));
    }

    @Test
    public void parcelSize_differentOrientation() {
        RemoteViews landscape = new RemoteViews(mPackage, R.layout.remote_views_test);
        RemoteViews portrait = new RemoteViews(mPackage, 33);

        // The application info will get written only once.
        RemoteViews views = new RemoteViews(landscape, portrait);
        assertTrue(getParcelSize(views) < (getParcelSize(landscape) + getParcelSize(portrait)));
        assertEquals(getParcelSize(views), getParcelSize(views.clone()));
    }

    private int getParcelSize(RemoteViews view) {
        Parcel parcel = Parcel.obtain();
        view.writeToParcel(parcel, 0);
        int size = parcel.dataSize();
        parcel.recycle();
        return size;
    }

    @Test
    public void asyncApply_fail() throws Exception {
        RemoteViews views = new RemoteViews(mPackage, R.layout.remote_view_test_bad_1);
        ViewAppliedListener listener = new ViewAppliedListener();
        views.applyAsync(mContext, mContainer, AsyncTask.THREAD_POOL_EXECUTOR, listener);

        exception.expect(Exception.class);
        listener.waitAndGetView();
    }

    @Test
    public void asyncApply() throws Exception {
        RemoteViews views = new RemoteViews(mPackage, R.layout.remote_views_test);
        views.setTextViewText(R.id.text, "Dummy");

        View syncView = views.apply(mContext, mContainer);

        ViewAppliedListener listener = new ViewAppliedListener();
        views.applyAsync(mContext, mContainer, AsyncTask.THREAD_POOL_EXECUTOR, listener);
        View asyncView = listener.waitAndGetView();

        verifyViewTree(syncView, asyncView, "Dummy");
    }

    @Test
    public void asyncApply_viewStub() throws Exception {
        RemoteViews views = new RemoteViews(mPackage, R.layout.remote_views_viewstub);
        views.setInt(R.id.viewStub, "setLayoutResource", R.layout.remote_views_text);
        // This will cause the view to be inflated
        views.setViewVisibility(R.id.viewStub, View.INVISIBLE);
        views.setTextViewText(R.id.stub_inflated, "Dummy");

        View syncView = views.apply(mContext, mContainer);

        ViewAppliedListener listener = new ViewAppliedListener();
        views.applyAsync(mContext, mContainer, AsyncTask.THREAD_POOL_EXECUTOR, listener);
        View asyncView = listener.waitAndGetView();

        verifyViewTree(syncView, asyncView, "Dummy");
    }

    @Test
    public void asyncApply_nestedViews() throws Exception {
        RemoteViews views = new RemoteViews(mPackage, R.layout.remote_view_host);
        views.removeAllViews(R.id.container);
        views.addView(R.id.container, createViewChained(1, "row1-c1", "row1-c2", "row1-c3"));
        views.addView(R.id.container, createViewChained(5, "row2-c1", "row2-c2"));
        views.addView(R.id.container, createViewChained(2, "row3-c1", "row3-c2"));

        View syncView = views.apply(mContext, mContainer);

        ViewAppliedListener listener = new ViewAppliedListener();
        views.applyAsync(mContext, mContainer, AsyncTask.THREAD_POOL_EXECUTOR, listener);
        View asyncView = listener.waitAndGetView();

        verifyViewTree(syncView, asyncView,
                "row1-c1", "row1-c2", "row1-c3", "row2-c1", "row2-c2", "row3-c1", "row3-c2");
    }

    @Test
    public void asyncApply_viewstub_nestedViews() throws Exception {
        RemoteViews viewstub = new RemoteViews(mPackage, R.layout.remote_views_viewstub);
        viewstub.setInt(R.id.viewStub, "setLayoutResource", R.layout.remote_view_host);
        // This will cause the view to be inflated
        viewstub.setViewVisibility(R.id.viewStub, View.INVISIBLE);
        viewstub.addView(R.id.stub_inflated, createViewChained(1, "row1-c1", "row1-c2", "row1-c3"));

        RemoteViews views = new RemoteViews(mPackage, R.layout.remote_view_host);
        views.removeAllViews(R.id.container);
        views.addView(R.id.container, viewstub);
        views.addView(R.id.container, createViewChained(5, "row2-c1", "row2-c2"));

        View syncView = views.apply(mContext, mContainer);

        ViewAppliedListener listener = new ViewAppliedListener();
        views.applyAsync(mContext, mContainer, AsyncTask.THREAD_POOL_EXECUTOR, listener);
        View asyncView = listener.waitAndGetView();

        verifyViewTree(syncView, asyncView, "row1-c1", "row1-c2", "row1-c3", "row2-c1", "row2-c2");
    }

    private RemoteViews createViewChained(int depth, String... texts) {
        RemoteViews result = new RemoteViews(mPackage, R.layout.remote_view_host);

        // Create depth
        RemoteViews parent = result;
        while(depth > 0) {
            depth--;
            RemoteViews child = new RemoteViews(mPackage, R.layout.remote_view_host);
            parent.addView(R.id.container, child);
            parent = child;
        }

        // Add texts
        for (String text : texts) {
            RemoteViews child = new RemoteViews(mPackage, R.layout.remote_views_text);
            child.setTextViewText(R.id.text, text);
            parent.addView(R.id.container, child);
        }
        return result;
    }

    private void verifyViewTree(View v1, View v2, String... texts) {
        ArrayList<String> expectedTexts = new ArrayList<>(Arrays.asList(texts));
        verifyViewTreeRecur(v1, v2, expectedTexts);
        // Verify that all expected texts were found
        assertEquals(0, expectedTexts.size());
    }

    private void verifyViewTreeRecur(View v1, View v2, ArrayList<String> expectedTexts) {
        assertEquals(v1.getClass(), v2.getClass());

        if (v1 instanceof TextView) {
            String text = ((TextView) v1).getText().toString();
            assertEquals(text, ((TextView) v2).getText().toString());
            // Verify that the text was one of the expected texts and remove it from the list
            assertTrue(expectedTexts.remove(text));
        } else if (v1 instanceof ViewGroup) {
            ViewGroup vg1 = (ViewGroup) v1;
            ViewGroup vg2 = (ViewGroup) v2;
            assertEquals(vg1.getChildCount(), vg2.getChildCount());
            for (int i = vg1.getChildCount() - 1; i >= 0; i--) {
                verifyViewTreeRecur(vg1.getChildAt(i), vg2.getChildAt(i), expectedTexts);
            }
        }
    }

    private class ViewAppliedListener implements RemoteViews.OnViewAppliedListener {

        private final CountDownLatch mLatch = new CountDownLatch(1);
        private View mView;
        private Exception mError;

        @Override
        public void onViewApplied(View v) {
            mView = v;
            mLatch.countDown();
        }

        @Override
        public void onError(Exception e) {
            mError = e;
            mLatch.countDown();
        }

        public View waitAndGetView() throws Exception {
            mLatch.await();

            if (mError != null) {
                throw new Exception(mError);
            }
            return mView;
        }
    }

    @Test
    public void nestedAddViews() {
        RemoteViews views = new RemoteViews(mPackage, R.layout.remote_views_test);
        for (int i = 0; i < 10; i++) {
            RemoteViews parent = new RemoteViews(mPackage, R.layout.remote_views_test);
            parent.addView(R.id.layout, views);
            views = parent;
        }
        views.clone();

        views = new RemoteViews(mPackage, R.layout.remote_views_test);
        for (int i = 0; i < 11; i++) {
            RemoteViews parent = new RemoteViews(mPackage, R.layout.remote_views_test);
            parent.addView(R.id.layout, views);
            views = parent;
        }
        exception.expect(IllegalArgumentException.class);
        views.clone();
    }

    @Test
    public void nestedLandscapeViews() {
        RemoteViews views = new RemoteViews(mPackage, R.layout.remote_views_test);
        for (int i = 0; i < 10; i++) {
            views = new RemoteViews(views,
                    new RemoteViews(mPackage, R.layout.remote_views_test));
        }
        views.clone();

        views = new RemoteViews(mPackage, R.layout.remote_views_test);
        for (int i = 0; i < 11; i++) {
            RemoteViews parent = new RemoteViews(mPackage, R.layout.remote_views_test);
            parent.addView(R.id.layout, views);
            views = parent;
        }
        exception.expect(IllegalArgumentException.class);
        views.clone();
    }
}
