/*
 * 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 android.telephony.ims;

import static java.nio.charset.StandardCharsets.UTF_8;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SystemApi;
import android.os.Build;
import android.os.Parcel;
import android.os.Parcelable;
import android.text.TextUtils;

import com.android.internal.telephony.SipMessageParsingUtils;

import java.util.Arrays;
import java.util.Objects;

/**
 * Represents a partially encoded SIP message. See RFC 3261 for more information on how SIP
 * messages are structured and used.
 * <p>
 * The SIP message is represented in a partially encoded form in order to allow for easier
 * verification and should not be used as a generic SIP message container.
 * @hide
 */
@SystemApi
public final class SipMessage implements Parcelable {
    // Should not be set to true for production!
    private static final boolean IS_DEBUGGING = Build.IS_ENG;
    private static final String CRLF = "\r\n";

    private final String mStartLine;
    private final String mHeaderSection;
    private final byte[] mContent;
    private final String mViaBranchParam;
    private final String mCallIdParam;

    /**
     * Represents a partially encoded SIP message.
     *
     * @param startLine The start line of the message, containing either the request-line or
     *                  status-line.
     * @param headerSection A String containing the full unencoded SIP message header.
     * @param content SIP message body.
     */
    public SipMessage(@NonNull String startLine, @NonNull String headerSection,
            @NonNull byte[] content) {
        Objects.requireNonNull(startLine, "Required parameter is null: startLine");
        Objects.requireNonNull(headerSection, "Required parameter is null: headerSection");
        Objects.requireNonNull(content, "Required parameter is null: content");

        mStartLine = startLine;
        mHeaderSection = headerSection;
        mContent = content;

        mViaBranchParam = SipMessageParsingUtils.getTransactionId(mHeaderSection);
        if (TextUtils.isEmpty(mViaBranchParam)) {
            throw new IllegalArgumentException("header section MUST contain a branch parameter "
                    + "inside of the Via header.");
        }
        mCallIdParam = SipMessageParsingUtils.getCallId(mHeaderSection);
    }

    /**
     * Private constructor used only for unparcelling.
     */
    private SipMessage(Parcel source) {
        mStartLine = source.readString();
        mHeaderSection = source.readString();
        mContent = new byte[source.readInt()];
        source.readByteArray(mContent);
        mViaBranchParam = source.readString();
        mCallIdParam = source.readString();
    }

    /**
     * @return The start line of the SIP message, which contains either the request-line or
     * status-line.
     */
    public @NonNull String getStartLine() {
        return mStartLine;
    }

    /**
     * @return The full, unencoded header section of the SIP message.
     */
    public @NonNull String getHeaderSection() {
        return mHeaderSection;
    }

    /**
     * @return the SIP message body.
     */
    public @NonNull byte[] getContent() {
        return mContent;
    }

    /**
     * @return the branch parameter enclosed in the Via header key's value. See RFC 3261 section
     * 20.42 for more information on the Via header.
     */
    public @NonNull String getViaBranchParameter() {
        return mViaBranchParam;
    }

    /**
     * @return the value associated with the call-id header of this SIP message. See RFC 3261
     * section 20.8 for more information on the call-id header. If {@code null}, then there was no
     * call-id header found in this SIP message's headers.
     */
    public @Nullable String getCallIdParameter() {
        return mCallIdParam;
    }

    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(@NonNull Parcel dest, int flags) {
        dest.writeString(mStartLine);
        dest.writeString(mHeaderSection);
        dest.writeInt(mContent.length);
        dest.writeByteArray(mContent);
        dest.writeString(mViaBranchParam);
        dest.writeString(mCallIdParam);
    }

    public static final @NonNull Creator<SipMessage> CREATOR = new Creator<SipMessage>() {
        @Override
        public SipMessage createFromParcel(Parcel source) {
            return new SipMessage(source);
        }

        @Override
        public SipMessage[] newArray(int size) {
            return new SipMessage[size];
        }
    };

    @Override
    public String toString() {
        StringBuilder b = new StringBuilder();
        b.append("StartLine: [");
        if (IS_DEBUGGING) {
            b.append(mStartLine);
        } else {
            b.append(sanitizeStartLineRequest(mStartLine));
        }
        b.append("], Header: [");
        if (IS_DEBUGGING) {
            b.append(mHeaderSection);
        } else {
            // only identify transaction id/call ID when it is available.
            b.append("***");
        }
        b.append("], Content: ");
        b.append(getContent().length == 0 ? "[NONE]" : "[NOT SHOWN]");
        return b.toString();
    }

    /**
     * Detect if this is a REQUEST and redact Request-URI portion here, as it contains PII.
     */
    private String sanitizeStartLineRequest(String startLine) {
        if (!SipMessageParsingUtils.isSipRequest(startLine)) return startLine;
        String[] splitLine = startLine.split(" ");
        return splitLine[0] + " <Request-URI> " + splitLine[2];
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        SipMessage that = (SipMessage) o;
        return mStartLine.equals(that.mStartLine)
                && mHeaderSection.equals(that.mHeaderSection)
                && Arrays.equals(mContent, that.mContent);
    }

    @Override
    public int hashCode() {
        int result = Objects.hash(mStartLine, mHeaderSection);
        result = 31 * result + Arrays.hashCode(mContent);
        return result;
    }

    /**
     * According RFC-3261 section 7, SIP is a text protocol and uses the UTF-8 charset. Its format
     * consists of a start-line, one or more header fields, an empty line indicating the end of the
     * header fields, and an optional message-body.
     *
     * <p>
     * Returns a byte array with UTF-8 format representation of the encoded SipMessage.
     *
     * @return byte array with UTF-8 format representation of the encoded SipMessage.
     */
    public @NonNull byte[] toEncodedMessage() {
        byte[] header = new StringBuilder()
                .append(mStartLine)
                .append(mHeaderSection)
                .append(CRLF)
                .toString().getBytes(UTF_8);
        byte[] sipMessage = new byte[header.length + mContent.length];
        System.arraycopy(header, 0, sipMessage, 0, header.length);
        System.arraycopy(mContent, 0, sipMessage, header.length, mContent.length);
        return sipMessage;
    }
}
