/*
 * Copyright 2022 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.internal.telephony;

import android.annotation.NonNull;
import android.os.AsyncResult;
import android.os.Build;
import android.os.Handler;
import android.os.HandlerExecutor;
import android.os.Message;
import android.telephony.CellBroadcastIdRange;
import android.telephony.SmsCbMessage;
import android.telephony.SubscriptionManager;
import android.telephony.TelephonyManager;
import android.util.IndentingPrintWriter;
import android.util.LocalLog;

import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.telephony.cdma.CdmaSmsBroadcastConfigInfo;
import com.android.internal.telephony.gsm.SmsBroadcastConfigInfo;
import com.android.internal.util.State;
import com.android.internal.util.StateMachine;

import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.function.Consumer;

/**
 * This class is to track the state to set cell broadcast config
 */

public final class CellBroadcastConfigTracker extends StateMachine {
    private static final boolean DBG = Build.IS_DEBUGGABLE;

    private static final int EVENT_REQUEST = 1;
    private static final int EVENT_CONFIGURATION_DONE = 2;
    private static final int EVENT_ACTIVATION_DONE = 3;
    private static final int EVENT_RADIO_OFF = 4;
    private static final int EVENT_SUBSCRIPTION_CHANGED = 5;
    @VisibleForTesting
    public static final int EVENT_RADIO_RESET = 6;

    private static final int SMS_CB_CODE_SCHEME_MIN = 0;
    private static final int SMS_CB_CODE_SCHEME_MAX = 255;

    // Cache of current cell broadcast id ranges of 3gpp
    private List<CellBroadcastIdRange> mCbRanges3gpp = new CopyOnWriteArrayList<>();
    // Cache of current cell broadcast id ranges of 3gpp2
    private List<CellBroadcastIdRange> mCbRanges3gpp2 = new CopyOnWriteArrayList<>();
    private Phone mPhone;
    private final LocalLog mLocalLog = new LocalLog(128);
    @VisibleForTesting
    public int mSubId;
    @VisibleForTesting
    public final SubscriptionManager.OnSubscriptionsChangedListener mSubChangedListener =
            new SubscriptionManager.OnSubscriptionsChangedListener() {
                @Override
                public void onSubscriptionsChanged() {
                    sendMessage(EVENT_SUBSCRIPTION_CHANGED);
                }
            };

    /**
     * The class is to present the request to set cell broadcast id ranges
     */
    private static class Request {
        private final List<CellBroadcastIdRange> mCbRangesRequest3gpp =
                new CopyOnWriteArrayList<>();
        private final List<CellBroadcastIdRange> mCbRangesRequest3gpp2 =
                new CopyOnWriteArrayList<>();
        Consumer<Integer> mCallback;

        Request(@NonNull List<CellBroadcastIdRange> ranges, @NonNull Consumer<Integer> callback) {
            ranges.forEach(r -> {
                if (r.getType() == SmsCbMessage.MESSAGE_FORMAT_3GPP) {
                    mCbRangesRequest3gpp.add(r);
                } else {
                    mCbRangesRequest3gpp2.add(r);
                }
            });
            mCallback = callback;
        }

        List<CellBroadcastIdRange> get3gppRanges() {
            return mCbRangesRequest3gpp;
        }

        List<CellBroadcastIdRange> get3gpp2Ranges() {
            return mCbRangesRequest3gpp2;
        }

        Consumer<Integer> getCallback() {
            return mCallback;
        }

        @Override
        public String toString() {
            return "Request[mCbRangesRequest3gpp = " + mCbRangesRequest3gpp + ", "
                    + "mCbRangesRequest3gpp2 = " + mCbRangesRequest3gpp2 + "]";
        }
    }

    /**
     * The default state.
     */
    private class DefaultState extends State {
        @Override
        public void enter() {
            mPhone.registerForRadioOffOrNotAvailable(getHandler(), EVENT_RADIO_OFF, null);
            mPhone.mCi.registerForModemReset(getHandler(), EVENT_RADIO_RESET, null);
            mPhone.getContext().getSystemService(SubscriptionManager.class)
                    .addOnSubscriptionsChangedListener(new HandlerExecutor(getHandler()),
                            mSubChangedListener);
        }

