/* Copyright (c) 2021, Red Hat, Inc.
 *
 * 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.
 */

#include <config.h>
#include <stdint.h>
#include <stdlib.h>

#include "lib/util.h"
#include "lib/dirs.h"
#include "socket-util.h"
#include "lib/ovsdb-cs.h"
#include "lib/ovsdb-error.h"
#include "lib/ovsdb-types.h"
#include "lib/vswitch-idl.h"
#include "odp-netlink.h"
#include "openvswitch/vlog.h"
#include "openvswitch/ofpbuf.h"
#include "openvswitch/rconn.h"
#include "openvswitch/ofp-msgs.h"
#include "openvswitch/ofp-meter.h"
#include "openvswitch/ofp-group.h"
#include "openvswitch/ofp-util.h"
#include "ovn/features.h"
#include "controller/ofctrl.h"

VLOG_DEFINE_THIS_MODULE(features);

/* Parses 'cap_name' from 'ovs_capabilities' and returns whether the
 * type of capability is supported or not. */
typedef bool ovs_feature_parse_func(const struct smap *ovs_capabilities,
                                    const char *cap_name);

struct ovs_feature {
    enum ovs_feature_value value;
    const char *name;
    ovs_feature_parse_func *parse;
};

static bool
bool_parser(const struct smap *ovs_capabilities, const char *cap_name)
{
    return smap_get_bool(ovs_capabilities, cap_name, false);
}

static bool
dp_hash_l4_sym_support_parser(const struct smap *ovs_capabilities,
                              const char *cap_name OVS_UNUSED)
{
    int max_hash_alg = smap_get_int(ovs_capabilities, "max_hash_alg", 0);

    return max_hash_alg == OVS_HASH_ALG_SYM_L4;
}

static struct ovs_feature all_ovs_features[] = {
    {
        .value = OVS_CT_ZERO_SNAT_SUPPORT,
        .name = "ct_zero_snat",
        .parse = bool_parser,
    },
    {
        .value = OVS_CT_TUPLE_FLUSH_SUPPORT,
        .name = "ct_flush",
        .parse = bool_parser,
    },
    {
        .value = OVS_DP_HASH_L4_SYM_SUPPORT,
        .name = "dp_hash_l4_sym_support",
        .parse = dp_hash_l4_sym_support_parser,
    },
};

/* A bitmap of OVS features that have been detected as 'supported'. */
static uint32_t supported_ovs_features;

/* Last set of received feature replies. */
static struct ofputil_meter_features ovs_meter_features_reply;
static struct ofputil_group_features ovs_group_features_reply;

/* Currently discovered set of features. */
static struct ofputil_meter_features ovs_meter_features;
static struct ofputil_group_features ovs_group_features;

/* Number of features replies still expected to receive for the requests
 * we sent already. */
static uint32_t n_features_reply_expected;

static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 5);

/* ovs-vswitchd connection. */
static struct rconn *swconn;
static uint32_t conn_seq_no;

static bool
ovs_feature_is_valid(enum ovs_feature_value feature)
{
    switch (feature) {
    case OVS_CT_ZERO_SNAT_SUPPORT:
    case OVS_DP_METER_SUPPORT:
    case OVS_CT_TUPLE_FLUSH_SUPPORT:
    case OVS_DP_HASH_L4_SYM_SUPPORT:
        return true;
    default:
        return false;
    }
}

bool
ovs_feature_is_supported(enum ovs_feature_value feature)
{
    ovs_assert(ovs_feature_is_valid(feature));
    return supported_ovs_features & feature;
}

static void
ovs_feature_rconn_setup(const char *br_name)
{
    if (!swconn) {
        swconn = rconn_create(0, 0, DSCP_DEFAULT, 1 << OFP15_VERSION);
    }

    if (!rconn_is_connected(swconn)) {
        char *target = xasprintf("unix:%s/%s.mgmt", ovs_rundir(), br_name);
        if (strcmp(target, rconn_get_target(swconn))) {
            VLOG_INFO("%s: connecting to switch", target);
            rconn_connect(swconn, target, target);
        }
        free(target);
    }
}

