/*
 * Copyright (C) 2023 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.systemui.screenshot.appclips;

import static com.android.systemui.screenshot.appclips.AppClipsEvent.SCREENSHOT_FOR_NOTE_ACCEPTED;
import static com.android.systemui.screenshot.appclips.AppClipsEvent.SCREENSHOT_FOR_NOTE_CANCELLED;
import static com.android.systemui.screenshot.appclips.AppClipsTrampolineActivity.ACTION_FINISH_FROM_TRAMPOLINE;
import static com.android.systemui.screenshot.appclips.AppClipsTrampolineActivity.EXTRA_CALLING_PACKAGE_NAME;
import static com.android.systemui.screenshot.appclips.AppClipsTrampolineActivity.EXTRA_RESULT_RECEIVER;
import static com.android.systemui.screenshot.appclips.AppClipsTrampolineActivity.EXTRA_SCREENSHOT_URI;
import static com.android.systemui.screenshot.appclips.AppClipsTrampolineActivity.PERMISSION_SELF;

import android.app.Activity;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.ApplicationInfoFlags;
import android.content.pm.PackageManager.NameNotFoundException;
import android.graphics.Bitmap;
import android.graphics.Rect;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Bundle;
import android.os.ResultReceiver;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.ImageView;

import androidx.activity.ComponentActivity;
import androidx.annotation.Nullable;
import androidx.lifecycle.ViewModelProvider;

import com.android.internal.logging.UiEventLogger;
import com.android.internal.logging.UiEventLogger.UiEventEnum;
import com.android.settingslib.Utils;
import com.android.systemui.res.R;
import com.android.systemui.screenshot.CropView;
import com.android.systemui.settings.UserTracker;

import javax.inject.Inject;

/**
 * An {@link Activity} to take a screenshot for the App Clips flow and presenting a screenshot
 * editing tool.
 *
 * <p>An App Clips flow includes:
 * <ul>
 *     <li>Checking if calling activity meets the prerequisites. This is done by
 *     {@link AppClipsTrampolineActivity}.
 *     <li>Performing the screenshot.
 *     <li>Showing a screenshot editing tool.
 *     <li>Returning the screenshot to the {@link AppClipsTrampolineActivity} so that it can return
 *     the screenshot to the calling activity after explicit user consent.
 * </ul>
 *
 * <p>This {@link Activity} runs in its own separate process to isolate memory intensive image
 * editing from SysUI process.
 *
 * TODO(b/267309532): Polish UI and animations.
 */
public class AppClipsActivity extends ComponentActivity {

    private static final String TAG = AppClipsActivity.class.getSimpleName();
    private static final ApplicationInfoFlags APPLICATION_INFO_FLAGS = ApplicationInfoFlags.of(0);

    private final AppClipsViewModel.Factory mViewModelFactory;
    private final PackageManager mPackageManager;
    private final UserTracker mUserTracker;
    private final UiEventLogger mUiEventLogger;
    private final BroadcastReceiver mBroadcastReceiver;
    private final IntentFilter mIntentFilter;

    private View mLayout;
    private View mRoot;
    private ImageView mPreview;
    private CropView mCropView;
    private Button mSave;
    private Button mCancel;
    private AppClipsViewModel mViewModel;

    private ResultReceiver mResultReceiver;
    @Nullable
    private String mCallingPackageName;
    private int mCallingPackageUid;

