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

import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SystemApi;
import android.compat.annotation.UnsupportedAppUsage;
import android.os.Build;
import android.os.Parcel;
import android.os.Parcelable;

import com.android.net.module.util.NetUtils;
import com.android.net.module.util.NetworkStackConstants;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Collection;
import java.util.Objects;

/**
 * Represents a network route.
 * <p>
 * This is used both to describe static network configuration and live network
 * configuration information.
 *
 * A route contains three pieces of information:
 * <ul>
 * <li>a destination {@link IpPrefix} specifying the network destinations covered by this route.
 *     If this is {@code null} it indicates a default route of the address family (IPv4 or IPv6)
 *     implied by the gateway IP address.
 * <li>a gateway {@link InetAddress} indicating the next hop to use.  If this is {@code null} it
 *     indicates a directly-connected route.
 * <li>an interface (which may be unspecified).
 * </ul>
 * Either the destination or the gateway may be {@code null}, but not both.  If the
 * destination and gateway are both specified, they must be of the same address family
 * (IPv4 or IPv6).
 */
public final class RouteInfo implements Parcelable {
    /** @hide */
    @IntDef(value = {
            RTN_UNICAST,
            RTN_UNREACHABLE,
            RTN_THROW,
    })
    @Retention(RetentionPolicy.SOURCE)
    public @interface RouteType {}

    /**
     * The IP destination address for this route.
     */
    @NonNull
    private final IpPrefix mDestination;

    /**
     * The gateway address for this route.
     */
    @UnsupportedAppUsage
    @Nullable
    private final InetAddress mGateway;

    /**
     * The interface for this route.
     */
    @Nullable
    private final String mInterface;


    /** Unicast route. @hide */
    @SystemApi
    public static final int RTN_UNICAST = 1;

    /** Unreachable route. @hide */
    @SystemApi
    public static final int RTN_UNREACHABLE = 7;

    /** Throw route. @hide */
    @SystemApi
    public static final int RTN_THROW = 9;

    /**
     * The type of this route; one of the RTN_xxx constants above.
     */
    private final int mType;

    /**
     * The maximum transmission unit size for this route.
     */
    private final int mMtu;

    // Derived data members.
    // TODO: remove these.
    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
    private final boolean mIsHost;
    private final boolean mHasGateway;

    /**
     * Constructs a RouteInfo object.
     *
     * If destination is null, then gateway must be specified and the
     * constructed route is either the IPv4 default route <code>0.0.0.0</code>
     * if the gateway is an instance of {@link Inet4Address}, or the IPv6 default
     * route <code>::/0</code> if gateway is an instance of
     * {@link Inet6Address}.
     * <p>
     * destination and gateway may not both be null.
     *
     * @param destination the destination prefix
     * @param gateway the IP address to route packets through
     * @param iface the interface name to send packets on
     * @param type the type of this route
     *
     * @hide
     */
    @SystemApi
    public RouteInfo(@Nullable IpPrefix destination, @Nullable InetAddress gateway,
            @Nullable String iface, @RouteType int type) {
        this(destination, gateway, iface, type, 0);
    }

    /**
     * Constructs a RouteInfo object.
     *
     * If destination is null, then gateway must be specified and the
     * constructed route is either the IPv4 default route <code>0.0.0.0</code>
     * if the gateway is an instance of {@link Inet4Address}, or the IPv6 default
     * route <code>::/0</code> if gateway is an instance of
     * {@link Inet6Address}.
     * <p>
     * destination and gateway may not both be null.
     *
     * @param destination the destination prefix
     * @param gateway the IP address to route packets through
     * @param iface the interface name to send packets on
     * @param type the type of this route
     * @param mtu the maximum transmission unit size for this route
     *
     * @hide
     */
    @SystemApi
    public RouteInfo(@Nullable IpPrefix destination, @Nullable InetAddress gateway,
            @Nullable String iface, @RouteType int type, int mtu) {
        switch (type) {
            case RTN_UNICAST:
            case RTN_UNREACHABLE:
            case RTN_THROW:
                // TODO: It would be nice to ensure that route types that don't have nexthops or
                // interfaces, such as unreachable or throw, can't be created if an interface or
                // a gateway is specified. This is a bit too complicated to do at the moment
                // because:
                //
                // - LinkProperties sets the interface on routes added to it, and modifies the
                //   interfaces of all the routes when its interface name changes.
                // - Even when the gateway is null, we store a non-null gateway here.
                //
                // For now, we just rely on the code that sets routes to do things properly.
                break;
            default:
                throw new IllegalArgumentException("Unknown route type " + type);
        }

        if (destination == null) {
            if (gateway != null) {
                if (gateway instanceof Inet4Address) {
                    destination = new IpPrefix(NetworkStackConstants.IPV4_ADDR_ANY, 0);
                } else {
                    destination = new IpPrefix(NetworkStackConstants.IPV6_ADDR_ANY, 0);
                }
            } else {
                // no destination, no gateway. invalid.
                throw new IllegalArgumentException("Invalid arguments passed in: " + gateway + "," +
                        destination);
            }
        }
        // TODO: set mGateway to null if there is no gateway. This is more correct, saves space, and
        // matches the documented behaviour. Before we can do this we need to fix all callers (e.g.,
        // ConnectivityService) to stop doing things like r.getGateway().equals(), ... .
        if (gateway == null) {
            if (destination.getAddress() instanceof Inet4Address) {
                gateway = NetworkStackConstants.IPV4_ADDR_ANY;
            } else {
                gateway = NetworkStackConstants.IPV6_ADDR_ANY;
            }
        }
        mHasGateway = (!gateway.isAnyLocalAddress());

        if ((destination.getAddress() instanceof Inet4Address
                && !(gateway instanceof Inet4Address))
                || (destination.getAddress() instanceof Inet6Address
                && !(gateway instanceof Inet6Address))) {
            throw new IllegalArgumentException("address family mismatch in RouteInfo constructor");
        }
        mDestination = destination;  // IpPrefix objects are immutable.
        mGateway = gateway;          // InetAddress objects are immutable.
        mInterface = iface;          // Strings are immutable.
        mType = type;
        mIsHost = isHost();
        mMtu = mtu;
    }

