/*
 * Copyright 2020 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.uwb;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.AttributionSource;
import android.os.CancellationSignal;
import android.os.PersistableBundle;
import android.os.Process;
import android.os.RemoteException;
import android.util.Log;

import java.util.Hashtable;
import java.util.List;
import java.util.concurrent.Executor;

/**
 * @hide
 */
public class RangingManager extends android.uwb.IUwbRangingCallbacks.Stub {
    private final String mTag = "Uwb.RangingManager[" + this + "]";

    private final IUwbAdapter mAdapter;
    private final Hashtable<SessionHandle, RangingSession> mRangingSessionTable = new Hashtable<>();
    private static int sNextSessionId = 1;

    public RangingManager(IUwbAdapter adapter) {
        mAdapter = adapter;
    }

    /**
     * Open a new ranging session
     *
     * @param attributionSource Attribution source to use for the enforcement of
     *                          {@link android.Manifest.permission#UWB_RANGING} runtime
     *                          permission.
     * @param params the parameters that define the ranging session
     * @param executor {@link Executor} to run callbacks
     * @param callbacks {@link RangingSession.Callback} to associate with the {@link RangingSession}
     *                  that is being opened.
     * @param chipId identifier of UWB chip to be used in ranging session, or {@code null} if
     *                the default chip should be used
     * @return a {@link CancellationSignal} that may be used to cancel the opening of the
     *         {@link RangingSession}.
     */
    public CancellationSignal openSession(@NonNull AttributionSource attributionSource,
            @NonNull PersistableBundle params,
            @NonNull Executor executor,
            @NonNull RangingSession.Callback callbacks,
            @Nullable String chipId) {
        if (chipId != null) {
            try {
                List<String> validChipIds = mAdapter.getChipIds();
                if (!validChipIds.contains(chipId)) {
                    throw new IllegalArgumentException("openSession - received invalid chipId: "
                            + chipId);
                }
            } catch (RemoteException e)  {
                e.rethrowFromSystemServer();
            }
        }

        synchronized (this) {
            SessionHandle sessionHandle =
                    new SessionHandle(sNextSessionId++, attributionSource, Process.myPid());
            RangingSession session =
                    new RangingSession(executor, callbacks, mAdapter, sessionHandle, chipId);
            Log.v(mTag, "openSession - sessionHandle: " + sessionHandle);
            mRangingSessionTable.put(sessionHandle, session);
            try {
                mAdapter.openRanging(attributionSource,
                        sessionHandle,
                        this,
                        params,
                        chipId);
            } catch (RemoteException e) {
                throw e.rethrowFromSystemServer();
            }

            CancellationSignal cancellationSignal = new CancellationSignal();
            cancellationSignal.setOnCancelListener(() -> session.close());
            return cancellationSignal;
        }
    }

    private boolean hasSession(SessionHandle sessionHandle) {
        return mRangingSessionTable.containsKey(sessionHandle);
    }

    @Override
    public void onRangingOpened(SessionHandle sessionHandle) {
        synchronized (this) {
            if (!hasSession(sessionHandle)) {
                Log.w(mTag,
                        "onRangingOpened - received unexpected SessionHandle: " + sessionHandle);
                return;
            }

            RangingSession session = mRangingSessionTable.get(sessionHandle);
            session.onRangingOpened();
        }
    }

    @Override
    public void onRangingOpenFailed(SessionHandle sessionHandle, @RangingChangeReason int reason,
            PersistableBundle parameters) {
        synchronized (this) {
            if (!hasSession(sessionHandle)) {
                Log.w(mTag,
                        "onRangingOpenedFailed - received unexpected SessionHandle: "
                                + sessionHandle);
                return;
            }

            RangingSession session = mRangingSessionTable.get(sessionHandle);
            session.onRangingOpenFailed(convertToReason(reason), parameters);
            mRangingSessionTable.remove(sessionHandle);
        }
    }

    @Override
    public void onRangingReconfigured(SessionHandle sessionHandle, PersistableBundle parameters) {
        synchronized (this) {
            if (!hasSession(sessionHandle)) {
                Log.w(mTag,
                        "onRangingReconfigured - received unexpected SessionHandle: "
                                + sessionHandle);
                return;
            }

            RangingSession session = mRangingSessionTable.get(sessionHandle);
            session.onRangingReconfigured(parameters);
        }
    }

