/*
 * 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.server.hdmi;

import android.annotation.CallSuper;
import android.hardware.hdmi.HdmiControlManager;
import android.hardware.hdmi.HdmiPortInfo;
import android.hardware.hdmi.IHdmiControlCallback;
import android.os.Handler;
import android.sysprop.HdmiProperties;
import android.util.Slog;

import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.hdmi.Constants.LocalActivePort;
import com.android.server.hdmi.HdmiAnnotations.ServiceThreadOnly;

import java.util.ArrayList;
import java.util.List;

/**
 * Represent a logical source device residing in Android system.
 */
abstract class HdmiCecLocalDeviceSource extends HdmiCecLocalDevice {

    private static final String TAG = "HdmiCecLocalDeviceSource";

    // Device has cec switch functionality or not.
    // Default is false.
    protected boolean mIsSwitchDevice = HdmiProperties.is_switch().orElse(false);

    // Routing port number used for Routing Control.
    // This records the default routing port or the previous valid routing port.
    // Default is HOME input.
    // Note that we don't save active path here because for source device,
    // new Active Source physical address might not match the active path
    @GuardedBy("mLock")
    @LocalActivePort
    private int mRoutingPort = Constants.CEC_SWITCH_HOME;

    // This records the current input of the device.
    // When device is switched to ARC input, mRoutingPort does not record it
    // since it's not an HDMI port used for Routing Control.
    // mLocalActivePort will record whichever input we switch to to keep tracking on
    // the current input status of the device.
    // This can help prevent duplicate switching and provide status information.
    @GuardedBy("mLock")
    @LocalActivePort
    protected int mLocalActivePort = Constants.CEC_SWITCH_HOME;

    // Whether the Routing Coutrol feature is enabled or not. False by default.
    @GuardedBy("mLock")
    protected boolean mRoutingControlFeatureEnabled;

    private Handler mHandler;

    protected HdmiCecLocalDeviceSource(HdmiControlService service, int deviceType) {
        super(service, deviceType);
        mHandler = new Handler(service.getServiceLooper());
    }

    @ServiceThreadOnly
    void queryDisplayStatus(IHdmiControlCallback callback) {
        assertRunOnServiceThread();
        List<DevicePowerStatusAction> actions = getActions(DevicePowerStatusAction.class);
        if (!actions.isEmpty()) {
            Slog.i(TAG, "queryDisplayStatus already in progress");
            actions.get(0).addCallback(callback);
            return;
        }
        DevicePowerStatusAction action = DevicePowerStatusAction.create(this, Constants.ADDR_TV,
                callback);
        if (action == null) {
            Slog.w(TAG, "Cannot initiate queryDisplayStatus");
            invokeCallback(callback, HdmiControlManager.POWER_STATUS_UNKNOWN);
            return;
        }
        addAndStartAction(action);
    }

    @Override
    @ServiceThreadOnly
    void onHotplug(int portId, boolean connected) {
        assertRunOnServiceThread();
        HdmiPortInfo portInfo = mService.getPortInfo(portId);
        if (portInfo != null && portInfo.getType() == HdmiPortInfo.PORT_OUTPUT) {
            mCecMessageCache.flushAll();
        }
        // We'll not invalidate the active source on the hotplug event to pass CETC 11.2.2-2 ~ 3.
        if (connected) {
            mService.wakeUp();
        }
    }

    @Override
    @ServiceThreadOnly
    protected void sendStandby(int deviceId) {
        assertRunOnServiceThread();
        String powerControlMode = mService.getHdmiCecConfig().getStringValue(
                HdmiControlManager.CEC_SETTING_NAME_POWER_CONTROL_MODE);
        if (powerControlMode.equals(HdmiControlManager.POWER_CONTROL_MODE_BROADCAST)) {
            mService.sendCecCommand(
                    HdmiCecMessageBuilder.buildStandby(
                            getDeviceInfo().getLogicalAddress(), Constants.ADDR_BROADCAST));
            return;
        }
        mService.sendCecCommand(
                HdmiCecMessageBuilder.buildStandby(
                        getDeviceInfo().getLogicalAddress(), Constants.ADDR_TV));
        if (powerControlMode.equals(HdmiControlManager.POWER_CONTROL_MODE_TV_AND_AUDIO_SYSTEM)) {
            mService.sendCecCommand(
                    HdmiCecMessageBuilder.buildStandby(
                            getDeviceInfo().getLogicalAddress(), Constants.ADDR_AUDIO_SYSTEM));
        }
    }