    /**
     * Constructs a {@code RouteInfo} object.
     *
     * If destination is null, then gateway must be specified and the
     * constructed route is either the IPv4 default route <code>0.0.0.0</code>
     * if the gateway is an instance of {@link Inet4Address}, or the IPv6 default
     * route <code>::/0</code> if gateway is an instance of {@link Inet6Address}.
     * <p>
     * Destination and gateway may not both be null.
     *
     * @param destination the destination address and prefix in an {@link IpPrefix}
     * @param gateway the {@link InetAddress} to route packets through
     * @param iface the interface name to send packets on
     *
     * @hide
     */
    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
    public RouteInfo(@Nullable IpPrefix destination, @Nullable InetAddress gateway,
            @Nullable String iface) {
        this(destination, gateway, iface, RTN_UNICAST);
    }

    /**
     * @hide
     */
    @UnsupportedAppUsage
    public RouteInfo(@Nullable LinkAddress destination, @Nullable InetAddress gateway,
            @Nullable String iface) {
        this(destination == null ? null :
                new IpPrefix(destination.getAddress(), destination.getPrefixLength()),
                gateway, iface);
    }

    /**
     * Constructs a {@code RouteInfo} object.
     *
     * If destination is null, then gateway must be specified and the
     * constructed route is either the IPv4 default route <code>0.0.0.0</code>
     * if the gateway is an instance of {@link Inet4Address}, or the IPv6 default
     * route <code>::/0</code> if gateway is an instance of {@link Inet6Address}.
     * <p>
     * Destination and gateway may not both be null.
     *
     * @param destination the destination address and prefix in an {@link IpPrefix}
     * @param gateway the {@link InetAddress} to route packets through
     *
     * @hide
     */
    public RouteInfo(@Nullable IpPrefix destination, @Nullable InetAddress gateway) {
        this(destination, gateway, null);
    }

    /**
     * @hide
     *
     * TODO: Remove this.
     */
    @UnsupportedAppUsage
    public RouteInfo(@Nullable LinkAddress destination, @Nullable InetAddress gateway) {
        this(destination, gateway, null);
    }

    /**
     * Constructs a default {@code RouteInfo} object.
     *
     * @param gateway the {@link InetAddress} to route packets through
     *
     * @hide
     */
    @UnsupportedAppUsage
    public RouteInfo(@NonNull InetAddress gateway) {
        this((IpPrefix) null, gateway, null);
    }

    /**
     * Constructs a {@code RouteInfo} object representing a direct connected subnet.
     *
     * @param destination the {@link IpPrefix} describing the address and prefix
     *                    length of the subnet.
     *
     * @hide
     */
    public RouteInfo(@NonNull IpPrefix destination) {
        this(destination, null, null);
    }

    /**
     * @hide
     */
    public RouteInfo(@NonNull LinkAddress destination) {
        this(destination, null, null);
    }

    /**
     * @hide
     */
    public RouteInfo(@NonNull IpPrefix destination, @RouteType int type) {
        this(destination, null, null, type);
    }

    /**
     * @hide
     */
    public static RouteInfo makeHostRoute(@NonNull InetAddress host, @Nullable String iface) {
        return makeHostRoute(host, null, iface);
    }