        @Override
        public void exit() {
            mPhone.unregisterForRadioOffOrNotAvailable(getHandler());
            mPhone.mCi.unregisterForModemReset(getHandler());
            mPhone.getContext().getSystemService(SubscriptionManager.class)
                    .removeOnSubscriptionsChangedListener(mSubChangedListener);
        }

        @Override
        public boolean processMessage(Message msg) {
            boolean retVal = HANDLED;
            if (DBG) {
                logd("DefaultState message:" + msg.what);
            }
            switch (msg.what) {
                case EVENT_RADIO_OFF:
                case EVENT_RADIO_RESET:
                    resetConfig();
                    break;
                case EVENT_SUBSCRIPTION_CHANGED:
                    int subId = mPhone.getSubId();
                    if (mSubId != subId) {
                        log("SubId changed from " + mSubId + " to " + subId);
                        mSubId = subId;
                        resetConfig();
                    }
                    break;
                default:
                    log("unexpected message!");
                    break;

            }

            return retVal;
        }
    }

    private DefaultState mDefaultState = new DefaultState();

    /*
     * The idle state which does not have ongoing radio request.
     */
    private class IdleState extends State {
        @Override
        public boolean processMessage(Message msg) {
            boolean retVal = NOT_HANDLED;
            if (DBG) {
                logd("IdleState message:" + msg.what);
            }
            switch (msg.what) {
                case EVENT_REQUEST:
                    Request request = (Request) msg.obj;
                    if (DBG) {
                        logd("IdleState handle EVENT_REQUEST with request:" + request);
                        mLocalLog.log("IdleState handle EVENT_REQUEST with request:" + request
                                + ", mCbRanges3gpp:" + mCbRanges3gpp
                                + ", mCbRanges3gpp2:" + mCbRanges3gpp2);
                    }
                    if (!mCbRanges3gpp.equals(request.get3gppRanges())) {
                        // set gsm config if the config is changed
                        setGsmConfig(request.get3gppRanges(), request);
                        transitionTo(mGsmConfiguringState);
                    } else if (!mCbRanges3gpp2.equals(request.get3gpp2Ranges())) {
                        // set cdma config directly if no gsm config change but cdma config is
                        // changed
                        setCdmaConfig(request.get3gpp2Ranges(), request);
                        transitionTo(mCdmaConfiguringState);
                    } else {
                        logd("Do nothing as the requested ranges are same as now");
                        request.getCallback().accept(
                                TelephonyManager.CELL_BROADCAST_RESULT_SUCCESS);
                    }
                    retVal = HANDLED;
                    break;
                default:
                    break;
            }
            return retVal;
        }
    }
    private IdleState mIdleState = new IdleState();

    /*
     * The state waiting for the result to set gsm config.
     */
    private class GsmConfiguringState extends State {
        @Override
        public boolean processMessage(Message msg) {
            boolean retVal = NOT_HANDLED;
            if (DBG) {
                logd("GsmConfiguringState message:" + msg.what);
            }
            switch (msg.what) {
                case EVENT_REQUEST:
                    deferMessage(msg);
                    retVal = HANDLED;
                    break;
                case EVENT_CONFIGURATION_DONE:
                    AsyncResult ar = (AsyncResult) msg.obj;
                    Request request = (Request) ar.userObj;
                    if (DBG) {
                        logd("GsmConfiguringState handle EVENT_CONFIGURATION_DONE with request:"
                                + request);
                    }
                    if (ar.exception == null) {
                        // set gsm activation and transit to gsm activating state
                        setActivation(SmsCbMessage.MESSAGE_FORMAT_3GPP,
                                !request.get3gppRanges().isEmpty(), request);
                        transitionTo(mGsmActivatingState);
                    } else {
                        logd("Failed to set gsm config");
                        mLocalLog.log("GsmConfiguringState Failed to set gsm config:" + request);
                        request.getCallback().accept(
                                TelephonyManager.CELL_BROADCAST_RESULT_FAIL_CONFIG);
                        // transit to idle state on the failure case
                        transitionTo(mIdleState);
                    }
                    retVal = HANDLED;
                    break;
                default:
                    break;
            }
            return retVal;
        }
    }
    private GsmConfiguringState mGsmConfiguringState = new GsmConfiguringState();

