/*
 * Copyright (C) 2019 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.net.ipsec.ike;

import static android.net.ipsec.ike.IkeManager.getIkeLog;
import static android.net.ipsec.ike.IkeManager.getIkeMetrics;

import android.net.ipsec.ike.exceptions.IkeException;
import android.os.Message;
import android.util.SparseArray;

import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.net.ipsec.ike.net.IkeConnectionController;
import com.android.internal.net.ipsec.ike.utils.IkeMetrics;
import com.android.internal.util.IState;
import com.android.internal.util.State;
import com.android.internal.util.StateMachine;

import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;

/**
 * This class represents the common information of both IkeSessionStateMachine and
 * ChildSessionStateMachine
 */
abstract class AbstractSessionStateMachine extends StateMachine {
    private static final int CMD_SHARED_BASE = 0;
    protected static final int CMD_CATEGORY_SIZE = 100;

    /**
     * Commands of Child local request that will be used in both IkeSessionStateMachine and
     * ChildSessionStateMachine.
     */
    protected static final int CMD_CHILD_LOCAL_REQUEST_BASE = CMD_SHARED_BASE;

    @VisibleForTesting
    static final int CMD_LOCAL_REQUEST_CREATE_CHILD = CMD_CHILD_LOCAL_REQUEST_BASE + 1;

    @VisibleForTesting
    static final int CMD_LOCAL_REQUEST_DELETE_CHILD = CMD_CHILD_LOCAL_REQUEST_BASE + 2;

    @VisibleForTesting
    static final int CMD_LOCAL_REQUEST_REKEY_CHILD = CMD_CHILD_LOCAL_REQUEST_BASE + 3;

    @VisibleForTesting
    static final int CMD_LOCAL_REQUEST_REKEY_CHILD_MOBIKE = CMD_CHILD_LOCAL_REQUEST_BASE + 4;

    @VisibleForTesting
    static final int CMD_LOCAL_REQUEST_MIGRATE_CHILD = CMD_CHILD_LOCAL_REQUEST_BASE + 5;

    static final int CMD_LOCAL_REQUEST_MIN = CMD_LOCAL_REQUEST_CREATE_CHILD;
    static final int CMD_LOCAL_REQUEST_MAX = CMD_LOCAL_REQUEST_MIGRATE_CHILD;

    /** Timeout commands. */
    protected static final int CMD_TIMEOUT_BASE = CMD_SHARED_BASE + CMD_CATEGORY_SIZE;
    /** Timeout when the remote side fails to send a Rekey-Delete request. */
    @VisibleForTesting static final int TIMEOUT_REKEY_REMOTE_DELETE = CMD_TIMEOUT_BASE + 1;

    /** Commands for generic usages */
    protected static final int CMD_GENERIC_BASE = CMD_SHARED_BASE + 2 * CMD_CATEGORY_SIZE;
    /** Force state machine to a target state for testing purposes. */
    @VisibleForTesting static final int CMD_FORCE_TRANSITION = CMD_GENERIC_BASE + 1;
    /** Force close the session. */
    @VisibleForTesting static final int CMD_KILL_SESSION = CMD_GENERIC_BASE + 2;

    /** Private commands for subclasses */
    protected static final int CMD_PRIVATE_BASE = CMD_SHARED_BASE + 3 * CMD_CATEGORY_SIZE;

    protected static final SparseArray<String> SHARED_CMD_TO_STR;

    static {
        SHARED_CMD_TO_STR = new SparseArray<>();
        SHARED_CMD_TO_STR.put(CMD_LOCAL_REQUEST_CREATE_CHILD, "Create Child");
        SHARED_CMD_TO_STR.put(CMD_LOCAL_REQUEST_DELETE_CHILD, "Delete Child");
        SHARED_CMD_TO_STR.put(CMD_LOCAL_REQUEST_REKEY_CHILD, "Rekey Child");
        SHARED_CMD_TO_STR.put(CMD_LOCAL_REQUEST_MIGRATE_CHILD, "Migrate Child SA");
        SHARED_CMD_TO_STR.put(CMD_LOCAL_REQUEST_REKEY_CHILD_MOBIKE, "Rekey Child (MOBIKE)");
        SHARED_CMD_TO_STR.put(CMD_KILL_SESSION, "Kill session");
        SHARED_CMD_TO_STR.put(TIMEOUT_REKEY_REMOTE_DELETE, "Timout rekey remote delete");
        SHARED_CMD_TO_STR.put(CMD_FORCE_TRANSITION, "Force transition");
    }

    // Use a value greater than the retransmit-failure timeout.
    static final long REKEY_DELETE_TIMEOUT_MS = TimeUnit.SECONDS.toMillis(180L);

    // Default delay time for retrying a request
    static final long RETRY_INTERVAL_MS = TimeUnit.SECONDS.toMillis(15L);

    @VisibleForTesting final IkeContext mIkeContext;

    protected final Executor mUserCbExecutor;
    private final String mLogTag;

    protected volatile boolean mIsClosing = false;

    protected AbstractSessionStateMachine(
            String name, IkeContext ikeContext, Executor userCbExecutor) {
        super(name, ikeContext.getLooper());

        mIkeContext = ikeContext;
        mLogTag = name;
        mUserCbExecutor = userCbExecutor;
    }

    /**
     * Top level state for handling uncaught exceptions for all subclasses.
     *
     * <p>All other state in SessionStateMachine MUST extend this state.
     *
     * <p>Only errors this state should catch are unexpected internal failures. Since this may be
     * run in critical processes, it must never take down the process if it fails
     */
    protected abstract class ExceptionHandlerBase extends State {
        @Override
        public final void enter() {
            try {
                enterState();
            } catch (RuntimeException e) {
                cleanUpAndQuit(e);
            }
        }

