/*
 * Copyright (C) 2018 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.car.systemupdater;

import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Handler;
import android.os.PowerManager;
import android.os.UpdateEngine;
import android.os.UpdateEngineCallback;
import android.text.format.Formatter;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;

import androidx.annotation.NonNull;
import androidx.annotation.StringRes;
import androidx.appcompat.app.AppCompatActivity;
import androidx.fragment.app.Fragment;

import com.android.car.ui.core.CarUi;
import com.android.car.ui.toolbar.MenuItem;
import com.android.car.ui.toolbar.ProgressBarController;
import com.android.car.ui.toolbar.ToolbarController;
import com.android.internal.util.Preconditions;

import java.io.File;
import java.io.IOException;
import java.util.Collections;

/** Display update state and progress. */
public class UpdateLayoutFragment extends Fragment implements UpFragment {
    public static final String EXTRA_RESUME_UPDATE = "resume_update";

    private static final String TAG = "UpdateLayoutFragment";
    private static final String EXTRA_UPDATE_FILE = "extra_update_file";
    private static final int PERCENT_MAX = 100;
    private static final String REBOOT_REASON = "reboot-ab-update";
    private static final String NOTIFICATION_CHANNEL_ID = "update";
    private static final int NOTIFICATION_ID = 1;

    private TextView mContentTitle;
    private TextView mContentInfo;
    private TextView mContentDetails;
    private File mUpdateFile;
    private ToolbarController mToolbar;
    private ProgressBarController mProgressBar;
    private PowerManager mPowerManager;
    private NotificationManager mNotificationManager;
    private final UpdateVerifier mPackageVerifier = new UpdateVerifier();
    private final UpdateEngine mUpdateEngine = new UpdateEngine();
    private boolean mInstallationInProgress = false;

    private final CarUpdateEngineCallback mCarUpdateEngineCallback = new CarUpdateEngineCallback();

    /** Create a {@link UpdateLayoutFragment}. */
    public static UpdateLayoutFragment getInstance(File file) {
        UpdateLayoutFragment fragment = new UpdateLayoutFragment();
        Bundle bundle = new Bundle();
        bundle.putString(EXTRA_UPDATE_FILE, file.getAbsolutePath());
        fragment.setArguments(bundle);
        return fragment;
    }

    /** Create a {@link UpdateLayoutFragment} showing an update in progress. */
    public static UpdateLayoutFragment newResumedInstance() {
        UpdateLayoutFragment fragment = new UpdateLayoutFragment();
        Bundle bundle = new Bundle();
        bundle.putBoolean(EXTRA_RESUME_UPDATE, true);
        fragment.setArguments(bundle);
        return fragment;
    }

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