    /*
     * The state waiting for the result to set gsm activation.
     */
    private class GsmActivatingState extends State {
        @Override
        public boolean processMessage(Message msg) {
            boolean retVal = NOT_HANDLED;
            if (DBG) {
                logd("GsmActivatingState message:" + msg.what);
            }
            switch (msg.what) {
                case EVENT_REQUEST:
                    deferMessage(msg);
                    retVal = HANDLED;
                    break;
                case EVENT_ACTIVATION_DONE:
                    AsyncResult ar = (AsyncResult) msg.obj;
                    Request request = (Request) ar.userObj;
                    if (DBG) {
                        logd("GsmActivatingState handle EVENT_ACTIVATION_DONE with request:"
                                + request);
                        mLocalLog.log("GsmActivatingState EVENT_ACTIVATION_DONE, exception:"
                                + ar.exception + ", request:" + request);
                    }
                    if (ar.exception == null) {
                        mCbRanges3gpp = request.get3gppRanges();
                        if (!mCbRanges3gpp2.equals(request.get3gpp2Ranges())) {
                            // set cdma config and transit to cdma configuring state if the config
                            // is changed.
                            setCdmaConfig(request.get3gpp2Ranges(), request);
                            transitionTo(mCdmaConfiguringState);
                        } else {
                            logd("Done as no need to update ranges for 3gpp2");
                            request.getCallback().accept(
                                    TelephonyManager.CELL_BROADCAST_RESULT_SUCCESS);
                            // transit to idle state if there is no cdma config change
                            transitionTo(mIdleState);
                        }
                    } else {
                        logd("Failed to set gsm activation");
                        request.getCallback().accept(
                                TelephonyManager.CELL_BROADCAST_RESULT_FAIL_ACTIVATION);
                        // transit to idle state on the failure case
                        transitionTo(mIdleState);
                    }
                    retVal = HANDLED;
                    break;
                default:
                    break;
            }
            return retVal;
        }
    }
    private GsmActivatingState mGsmActivatingState = new GsmActivatingState();

    /*
     * The state waiting for the result to set cdma config.
     */
    private class CdmaConfiguringState extends State {
        @Override
        public boolean processMessage(Message msg) {
            boolean retVal = NOT_HANDLED;
            if (DBG) {
                logd("CdmaConfiguringState message:" + msg.what);
            }
            switch (msg.what) {
                case EVENT_REQUEST:
                    deferMessage(msg);
                    retVal = HANDLED;
                    break;
                case EVENT_CONFIGURATION_DONE:
                    AsyncResult ar = (AsyncResult) msg.obj;
                    Request request = (Request) ar.userObj;
                    if (DBG) {
                        logd("CdmaConfiguringState handle EVENT_ACTIVATION_DONE with request:"
                                + request);
                    }
                    if (ar.exception == null) {
                        // set cdma activation and transit to cdma activating state
                        setActivation(SmsCbMessage.MESSAGE_FORMAT_3GPP2,
                                !request.get3gpp2Ranges().isEmpty(), request);
                        transitionTo(mCdmaActivatingState);
                    } else {
                        logd("Failed to set cdma config");
                        mLocalLog.log("CdmaConfiguringState Failed to set cdma config:" + request);
                        request.getCallback().accept(
                                TelephonyManager.CELL_BROADCAST_RESULT_FAIL_CONFIG);
                        // transit to idle state on the failure case
                        transitionTo(mIdleState);
                    }
                    retVal = HANDLED;
                    break;
                default:
                    break;
            }
            return retVal;
        }
    }
    private CdmaConfiguringState mCdmaConfiguringState = new CdmaConfiguringState();