        private String getCmdStr(int cmd) {
            String cmdName = SHARED_CMD_TO_STR.get(cmd);
            if (cmdName != null) {
                return cmdName;
            }

            cmdName = getCmdString(cmd);
            if (cmdName != null) {
                return cmdName;
            }

            // Unrecognized message
            return Integer.toString(cmd);
        }

        @Override
        public final boolean processMessage(Message message) {
            try {
                if (mIsClosing && message.what != CMD_KILL_SESSION) {
                    logd(
                            "Ignore "
                                    + getCmdStr(message.what)
                                    + " since this session is going to be closed");
                    return HANDLED;
                } else {
                    logd("processStateMessage: " + getCmdStr(message.what));
                    return processStateMessage(message);
                }
            } catch (RuntimeException e) {
                cleanUpAndQuit(e);
                return HANDLED;
            }
        }

        @Override
        public final void exit() {
            try {
                exitState();
            } catch (RuntimeException e) {
                cleanUpAndQuit(e);
            }
        }

        protected void enterState() {
            // Do nothing. Subclasses MUST override it if they care.
        }

        protected boolean processStateMessage(Message message) {
            return NOT_HANDLED;
        }

        protected void exitState() {
            // Do nothing. Subclasses MUST override it if they care.
        }

        protected abstract void cleanUpAndQuit(RuntimeException e);

        protected abstract String getCmdString(int cmd);

        protected abstract @IkeMetrics.IkeState int getMetricsStateCode();
    }

    protected void executeUserCallback(Runnable r) {
        try {
            mUserCbExecutor.execute(r);
        } catch (Exception e) {
            logd("Callback execution failed", e);
        }
    }

    /** Forcibly close this session. */
    public void killSession() {
        log("killSession");

        mIsClosing = true;
        sendMessage(CMD_KILL_SESSION);
    }

    /**
     * Quit SessionStateMachine immediately.
     *
     * <p>This method pushes SM_QUIT_CMD in front of the message queue and mark mIsClosing as true.
     * All currently queued messages will be discarded.
     *
     * <p>Subclasses MUST call this method instead of quitNow()
     */
    // quitNow() is a public final method in the base class. Thus there is no good way to prevent
    // caller from calling it within the current inheritance structure.
    protected void quitSessionNow() {
        mIsClosing = true;
        quitNow();
    }

    protected String getCurrentStateName() {
        final IState state = getCurrentState();
        if (state != null) {
            return state.getName();
        }

        return "Null State";
    }

    private @IkeMetrics.IkeState int getMetricsIkeStateCode() {
        final IState currentState = getCurrentState();
        return currentState instanceof ExceptionHandlerBase
                ? ((ExceptionHandlerBase) currentState).getMetricsStateCode()
                : IkeMetrics.IKE_STATE_UNKNOWN;
    }

    protected void recordMetricsEvent_sessionTerminated(IkeException exception) {
        final @IkeMetrics.IkeError int exceptionCode =
                exception == null ? IkeMetrics.IKE_ERROR_NONE : exception.getMetricsErrorCode();

        getIkeMetrics()
                .logSessionTerminated(
                        mIkeContext.getIkeCaller(),
                        getMetricsSessionType(),
                        getMetricsIkeStateCode(),
                        exceptionCode);
    }

    protected void recordMetricsEvent_LivenssCheckCompletion(
            IkeConnectionController connectionController,
            int elapsedTimeInMillis,
            int numberOfOnGoing,
            boolean resultSuccess) {
        getIkeMetrics()
                .logLivenessCheckCompleted(
                        mIkeContext.getIkeCaller(),
                        getMetricsIkeStateCode(),
                        connectionController.getMetricsNetworkType(),
                        elapsedTimeInMillis,
                        numberOfOnGoing,
                        resultSuccess);
    }

    protected void recordMetricsEvent_SaNegotiation(
            int dhGroup,
            int encryptionAlgorithm,
            int keyLength,
            int integrityAlgorithm,
            int prfAlgorithm,
            IkeException exception) {
        final @IkeMetrics.IkeError int exceptionCode =
                exception == null ? IkeMetrics.IKE_ERROR_NONE : exception.getMetricsErrorCode();
        getIkeMetrics()
                .logSaNegotiation(
                        mIkeContext.getIkeCaller(),
                        getMetricsSessionType(),
                        getMetricsIkeStateCode(),
                        dhGroup,
                        encryptionAlgorithm,
                        keyLength,
                        integrityAlgorithm,
                        prfAlgorithm,
                        exceptionCode);
    }

    protected abstract @IkeMetrics.IkeSessionType int getMetricsSessionType();

    @Override
    protected void log(String s) {
        getIkeLog().d(mLogTag, s);
    }

    @Override
    protected void logd(String s) {
        getIkeLog().d(mLogTag, s);
    }

    protected void logd(String s, Throwable e) {
        getIkeLog().d(mLogTag, s, e);
    }

    @Override
    protected void logv(String s) {
        getIkeLog().v(mLogTag, s);
    }

    @Override
    protected void logi(String s) {
        getIkeLog().i(mLogTag, s);
    }

    protected void logi(String s, Throwable cause) {
        getIkeLog().i(mLogTag, s, cause);
    }

    @Override
    protected void logw(String s) {
        getIkeLog().w(mLogTag, s);
    }

    @Override
    protected void loge(String s) {
        getIkeLog().e(mLogTag, s);
    }

    @Override
    protected void loge(String s, Throwable e) {
        getIkeLog().e(mLogTag, s, e);
    }

    protected void logWtf(String s) {
        getIkeLog().wtf(mLogTag, s);
    }

    protected void logWtf(String s, Throwable e) {
        getIkeLog().wtf(mLogTag, s, e);
    }
}
