// SPDX-License-Identifier: GPL-2.0-only
 /*
  * mac80211 - channel management
+ * Copyright 2020 - 2021 Intel Corporation
  */
 
 #include <linux/nl80211.h>
  * the max of min required widths of all the interfaces bound to this
  * channel context.
  */
-void ieee80211_recalc_chanctx_min_def(struct ieee80211_local *local,
-                                     struct ieee80211_chanctx *ctx)
+static u32 _ieee80211_recalc_chanctx_min_def(struct ieee80211_local *local,
+                                            struct ieee80211_chanctx *ctx)
 {
        enum nl80211_chan_width max_bw;
        struct cfg80211_chan_def min_def;
            ctx->conf.def.width == NL80211_CHAN_WIDTH_16 ||
            ctx->conf.radar_enabled) {
                ctx->conf.min_def = ctx->conf.def;
-               return;
+               return 0;
        }
 
        max_bw = ieee80211_get_chanctx_max_required_bw(local, &ctx->conf);
                ieee80211_chandef_downgrade(&min_def);
 
        if (cfg80211_chandef_identical(&ctx->conf.min_def, &min_def))
-               return;
+               return 0;
 
        ctx->conf.min_def = min_def;
        if (!ctx->driver_present)
-               return;
+               return 0;
 
-       drv_change_chanctx(local, ctx, IEEE80211_CHANCTX_CHANGE_MIN_WIDTH);
+       return IEEE80211_CHANCTX_CHANGE_MIN_WIDTH;
 }
 
+/* calling this function is assuming that station vif is updated to
+ * lates changes by calling ieee80211_vif_update_chandef
+ */
 static void ieee80211_chan_bw_change(struct ieee80211_local *local,
-                                    struct ieee80211_chanctx *ctx)
+                                    struct ieee80211_chanctx *ctx,
+                                    bool narrowed)
 {
        struct sta_info *sta;
        struct ieee80211_supported_band *sband =
                        continue;
 
                new_sta_bw = ieee80211_sta_cur_vht_bw(sta);
+
+               /* nothing change */
                if (new_sta_bw == sta->sta.bandwidth)
                        continue;
 
+               /* vif changed to narrow BW and narrow BW for station wasn't
+                * requested or vise versa */
+               if ((new_sta_bw < sta->sta.bandwidth) == !narrowed)
+                       continue;
+
                sta->sta.bandwidth = new_sta_bw;
                rate_control_rate_update(local, sband, sta,
                                         IEEE80211_RC_BW_CHANGED);
        rcu_read_unlock();
 }
 
-static void ieee80211_change_chanctx(struct ieee80211_local *local,
-                                    struct ieee80211_chanctx *ctx,
-                                    const struct cfg80211_chan_def *chandef)
+/*
+ * recalc the min required chan width of the channel context, which is
+ * the max of min required widths of all the interfaces bound to this
+ * channel context.
+ */
+void ieee80211_recalc_chanctx_min_def(struct ieee80211_local *local,
+                                     struct ieee80211_chanctx *ctx)
 {
-       enum nl80211_chan_width width;
+       u32 changed = _ieee80211_recalc_chanctx_min_def(local, ctx);
 
-       if (cfg80211_chandef_identical(&ctx->conf.def, chandef)) {
-               ieee80211_recalc_chanctx_min_def(local, ctx);
+       if (!changed)
                return;
-       }
 
-       WARN_ON(!cfg80211_chandef_compatible(&ctx->conf.def, chandef));
+       /* check is BW narrowed */
+       ieee80211_chan_bw_change(local, ctx, true);
 
-       width = ctx->conf.def.width;
-       ctx->conf.def = *chandef;
+       drv_change_chanctx(local, ctx, changed);
+
+       /* check is BW wider */
+       ieee80211_chan_bw_change(local, ctx, false);
+}
+
+static void ieee80211_change_chanctx(struct ieee80211_local *local,
+                                    struct ieee80211_chanctx *ctx,
+                                    struct ieee80211_chanctx *old_ctx,
+                                    const struct cfg80211_chan_def *chandef)
+{
+       u32 changed;
 
        /* expected to handle only 20/40/80/160 channel widths */
        switch (chandef->width) {
                WARN_ON(1);
        }
 
-       if (chandef->width < width)
-               ieee80211_chan_bw_change(local, ctx);
+       /* Check maybe BW narrowed - we do this _before_ calling recalc_chanctx_min_def
+        * due to maybe not returning from it, e.g in case new context was added
+        * first time with all parameters up to date.
+        */
+       ieee80211_chan_bw_change(local, old_ctx, true);
+
+       if (cfg80211_chandef_identical(&ctx->conf.def, chandef)) {
+               ieee80211_recalc_chanctx_min_def(local, ctx);
+               return;
+       }
 
-       drv_change_chanctx(local, ctx, IEEE80211_CHANCTX_CHANGE_WIDTH);
-       ieee80211_recalc_chanctx_min_def(local, ctx);
+       WARN_ON(!cfg80211_chandef_compatible(&ctx->conf.def, chandef));
+
+       ctx->conf.def = *chandef;
+
+       /* check if min chanctx also changed */
+       changed = IEEE80211_CHANCTX_CHANGE_WIDTH |
+                 _ieee80211_recalc_chanctx_min_def(local, ctx);
+       drv_change_chanctx(local, ctx, changed);
 
        if (!local->use_chanctx) {
                local->_oper_chandef = *chandef;
                ieee80211_hw_config(local, 0);
        }
 
-       if (chandef->width > width)
-               ieee80211_chan_bw_change(local, ctx);
+       /* check is BW wider */
+       ieee80211_chan_bw_change(local, old_ctx, false);
 }
 
 static struct ieee80211_chanctx *
                if (!compat)
                        continue;
 