    @Override
    public void onRangingReconfigureFailed(SessionHandle sessionHandle,
            @RangingChangeReason int reason, PersistableBundle params) {
        synchronized (this) {
            if (!hasSession(sessionHandle)) {
                Log.w(mTag, "onRangingReconfigureFailed - received unexpected SessionHandle: "
                        + sessionHandle);
                return;
            }

            RangingSession session = mRangingSessionTable.get(sessionHandle);
            session.onRangingReconfigureFailed(convertToReason(reason), params);
        }
    }


    @Override
    public void onRangingStarted(SessionHandle sessionHandle, PersistableBundle parameters) {
        synchronized (this) {
            if (!hasSession(sessionHandle)) {
                Log.w(mTag,
                        "onRangingStarted - received unexpected SessionHandle: " + sessionHandle);
                return;
            }

            RangingSession session = mRangingSessionTable.get(sessionHandle);
            session.onRangingStarted(parameters);
        }
    }

    @Override
    public void onRangingStartFailed(SessionHandle sessionHandle, @RangingChangeReason int reason,
            PersistableBundle params) {
        synchronized (this) {
            if (!hasSession(sessionHandle)) {
                Log.w(mTag, "onRangingStartFailed - received unexpected SessionHandle: "
                        + sessionHandle);
                return;
            }

            RangingSession session = mRangingSessionTable.get(sessionHandle);
            session.onRangingStartFailed(convertToReason(reason), params);
        }
    }

    @Override
    public void onRangingStopped(SessionHandle sessionHandle, @RangingChangeReason int reason,
            PersistableBundle params) {
        synchronized (this) {
            if (!hasSession(sessionHandle)) {
                Log.w(mTag, "onRangingStopped - received unexpected SessionHandle: "
                        + sessionHandle);
                return;
            }

            RangingSession session = mRangingSessionTable.get(sessionHandle);
            session.onRangingStopped(convertToReason(reason), params);
        }
    }

    @Override
    public void onRangingStopFailed(SessionHandle sessionHandle, @RangingChangeReason int reason,
            PersistableBundle parameters) {
        synchronized (this) {
            if (!hasSession(sessionHandle)) {
                Log.w(mTag, "onRangingStopFailed - received unexpected SessionHandle: "
                        + sessionHandle);
                return;
            }

            RangingSession session = mRangingSessionTable.get(sessionHandle);
            session.onRangingStopFailed(convertToReason(reason), parameters);
        }
    }

    @Override
    public void onRangingClosed(SessionHandle sessionHandle, @RangingChangeReason int reason,
            PersistableBundle params) {
        synchronized (this) {
            if (!hasSession(sessionHandle)) {
                Log.w(mTag, "onRangingClosed - received unexpected SessionHandle: "
                        + sessionHandle);
                return;
            }

            RangingSession session = mRangingSessionTable.get(sessionHandle);
            session.onRangingClosed(convertToReason(reason), params);
            mRangingSessionTable.remove(sessionHandle);
        }
    }

    @Override
    public void onRangingResult(SessionHandle sessionHandle, RangingReport result) {
        synchronized (this) {
            if (!hasSession(sessionHandle)) {
                Log.w(mTag, "onRangingResult - received unexpected SessionHandle: "
                        + sessionHandle);
                return;
            }

            RangingSession session = mRangingSessionTable.get(sessionHandle);
            session.onRangingResult(result);
        }
    }

    @Override
    public void onControleeAdded(SessionHandle sessionHandle, PersistableBundle parameters) {
        synchronized (this) {
            if (!hasSession(sessionHandle)) {
                Log.w(mTag, "onControleeAdded - received unexpected SessionHandle: "
                        + sessionHandle);
                return;
            }

            RangingSession session = mRangingSessionTable.get(sessionHandle);
            session.onControleeAdded(parameters);
        }
    }

    @Override
    public void onControleeAddFailed(SessionHandle sessionHandle, @RangingChangeReason int reason,
            PersistableBundle parameters) {
        synchronized (this) {
            if (!hasSession(sessionHandle)) {
                Log.w(mTag, "onControleeAddFailed - received unexpected SessionHandle: "
                        + sessionHandle);
                return;
            }

            RangingSession session = mRangingSessionTable.get(sessionHandle);
            session.onControleeAddFailed(reason, parameters);
        }
    }

    @Override
    public void onControleeRemoved(SessionHandle sessionHandle, PersistableBundle parameters) {
        synchronized (this) {
            if (!hasSession(sessionHandle)) {
                Log.w(mTag, "onControleeRemoved - received unexpected SessionHandle: "
                        + sessionHandle);
                return;
            }

            RangingSession session = mRangingSessionTable.get(sessionHandle);
            session.onControleeRemoved(parameters);
        }
    }