static bool
ovs_feature_get_openflow_cap(const char *br_name)
{
    struct ofpbuf *msg;

    if (!br_name) {
        return false;
    }

    ovs_feature_rconn_setup(br_name);

    rconn_run(swconn);
    if (!rconn_is_connected(swconn)) {
        rconn_run_wait(swconn);
        rconn_recv_wait(swconn);
        return false;
    }

    /* send new requests just after reconnect. */
    if (conn_seq_no != rconn_get_connection_seqno(swconn)) {
        n_features_reply_expected = 0;

        /* Dump OpenFlow switch meter capabilities. */
        msg = ofpraw_alloc(OFPRAW_OFPST13_METER_FEATURES_REQUEST,
                           rconn_get_version(swconn), 0);
        rconn_send(swconn, msg, NULL);
        n_features_reply_expected++;
        /* Dump OpenFlow switch group capabilities. */
        msg = ofputil_encode_group_features_request(rconn_get_version(swconn));
        rconn_send(swconn, msg, NULL);
        n_features_reply_expected++;
    }
    conn_seq_no = rconn_get_connection_seqno(swconn);

    bool ret = false;
    for (int i = 0; i < 50; i++) {
        msg = rconn_recv(swconn);
        if (!msg) {
            break;
        }

        const struct ofp_header *oh = msg->data;
        enum ofptype type;
        ofptype_decode(&type, oh);

        if (type == OFPTYPE_METER_FEATURES_STATS_REPLY) {
            ofputil_decode_meter_features(oh, &ovs_meter_features_reply);
            ovs_assert(n_features_reply_expected);
            n_features_reply_expected--;
        } else if (type == OFPTYPE_GROUP_FEATURES_STATS_REPLY) {
            ofputil_decode_group_features_reply(oh, &ovs_group_features_reply);
            ovs_assert(n_features_reply_expected);
            n_features_reply_expected--;
        } else if (type == OFPTYPE_ECHO_REQUEST) {
            rconn_send(swconn, ofputil_encode_echo_reply(oh), NULL);
        }
        ofpbuf_delete(msg);
    }
    rconn_run_wait(swconn);
    rconn_recv_wait(swconn);

    /* If all feature replies were received, update the set of supported
     * features. */
    if (!n_features_reply_expected) {
        if (memcmp(&ovs_meter_features, &ovs_meter_features_reply,
                   sizeof ovs_meter_features_reply)) {
            ovs_meter_features = ovs_meter_features_reply;
            if (ovs_meter_features.max_meters) {
                supported_ovs_features |= OVS_DP_METER_SUPPORT;
            } else {
                supported_ovs_features &= ~OVS_DP_METER_SUPPORT;
            }
            ret = true;
        }
        if (memcmp(&ovs_group_features, &ovs_group_features_reply,
                   sizeof ovs_group_features_reply)) {
            ovs_group_features = ovs_group_features_reply;
            ret = true;
        }
    }

    return ret;
}

/* Monitoring how many prefixes flow tables can have.
 * TODO: Remove this code once the server column type is available via IDL. */
static struct ovsdb_cs *vswitch_cs;
static size_t max_flow_table_prefixes;

static struct json *
vswitch_schema_prefixes_find(const struct json *schema_json,
                             void *ctx OVS_UNUSED)
{
    /* We must return a monitor request object, but we do not actually want
     * to monitor the data, so returning an empty one. */
    struct json *monitor_request = json_object_create();
    struct ovsdb_idl_column *prefixes_col;

    prefixes_col = &ovsrec_flow_table_columns[OVSREC_FLOW_TABLE_COL_PREFIXES];

    if (schema_json->type != JSON_OBJECT) {
        VLOG_WARN_RL(&rl, "OVS database schema is not a JSON object");
        return monitor_request;
    }

    const char *ft_name = ovsrec_table_classes[OVSREC_TABLE_FLOW_TABLE].name;
    const char *nested_objects[] = {
        "tables", ft_name, "columns", prefixes_col->name, "type",
    };
    const struct json *nested = schema_json;

    for (size_t i = 0; i < ARRAY_SIZE(nested_objects); i++) {
        nested = shash_find_data(json_object(nested), nested_objects[i]);
        if (!nested || nested->type != JSON_OBJECT) {
            VLOG_WARN_RL(&rl, "OVS database schema has no valid '%s'.",
                         nested_objects[i]);
            return monitor_request;
        }
    }

    struct ovsdb_type server_type;
    struct ovsdb_error *error;

    error = ovsdb_type_from_json(&server_type, nested);
    if (error) {
        char *msg = ovsdb_error_to_string_free(error);

        VLOG_WARN_RL(&rl, "Failed to parse type of %s column in %s table: %s",
                     prefixes_col->name, ft_name, msg);
        free(msg);
        return monitor_request;
    }

    if (max_flow_table_prefixes != server_type.n_max) {
        VLOG_INFO_RL(&rl, "OVS DB schema supports %d flow table prefixes, "
                          "our IDL supports: %d",
                     server_type.n_max, prefixes_col->type.n_max);
        max_flow_table_prefixes = server_type.n_max;
    }

    return monitor_request;
}