        if (!getArguments().getBoolean(EXTRA_RESUME_UPDATE)) {
            mUpdateFile = new File(getArguments().getString(EXTRA_UPDATE_FILE));
        }
        mPowerManager = (PowerManager) getContext().getSystemService(Context.POWER_SERVICE);
        mNotificationManager =
                (NotificationManager) getContext().getSystemService(NotificationManager.class);
        mNotificationManager.createNotificationChannel(
                new NotificationChannel(
                        NOTIFICATION_CHANNEL_ID,
                        getContext().getString(R.id.system_update_auto_content_title),
                        NotificationManager.IMPORTANCE_DEFAULT));
    }

    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
            Bundle savedInstanceState) {
        return inflater.inflate(R.layout.system_update_auto_content, container, false);
    }

    @Override
    public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
        mContentTitle = view.findViewById(R.id.system_update_auto_content_title);
        mContentInfo = view.findViewById(R.id.system_update_auto_content_info);
        mContentDetails = view.findViewById(R.id.system_update_auto_content_details);
    }

    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        AppCompatActivity activity = (AppCompatActivity) getActivity();
        mToolbar = CarUi.requireToolbar(getActivity());
        mProgressBar = mToolbar.getProgressBar();
        mProgressBar.setIndeterminate(true);
        mProgressBar.setVisible(true);
        showStatus(R.string.verify_in_progress);

        if (getArguments().getBoolean(EXTRA_RESUME_UPDATE)) {
            // Rejoin the update already in progress.
            showInstallationInProgress();
        } else {
            // Extract the necessary information and begin the update.
            mPackageVerifier.execute(mUpdateFile);
        }
    }

    @Override
    public void onStop() {
        super.onStop();
        if (mPackageVerifier != null) {
            mPackageVerifier.cancel(true);
        }
    }

    /** Update the status information. */
    private void showStatus(@StringRes int status) {
        mContentTitle.setText(status);
        if (mInstallationInProgress) {
            mNotificationManager.notify(NOTIFICATION_ID, createNotification(getContext(), status));
        } else {
            mNotificationManager.cancel(NOTIFICATION_ID);
        }
    }

    /** Show the install now button. */
    private void showInstallNow(UpdateParser.ParsedUpdate update) {
        mContentTitle.setText(R.string.install_ready);
        mContentInfo.append(getString(R.string.update_file_name, mUpdateFile.getName()));
        mContentInfo.append(System.getProperty("line.separator"));
        mContentInfo.append(getString(R.string.update_file_size));
        mContentInfo.append(Formatter.formatFileSize(getContext(), mUpdateFile.length()));
        mContentDetails.setText(null);
        MenuItem installButton = MenuItem.builder(getActivity())
                .setTitle(R.string.install_now)
                .setOnClickListener(i -> installUpdate(update))
                .build();
        mToolbar.setMenuItems(Collections.singletonList(installButton));
    }

    /** Reboot the system. */
    private void rebootNow() {
        if (Log.isLoggable(TAG, Log.INFO)) {
            Log.i(TAG, "Rebooting Now.");
        }
        mPowerManager.reboot(REBOOT_REASON);
    }

    /** Attempt to install the update that is copied to the device. */
    private void installUpdate(UpdateParser.ParsedUpdate parsedUpdate) {
        showInstallationInProgress();
        mUpdateEngine.applyPayload(
                parsedUpdate.mUrl, parsedUpdate.mOffset, parsedUpdate.mSize, parsedUpdate.mProps);
    }

    /** Set the layout to show installation progress. */
    private void showInstallationInProgress() {
        mInstallationInProgress = true;
        mProgressBar.setIndeterminate(false);
        mProgressBar.setVisible(true);
        mProgressBar.setMax(PERCENT_MAX);
        mToolbar.setMenuItems(null); // Remove install button
        showStatus(R.string.install_in_progress);

        mUpdateEngine.bind(mCarUpdateEngineCallback, new Handler(getContext().getMainLooper()));
    }

    /** Attempt to verify the update and extract information needed for installation. */
    private class UpdateVerifier extends AsyncTask<File, Void, UpdateParser.ParsedUpdate> {

        @Override
        protected UpdateParser.ParsedUpdate doInBackground(File... files) {
            Preconditions.checkArgument(files.length > 0, "No file specified");
            File file = files[0];
            try {
                return UpdateParser.parse(file);
            } catch (IOException e) {
                Log.e(TAG, String.format("For file %s", file), e);
                return null;
            }
        }

        @Override
        protected void onPostExecute(UpdateParser.ParsedUpdate result) {
            mProgressBar.setVisible(false);
            if (result == null) {
                showStatus(R.string.verify_failure);
                return;
            }
            if (!result.isValid()) {
                showStatus(R.string.verify_failure);
                Log.e(TAG, String.format("Failed verification %s", result));
                return;
            }
            if (Log.isLoggable(TAG, Log.INFO)) {
                Log.i(TAG, result.toString());
            }

            showInstallNow(result);
        }
    }

    /** Handles events from the UpdateEngine. */
    public class CarUpdateEngineCallback extends UpdateEngineCallback {

        @Override
        public void onStatusUpdate(int status, float percent) {
            if (Log.isLoggable(TAG, Log.DEBUG)) {
                Log.d(TAG, String.format("onStatusUpdate %d, Percent %.2f", status, percent));
            }
            switch (status) {
                case UpdateEngine.UpdateStatusConstants.UPDATED_NEED_REBOOT:
                    rebootNow();
                    break;
                case UpdateEngine.UpdateStatusConstants.DOWNLOADING:
                    mProgressBar.setProgress((int) (percent * 100));
                    break;
                default:
                    // noop
            }
        }

        @Override
        public void onPayloadApplicationComplete(int errorCode) {
            Log.w(TAG, String.format("onPayloadApplicationComplete %d", errorCode));
            mInstallationInProgress = false;
            showStatus(errorCode == UpdateEngine.ErrorCodeConstants.SUCCESS
                    ? R.string.install_success
                    : R.string.install_failed);
            mProgressBar.setVisible(false);
            mToolbar.setMenuItems(null); // Remove install now button
        }
    }

    /** Build a notification to show the installation status. */
    private static Notification createNotification(Context context, @StringRes int contents) {
        Intent intent = new Intent();
        intent.setComponent(new ComponentName(context, SystemUpdaterActivity.class));
        intent.putExtra(EXTRA_RESUME_UPDATE, true);
        PendingIntent pendingIntent =
                PendingIntent.getActivity(
                        context,
                        /* requestCode= */ 0,
                        intent,
                        PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT);

        return new Notification.Builder(context, NOTIFICATION_CHANNEL_ID)
                .setVisibility(Notification.VISIBILITY_PUBLIC)
                .setContentTitle(context.getString(contents))
                .setSmallIcon(R.drawable.ic_system_update_alt_black_48dp)
                .setContentIntent(pendingIntent)
                .setShowWhen(false)
                .setOngoing(true)
                .setAutoCancel(false)
                .build();
    }
}