    @Override
    public void onControleeRemoveFailed(SessionHandle sessionHandle,
            @RangingChangeReason int reason, PersistableBundle parameters) {
        synchronized (this) {
            if (!hasSession(sessionHandle)) {
                Log.w(mTag, "onControleeRemoveFailed - received unexpected SessionHandle: "
                        + sessionHandle);
                return;
            }

            RangingSession session = mRangingSessionTable.get(sessionHandle);
            session.onControleeRemoveFailed(reason, parameters);
        }
    }

    @Override
    public void onRangingPaused(SessionHandle sessionHandle, PersistableBundle parameters) {
        synchronized (this) {
            if (!hasSession(sessionHandle)) {
                Log.w(mTag, "onRangingPaused - received unexpected SessionHandle: "
                        + sessionHandle);
                return;
            }

            RangingSession session = mRangingSessionTable.get(sessionHandle);
            session.onRangingPaused(parameters);
        }
    }

    @Override
    public void onRangingPauseFailed(SessionHandle sessionHandle, @RangingChangeReason int reason,
            PersistableBundle parameters) {
        synchronized (this) {
            if (!hasSession(sessionHandle)) {
                Log.w(mTag, "onRangingPauseFailed - received unexpected SessionHandle: "
                        + sessionHandle);
                return;
            }

            RangingSession session = mRangingSessionTable.get(sessionHandle);
            session.onRangingPauseFailed(reason, parameters);
        }
    }

    @Override
    public void onRangingResumed(SessionHandle sessionHandle, PersistableBundle parameters) {
        synchronized (this) {
            if (!hasSession(sessionHandle)) {
                Log.w(mTag, "onRangingResumed - received unexpected SessionHandle: "
                        + sessionHandle);
                return;
            }

            RangingSession session = mRangingSessionTable.get(sessionHandle);
            session.onRangingResumed(parameters);
        }
    }

    @Override
    public void onRangingResumeFailed(SessionHandle sessionHandle, @RangingChangeReason int reason,
            PersistableBundle parameters) {
        synchronized (this) {
            if (!hasSession(sessionHandle)) {
                Log.w(mTag, "onRangingResumeFailed - received unexpected SessionHandle: "
                        + sessionHandle);
                return;
            }

            RangingSession session = mRangingSessionTable.get(sessionHandle);
            session.onRangingResumeFailed(reason, parameters);
        }
    }

    @Override
    public void onDataSent(SessionHandle sessionHandle, UwbAddress remoteDeviceAddress,
            PersistableBundle parameters) {
        synchronized (this) {
            if (!hasSession(sessionHandle)) {
                Log.w(mTag, "onDataSent - received unexpected SessionHandle: " + sessionHandle);
                return;
            }

            RangingSession session = mRangingSessionTable.get(sessionHandle);
            session.onDataSent(remoteDeviceAddress, parameters);
        }
    }

    @Override
    public void onDataSendFailed(SessionHandle sessionHandle, UwbAddress remoteDeviceAddress,
            @RangingChangeReason int reason, PersistableBundle parameters) {
        synchronized (this) {
            if (!hasSession(sessionHandle)) {
                Log.w(mTag, "onDataSendFailed - received unexpected SessionHandle: "
                        + sessionHandle);
                return;
            }

            RangingSession session = mRangingSessionTable.get(sessionHandle);
            session.onDataSendFailed(remoteDeviceAddress, reason, parameters);
        }
    }

    @Override
    public void onDataTransferPhaseConfigured(SessionHandle sessionHandle,
            PersistableBundle parameters) {
        synchronized (this) {
            if (!hasSession(sessionHandle)) {
                Log.w(mTag, "onDataTransferPhaseConfigured - received unexpected SessionHandle: "
                        + sessionHandle);
                return;
            }

            RangingSession session = mRangingSessionTable.get(sessionHandle);
            session.onDataTransferPhaseConfigured(parameters);
        }
    }

    @Override
    public void onDataTransferPhaseConfigFailed(SessionHandle sessionHandle,
            @RangingChangeReason int reason, PersistableBundle parameters) {
        synchronized (this) {
            if (!hasSession(sessionHandle)) {
                Log.w(mTag, "onDataTransferPhaseConfigFailed - received unknown SessionHandle: "
                        + sessionHandle);
                return;
            }

            RangingSession session = mRangingSessionTable.get(sessionHandle);
            session.onDataTransferPhaseConfigFailed(reason, parameters);
        }
    }