    @Inject
    public AppClipsActivity(AppClipsViewModel.Factory viewModelFactory,
            PackageManager packageManager, UserTracker userTracker, UiEventLogger uiEventLogger) {
        mViewModelFactory = viewModelFactory;
        mPackageManager = packageManager;
        mUserTracker = userTracker;
        mUiEventLogger = uiEventLogger;

        mBroadcastReceiver = new BroadcastReceiver() {
            @Override
            public void onReceive(Context context, Intent intent) {
                // Trampoline activity was dismissed so finish this activity.
                if (ACTION_FINISH_FROM_TRAMPOLINE.equals(intent.getAction())) {
                    if (!isFinishing()) {
                        // Nullify the ResultReceiver so that result cannot be sent as trampoline
                        // activity is already finishing.
                        mResultReceiver = null;
                        finish();
                    }
                }
            }
        };

        mIntentFilter = new IntentFilter(ACTION_FINISH_FROM_TRAMPOLINE);
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        overridePendingTransition(0, 0);
        super.onCreate(savedInstanceState);

        // Register the broadcast receiver that informs when the trampoline activity is dismissed.
        registerReceiver(mBroadcastReceiver, mIntentFilter, PERMISSION_SELF, null,
                RECEIVER_NOT_EXPORTED);

        Intent intent = getIntent();
        setUpUiLogging(intent);
        mResultReceiver = intent.getParcelableExtra(EXTRA_RESULT_RECEIVER, ResultReceiver.class);
        if (mResultReceiver == null) {
            setErrorThenFinish(Intent.CAPTURE_CONTENT_FOR_NOTE_FAILED);
            return;
        }

        // Inflate layout but don't add it yet as it should be added after the screenshot is ready
        // for preview.
        mLayout = getLayoutInflater().inflate(R.layout.app_clips_screenshot, null);
        mRoot = mLayout.findViewById(R.id.root);

        mSave = mLayout.findViewById(R.id.save);
        mCancel = mLayout.findViewById(R.id.cancel);
        mSave.setOnClickListener(this::onClick);
        mCancel.setOnClickListener(this::onClick);


        mCropView = mLayout.findViewById(R.id.crop_view);

        mPreview = mLayout.findViewById(R.id.preview);
        mPreview.addOnLayoutChangeListener(
                (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) ->
                        updateImageDimensions());

        mViewModel = new ViewModelProvider(this, mViewModelFactory).get(AppClipsViewModel.class);
        mViewModel.getScreenshot().observe(this, this::setScreenshot);
        mViewModel.getResultLiveData().observe(this, this::setResultThenFinish);
        mViewModel.getErrorLiveData().observe(this, this::setErrorThenFinish);

        if (savedInstanceState == null) {
            mViewModel.performScreenshot();
        }
    }

    @Override
    public void finish() {
        super.finish();
        overridePendingTransition(0, 0);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();

        unregisterReceiver(mBroadcastReceiver);

        // If neither error nor result was set, it implies that the activity is finishing due to
        // some other reason such as user dismissing this activity using back gesture. Inform error.
        if (isFinishing() && mViewModel.getErrorLiveData().getValue() == null
                && mViewModel.getResultLiveData().getValue() == null) {
            // Set error but don't finish as the activity is already finishing.
            setError(Intent.CAPTURE_CONTENT_FOR_NOTE_FAILED);
        }
    }

    private void setUpUiLogging(Intent intent) {
        mCallingPackageName = intent.getStringExtra(EXTRA_CALLING_PACKAGE_NAME);
        mCallingPackageUid = 0;
        try {
            mCallingPackageUid = mPackageManager.getApplicationInfoAsUser(mCallingPackageName,
                    APPLICATION_INFO_FLAGS, mUserTracker.getUserId()).uid;
        } catch (NameNotFoundException e) {
            Log.d(TAG, "Couldn't find notes app UID " + e);
        }
    }

    private void setScreenshot(Bitmap screenshot) {
        // Set background, status and navigation bar colors as the activity is no longer
        // translucent.
        int colorBackgroundFloating = Utils.getColorAttr(this,
                android.R.attr.colorBackgroundFloating).getDefaultColor();
        mRoot.setBackgroundColor(colorBackgroundFloating);

        BitmapDrawable drawable = new BitmapDrawable(getResources(), screenshot);
        mPreview.setImageDrawable(drawable);
        mPreview.setAlpha(1f);

        // Screenshot is now available so set content view.
        setContentView(mLayout);
    }

    private void onClick(View view) {
        mSave.setEnabled(false);
        mCancel.setEnabled(false);

        int id = view.getId();
        if (id == R.id.save) {
            saveScreenshotThenFinish();
        } else {
            setErrorThenFinish(Intent.CAPTURE_CONTENT_FOR_NOTE_USER_CANCELED);
        }
    }