    /*
     * The state waiting for the result to set cdma activation.
     */
    private class CdmaActivatingState extends State {
        @Override
        public boolean processMessage(Message msg) {
            boolean retVal = NOT_HANDLED;
            if (DBG) {
                logd("CdmaActivatingState message:" + msg.what);
            }
            switch (msg.what) {
                case EVENT_REQUEST:
                    deferMessage(msg);
                    retVal = HANDLED;
                    break;
                case EVENT_ACTIVATION_DONE:
                    AsyncResult ar = (AsyncResult) msg.obj;
                    Request request = (Request) ar.userObj;
                    if (DBG) {
                        logd("CdmaActivatingState handle EVENT_ACTIVATION_DONE with request:"
                                + request);
                        mLocalLog.log("CdmaActivatingState EVENT_ACTIVATION_DONE, exception:"
                                + ar.exception + ", request:" + request);
                    }
                    if (ar.exception == null) {
                        mCbRanges3gpp2 = request.get3gpp2Ranges();
                        request.getCallback().accept(
                                    TelephonyManager.CELL_BROADCAST_RESULT_SUCCESS);
                    } else {
                        logd("Failed to set cdma activation");
                        request.getCallback().accept(
                                TelephonyManager.CELL_BROADCAST_RESULT_FAIL_ACTIVATION);
                    }
                    // transit to idle state anyway
                    transitionTo(mIdleState);
                    retVal = HANDLED;
                    break;
                default:
                    break;
            }
            return retVal;
        }
    }
    private CdmaActivatingState mCdmaActivatingState = new CdmaActivatingState();

    private CellBroadcastConfigTracker(Phone phone) {
        super("CellBroadcastConfigTracker-" + phone.getPhoneId());
        init(phone);
    }

    private CellBroadcastConfigTracker(Phone phone, Handler handler) {
        super("CellBroadcastConfigTracker-" + phone.getPhoneId(), handler);
        init(phone);
    }

    private void init(Phone phone) {
        logd("init");
        mPhone = phone;
        mSubId = mPhone.getSubId();

        addState(mDefaultState);
        addState(mIdleState, mDefaultState);
        addState(mGsmConfiguringState, mDefaultState);
        addState(mGsmActivatingState, mDefaultState);
        addState(mCdmaConfiguringState, mDefaultState);
        addState(mCdmaActivatingState, mDefaultState);
        setInitialState(mIdleState);
    }

    /**
     * create a CellBroadcastConfigTracker instance for the phone
     */
    public static CellBroadcastConfigTracker make(Phone phone, Handler handler,
            boolean shouldStart) {
        CellBroadcastConfigTracker tracker = handler == null
                ? new CellBroadcastConfigTracker(phone)
                : new CellBroadcastConfigTracker(phone, handler);
        if (shouldStart) {
            tracker.start();
        }
        return tracker;
    }

    /**
     * Return current cell broadcast ranges.
     */
    @NonNull public List<CellBroadcastIdRange> getCellBroadcastIdRanges() {
        List<CellBroadcastIdRange> ranges = new ArrayList<>();
        ranges.addAll(mCbRanges3gpp);
        ranges.addAll(mCbRanges3gpp2);
        return ranges;
    }

    /**
     * Set reception of cell broadcast messages with the list of the given ranges.
     */
    public void setCellBroadcastIdRanges(
            @NonNull List<CellBroadcastIdRange> ranges, @NonNull Consumer<Integer> callback) {
        if (DBG) {
            logd("setCellBroadcastIdRanges with ranges:" + ranges);
        }
        ranges = mergeRangesAsNeeded(ranges);
        sendMessage(EVENT_REQUEST, new Request(ranges, callback));
    }

