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


import android.hardware.hdmi.HdmiControlManager;
import android.hardware.hdmi.IHdmiControlCallback;
import android.hardware.hdmi.IHdmiControlService;
import android.os.RemoteException;
import android.os.ShellCommand;
import android.util.Slog;

import java.io.PrintWriter;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

final class HdmiControlShellCommand extends ShellCommand {

    private static final String TAG = "HdmiShellCommand";

    private final IHdmiControlService.Stub mBinderService;

    final CountDownLatch mLatch;
    AtomicInteger mCecResult;
    IHdmiControlCallback.Stub mHdmiControlCallback;

    HdmiControlShellCommand(IHdmiControlService.Stub binderService) {
        mBinderService = binderService;
        mLatch = new CountDownLatch(1);
        mCecResult = new AtomicInteger();
        mHdmiControlCallback =
                new IHdmiControlCallback.Stub() {
                    @Override
                    public void onComplete(int result) {
                        getOutPrintWriter().println(" done (" + getResultString(result) + ")");
                        mCecResult.set(result);
                        mLatch.countDown();
                    }
                };
    }

    @Override
    public int onCommand(String cmd) {
        if (cmd == null) {
            return handleDefaultCommands(cmd);
        }

        try {
            return handleShellCommand(cmd);
        } catch (Exception e) {
            getErrPrintWriter().println(
                    "Caught error for command '" + cmd + "': " + e.getMessage());
            Slog.e(TAG, "Error handling hdmi_control shell command: " + cmd, e);
            return 1;
        }
    }

    @Override
    public void onHelp() {
        PrintWriter pw = getOutPrintWriter();

        pw.println("HdmiControlManager (hdmi_control) commands:");
        pw.println("  help");
        pw.println("      Print this help text.");
        pw.println("  onetouchplay, otp");
        pw.println("      Send the \"One Touch Play\" feature from a source to the TV");
        pw.println("  vendorcommand --device_type <originating device type>");
        pw.println("                --destination <destination device>");
        pw.println("                --args <vendor specific arguments>");
        pw.println("                [--id <true if vendor command should be sent with vendor id>]");
        pw.println("      Send a Vendor Command to the given target device");
        pw.println("  cec_setting get <setting name>");
        pw.println("      Get the current value of a CEC setting");
        pw.println("  cec_setting set <setting name> <value>");
        pw.println("      Set the value of a CEC setting");
        pw.println("  setsystemaudiomode, setsam [on|off]");
        pw.println("      Sets the System Audio Mode feature on or off on TV devices");
        pw.println("  setarc [on|off]");
        pw.println("      Sets the ARC feature on or off on TV devices");
        pw.println("  deviceselect <device id>");
        pw.println("      Switch to device with given id");
        pw.println("      The device's id is represented by its logical address.");
        pw.println("  history_size get");
        pw.println("      Gets the number of messages that can be stored in dumpsys history");
        pw.println("  history_size set <new_size>");
        pw.println("      Changes the number of messages that can be stored in dumpsys history to"
                       + " new_size");
    }

    private int handleShellCommand(String cmd) throws RemoteException {
        PrintWriter pw = getOutPrintWriter();

        switch (cmd) {
            case "otp":
            case "onetouchplay":
                return oneTouchPlay(pw);
            case "vendorcommand":
                return vendorCommand(pw);
            case "cec_setting":
                return cecSetting(pw);
            case "setsystemaudiomode":
            case "setsam":
                return setSystemAudioMode(pw);
            case "setarc":
                return setArcMode(pw);
            case "deviceselect":
                return deviceSelect(pw);
            case "history_size":
                return historySize(pw);
        }

        return handleDefaultCommands(cmd);
    }

    private int deviceSelect(PrintWriter pw) throws RemoteException {
        if (getRemainingArgsCount() != 1) {
            throw new IllegalArgumentException("Expected exactly 1 argument.");
        }
        int deviceId = Integer.parseInt(getNextArg());

        pw.print("Sending Device Select...");
        mBinderService.deviceSelect(deviceId, mHdmiControlCallback);

        if (!receiveCallback("Device Select")) {
            return 1;
        }

        return mCecResult.get() == HdmiControlManager.RESULT_SUCCESS ? 0 : 1;
    }

    private int oneTouchPlay(PrintWriter pw) throws RemoteException {
        pw.print("Sending One Touch Play...");
        mBinderService.oneTouchPlay(mHdmiControlCallback);

        if (!receiveCallback("One Touch Play")) {
            return 1;
        }

        return mCecResult.get() == HdmiControlManager.RESULT_SUCCESS ? 0 : 1;
    }

    private int vendorCommand(PrintWriter pw) throws RemoteException {
        if (6 > getRemainingArgsCount()) {
            throw new IllegalArgumentException("Expected 3 arguments.");
        }

        int deviceType = -1;
        int destination = -1;
        String parameters = "";
        boolean hasVendorId = false;

        String arg = getNextOption();
        while (arg != null) {
            switch (arg) {
                case "-t":
                case "--device_type":
                    deviceType = Integer.parseInt(getNextArgRequired());
                    break;
                case "-d":
                case "--destination":
                    destination = Integer.parseInt(getNextArgRequired());
                    break;
                case "-a":
                case "--args":
                    parameters = getNextArgRequired();
                    break;
                case "-i":
                case "--id":
                    hasVendorId = Boolean.parseBoolean(getNextArgRequired());
                    break;
                default:
                    throw new IllegalArgumentException("Unknown argument: " + arg);
            }
            arg = getNextArg();
        }

        String[] parts = parameters.split(":");
        byte[] params = new byte[parts.length];
        for (int i = 0; i < params.length; i++) {
            params[i] = (byte) Integer.parseInt(parts[i], 16);
        }

        pw.println("Sending <Vendor Command>");
        mBinderService.sendVendorCommand(deviceType, destination, params, hasVendorId);
        return 0;
    }