static struct ovsdb_cs_ops feature_cs_ops = {
    .compose_monitor_requests = vswitch_schema_prefixes_find,
};

static void
vswitch_schema_prefixes_run(void)
{
    struct ovsdb_cs_event *event;
    struct ovs_list events;

    ovsdb_cs_run(vswitch_cs, &events);
    LIST_FOR_EACH_POP (event, list_node, &events) {
        /* We're not really interested in events.  We only care about the
         * monitor request callback being invoked on re-connections or
         * schema changes. */
        ovsdb_cs_event_destroy(event);
    }
    ovsdb_cs_wait(vswitch_cs);
}

void
ovs_feature_support_destroy(void)
{
    rconn_destroy(swconn);
    swconn = NULL;
    ovsdb_cs_destroy(vswitch_cs);
    vswitch_cs = NULL;
}

/* Returns 'true' if the set of tracked OVS features has been updated. */
bool
ovs_feature_support_run(const struct smap *ovs_capabilities,
                        const char *br_name,
                        const char *db_target)
{
    static struct smap empty_caps = SMAP_INITIALIZER(&empty_caps);
    bool updated = false;

    if (!ovs_capabilities) {
        ovs_capabilities = &empty_caps;
    }

    if (ovs_feature_get_openflow_cap(br_name)) {
        updated = true;
    }

    for (size_t i = 0; i < ARRAY_SIZE(all_ovs_features); i++) {
        struct ovs_feature *feature = &all_ovs_features[i];
        bool old_state = supported_ovs_features & feature->value;
        bool new_state = feature->parse(ovs_capabilities, feature->name);
        if (new_state != old_state) {
            updated = true;
            if (new_state) {
                supported_ovs_features |= feature->value;
            } else {
                supported_ovs_features &= ~feature->value;
            }
            VLOG_INFO_RL(&rl, "OVS Feature: %s, state: %s", feature->name,
                         new_state ? "supported" : "not supported");
        }
    }

    if (!vswitch_cs) {
        vswitch_cs = ovsdb_cs_create(ovsrec_idl_class.database, 3,
                                     &feature_cs_ops, NULL);
    }
    ovsdb_cs_set_remote(vswitch_cs, db_target, true);
    /* This feature doesn't affect OpenFlow rules, so not touching 'updated'
     * even if changed. */
    vswitch_schema_prefixes_run();

    return updated;
}

bool
ovs_feature_set_discovered(void)
{
    /* The supported feature set has been discovered if we're connected
     * to OVS and it replied to all our feature request messages. */
    return swconn && rconn_is_connected(swconn) &&
           n_features_reply_expected == 0;
}

/* Returns the number of meters the OVS datapath supports. */
uint32_t
ovs_feature_max_meters_get(void)
{
    return ovs_meter_features.max_meters;
}

/* Returns the number of select groups the OVS datapath supports. */
uint32_t
ovs_feature_max_select_groups_get(void)
{
    return ovs_group_features.max_groups[OFPGT11_SELECT];
}

/* Returns the number of flow table prefixes that can be configured in
 * the OVS database. */
size_t
ovs_features_max_flow_table_prefixes_get(void)
{
    return max_flow_table_prefixes;
}