    private void saveScreenshotThenFinish() {
        Drawable drawable = mPreview.getDrawable();
        if (drawable == null) {
            setErrorThenFinish(Intent.CAPTURE_CONTENT_FOR_NOTE_FAILED);
            return;
        }

        Rect bounds = mCropView.getCropBoundaries(drawable.getIntrinsicWidth(),
                drawable.getIntrinsicHeight());

        if (bounds.isEmpty()) {
            setErrorThenFinish(Intent.CAPTURE_CONTENT_FOR_NOTE_FAILED);
            return;
        }

        updateImageDimensions();
        mViewModel.saveScreenshotThenFinish(drawable, bounds, getUser());
    }

    private void setResultThenFinish(Uri uri) {
        if (mResultReceiver == null) {
            return;
        }

        // Grant permission here instead of in the trampoline activity because this activity can run
        // as work profile user so the URI can belong to the work profile user while the trampoline
        // activity always runs as main user.
        grantUriPermission(mCallingPackageName, uri, Intent.FLAG_GRANT_READ_URI_PERMISSION);

        Bundle data = new Bundle();
        data.putInt(Intent.EXTRA_CAPTURE_CONTENT_FOR_NOTE_STATUS_CODE,
                Intent.CAPTURE_CONTENT_FOR_NOTE_SUCCESS);
        data.putParcelable(EXTRA_SCREENSHOT_URI, uri);
        try {
            mResultReceiver.send(Activity.RESULT_OK, data);
            logUiEvent(SCREENSHOT_FOR_NOTE_ACCEPTED);
        } catch (Exception e) {
            // Do nothing.
        }

        // Nullify the ResultReceiver before finishing to avoid resending the result.
        mResultReceiver = null;
        finish();
    }

    private void setErrorThenFinish(int errorCode) {
        setError(errorCode);
        finish();
    }

    private void setError(int errorCode) {
        if (mResultReceiver == null) {
            return;
        }

        Bundle data = new Bundle();
        data.putInt(Intent.EXTRA_CAPTURE_CONTENT_FOR_NOTE_STATUS_CODE, errorCode);
        try {
            mResultReceiver.send(RESULT_OK, data);
            if (errorCode == Intent.CAPTURE_CONTENT_FOR_NOTE_USER_CANCELED) {
                logUiEvent(SCREENSHOT_FOR_NOTE_CANCELLED);
            }
        } catch (Exception e) {
            // Do nothing.
        }

        // Nullify the ResultReceiver to avoid resending the result.
        mResultReceiver = null;
    }

    private void logUiEvent(UiEventEnum uiEvent) {
        mUiEventLogger.log(uiEvent, mCallingPackageUid, mCallingPackageName);
    }

    private void updateImageDimensions() {
        Drawable drawable = mPreview.getDrawable();
        if (drawable == null) {
            return;
        }

        Rect bounds = drawable.getBounds();
        float imageRatio = bounds.width() / (float) bounds.height();
        int previewWidth = mPreview.getWidth() - mPreview.getPaddingLeft()
                - mPreview.getPaddingRight();
        int previewHeight = mPreview.getHeight() - mPreview.getPaddingTop()
                - mPreview.getPaddingBottom();
        float viewRatio = previewWidth / (float) previewHeight;

        if (imageRatio > viewRatio) {
            // Image is full width and height is constrained, compute extra padding to inform
            // CropView.
            int imageHeight = (int) (previewHeight * viewRatio / imageRatio);
            int extraPadding = (previewHeight - imageHeight) / 2;
            mCropView.setExtraPadding(extraPadding, extraPadding);
            mCropView.setImageWidth(previewWidth);
        } else {
            // Image is full height.
            mCropView.setExtraPadding(mPreview.getPaddingTop(), mPreview.getPaddingBottom());
            mCropView.setImageWidth((int) (previewHeight * imageRatio));
        }
    }
}