-               ieee80211_change_chanctx(local, ctx, compat);
+               ieee80211_change_chanctx(local, ctx, ctx, compat);
 
                return ctx;
        }
        if (!compat)
                return;
 
-       ieee80211_change_chanctx(local, ctx, compat);
+       ieee80211_change_chanctx(local, ctx, ctx, compat);
 }
 
 static void ieee80211_recalc_radar_chanctx(struct ieee80211_local *local,
        if (WARN_ON(!chandef))
                return -EINVAL;
 
-       if (old_ctx->conf.def.width > new_ctx->conf.def.width)
-               ieee80211_chan_bw_change(local, new_ctx);
+       if (sdata->vif.bss_conf.chandef.width != sdata->reserved_chandef.width)
+               changed = BSS_CHANGED_BANDWIDTH;
 
-       ieee80211_change_chanctx(local, new_ctx, chandef);
+       ieee80211_vif_update_chandef(sdata, &sdata->reserved_chandef);
 
-       if (old_ctx->conf.def.width < new_ctx->conf.def.width)
-               ieee80211_chan_bw_change(local, new_ctx);
+       ieee80211_change_chanctx(local, new_ctx, old_ctx, chandef);
 
        vif_chsw[0].vif = &sdata->vif;
        vif_chsw[0].old_ctx = &old_ctx->conf;
        if (ieee80211_chanctx_refcount(local, old_ctx) == 0)
                ieee80211_free_chanctx(local, old_ctx);
 
-       if (sdata->vif.bss_conf.chandef.width != sdata->reserved_chandef.width)
-               changed = BSS_CHANGED_BANDWIDTH;
-
-       ieee80211_vif_update_chandef(sdata, &sdata->reserved_chandef);
-
+       ieee80211_recalc_chanctx_min_def(local, new_ctx);
        ieee80211_recalc_smps_chanctx(local, new_ctx);
        ieee80211_recalc_radar_chanctx(local, new_ctx);
-       ieee80211_recalc_chanctx_min_def(local, new_ctx);
 
        if (changed)
                ieee80211_bss_info_change_notify(sdata, changed);
        if (WARN_ON(!chandef))
                return -EINVAL;
 
-       ieee80211_change_chanctx(local, new_ctx, chandef);
+       ieee80211_change_chanctx(local, new_ctx, new_ctx, chandef);
 
        list_del(&sdata->reserved_chanctx_list);
        sdata->reserved_chanctx = NULL;
                ieee80211_recalc_smps_chanctx(local, ctx);
                ieee80211_recalc_radar_chanctx(local, ctx);
                ieee80211_recalc_chanctx_min_def(local, ctx);
-               ieee80211_chan_bw_change(local, ctx);
 
                list_for_each_entry_safe(sdata, sdata_tmp, &ctx->reserved_vifs,
                                         reserved_chanctx_list) {
 
        struct cfg80211_chan_def chandef;
        u16 ht_opmode;
        u32 flags;
-       enum ieee80211_sta_rx_bandwidth new_sta_bw;
        u32 vht_cap_info = 0;
        int ret;
 
                return -EINVAL;
        }
 
-       switch (chandef.width) {
-       case NL80211_CHAN_WIDTH_20_NOHT:
-       case NL80211_CHAN_WIDTH_20:
-               new_sta_bw = IEEE80211_STA_RX_BW_20;
-               break;
-       case NL80211_CHAN_WIDTH_40:
-               new_sta_bw = IEEE80211_STA_RX_BW_40;
-               break;
-       case NL80211_CHAN_WIDTH_80:
-               new_sta_bw = IEEE80211_STA_RX_BW_80;
-               break;
-       case NL80211_CHAN_WIDTH_80P80:
-       case NL80211_CHAN_WIDTH_160:
-               new_sta_bw = IEEE80211_STA_RX_BW_160;
-               break;
-       default:
-               return -EINVAL;
-       }
-
-       if (new_sta_bw > sta->cur_max_bandwidth)
-               new_sta_bw = sta->cur_max_bandwidth;
-
-       if (new_sta_bw < sta->sta.bandwidth) {
-               sta->sta.bandwidth = new_sta_bw;
-               rate_control_rate_update(local, sband, sta,
-                                        IEEE80211_RC_BW_CHANGED);
-       }
-
        ret = ieee80211_vif_change_bandwidth(sdata, &chandef, changed);
+
        if (ret) {
                sdata_info(sdata,
                           "AP %pM changed bandwidth to incompatible one - disconnect\n",
                return ret;
        }
 
-       if (new_sta_bw > sta->sta.bandwidth) {
-               sta->sta.bandwidth = new_sta_bw;
-               rate_control_rate_update(local, sband, sta,
-                                        IEEE80211_RC_BW_CHANGED);
-       }
-
        return 0;
 }
 
         */
 
        if (sdata->reserved_chanctx) {
-               struct ieee80211_supported_band *sband = NULL;
-               struct sta_info *mgd_sta = NULL;
-               enum ieee80211_sta_rx_bandwidth bw = IEEE80211_STA_RX_BW_20;
-
                /*
                 * with multi-vif csa driver may call ieee80211_csa_finish()
                 * many times while waiting for other interfaces to use their
                if (sdata->reserved_ready)
                        goto out;
 
-               if (sdata->vif.bss_conf.chandef.width !=
-                   sdata->csa_chandef.width) {
-                       /*
-                        * For managed interface, we need to also update the AP
-                        * station bandwidth and align the rate scale algorithm
-                        * on the bandwidth change. Here we only consider the
-                        * bandwidth of the new channel definition (as channel
-                        * switch flow does not have the full HT/VHT/HE
-                        * information), assuming that if additional changes are
-                        * required they would be done as part of the processing
-                        * of the next beacon from the AP.
-                        */
-                       switch (sdata->csa_chandef.width) {
-                       case NL80211_CHAN_WIDTH_20_NOHT:
-                       case NL80211_CHAN_WIDTH_20:
-                       default:
-                               bw = IEEE80211_STA_RX_BW_20;
-                               break;
-                       case NL80211_CHAN_WIDTH_40:
-                               bw = IEEE80211_STA_RX_BW_40;
-                               break;
-                       case NL80211_CHAN_WIDTH_80:
-                               bw = IEEE80211_STA_RX_BW_80;
-                               break;
-                       case NL80211_CHAN_WIDTH_80P80:
-                       case NL80211_CHAN_WIDTH_160:
-                               bw = IEEE80211_STA_RX_BW_160;
-                               break;
-                       }
-
-                       mgd_sta = sta_info_get(sdata, ifmgd->bssid);
-                       sband =
-                               local->hw.wiphy->bands[sdata->csa_chandef.chan->band];
-               }
-
-               if (sdata->vif.bss_conf.chandef.width >
-                   sdata->csa_chandef.width) {
-                       mgd_sta->sta.bandwidth = bw;
-                       rate_control_rate_update(local, sband, mgd_sta,
-                                                IEEE80211_RC_BW_CHANGED);
-               }
-
                ret = ieee80211_vif_use_reserved_context(sdata);
                if (ret) {
                        sdata_info(sdata,
                        goto out;
                }
 
-               if (sdata->vif.bss_conf.chandef.width <
-                   sdata->csa_chandef.width) {
-                       mgd_sta->sta.bandwidth = bw;
-                       rate_control_rate_update(local, sband, mgd_sta,
-                                                IEEE80211_RC_BW_CHANGED);
-               }
-
                goto out;
        }