    @ServiceThreadOnly
    void oneTouchPlay(IHdmiControlCallback callback) {
        assertRunOnServiceThread();
        List<OneTouchPlayAction> actions = getActions(OneTouchPlayAction.class);
        if (!actions.isEmpty()) {
            Slog.i(TAG, "oneTouchPlay already in progress");
            actions.get(0).addCallback(callback);
            return;
        }
        OneTouchPlayAction action = OneTouchPlayAction.create(this, Constants.ADDR_TV,
                callback);
        if (action == null) {
            Slog.w(TAG, "Cannot initiate oneTouchPlay");
            invokeCallback(callback, HdmiControlManager.RESULT_EXCEPTION);
            return;
        }
        addAndStartAction(action);
    }

    @ServiceThreadOnly
    void toggleAndFollowTvPower() {
        assertRunOnServiceThread();
        if (mService.getPowerManager().isInteractive()) {
            mService.pauseActiveMediaSessions();
        } else {
            // Wake up Android framework to take over CEC control from the microprocessor.
            mService.wakeUp();
        }
        mService.queryDisplayStatus(new IHdmiControlCallback.Stub() {
            @Override
            public void onComplete(int status) {
                if (status == HdmiControlManager.POWER_STATUS_UNKNOWN) {
                    Slog.i(TAG, "TV power toggle: TV power status unknown");
                    sendUserControlPressedAndReleased(Constants.ADDR_TV,
                            HdmiCecKeycode.CEC_KEYCODE_POWER_TOGGLE_FUNCTION);
                    // Source device remains awake.
                } else if (status == HdmiControlManager.POWER_STATUS_ON
                        || status == HdmiControlManager.POWER_STATUS_TRANSIENT_TO_ON) {
                    Slog.i(TAG, "TV power toggle: turning off TV");
                    sendStandby(0 /*unused */);
                    // Source device goes to standby, to follow the toggled TV power state.
                    mService.standby();
                } else if (status == HdmiControlManager.POWER_STATUS_STANDBY
                        || status == HdmiControlManager.POWER_STATUS_TRANSIENT_TO_STANDBY) {
                    Slog.i(TAG, "TV power toggle: turning on TV");
                    oneTouchPlay(new IHdmiControlCallback.Stub() {
                        @Override
                        public void onComplete(int result) {
                            if (result != HdmiControlManager.RESULT_SUCCESS) {
                                Slog.w(TAG, "Failed to complete One Touch Play. result=" + result);
                                sendUserControlPressedAndReleased(Constants.ADDR_TV,
                                        HdmiCecKeycode.CEC_KEYCODE_POWER_TOGGLE_FUNCTION);
                            }
                        }
                    });
                    // Source device remains awake, to follow the toggled TV power state.
                }
            }
        });
    }

    @ServiceThreadOnly
    protected void onActiveSourceLost() {
        // Nothing to do.
    }

    @Override
    @CallSuper
    @ServiceThreadOnly
    void setActiveSource(int logicalAddress, int physicalAddress, String caller) {
        boolean wasActiveSource = isActiveSource();
        super.setActiveSource(logicalAddress, physicalAddress, caller);
        if (!isActiveSource()) {
            // Prevent focus stealing when losing active source.
            removeAction(ActiveSourceAction.class);
            if (wasActiveSource) {
                onActiveSourceLost();
            }
        }
    }

    @ServiceThreadOnly
    protected void setActiveSource(int physicalAddress, String caller) {
        assertRunOnServiceThread();
        // Invalidate the internal active source record.
        ActiveSource activeSource = ActiveSource.of(Constants.ADDR_INVALID, physicalAddress);
        setActiveSource(activeSource, caller);
    }

    @ServiceThreadOnly
    @Constants.HandleMessageResult
    protected int handleActiveSource(HdmiCecMessage message) {
        assertRunOnServiceThread();
        int logicalAddress = message.getSource();
        int physicalAddress = HdmiUtils.twoBytesToInt(message.getParams());
        ActiveSource activeSource = ActiveSource.of(logicalAddress, physicalAddress);
        if (!getActiveSource().equals(activeSource)) {
            setActiveSource(activeSource, "HdmiCecLocalDeviceSource#handleActiveSource()");
        }
        updateDevicePowerStatus(logicalAddress, HdmiControlManager.POWER_STATUS_ON);
        if (isRoutingControlFeatureEnabled()) {
            switchInputOnReceivingNewActivePath(physicalAddress);
        }
        return Constants.HANDLED;
    }

    @Override
    @ServiceThreadOnly
    @Constants.HandleMessageResult
    protected int handleRequestActiveSource(HdmiCecMessage message) {
        assertRunOnServiceThread();
        maySendActiveSource(message.getSource());
        return Constants.HANDLED;
    }