    private int cecSetting(PrintWriter pw) throws RemoteException {
        if (getRemainingArgsCount() < 1) {
            throw new IllegalArgumentException("Expected at least 1 argument (operation).");
        }
        String operation = getNextArgRequired();
        switch (operation) {
            case "get": {
                String setting = getNextArgRequired();
                try {
                    String value = mBinderService.getCecSettingStringValue(setting);
                    pw.println(setting + " = " + value);
                } catch (IllegalArgumentException e) {
                    int intValue = mBinderService.getCecSettingIntValue(setting);
                    pw.println(setting + " = " + intValue);
                }
                return 0;
            }
            case "set": {
                String setting = getNextArgRequired();
                String value = getNextArgRequired();
                try {
                    mBinderService.setCecSettingStringValue(setting, value);
                    pw.println(setting + " = " + value);
                } catch (IllegalArgumentException e) {
                    int intValue = Integer.parseInt(value);
                    mBinderService.setCecSettingIntValue(setting, intValue);
                    pw.println(setting + " = " + intValue);
                }
                return 0;
            }
            default:
                throw new IllegalArgumentException("Unknown operation: " + operation);
        }
    }

    private int setSystemAudioMode(PrintWriter pw) throws RemoteException {
        if (1 > getRemainingArgsCount()) {
            throw new IllegalArgumentException(
                    "Please indicate if System Audio Mode should be turned \"on\" or \"off\".");
        }

        String arg = getNextArg();
        if (arg.equals("on")) {
            pw.println("Setting System Audio Mode on");
            mBinderService.setSystemAudioMode(true, mHdmiControlCallback);
        } else if (arg.equals("off")) {
            pw.println("Setting System Audio Mode off");
            mBinderService.setSystemAudioMode(false, mHdmiControlCallback);
        } else {
            throw new IllegalArgumentException(
                    "Please indicate if System Audio Mode should be turned \"on\" or \"off\".");
        }

        if (!receiveCallback("Set System Audio Mode")) {
            return 1;
        }

        return mCecResult.get() == HdmiControlManager.RESULT_SUCCESS ? 0 : 1;
    }

    private int setArcMode(PrintWriter pw) throws RemoteException {
        if (1 > getRemainingArgsCount()) {
            throw new IllegalArgumentException(
                    "Please indicate if ARC mode should be turned \"on\" or \"off\".");
        }

        String arg = getNextArg();
        if (arg.equals("on")) {
            pw.println("Setting ARC mode on");
            mBinderService.setArcMode(true);
        } else if (arg.equals("off")) {
            pw.println("Setting ARC mode off");
            mBinderService.setArcMode(false);
        } else {
            throw new IllegalArgumentException(
                    "Please indicate if ARC mode should be turned \"on\" or \"off\".");
        }

        return 0;
    }

    private int historySize(PrintWriter pw) throws RemoteException {
        if (1 > getRemainingArgsCount()) {
            throw new IllegalArgumentException("Use 'set' or 'get' for the command action");
        }

        String operation = getNextArgRequired();
        switch (operation) {
            case "get": {
                int value = mBinderService.getMessageHistorySize();
                pw.println("CEC dumpsys message history size = " + value);
                return 0;
            }
            case "set": {
                String arg = getNextArgRequired();
                int value;
                try {
                    value = Integer.parseInt(arg);
                } catch (NumberFormatException nfe) {
                    pw.println("Cannot set CEC dumpsys message history size to " + arg);
                    return 1;
                }
                if (mBinderService.setMessageHistorySize(value)) {
                    pw.println("Setting CEC dumpsys message history size to " + value);
                } else {
                    pw.println(
                            "Message history size not changed, was it lower than the minimum "
                                    + "size?");
                }
                return 0;
            }
            default:
                throw new IllegalArgumentException("Unknown operation: " + operation);
        }
    }

    private boolean receiveCallback(String command) {
        try {
            if (!mLatch.await(HdmiConfig.TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
                getErrPrintWriter().println(command + " timed out.");
                return false;
            }
        } catch (InterruptedException e) {
            getErrPrintWriter().println("Caught InterruptedException");
            Thread.currentThread().interrupt();
        }
        return true;
    }

    private String getResultString(int result) {
        switch (result) {
            case HdmiControlManager.RESULT_SUCCESS:
                return "Success";
            case HdmiControlManager.RESULT_TIMEOUT:
                return "Timeout";
            case HdmiControlManager.RESULT_SOURCE_NOT_AVAILABLE:
                return "Source not available";
            case HdmiControlManager.RESULT_TARGET_NOT_AVAILABLE:
                return "Target not available";
            case HdmiControlManager.RESULT_EXCEPTION:
                return "Exception";
            case HdmiControlManager.RESULT_INCORRECT_MODE:
                return "Incorrect mode";
            case HdmiControlManager.RESULT_COMMUNICATION_FAILED:
                return "Communication Failed";
        }
        return Integer.toString(result);
    }
}