    /**
     * @hide
     */
    public static RouteInfo makeHostRoute(@Nullable InetAddress host, @Nullable InetAddress gateway,
            @Nullable String iface) {
        if (host == null) return null;

        if (host instanceof Inet4Address) {
            return new RouteInfo(new IpPrefix(host, 32), gateway, iface);
        } else {
            return new RouteInfo(new IpPrefix(host, 128), gateway, iface);
        }
    }

    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
    private boolean isHost() {
        return (mDestination.getAddress() instanceof Inet4Address &&
                mDestination.getPrefixLength() == 32) ||
               (mDestination.getAddress() instanceof Inet6Address &&
                mDestination.getPrefixLength() == 128);
    }

    /**
     * Retrieves the destination address and prefix length in the form of an {@link IpPrefix}.
     *
     * @return {@link IpPrefix} specifying the destination.  This is never {@code null}.
     */
    @NonNull
    public IpPrefix getDestination() {
        return mDestination;
    }

    /**
     * TODO: Convert callers to use IpPrefix and then remove.
     * @hide
     */
    @NonNull
    public LinkAddress getDestinationLinkAddress() {
        return new LinkAddress(mDestination.getAddress(), mDestination.getPrefixLength());
    }

    /**
     * Retrieves the gateway or next hop {@link InetAddress} for this route.
     *
     * @return {@link InetAddress} specifying the gateway or next hop.  This may be
     *                             {@code null} for a directly-connected route."
     */
    @Nullable
    public InetAddress getGateway() {
        return mGateway;
    }

    /**
     * Retrieves the interface used for this route if specified, else {@code null}.
     *
     * @return The name of the interface used for this route.
     */
    @Nullable
    public String getInterface() {
        return mInterface;
    }

    /**
     * Retrieves the type of this route.
     *
     * @return The type of this route; one of the {@code RTN_xxx} constants defined in this class.
     *
     * @hide
     */
    @SystemApi
    @RouteType
    public int getType() {
        return mType;
    }

    /**
     * Retrieves the MTU size for this route.
     *
     * @return The MTU size, or 0 if it has not been set.
     * @hide
     */
    @SystemApi
    public int getMtu() {
        return mMtu;
    }

    /**
     * Indicates if this route is a default route (ie, has no destination specified).
     *
     * @return {@code true} if the destination has a prefix length of 0.
     */
    public boolean isDefaultRoute() {
        return mType == RTN_UNICAST && mDestination.getPrefixLength() == 0;
    }

    /**
     * Indicates if this route is an unreachable default route.
     *
     * @return {@code true} if it's an unreachable route with prefix length of 0.
     * @hide
     */
    private boolean isUnreachableDefaultRoute() {
        return mType == RTN_UNREACHABLE && mDestination.getPrefixLength() == 0;
    }

    /**
     * Indicates if this route is an IPv4 default route.
     * @hide
     */
    public boolean isIPv4Default() {
        return isDefaultRoute() && mDestination.getAddress() instanceof Inet4Address;
    }

    /**
     * Indicates if this route is an IPv4 unreachable default route.
     * @hide
     */
    public boolean isIPv4UnreachableDefault() {
        return isUnreachableDefaultRoute() && mDestination.getAddress() instanceof Inet4Address;
    }

    /**
     * Indicates if this route is an IPv6 default route.
     * @hide
     */
    public boolean isIPv6Default() {
        return isDefaultRoute() && mDestination.getAddress() instanceof Inet6Address;
    }

    /**
     * Indicates if this route is an IPv6 unreachable default route.
     * @hide
     */
    public boolean isIPv6UnreachableDefault() {
        return isUnreachableDefaultRoute() && mDestination.getAddress() instanceof Inet6Address;
    }

    /**
     * Indicates if this route is a host route (ie, matches only a single host address).
     *
     * @return {@code true} if the destination has a prefix length of 32 or 128 for IPv4 or IPv6,
     * respectively.
     * @hide
     */
    public boolean isHostRoute() {
        return mIsHost;
    }

    /**
     * Indicates if this route has a next hop ({@code true}) or is directly-connected
     * ({@code false}).
     *
     * @return {@code true} if a gateway is specified
     */
    public boolean hasGateway() {
        return mHasGateway;
    }

    /**
     * Determines whether the destination and prefix of this route includes the specified
     * address.
     *
     * @param destination A {@link InetAddress} to test to see if it would match this route.
     * @return {@code true} if the destination and prefix length cover the given address.
     */
    public boolean matches(InetAddress destination) {
        return mDestination.contains(destination);
    }

    /**
     * Find the route from a Collection of routes that best matches a given address.
     * May return null if no routes are applicable.
     * @param routes a Collection of RouteInfos to chose from
     * @param dest the InetAddress your trying to get to
     * @return the RouteInfo from the Collection that best fits the given address
     *
     * @hide
     */
    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
    @Nullable
    public static RouteInfo selectBestRoute(Collection<RouteInfo> routes, InetAddress dest) {
        return NetUtils.selectBestRoute(routes, dest);
    }

