/*
 * 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 com.android.car.cluster;

import static com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport.DUMP_INFO;

import android.car.Car;
import android.car.CarAppFocusManager;
import android.car.builtin.util.Slogf;
import android.car.cluster.navigation.NavigationState.Maneuver;
import android.car.cluster.navigation.NavigationState.NavigationStateProto;
import android.car.cluster.navigation.NavigationState.Step;
import android.car.cluster.renderer.IInstrumentClusterNavigation;
import android.car.navigation.CarNavigationInstrumentCluster;
import android.content.Context;
import android.os.Binder;
import android.os.Bundle;
import android.util.Log;

import com.android.car.AppFocusService;
import com.android.car.AppFocusService.FocusOwnershipCallback;
import com.android.car.CarLocalServices;
import com.android.car.CarLog;
import com.android.car.CarServiceBase;
import com.android.car.CarServiceUtils;
import com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport;
import com.android.car.internal.util.IndentingPrintWriter;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;

import com.google.protobuf.InvalidProtocolBufferException;

import java.util.Objects;

/**
 * Service responsible for Navigation focus management and {@code NavigationState} change.
 *
 * @hide
 */
public class ClusterNavigationService extends IInstrumentClusterNavigation.Stub
        implements CarServiceBase, FocusOwnershipCallback {

    @VisibleForTesting
    static final String TAG = CarLog.TAG_CLUSTER;

    private static final ContextOwner NO_OWNER = new ContextOwner(0, 0);
    private static final String NAV_STATE_PROTO_BUNDLE_KEY = "navstate2";

    private final Context mContext;
    private final AppFocusService mAppFocusService;

    private final Object mLock = new Object();

    @GuardedBy("mLock")
    private ContextOwner mNavContextOwner = NO_OWNER;

    interface ClusterNavigationServiceCallback {
        void onNavigationStateChanged(Bundle bundle);

        CarNavigationInstrumentCluster getInstrumentClusterInfo();

        void notifyNavContextOwnerChanged(ContextOwner owner);
    }

    @GuardedBy("mLock")
    ClusterNavigationServiceCallback mClusterServiceCallback;

    @Override
    public void onNavigationStateChanged(Bundle bundle) {
        CarServiceUtils.assertPermission(mContext, Car.PERMISSION_CAR_NAVIGATION_MANAGER);
        assertNavigationFocus();
        assertNavStateProtoValid(bundle);
        ClusterNavigationServiceCallback callback;
        synchronized (mLock) {
            callback = mClusterServiceCallback;
        }
        if (callback == null) return;
        callback.onNavigationStateChanged(bundle);
    }

    @Override
    public CarNavigationInstrumentCluster getInstrumentClusterInfo() {
        CarServiceUtils.assertPermission(mContext, Car.PERMISSION_CAR_NAVIGATION_MANAGER);
        ClusterNavigationServiceCallback callback;
        synchronized (mLock) {
            callback = mClusterServiceCallback;
        }
        if (callback == null) return null;
        return callback.getInstrumentClusterInfo();
    }

    public ClusterNavigationService(Context context, AppFocusService appFocusService) {
        mContext = context;
        mAppFocusService = appFocusService;
    }

    public void setClusterServiceCallback(
            ClusterNavigationServiceCallback clusterServiceCallback) {
        synchronized (mLock) {
            mClusterServiceCallback = clusterServiceCallback;
        }
    }

    @Override
    public void init() {
        if (Slogf.isLoggable(TAG, Log.DEBUG)) {
            Slogf.d(TAG, "initClusterNavigationService");
        }
        mAppFocusService.registerContextOwnerChangedCallback(this /* FocusOwnershipCallback */);
    }

    @Override
    public void release() {
        if (Slogf.isLoggable(TAG, Log.DEBUG)) {
            Slogf.d(TAG, "releaseClusterNavigationService");
        }
        setClusterServiceCallback(null);
        mAppFocusService.unregisterContextOwnerChangedCallback(this);
    }

    @Override
    @ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO)
    public void dump(IndentingPrintWriter writer) {
        writer.println("**" + getClass().getSimpleName() + "**");
        synchronized (mLock) {
            writer.println("context owner: " + mNavContextOwner);
        }
    }

    @Override
    public void onFocusAcquired(int appType, int uid, int pid) {
        changeNavContextOwner(appType, uid, pid, true);
    }

    @Override
    public void onFocusAbandoned(int appType, int uid, int pid) {
        changeNavContextOwner(appType, uid, pid, false);
    }

    private void assertNavigationFocus() {
        int uid = Binder.getCallingUid();
        int pid = Binder.getCallingPid();
        synchronized (mLock) {
            if (uid == mNavContextOwner.uid && pid == mNavContextOwner.pid) {
                return;
            }
        }
        // Stored one failed. There can be a delay, so check with real one again.
        AppFocusService afs = CarLocalServices.getService(AppFocusService.class);
        if (afs != null && afs.isFocusOwner(uid, pid,
                CarAppFocusManager.APP_FOCUS_TYPE_NAVIGATION)) {
            return;
        }
        throw new IllegalStateException("Client not owning APP_FOCUS_TYPE_NAVIGATION");
    }

    private void changeNavContextOwner(int appType, int uid, int pid, boolean acquire) {
        if (appType != CarAppFocusManager.APP_FOCUS_TYPE_NAVIGATION) {
            return;
        }
        ContextOwner requester = new ContextOwner(uid, pid);
        ContextOwner newOwner = acquire ? requester : NO_OWNER;
        ClusterNavigationServiceCallback callback;
        synchronized (mLock) {
            if ((acquire && Objects.equals(mNavContextOwner, requester))
                    || (!acquire && !Objects.equals(mNavContextOwner, requester))) {
                // Nothing to do here. Either the same owner is acquiring twice, or someone is
                // abandoning a focus they didn't have.
                Slogf.w(TAG, "Invalid nav context owner change (acquiring: " + acquire
                        + "), current owner: [" + mNavContextOwner
                        + "], requester: [" + requester + "]");
                return;
            }

            mNavContextOwner = newOwner;
            callback = mClusterServiceCallback;
        }
        if (callback == null) return;

        callback.notifyNavContextOwnerChanged(newOwner);
    }

    private void assertNavStateProtoValid(Bundle bundle) {
        byte[] protoBytes = bundle.getByteArray(NAV_STATE_PROTO_BUNDLE_KEY);
        if (protoBytes == null) {
            throw new IllegalArgumentException("Received navigation state byte array is null.");
        }
        try {
            NavigationStateProto navigationStateProto = NavigationStateProto.parseFrom(protoBytes);
            if (navigationStateProto.getStepsCount() == 0) {
                return;
            }
            for (Step step : navigationStateProto.getStepsList()) {
                Maneuver maneuver = step.getManeuver();
                if (!Maneuver.TypeV2.UNKNOWN_V2.equals(maneuver.getTypeV2())
                        && Maneuver.Type.UNKNOWN.equals(maneuver.getType())) {
                    throw new IllegalArgumentException(
                        "Maneuver#type must be populated if Maneuver#typeV2 is also populated.");
                }
            }
        } catch (InvalidProtocolBufferException e) {
            throw new IllegalArgumentException("Error parsing navigation state proto", e);
        }
    }

    static class ContextOwner {
        final int uid;
        final int pid;

        ContextOwner(int uid, int pid) {
            this.uid = uid;
            this.pid = pid;
        }

        @Override
        public String toString() {
            return "uid: " + uid + ", pid: " + pid;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            ContextOwner that = (ContextOwner) o;
            return uid == that.uid && pid == that.pid;
        }

        @Override
        public int hashCode() {
            return Objects.hash(uid, pid);
        }
    }
}