    /**
     * Merge the overlapped CellBroadcastIdRanges in the list as needed
     * @param ranges the list of CellBroadcastIdRanges
     * @return the list of CellBroadcastIdRanges without overlapping
     *
     * @throws IllegalArgumentException if there is conflict of the ranges. For instance,
     * the channel is enabled in some range, but disable in others.
     */
    @VisibleForTesting
    public static @NonNull List<CellBroadcastIdRange> mergeRangesAsNeeded(
            @NonNull List<CellBroadcastIdRange> ranges) throws IllegalArgumentException {
        ranges.sort((r1, r2) -> r1.getType() != r2.getType() ? r1.getType() - r2.getType()
                : (r1.getStartId() != r2.getStartId() ? r1.getStartId() - r2.getStartId()
                : r2.getEndId() - r1.getEndId()));
        final List<CellBroadcastIdRange> newRanges = new ArrayList<>();
        ranges.forEach(r -> {
            if (newRanges.isEmpty() || newRanges.get(newRanges.size() - 1).getType() != r.getType()
                    || newRanges.get(newRanges.size() - 1).getEndId() + 1 < r.getStartId()
                    || (newRanges.get(newRanges.size() - 1).getEndId() + 1 == r.getStartId()
                    && newRanges.get(newRanges.size() - 1).isEnabled() != r.isEnabled())) {
                newRanges.add(new CellBroadcastIdRange(r.getStartId(), r.getEndId(),
                        r.getType(), r.isEnabled()));
            } else {
                if (newRanges.get(newRanges.size() - 1).isEnabled() != r.isEnabled()) {
                    throw new IllegalArgumentException("range conflict " + r);
                }
                if (r.getEndId() > newRanges.get(newRanges.size() - 1).getEndId()) {
                    CellBroadcastIdRange range = newRanges.get(newRanges.size() - 1);
                    newRanges.set(newRanges.size() - 1, new CellBroadcastIdRange(
                            range.getStartId(), r.getEndId(), range.getType(), range.isEnabled()));
                }
            }
        });
        return newRanges;
    }

    private void resetConfig() {
        mCbRanges3gpp.clear();
        mCbRanges3gpp2.clear();
    }

    private void setGsmConfig(List<CellBroadcastIdRange> ranges, Request request) {
        if (DBG) {
            logd("setGsmConfig with " + ranges);
        }

        SmsBroadcastConfigInfo[] configs = new SmsBroadcastConfigInfo[ranges.size()];
        for (int i = 0; i < configs.length; i++) {
            CellBroadcastIdRange r = ranges.get(i);
            configs[i] = new SmsBroadcastConfigInfo(r.getStartId(), r.getEndId(),
                    SMS_CB_CODE_SCHEME_MIN, SMS_CB_CODE_SCHEME_MAX, r.isEnabled());
        }

        Message response = obtainMessage(EVENT_CONFIGURATION_DONE, request);
        mPhone.mCi.setGsmBroadcastConfig(configs, response);
    }

    private void setCdmaConfig(List<CellBroadcastIdRange> ranges, Request request) {
        if (DBG) {
            logd("setCdmaConfig with " + ranges);
        }

        CdmaSmsBroadcastConfigInfo[] configs =
                new CdmaSmsBroadcastConfigInfo[ranges.size()];
        for (int i = 0; i < configs.length; i++) {
            CellBroadcastIdRange r = ranges.get(i);
            configs[i] = new CdmaSmsBroadcastConfigInfo(
                    r.getStartId(), r.getEndId(), 1, r.isEnabled());
        }

        Message response = obtainMessage(EVENT_CONFIGURATION_DONE, request);
        mPhone.mCi.setCdmaBroadcastConfig(configs, response);
    }

    private void setActivation(int type, boolean activate, Request request) {
        if (DBG) {
            logd("setActivation(" + type + "." + activate + ')');
        }

        Message response = obtainMessage(EVENT_ACTIVATION_DONE, request);

        if (type == SmsCbMessage.MESSAGE_FORMAT_3GPP) {
            mPhone.mCi.setGsmBroadcastActivation(activate, response);
        } else if (type == SmsCbMessage.MESSAGE_FORMAT_3GPP2) {
            mPhone.mCi.setCdmaBroadcastActivation(activate, response);
        }
    }

    /**
     * Dump the state of CellBroadcastConfigTracker
     *
     * @param fd File descriptor
     * @param printWriter Print writer
     * @param args Arguments
     */
    public void dump(FileDescriptor fd, PrintWriter printWriter, String[] args) {
        IndentingPrintWriter pw = new IndentingPrintWriter(printWriter, "  ");
        pw.println(CellBroadcastConfigTracker.class.getSimpleName()
                + "-" + mPhone.getPhoneId() + ":");
        pw.increaseIndent();
        pw.println("Current mCbRanges3gpp:" + mCbRanges3gpp);
        pw.println("Current mCbRanges3gpp2:" + mCbRanges3gpp2);
        pw.decreaseIndent();

        pw.println("Local logs:");
        pw.increaseIndent();
        mLocalLog.dump(fd, pw, args);
        pw.decreaseIndent();
    }
}