    /**
     * Returns a human-readable description of this object.
     */
    public String toString() {
        String val = "";
        if (mDestination != null) val = mDestination.toString();
        if (mType == RTN_UNREACHABLE) {
            val += " unreachable";
        } else if (mType == RTN_THROW) {
            val += " throw";
        } else {
            val += " ->";
            if (mGateway != null) val += " " + mGateway.getHostAddress();
            if (mInterface != null) val += " " + mInterface;
            if (mType != RTN_UNICAST) {
                val += " unknown type " + mType;
            }
        }
        val += " mtu " + mMtu;
        return val;
    }

    /**
     * Compares this RouteInfo object against the specified object and indicates if they are equal.
     * @return {@code true} if the objects are equal, {@code false} otherwise.
     */
    public boolean equals(@Nullable Object obj) {
        if (this == obj) return true;

        if (!(obj instanceof RouteInfo)) return false;

        RouteInfo target = (RouteInfo) obj;

        return Objects.equals(mDestination, target.getDestination()) &&
                Objects.equals(mGateway, target.getGateway()) &&
                Objects.equals(mInterface, target.getInterface()) &&
                mType == target.getType() && mMtu == target.getMtu();
    }

    /**
     * A helper class that contains the destination, the gateway and the interface in a
     * {@code RouteInfo}, used by {@link ConnectivityService#updateRoutes} or
     * {@link LinkProperties#addRoute} to calculate the list to be updated.
     * {@code RouteInfo} objects with different interfaces are treated as different routes because
     * *usually* on Android different interfaces use different routing tables, and moving a route
     * to a new routing table never constitutes an update, but is always a remove and an add.
     *
     * @hide
     */
    public static class RouteKey {
        @NonNull private final IpPrefix mDestination;
        @Nullable private final InetAddress mGateway;
        @Nullable private final String mInterface;

        RouteKey(@NonNull IpPrefix destination, @Nullable InetAddress gateway,
                @Nullable String iface) {
            mDestination = destination;
            mGateway = gateway;
            mInterface = iface;
        }

        @Override
        public boolean equals(@Nullable Object o) {
            if (!(o instanceof RouteKey)) {
                return false;
            }
            RouteKey p = (RouteKey) o;
            // No need to do anything special for scoped addresses. Inet6Address#equals does not
            // consider the scope ID, but the netd route IPCs (e.g., INetd#networkAddRouteParcel)
            // and the kernel ignore scoped addresses both in the prefix and in the nexthop and only
            // look at RTA_OIF.
            return Objects.equals(p.mDestination, mDestination)
                    && Objects.equals(p.mGateway, mGateway)
                    && Objects.equals(p.mInterface, mInterface);
        }

        @Override
        public int hashCode() {
            return Objects.hash(mDestination, mGateway, mInterface);
        }
    }

    /**
     * Get {@code RouteKey} of this {@code RouteInfo}.
     * @return a {@code RouteKey} object.
     *
     * @hide
     */
    @NonNull
    public RouteKey getRouteKey() {
        return new RouteKey(mDestination, mGateway, mInterface);
    }

    /**
     *  Returns a hashcode for this <code>RouteInfo</code> object.
     */
    public int hashCode() {
        return (mDestination.hashCode() * 41)
                + (mGateway == null ? 0 :mGateway.hashCode() * 47)
                + (mInterface == null ? 0 :mInterface.hashCode() * 67)
                + (mType * 71) + (mMtu * 89);
    }

    /**
     * Implement the Parcelable interface
     */
    public int describeContents() {
        return 0;
    }

    /**
     * Implement the Parcelable interface
     */
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeParcelable(mDestination, flags);
        byte[] gatewayBytes = (mGateway == null) ? null : mGateway.getAddress();
        dest.writeByteArray(gatewayBytes);
        dest.writeString(mInterface);
        dest.writeInt(mType);
        dest.writeInt(mMtu);
    }

    /**
     * Implement the Parcelable interface.
     */
    public static final @android.annotation.NonNull Creator<RouteInfo> CREATOR =
        new Creator<RouteInfo>() {
        public RouteInfo createFromParcel(Parcel in) {
            IpPrefix dest = in.readParcelable(null);

            InetAddress gateway = null;
            byte[] addr = in.createByteArray();
            try {
                gateway = InetAddress.getByAddress(addr);
            } catch (UnknownHostException e) {}

            String iface = in.readString();
            int type = in.readInt();
            int mtu = in.readInt();

            return new RouteInfo(dest, gateway, iface, type, mtu);
        }

        public RouteInfo[] newArray(int size) {
            return new RouteInfo[size];
        }
    };
}