    @Override
    @ServiceThreadOnly
    @Constants.HandleMessageResult
    protected int handleSetStreamPath(HdmiCecMessage message) {
        assertRunOnServiceThread();
        int physicalAddress = HdmiUtils.twoBytesToInt(message.getParams());
        int localDevicePhysicalAddress = mService.getPhysicalAddress();
        // If current device is the target path and it's a playback device, set it as Active Source.
        if (physicalAddress == localDevicePhysicalAddress && mService.isPlaybackDevice()) {
            setAndBroadcastActiveSource(message, physicalAddress,
                    "HdmiCecLocalDeviceSource#handleSetStreamPath()");
        } else if (isActiveSource()
                && HdmiUtils.isTailOfActivePath(localDevicePhysicalAddress, physicalAddress)) {
            // If <Set Stream Path> targeting an ancestor address is received while this device is
            // the active source, wait to see if a new Active Source is declared, as a parent switch
            // might be changing paths.
            // E.g. TV [0.0.0.0] ------ AVR [3.0.0.0] ------ Playback [3.1.0.0] (Active Source)
            // TV sends <Set Stream Path> [3.0.0.0]
            //    AVR sends <Routing Information> [3.2.0.0] -> Playback [3.1.0.0] is no longer AS.
            //    AVR sends <Routing Information> [3.1.0.0] -> Playback [3.1.0.0] stays <AS>.
            //    AVR sends no relevant CEC message         -> Playback [3.1.0.0] stays <AS>.
            return Constants.HANDLED;
        } else if (physicalAddress != localDevicePhysicalAddress || !isActiveSource()) {
            // Invalidate the active source if stream path is set to another physical address or
            // our physical address while not active source
            setActiveSource(physicalAddress, "HdmiCecLocalDeviceSource#handleSetStreamPath()");
        }
        switchInputOnReceivingNewActivePath(physicalAddress);
        return Constants.HANDLED;
    }

    @Override
    @ServiceThreadOnly
    @Constants.HandleMessageResult
    protected int handleRoutingChange(HdmiCecMessage message) {
        assertRunOnServiceThread();
        int physicalAddress = HdmiUtils.twoBytesToInt(message.getParams(), 2);
        if (physicalAddress != mService.getPhysicalAddress() || !isActiveSource()) {
            // Invalidate the active source if routing is changed to other physical address or
            // our physical address while not active source
            setActiveSource(physicalAddress, "HdmiCecLocalDeviceSource#handleRoutingChange()");
        }
        if (!isRoutingControlFeatureEnabled()) {
            return Constants.ABORT_REFUSED;
        }
        handleRoutingChangeAndInformation(physicalAddress, message);
        return Constants.HANDLED;
    }

    @Override
    @ServiceThreadOnly
    @Constants.HandleMessageResult
    protected int handleRoutingInformation(HdmiCecMessage message) {
        assertRunOnServiceThread();
        int physicalAddress = HdmiUtils.twoBytesToInt(message.getParams());
        if (physicalAddress != mService.getPhysicalAddress() || !isActiveSource()) {
            // Invalidate the active source if routing is changed to other physical address or
            // our physical address while not active source
            setActiveSource(physicalAddress, "HdmiCecLocalDeviceSource#handleRoutingInformation()");
        }
        if (!isRoutingControlFeatureEnabled()) {
            return Constants.ABORT_REFUSED;
        }
        handleRoutingChangeAndInformation(physicalAddress, message);
        return Constants.HANDLED;
    }

    // Method to switch Input with the new Active Path.
    // All the devices with Switch functionality should implement this.
    protected void switchInputOnReceivingNewActivePath(int physicalAddress) {
        // do nothing
    }

    // Only source devices that react to routing control messages should implement
    // this method (e.g. a TV with built in switch).
    protected void handleRoutingChangeAndInformation(int physicalAddress, HdmiCecMessage message) {
        // do nothing
    }

    @Override
    @ServiceThreadOnly
    protected void disableDevice(boolean initiatedByCec, PendingActionClearedCallback callback) {
        removeAction(OneTouchPlayAction.class);
        removeAction(DevicePowerStatusAction.class);

        super.disableDevice(initiatedByCec, callback);
    }

    // Update the power status of the devices connected to the current device.
    // This only works if the current device is a switch and keeps tracking the device info
    // of the device connected to it.
    protected void updateDevicePowerStatus(int logicalAddress, int newPowerStatus) {
        // do nothing
    }

    @Constants.RcProfile
    @Override
    protected int getRcProfile() {
        return Constants.RC_PROFILE_SOURCE;
    }