    @Override
    public void onDataReceived(SessionHandle sessionHandle, UwbAddress remoteDeviceAddress,
            PersistableBundle parameters, byte[] data) {
        synchronized (this) {
            if (!hasSession(sessionHandle)) {
                Log.w(mTag, "onDataReceived - received unexpected SessionHandle: "
                        + sessionHandle);
                return;
            }

            RangingSession session = mRangingSessionTable.get(sessionHandle);
            session.onDataReceived(remoteDeviceAddress, parameters, data);
        }
    }

    @Override
    public void onDataReceiveFailed(SessionHandle sessionHandle, UwbAddress remoteDeviceAddress,
            @RangingChangeReason int reason, PersistableBundle parameters) {
        synchronized (this) {
            if (!hasSession(sessionHandle)) {
                Log.w(mTag, "onDataReceiveFailed - received unexpected SessionHandle: "
                        + sessionHandle);
                return;
            }

            RangingSession session = mRangingSessionTable.get(sessionHandle);
            session.onDataReceiveFailed(remoteDeviceAddress, reason, parameters);
        }
    }

    @Override
    public void onControleeRoleChanged(SessionHandle sessionHandle, int deviceRole) {
        synchronized (this) {
            if (!hasSession(sessionHandle)) {
                Log.w(mTag, "onControleeRoleChanged - received unexpected SessionHandle: "
                        + sessionHandle);
                return;
            }

            mRangingSessionTable.get(sessionHandle).onControleeRoleChanged(deviceRole);
        }
    }

    @Override
    public void onServiceDiscovered(SessionHandle sessionHandle,
            @NonNull PersistableBundle parameters) {
        synchronized (this) {
            if (!hasSession(sessionHandle)) {
                Log.w(mTag, "onServiceDiscovered - received unexpected SessionHandle: "
                        + sessionHandle);
                return;
            }

            RangingSession session = mRangingSessionTable.get(sessionHandle);
            session.onServiceDiscovered(parameters);
        }
    }

    @Override
    public void onHybridSessionControllerConfigured(SessionHandle sessionHandle,
            PersistableBundle parameters) {
        synchronized (this) {
            if (!hasSession(sessionHandle)) {
                Log.w(mTag, "onHybridSessionControllerConfigured - received unexpected"
                        + "SessionHandle: " + sessionHandle);
                return;
            }

            RangingSession session = mRangingSessionTable.get(sessionHandle);
            session.onHybridSessionControllerConfigured(parameters);
        }
    }

    @Override
    public void onHybridSessionControllerConfigurationFailed(SessionHandle sessionHandle,
            @RangingChangeReason int reason, PersistableBundle parameters) {
        synchronized (this) {
            if (!hasSession(sessionHandle)) {
                Log.w(mTag, "onHybridSessionControllerConfigurationFailed - received"
                        + "unexpected SessionHandle: " + sessionHandle);
                return;
            }

            RangingSession session = mRangingSessionTable.get(sessionHandle);
            session.onHybridSessionControllerConfigurationFailed(reason, parameters);
        }
    }

    @Override
    public void onHybridSessionControleeConfigured(SessionHandle sessionHandle,
            PersistableBundle parameters) {
        synchronized (this) {
            if (!hasSession(sessionHandle)) {
                Log.w(mTag, "onHybridSessionControleeConfigured - received unexpected"
                        + "SessionHandle: " + sessionHandle);
                return;
            }

            RangingSession session = mRangingSessionTable.get(sessionHandle);
            session.onHybridSessionControleeConfigured(parameters);
        }
    }

    @Override
    public void onHybridSessionControleeConfigurationFailed(SessionHandle sessionHandle,
            @RangingChangeReason int reason, PersistableBundle parameters) {
        synchronized (this) {
            if (!hasSession(sessionHandle)) {
                Log.w(mTag, "onHybridSessionControleeConfigurationFailed - received"
                        + "unexpected SessionHandle: " + sessionHandle);
                return;
            }

            RangingSession session = mRangingSessionTable.get(sessionHandle);
            session.onHybridSessionControleeConfigurationFailed(reason, parameters);
        }
    }

    @Override
    public void onServiceConnected(SessionHandle sessionHandle,
            @NonNull PersistableBundle parameters) {
        synchronized (this) {
            if (!hasSession(sessionHandle)) {
                Log.w(mTag, "onServiceConnected - received unexpected SessionHandle: "
                        + sessionHandle);
                return;
            }

            RangingSession session = mRangingSessionTable.get(sessionHandle);
            session.onServiceConnected(parameters);
        }
    }

