/*
 * Copyright (C) 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 com.android.server.timezonedetector.location;

import static com.android.server.timezonedetector.location.LocationTimeZoneManagerService.debugLog;
import static com.android.server.timezonedetector.location.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_DESTROYED;
import static com.android.server.timezonedetector.location.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_PERM_FAILED;
import static com.android.server.timezonedetector.location.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_STARTED_CERTAIN;
import static com.android.server.timezonedetector.location.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_STARTED_INITIALIZING;
import static com.android.server.timezonedetector.location.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_STARTED_UNCERTAIN;
import static com.android.server.timezonedetector.location.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_STOPPED;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.os.RemoteCallback;
import android.util.IndentingPrintWriter;

import java.time.Duration;
import java.util.Objects;

/**
 * The real, system-server side implementation of a binder call backed {@link
 * LocationTimeZoneProvider}. It handles keeping track of current state, timeouts and ensuring
 * events are passed to the {@link LocationTimeZoneProviderController} on the required thread.
 */
class BinderLocationTimeZoneProvider extends LocationTimeZoneProvider {

    @NonNull private final LocationTimeZoneProviderProxy mProxy;

    BinderLocationTimeZoneProvider(
            @NonNull ProviderMetricsLogger providerMetricsLogger,
            @NonNull ThreadingDomain threadingDomain,
            @NonNull String providerName,
            @NonNull LocationTimeZoneProviderProxy proxy) {
        super(providerMetricsLogger, threadingDomain, providerName,
                new ZoneInfoDbTimeZoneProviderEventPreProcessor());
        mProxy = Objects.requireNonNull(proxy);
    }

    @Override
    void onInitialize() {
        mProxy.initialize(new LocationTimeZoneProviderProxy.Listener() {
            @Override
            public void onReportTimeZoneProviderEvent(
                    @NonNull TimeZoneProviderEvent timeZoneProviderEvent) {
                handleTimeZoneProviderEvent(timeZoneProviderEvent);
            }

            @Override
            public void onProviderBound() {
                handleOnProviderBound();
            }

            @Override
            public void onProviderUnbound() {
                handleProviderLost("onProviderUnbound()");
            }
        });
    }

    @Override
    void onDestroy() {
        mProxy.destroy();
    }

    private void handleProviderLost(String reason) {
        mThreadingDomain.assertCurrentThread();

        synchronized (mSharedLock) {
            ProviderState currentState = mCurrentState.get();
            switch (currentState.stateEnum) {
                case PROVIDER_STATE_STARTED_INITIALIZING:
                case PROVIDER_STATE_STARTED_UNCERTAIN:
                case PROVIDER_STATE_STARTED_CERTAIN: {
                    // Losing a remote provider is treated as becoming uncertain.
                    String msg = "handleProviderLost reason=" + reason
                            + ", mProviderName=" + mProviderName
                            + ", currentState=" + currentState;
                    debugLog(msg);
                    // This is an unusual PROVIDER_STATE_STARTED_UNCERTAIN state because
                    // event == null
                    ProviderState newState = currentState.newState(
                            PROVIDER_STATE_STARTED_UNCERTAIN, null,
                            currentState.currentUserConfiguration, msg);
                    setCurrentState(newState, true);
                    break;
                }
                case PROVIDER_STATE_STOPPED: {
                    debugLog("handleProviderLost reason=" + reason
                            + ", mProviderName=" + mProviderName
                            + ", currentState=" + currentState
                            + ": No state change required, provider is stopped.");
                    break;
                }
                case PROVIDER_STATE_PERM_FAILED:
                case PROVIDER_STATE_DESTROYED: {
                    debugLog("handleProviderLost reason=" + reason
                            + ", mProviderName=" + mProviderName
                            + ", currentState=" + currentState
                            + ": No state change required, provider is terminated.");
                    break;
                }
                default: {
                    throw new IllegalStateException("Unknown currentState=" + currentState);
                }
            }
        }
    }

    private void handleOnProviderBound() {
        mThreadingDomain.assertCurrentThread();

        synchronized (mSharedLock) {
            ProviderState currentState = mCurrentState.get();
            switch (currentState.stateEnum) {
                case PROVIDER_STATE_STARTED_INITIALIZING:
                case PROVIDER_STATE_STARTED_CERTAIN:
                case PROVIDER_STATE_STARTED_UNCERTAIN: {
                    debugLog("handleOnProviderBound mProviderName=" + mProviderName
                            + ", currentState=" + currentState + ": Provider is started.");
                    break;
                }
                case PROVIDER_STATE_STOPPED: {
                    debugLog("handleOnProviderBound mProviderName=" + mProviderName
                            + ", currentState=" + currentState + ": Provider is stopped.");
                    break;
                }
                case PROVIDER_STATE_PERM_FAILED:
                case PROVIDER_STATE_DESTROYED: {
                    debugLog("handleOnProviderBound"
                            + ", mProviderName=" + mProviderName
                            + ", currentState=" + currentState
                            + ": No state change required, provider is terminated.");
                    break;
                }
                default: {
                    throw new IllegalStateException("Unknown currentState=" + currentState);
                }
            }
        }
    }

    @Override
    void onStartUpdates(@NonNull Duration initializationTimeout) {
        // Set a request on the proxy - it will be sent immediately if the service is bound,
        // or will be sent as soon as the service becomes bound.
        TimeZoneProviderRequest request =
                TimeZoneProviderRequest.createStartUpdatesRequest(initializationTimeout);
        mProxy.setRequest(request);
    }

    @Override
    void onStopUpdates() {
        TimeZoneProviderRequest request = TimeZoneProviderRequest.createStopUpdatesRequest();
        mProxy.setRequest(request);
    }

    /**
     * Passes the supplied test command to the current proxy.
     */
    @Override
    void handleTestCommand(@NonNull TestCommand testCommand, @Nullable RemoteCallback callback) {
        mThreadingDomain.assertCurrentThread();

        mProxy.handleTestCommand(testCommand, callback);
    }

    @Override
    public void dump(@NonNull IndentingPrintWriter ipw, @Nullable String[] args) {
        synchronized (mSharedLock) {
            ipw.println("{BinderLocationTimeZoneProvider}");
            ipw.println("mProviderName=" + mProviderName);
            ipw.println("mCurrentState=" + mCurrentState);
            ipw.println("mProxy=" + mProxy);

            ipw.println("State history:");
            ipw.increaseIndent();
            mCurrentState.dump(ipw);
            ipw.decreaseIndent();

            ipw.println("Proxy details:");
            ipw.increaseIndent();
            mProxy.dump(ipw, args);
            ipw.decreaseIndent();
        }
    }

    @Override
    public String toString() {
        synchronized (mSharedLock) {
            return "BinderLocationTimeZoneProvider{"
                    + "mProviderName=" + mProviderName
                    + ", mCurrentState=" + mCurrentState
                    + ", mProxy=" + mProxy
                    + '}';
        }
    }
}