    @Override
    protected List<Integer> getRcFeatures() {
        List<Integer> features = new ArrayList<>();
        HdmiCecConfig hdmiCecConfig = mService.getHdmiCecConfig();
        if (hdmiCecConfig.getIntValue(
                HdmiControlManager.CEC_SETTING_NAME_RC_PROFILE_SOURCE_HANDLES_ROOT_MENU)
                == HdmiControlManager.RC_PROFILE_SOURCE_MENU_HANDLED) {
            features.add(Constants.RC_PROFILE_SOURCE_HANDLES_ROOT_MENU);
        }
        if (hdmiCecConfig.getIntValue(
                HdmiControlManager.CEC_SETTING_NAME_RC_PROFILE_SOURCE_HANDLES_SETUP_MENU)
                == HdmiControlManager.RC_PROFILE_SOURCE_MENU_HANDLED) {
            features.add(Constants.RC_PROFILE_SOURCE_HANDLES_SETUP_MENU);
        }
        if (hdmiCecConfig.getIntValue(
                HdmiControlManager.CEC_SETTING_NAME_RC_PROFILE_SOURCE_HANDLES_CONTENTS_MENU)
                == HdmiControlManager.RC_PROFILE_SOURCE_MENU_HANDLED) {
            features.add(Constants.RC_PROFILE_SOURCE_HANDLES_CONTENTS_MENU);
        }
        if (hdmiCecConfig.getIntValue(
                HdmiControlManager.CEC_SETTING_NAME_RC_PROFILE_SOURCE_HANDLES_TOP_MENU)
                == HdmiControlManager.RC_PROFILE_SOURCE_MENU_HANDLED) {
            features.add(Constants.RC_PROFILE_SOURCE_HANDLES_TOP_MENU);
        }
        if (hdmiCecConfig.getIntValue(HdmiControlManager
                .CEC_SETTING_NAME_RC_PROFILE_SOURCE_HANDLES_MEDIA_CONTEXT_SENSITIVE_MENU)
                == HdmiControlManager.RC_PROFILE_SOURCE_MENU_HANDLED) {
            features.add(Constants.RC_PROFILE_SOURCE_HANDLES_MEDIA_CONTEXT_SENSITIVE_MENU);
        }
        return features;
    }

    // Active source claiming needs to be handled in Service
    // since service can decide who will be the active source when the device supports
    // multiple device types in this method.
    // This method should only be called when the device can be the active source.
    protected void setAndBroadcastActiveSource(HdmiCecMessage message, int physicalAddress,
            String caller) {
        mService.setAndBroadcastActiveSource(
                physicalAddress, getDeviceInfo().getDeviceType(), message.getSource(), caller);
    }

    // Indicates if current device is the active source or not
    @ServiceThreadOnly
    protected boolean isActiveSource() {
        if (getDeviceInfo() == null) {
            return false;
        }

        return getActiveSource().equals(getDeviceInfo().getLogicalAddress(),
                getDeviceInfo().getPhysicalAddress());
    }

    protected void wakeUpIfActiveSource() {
        if (!isActiveSource()) {
            return;
        }
        // Wake up the device. This will also exit dream mode.
        mService.wakeUp();
        return;
    }

    protected void maySendActiveSource(int dest) {
        if (!isActiveSource()) {
            return;
        }
        mService.setWakeUpMessageReceived(false);
        addAndStartAction(new ActiveSourceAction(this, dest), true);
    }

    /**
     * Set {@link #mRoutingPort} to a specific {@link LocalActivePort} to record the current active
     * CEC Routing Control related port.
     *
     * @param portId The portId of the new routing port.
     */
    @VisibleForTesting
    protected void setRoutingPort(@LocalActivePort int portId) {
        synchronized (mLock) {
            mRoutingPort = portId;
        }
    }

    /**
     * Get {@link #mRoutingPort}. This is useful when the device needs to route to the last valid
     * routing port.
     */
    @LocalActivePort
    protected int getRoutingPort() {
        synchronized (mLock) {
            return mRoutingPort;
        }
    }

    /**
     * Get {@link #mLocalActivePort}. This is useful when device needs to know the current active
     * port.
     */
    @LocalActivePort
    protected int getLocalActivePort() {
        synchronized (mLock) {
            return mLocalActivePort;
        }
    }

    /**
     * Set {@link #mLocalActivePort} to a specific {@link LocalActivePort} to record the current
     * active port.
     *
     * <p>It does not have to be a Routing Control related port. For example it can be
     * set to {@link Constants#CEC_SWITCH_ARC} but this port is System Audio related.
     *
     * @param activePort The portId of the new active port.
     */
    protected void setLocalActivePort(@LocalActivePort int activePort) {
        synchronized (mLock) {
            mLocalActivePort = activePort;
        }
    }

    boolean isRoutingControlFeatureEnabled() {
        synchronized (mLock) {
            return mRoutingControlFeatureEnabled;
        }
    }

    // Check if the device is trying to switch to the same input that is active right now.
    // This can help avoid redundant port switching.
    protected boolean isSwitchingToTheSameInput(@LocalActivePort int activePort) {
        return activePort == getLocalActivePort();
    }
}