    @Override
    public void onRangingRoundsUpdateDtTagStatus(SessionHandle sessionHandle,
            @NonNull PersistableBundle parameters) {
        synchronized (this) {
            if (!hasSession(sessionHandle)) {
                Log.w(mTag, "onRangingRoundsUpdateDtTagStatus - received unexpected "
                        + "SessionHandle: " + sessionHandle);
                return;
            }

            RangingSession session = mRangingSessionTable.get(sessionHandle);
            session.onRangingRoundsUpdateDtTagStatus(parameters);
        }
    }

    @Override
    public void onLogicalLinkCreated(SessionHandle sessionHandle, LogicalLinkCreationParams params,
            int connectId) {
        synchronized (this) {
            if (!hasSession(sessionHandle)) {
                Log.w(mTag, "onLogicalLinkCreated - received unexpected SessionHandle: "
                        + sessionHandle);
                return;
            }

            RangingSession session = mRangingSessionTable.get(sessionHandle);
            session.onLogicalLinkCreated(params, connectId);
        }
    }

    @Override
    public void onLogicalLinkCreationFailed(SessionHandle sessionHandle,
            LogicalLinkCreationParams params, int status) {
        synchronized (this) {
            if (!hasSession(sessionHandle)) {
                Log.w(mTag, "onLogicalLinkCreationFailed - received unexpected SessionHandle: "
                        + sessionHandle);
                return;
            }

            RangingSession session = mRangingSessionTable.get(sessionHandle);
            session.onLogicalLinkCreationFailed(params, status);
        }
    }

    @Override
    public void onLogicalLinkClosed(SessionHandle sessionHandle, int connectId, int reason) {
        synchronized (this) {
            if (!hasSession(sessionHandle)) {
                Log.w(mTag, "onLogicalLinkClosed - received unexpected SessionHandle: "
                        + sessionHandle);
                return;
            }

            RangingSession session = mRangingSessionTable.get(sessionHandle);
            session.onLogicalLinkClosed(connectId, reason);
        }
    }

    @Override
    public void onLogicalLinkClosureFailed(SessionHandle sessionHandle, int connectId, int status) {
        synchronized (this) {
            if (!hasSession(sessionHandle)) {
                Log.w(mTag, "onLogicalLinkClosureFailed - received unexpected SessionHandle: "
                        + sessionHandle);
                return;
            }

            RangingSession session = mRangingSessionTable.get(sessionHandle);
            session.onLogicalLinkClosureFailed(connectId, status);
        }
    }

    @Override
    public void onRemoteLogicalLinkRequested(SessionHandle sessionHandle,
            LogicalLinkConnectionRequest linkInfo) {
        synchronized (this) {
            if (!hasSession(sessionHandle)) {
                Log.w(mTag, "onRemoteLogicalLinkRequested - received unexpected SessionHandle: "
                        + sessionHandle);
                return;
            }

            RangingSession session = mRangingSessionTable.get(sessionHandle);
            session.onRemoteLogicalLinkRequested(linkInfo);
        }
    }


    // TODO(b/211025367): Remove this conversion and use direct API values.
    @RangingSession.Callback.Reason
    private static int convertToReason(@RangingChangeReason int reason) {
        switch (reason) {
            case RangingChangeReason.LOCAL_API:
                return RangingSession.Callback.REASON_LOCAL_REQUEST;

            case RangingChangeReason.MAX_SESSIONS_REACHED:
                return RangingSession.Callback.REASON_MAX_SESSIONS_REACHED;

            case RangingChangeReason.SYSTEM_POLICY:
                return RangingSession.Callback.REASON_SYSTEM_POLICY;

            case RangingChangeReason.SYSTEM_REGULATION:
                return RangingSession.Callback.REASON_SYSTEM_REGULATION;

            case RangingChangeReason.REMOTE_REQUEST:
                return RangingSession.Callback.REASON_REMOTE_REQUEST;

            case RangingChangeReason.PROTOCOL_SPECIFIC:
                return RangingSession.Callback.REASON_PROTOCOL_SPECIFIC_ERROR;

            case RangingChangeReason.BAD_PARAMETERS:
                return RangingSession.Callback.REASON_BAD_PARAMETERS;

            case RangingChangeReason.MAX_RR_RETRY_REACHED:
                return RangingSession.Callback.REASON_MAX_RR_RETRY_REACHED;

            case RangingChangeReason.INSUFFICIENT_SLOTS_PER_RR:
                return RangingSession.Callback.REASON_INSUFFICIENT_SLOTS_PER_RR;

            case RangingChangeReason.INBAND_SESSION_STOP:
                return RangingSession.Callback.REASON_INBAND_SESSION_STOP;

            case RangingChangeReason.UNKNOWN:
            default:
                return RangingSession.Callback.REASON_UNKNOWN;
        }
    }
}
