# Copyright 2021 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

import math
import time
from enum import Enum

import common

from autotest_lib.server.cros.cellular.simulation_utils.BaseSimulation import BaseSimulation
from autotest_lib.server.cros.cellular.simulation_utils import BaseCellularDut


class TransmissionMode(Enum):
    """ Transmission modes for LTE (e.g., TM1, TM4, ...) """
    TM1 = "TM1"
    TM2 = "TM2"
    TM3 = "TM3"
    TM4 = "TM4"
    TM7 = "TM7"
    TM8 = "TM8"
    TM9 = "TM9"


class MimoMode(Enum):
    """ Mimo modes """
    MIMO_1x1 = "1x1"
    MIMO_2x2 = "2x2"
    MIMO_4x4 = "4x4"


class SchedulingMode(Enum):
    """ Traffic scheduling modes (e.g., STATIC, DYNAMIC) """
    DYNAMIC = "DYNAMIC"
    STATIC = "STATIC"


class DuplexMode(Enum):
    """ DL/UL Duplex mode """
    FDD = "FDD"
    TDD = "TDD"


class ModulationType(Enum):
    """DL/UL Modulation order."""
    QPSK = 'QPSK'
    Q16 = '16QAM'
    Q64 = '64QAM'
    Q256 = '256QAM'


class LteSimulation(BaseSimulation):
    """ Single-carrier LTE simulation. """

    # Simulation config keywords contained in the test name
    PARAM_FRAME_CONFIG = "tddconfig"
    PARAM_BW = "bw"
    PARAM_SCHEDULING = "scheduling"
    PARAM_SCHEDULING_STATIC = "static"
    PARAM_SCHEDULING_DYNAMIC = "dynamic"
    PARAM_PATTERN = "pattern"
    PARAM_TM = "tm"
    PARAM_UL_PW = 'pul'
    PARAM_DL_PW = 'pdl'
    PARAM_BAND = "band"
    PARAM_MIMO = "mimo"
    PARAM_DL_MCS = 'dlmcs'
    PARAM_UL_MCS = 'ulmcs'
    PARAM_SSF = 'ssf'
    PARAM_CFI = 'cfi'
    PARAM_PAGING = 'paging'
    PARAM_PHICH = 'phich'
    PARAM_RRC_STATUS_CHANGE_TIMER = "rrcstatuschangetimer"
    PARAM_DRX = 'drx'

    # Test config keywords
    KEY_TBS_PATTERN = "tbs_pattern_on"
    KEY_DL_256_QAM = "256_qam_dl"
    KEY_UL_64_QAM = "64_qam_ul"

    # Units in which signal level is defined in DOWNLINK_SIGNAL_LEVEL_DICTIONARY
    DOWNLINK_SIGNAL_LEVEL_UNITS = "RSRP"

    # RSRP signal levels thresholds (as reported by Android) in dBm/15KHz.
    # Excellent is set to -75 since callbox B Tx power is limited to -30 dBm
    DOWNLINK_SIGNAL_LEVEL_DICTIONARY = {
            'excellent': -75,
            'high': -110,
            'medium': -115,
            'weak': -120,
            'disconnected': -170
    }

    # Transmitted output power for the phone (dBm)
    UPLINK_SIGNAL_LEVEL_DICTIONARY = {
            'max': 24,
            'high': 13,
            'medium': 3,
            'low': -20
    }

    # Bandwidth [MHz] to total RBs mapping
    total_rbs_dictionary = {20: 100, 15: 75, 10: 50, 5: 25, 3: 15, 1.4: 6}

    # Bandwidth [MHz] to RB group size
    rbg_dictionary = {20: 4, 15: 4, 10: 3, 5: 2, 3: 2, 1.4: 1}

    # Bandwidth [MHz] to minimum number of DL RBs that can be assigned to a UE
    min_dl_rbs_dictionary = {20: 16, 15: 12, 10: 9, 5: 4, 3: 4, 1.4: 2}

    # Bandwidth [MHz] to minimum number of UL RBs that can be assigned to a UE
    min_ul_rbs_dictionary = {20: 8, 15: 6, 10: 4, 5: 2, 3: 2, 1.4: 1}

    # Allowed bandwidth for each band.
    allowed_bandwidth_dictionary = {
            1: [5, 10, 15, 20],
            2: [1.4, 3, 5, 10, 15, 20],
            3: [1.4, 3, 5, 10, 15, 20],
            4: [1.4, 3, 5, 10, 15, 20],
            5: [1.4, 3, 5, 10],
            7: [5, 10, 15, 20],
            8: [1.4, 3, 5, 10],
            10: [5, 10, 15, 20],
            11: [5, 10],
            12: [1.4, 3, 5, 10],
            13: [5, 10],
            14: [5, 10],
            17: [5, 10],
            18: [5, 10, 15],
            19: [5, 10, 15],
            20: [5, 10, 15, 20],
            21: [5, 10, 15],
            22: [5, 10, 15, 20],
            24: [5, 10],
            25: [1.4, 3, 5, 10, 15, 20],
            26: [1.4, 3, 5, 10, 15],
            27: [1.4, 3, 5, 10],
            28: [3, 5, 10, 15, 20],
            29: [3, 5, 10],
            30: [5, 10],
            31: [1.4, 3, 5],
            32: [5, 10, 15, 20],
            33: [5, 10, 15, 20],
            34: [5, 10, 15],
            35: [1.4, 3, 5, 10, 15, 20],
            36: [1.4, 3, 5, 10, 15, 20],
            37: [5, 10, 15, 20],
            38: [20],
            39: [5, 10, 15, 20],
            40: [5, 10, 15, 20],
            41: [5, 10, 15, 20],
            42: [5, 10, 15, 20],
            43: [5, 10, 15, 20],
            44: [3, 5, 10, 15, 20],
            45: [5, 10, 15, 20],
            46: [10, 20],
            47: [10, 20],
            48: [5, 10, 15, 20],
            49: [10, 20],
            50: [3, 5, 10, 15, 20],
            51: [3, 5],
            52: [5, 10, 15, 20],
            65: [5, 10, 15, 20],
            66: [1.4, 3, 5, 10, 15, 20],
            67: [5, 10, 15, 20],
            68: [5, 10, 15],
            69: [5],
            70: [5, 10, 15],
            71: [5, 10, 15, 20],
            72: [1.4, 3, 5],
            73: [1.4, 3, 5],
            74: [1.4, 3, 5, 10, 15, 20],
            75: [5, 10, 15, 20],
            76: [5],
            85: [5, 10],
            252: [20],
            255: [20]
    }

    # Peak throughput lookup tables for each TDD subframe
    # configuration and bandwidth
    # yapf: disable
    tdd_config4_tput_lut = {
        0: {
            5: {'DL': 3.82, 'UL': 2.63},
            10: {'DL': 11.31,'UL': 9.03},
            15: {'DL': 16.9, 'UL': 20.62},
            20: {'DL': 22.88, 'UL': 28.43}
        },
        1: {
            5: {'DL': 6.13, 'UL': 4.08},
            10: {'DL': 18.36, 'UL': 9.69},
            15: {'DL': 28.62, 'UL': 14.21},
            20: {'DL': 39.04, 'UL': 19.23}
        },
        2: {
            5: {'DL': 5.68, 'UL': 2.30},
            10: {'DL': 25.51, 'UL': 4.68},
            15: {'DL': 39.3, 'UL': 7.13},
            20: {'DL': 53.64, 'UL': 9.72}
        },
        3: {
            5: {'DL': 8.26, 'UL': 3.45},
            10: {'DL': 23.20, 'UL': 6.99},
            15: {'DL': 35.35, 'UL': 10.75},
            20: {'DL': 48.3, 'UL': 14.6}
        },
        4: {
            5: {'DL': 6.16, 'UL': 2.30},
            10: {'DL': 26.77, 'UL': 4.68},
            15: {'DL': 40.7, 'UL': 7.18},
            20: {'DL': 55.6, 'UL': 9.73}
        },
        5: {
            5: {'DL': 6.91, 'UL': 1.12},
            10: {'DL': 30.33, 'UL': 2.33},
            15: {'DL': 46.04, 'UL': 3.54},
            20: {'DL': 62.9, 'UL': 4.83}
        },
        6: {
            5: {'DL': 6.13, 'UL': 4.13},
            10: {'DL': 14.79, 'UL': 11.98},
            15: {'DL': 23.28, 'UL': 17.46},
            20: {'DL': 31.75, 'UL': 23.95}
        }
    }

    tdd_config3_tput_lut = {
        0: {
            5: {'DL': 5.04, 'UL': 3.7},
            10: {'DL': 15.11, 'UL': 17.56},
            15: {'DL': 22.59, 'UL': 30.31},
            20: {'DL': 30.41, 'UL': 41.61}
        },
        1: {
            5: {'DL': 8.07, 'UL': 5.66},
            10: {'DL': 24.58, 'UL': 13.66},
            15: {'DL': 39.05, 'UL': 20.68},
            20: {'DL': 51.59, 'UL': 28.76}
        },
        2: {
            5: {'DL': 7.59, 'UL': 3.31},
            10: {'DL': 34.08, 'UL': 6.93},
            15: {'DL': 53.64, 'UL': 10.51},
            20: {'DL': 70.55, 'UL': 14.41}
        },
        3: {
            5: {'DL': 10.9, 'UL': 5.0},
            10: {'DL': 30.99, 'UL': 10.25},
            15: {'DL': 48.3, 'UL': 15.81},
            20: {'DL': 63.24, 'UL': 21.65}
        },
        4: {
            5: {'DL': 8.11, 'UL': 3.32},
            10: {'DL': 35.74, 'UL': 6.95},
            15: {'DL': 55.6, 'UL': 10.51},
            20: {'DL': 72.72, 'UL': 14.41}
        },
        5: {
            5: {'DL': 9.28, 'UL': 1.57},
            10: {'DL': 40.49, 'UL': 3.44},
            15: {'DL': 62.9, 'UL': 5.23},
            20: {'DL': 82.21, 'UL': 7.15}
        },
        6: {
            5: {'DL': 8.06, 'UL': 5.74},
            10: {'DL': 19.82, 'UL': 17.51},
            15: {'DL': 31.75, 'UL': 25.77},
            20: {'DL': 42.12, 'UL': 34.91}
        }
    }

    tdd_config2_tput_lut = {
        0: {
            5: {'DL': 3.11, 'UL': 2.55},
            10: {'DL': 9.93, 'UL': 11.1},
            15: {'DL': 13.9, 'UL': 21.51},
            20: {'DL': 20.02, 'UL': 41.66}
        },
        1: {
            5: {'DL': 5.33, 'UL': 4.27},
            10: {'DL': 15.14, 'UL': 13.95},
            15: {'DL': 33.84, 'UL': 19.73},
            20: {'DL': 44.61, 'UL': 27.35}
        },
        2: {
            5: {'DL': 6.87, 'UL': 3.32},
            10: {'DL': 17.06, 'UL': 6.76},
            15: {'DL': 49.63, 'UL': 10.5},
            20: {'DL': 65.2, 'UL': 14.41}
        },
        3: {
            5: {'DL': 5.41, 'UL': 4.17},
            10: {'DL': 16.89, 'UL': 9.73},
            15: {'DL': 44.29, 'UL': 15.7},
            20: {'DL': 53.95, 'UL': 19.85}
        },
        4: {
            5: {'DL': 8.7, 'UL': 3.32},
            10: {'DL': 17.58, 'UL': 6.76},
            15: {'DL': 51.08, 'UL': 10.47},
            20: {'DL': 66.45, 'UL': 14.38}
        },
        5: {
            5: {'DL': 9.46, 'UL': 1.55},
            10: {'DL': 19.02, 'UL': 3.48},
            15: {'DL': 58.89, 'UL': 5.23},
            20: {'DL': 76.85, 'UL': 7.1}
        },
        6: {
            5: {'DL': 4.74, 'UL': 3.9},
            10: {'DL': 12.32, 'UL': 13.37},
            15: {'DL': 27.74, 'UL': 25.02},
            20: {'DL': 35.48, 'UL': 32.95}
        }
    }

    tdd_config1_tput_lut = {
        0: {
            5: {'DL': 4.25, 'UL': 3.35},
            10: {'DL': 8.38, 'UL': 7.22},
            15: {'DL': 12.41, 'UL': 13.91},
            20: {'DL': 16.27, 'UL': 24.09}
        },
        1: {
            5: {'DL': 7.28, 'UL': 4.61},
            10: {'DL': 14.73, 'UL': 9.69},
            15: {'DL': 21.91, 'UL': 13.86},
            20: {'DL': 27.63, 'UL': 17.18}
        },
        2: {
            5: {'DL': 10.37, 'UL': 2.27},
            10: {'DL': 20.92, 'UL': 4.66},
            15: {'DL': 31.01, 'UL': 7.04},
            20: {'DL': 42.03, 'UL': 9.75}
        },
        3: {
            5: {'DL': 9.25, 'UL': 3.44},
            10: {'DL': 18.38, 'UL': 6.95},
            15: {'DL': 27.59, 'UL': 10.62},
            20: {'DL': 34.85, 'UL': 13.45}
        },
        4: {
            5: {'DL': 10.71, 'UL': 2.26},
            10: {'DL': 21.54, 'UL': 4.67},
            15: {'DL': 31.91, 'UL': 7.2},
            20: {'DL': 43.35, 'UL': 9.74}
        },
        5: {
            5: {'DL': 12.34, 'UL': 1.08},
            10: {'DL': 24.78, 'UL': 2.34},
            15: {'DL': 36.68, 'UL': 3.57},
            20: {'DL': 49.84, 'UL': 4.81}
        },
        6: {
            5: {'DL': 5.76, 'UL': 4.41},
            10: {'DL': 11.68, 'UL': 9.7},
            15: {'DL': 17.34, 'UL': 17.95},
            20: {'DL': 23.5, 'UL': 23.42}
        }
    }
    # yapf: enable

    # Peak throughput lookup table dictionary
    tdd_config_tput_lut_dict = {
            'TDD_CONFIG1':
            tdd_config1_tput_lut,  # DL 256QAM, UL 64QAM & TBS turned OFF
            'TDD_CONFIG2':
            tdd_config2_tput_lut,  # DL 256QAM, UL 64 QAM turned ON & TBS OFF
            'TDD_CONFIG3':
            tdd_config3_tput_lut,  # DL 256QAM, UL 64QAM & TBS turned ON
            'TDD_CONFIG4':
            tdd_config4_tput_lut  # DL 256QAM, UL 64 QAM turned OFF & TBS ON
    }

    class BtsConfig(BaseSimulation.BtsConfig):
        """ Extension of the BaseBtsConfig to implement parameters that are
         exclusive to LTE.

        Attributes:
            band: an integer indicating the required band number.
            dlul_config: an integer indicating the TDD config number.
            ssf_config: an integer indicating the Special Sub-Frame config.
            bandwidth: a float indicating the required channel bandwidth.
            mimo_mode: an instance of LteSimulation.MimoMode indicating the
                required MIMO mode for the downlink signal.
            transmission_mode: an instance of LteSimulation.TransmissionMode
                indicating the required TM.
            scheduling_mode: an instance of LteSimulation.SchedulingMode
                indicating whether to use Static or Dynamic scheduling.
            dl_rbs: an integer indicating the number of downlink RBs
            ul_rbs: an integer indicating the number of uplink RBs
            dl_mcs: an integer indicating the MCS for the downlink signal
            ul_mcs: an integer indicating the MCS for the uplink signal
            dl_modulation_order: a string indicating a DL modulation scheme
            ul_modulation_order: a string indicating an UL modulation scheme
            tbs_pattern_on: a boolean indicating whether full allocation mode
                should be used or not
            dl_channel: an integer indicating the downlink channel number
            cfi: an integer indicating the Control Format Indicator
            paging_cycle: an integer indicating the paging cycle duration in
                milliseconds
            phich: a string indicating the PHICH group size parameter
            drx_connected_mode: a boolean indicating whether cDRX mode is
                on or off
            drx_on_duration_timer: number of PDCCH subframes representing
                DRX on duration
            drx_inactivity_timer: number of PDCCH subframes to wait before
                entering DRX mode
            drx_retransmission_timer: number of consecutive PDCCH subframes
                to wait for retransmission
            drx_long_cycle: number of subframes representing one long DRX cycle.
                One cycle consists of DRX sleep + DRX on duration
            drx_long_cycle_offset: number representing offset in range
                0 to drx_long_cycle - 1
        """

        def __init__(self):
            """ Initialize the base station config by setting all its
            parameters to None. """
            super(LteSimulation.BtsConfig, self).__init__()
            self.band = None
            self.dlul_config = None
            self.ssf_config = None
            self.bandwidth = None
            self.mimo_mode = None
            self.transmission_mode = None
            self.scheduling_mode = None
            self.dl_rbs = None
            self.ul_rbs = None
            self.dl_mcs = None
            self.ul_mcs = None
            self.dl_modulation_order = None
            self.ul_modulation_order = None
            self.tbs_pattern_on = None
            self.dl_channel = None
            self.cfi = None
            self.paging_cycle = None
            self.phich = None
            self.drx_connected_mode = None
            self.drx_on_duration_timer = None
            self.drx_inactivity_timer = None
            self.drx_retransmission_timer = None
            self.drx_long_cycle = None
            self.drx_long_cycle_offset = None

    def __init__(self, simulator, log, dut, test_config, calibration_table):
        """ Initializes the simulator for a single-carrier LTE simulation.

        Loads a simple LTE simulation environment with 1 basestation.

        Args:
            simulator: a cellular simulator controller
            log: a logger handle
            dut: a device handler implementing BaseCellularDut
            test_config: test configuration obtained from the config file
            calibration_table: a dictionary containing path losses for
                different bands.

        """

        super(LteSimulation, self).__init__(simulator, log, dut, test_config,
                                            calibration_table)

        self.dut.set_preferred_network_type(
                BaseCellularDut.PreferredNetworkType.LTE_ONLY)

        # Get TBS pattern setting from the test configuration
        if self.KEY_TBS_PATTERN not in test_config:
            self.log.warning("The key '{}' is not set in the config file. "
                             "Setting to true by default.".format(
                                     self.KEY_TBS_PATTERN))
        self.primary_config.tbs_pattern_on = test_config.get(
                self.KEY_TBS_PATTERN, True)

        # Get the 256-QAM setting from the test configuration
        if self.KEY_DL_256_QAM not in test_config:
            self.log.warning("The key '{}' is not set in the config file. "
                             "Setting to false by default.".format(
                                     self.KEY_DL_256_QAM))

        self.dl_256_qam = test_config.get(self.KEY_DL_256_QAM, False)

        if self.dl_256_qam:
            if not self.simulator.LTE_SUPPORTS_DL_256QAM:
                self.log.warning("The key '{}' is set to true but the "
                                 "simulator doesn't support that modulation "
                                 "order.".format(self.KEY_DL_256_QAM))
                self.dl_256_qam = False
            else:
                self.primary_config.dl_modulation_order = ModulationType.Q256

        else:
            self.log.warning(
                    'dl modulation 256QAM is not specified in config, '
                    'setting to default value 64QAM')
            self.primary_config.dl_modulation_order = ModulationType.Q64
        # Get the 64-QAM setting from the test configuration
        if self.KEY_UL_64_QAM not in test_config:
            self.log.warning("The key '{}' is not set in the config file. "
                             "Setting to false by default.".format(
                                     self.KEY_UL_64_QAM))

        self.ul_64_qam = test_config.get(self.KEY_UL_64_QAM, False)

        if self.ul_64_qam:
            if not self.simulator.LTE_SUPPORTS_UL_64QAM:
                self.log.warning("The key '{}' is set to true but the "
                                 "simulator doesn't support that modulation "
                                 "order.".format(self.KEY_UL_64_QAM))
                self.ul_64_qam = False
            else:
                self.primary_config.ul_modulation_order = ModulationType.Q64
        else:
            self.log.warning('ul modulation 64QAM is not specified in config, '
                             'setting to default value 16QAM')
            self.primary_config.ul_modulation_order = ModulationType.Q16

        self.simulator.configure_bts(self.primary_config)

    def setup_simulator(self):
        """ Do initial configuration in the simulator. """
        self.simulator.setup_lte_scenario()

    def parse_parameters(self, parameters):
        """ Configs an LTE simulation using a list of parameters.

        Calls the parent method first, then consumes parameters specific to LTE.

        Args:
            parameters: list of parameters
        """

        # Instantiate a new configuration object
        new_config = self.BtsConfig()

        # Setup band

        values = self.consume_parameter(parameters, self.PARAM_BAND, 1)

        if not values:
            raise ValueError(
                    "The test name needs to include parameter '{}' followed by "
                    "the required band number.".format(self.PARAM_BAND))

        new_config.band = values[1]

        # Set TDD-only configs
        if self.get_duplex_mode(new_config.band) == DuplexMode.TDD:

            # Sub-frame DL/UL config
            values = self.consume_parameter(parameters,
                                            self.PARAM_FRAME_CONFIG, 1)
            if not values:
                raise ValueError(
                        "When a TDD band is selected the frame "
                        "structure has to be indicated with the '{}' "
                        "parameter followed by a number from 0 to 6.".format(
                                self.PARAM_FRAME_CONFIG))

            new_config.dlul_config = int(values[1])

            # Special Sub-Frame configuration
            values = self.consume_parameter(parameters, self.PARAM_SSF, 1)

            if not values:
                self.log.warning(
                        'The {} parameter was not provided. Setting '
                        'Special Sub-Frame config to 6 by default.'.format(
                                self.PARAM_SSF))
                new_config.ssf_config = 6
            else:
                new_config.ssf_config = int(values[1])

        # Setup bandwidth

        values = self.consume_parameter(parameters, self.PARAM_BW, 1)

        if not values:
            raise ValueError(
                    "The test name needs to include parameter {} followed by an "
                    "int value (to indicate 1.4 MHz use 14).".format(
                            self.PARAM_BW))

        bw = float(values[1])

        if bw == 14:
            bw = 1.4

        new_config.bandwidth = bw

        # Setup mimo mode

        values = self.consume_parameter(parameters, self.PARAM_MIMO, 1)

        if not values:
            raise ValueError(
                    "The test name needs to include parameter '{}' followed by the "
                    "mimo mode.".format(self.PARAM_MIMO))

        for mimo_mode in MimoMode:
            if values[1] == mimo_mode.value:
                new_config.mimo_mode = mimo_mode
                break
        else:
            raise ValueError("The {} parameter needs to be followed by either "
                             "1x1, 2x2 or 4x4.".format(self.PARAM_MIMO))

        if (new_config.mimo_mode == MimoMode.MIMO_4x4
                    and not self.simulator.LTE_SUPPORTS_4X4_MIMO):
            raise ValueError("The test requires 4x4 MIMO, but that is not "
                             "supported by the cellular simulator.")

        # Setup transmission mode

        values = self.consume_parameter(parameters, self.PARAM_TM, 1)

        if not values:
            raise ValueError(
                    "The test name needs to include parameter {} followed by an "
                    "int value from 1 to 4 indicating transmission mode.".
                    format(self.PARAM_TM))

        for tm in TransmissionMode:
            if values[1] == tm.value[2:]:
                new_config.transmission_mode = tm
                break
        else:
            raise ValueError("The {} parameter needs to be followed by either "
                             "TM1, TM2, TM3, TM4, TM7, TM8 or TM9.".format(
                                     self.PARAM_MIMO))

        # Setup scheduling mode

        values = self.consume_parameter(parameters, self.PARAM_SCHEDULING, 1)

        if not values:
            new_config.scheduling_mode = SchedulingMode.STATIC
            self.log.warning(
                    "The test name does not include the '{}' parameter. Setting to "
                    "static by default.".format(self.PARAM_SCHEDULING))
        elif values[1] == self.PARAM_SCHEDULING_DYNAMIC:
            new_config.scheduling_mode = SchedulingMode.DYNAMIC
        elif values[1] == self.PARAM_SCHEDULING_STATIC:
            new_config.scheduling_mode = SchedulingMode.STATIC
        else:
            raise ValueError(
                    "The test name parameter '{}' has to be followed by either "
                    "'dynamic' or 'static'.".format(self.PARAM_SCHEDULING))

        if new_config.scheduling_mode == SchedulingMode.STATIC:

            values = self.consume_parameter(parameters, self.PARAM_PATTERN, 2)

            if not values:
                self.log.warning(
                        "The '{}' parameter was not set, using 100% RBs for both "
                        "DL and UL. To set the percentages of total RBs include "
                        "the '{}' parameter followed by two ints separated by an "
                        "underscore indicating downlink and uplink percentages."
                        .format(self.PARAM_PATTERN, self.PARAM_PATTERN))
                dl_pattern = 100
                ul_pattern = 100
            else:
                dl_pattern = int(values[1])
                ul_pattern = int(values[2])

            if not (0 <= dl_pattern <= 100 and 0 <= ul_pattern <= 100):
                raise ValueError(
                        "The scheduling pattern parameters need to be two "
                        "positive numbers between 0 and 100.")

            new_config.dl_rbs, new_config.ul_rbs = (
                    self.allocation_percentages_to_rbs(
                            new_config.bandwidth, new_config.transmission_mode,
                            dl_pattern, ul_pattern))

            # Look for a DL MCS configuration in the test parameters. If it is
            # not present, use a default value.
            dlmcs = self.consume_parameter(parameters, self.PARAM_DL_MCS, 1)

            if dlmcs:
                new_config.dl_mcs = int(dlmcs[1])
            else:
                self.log.warning(
                        'The test name does not include the {} parameter. Setting '
                        'to the max value by default'.format(
                                self.PARAM_DL_MCS))
                if self.dl_256_qam and new_config.bandwidth == 1.4:
                    new_config.dl_mcs = 26
                elif (not self.dl_256_qam
                      and self.primary_config.tbs_pattern_on
                      and new_config.bandwidth != 1.4):
                    new_config.dl_mcs = 28
                else:
                    new_config.dl_mcs = 27

            # Look for an UL MCS configuration in the test parameters. If it is
            # not present, use a default value.
            ulmcs = self.consume_parameter(parameters, self.PARAM_UL_MCS, 1)

            if ulmcs:
                new_config.ul_mcs = int(ulmcs[1])
            else:
                self.log.warning(
                        'The test name does not include the {} parameter. Setting '
                        'to the max value by default'.format(
                                self.PARAM_UL_MCS))
                if self.ul_64_qam:
                    new_config.ul_mcs = 28
                else:
                    new_config.ul_mcs = 23

        # Configure the simulation for DRX mode

        drx = self.consume_parameter(parameters, self.PARAM_DRX, 5)

        if drx and len(drx) == 6:
            new_config.drx_connected_mode = True
            new_config.drx_on_duration_timer = drx[1]
            new_config.drx_inactivity_timer = drx[2]
            new_config.drx_retransmission_timer = drx[3]
            new_config.drx_long_cycle = drx[4]
            try:
                long_cycle = int(drx[4])
                long_cycle_offset = int(drx[5])
                if long_cycle_offset in range(0, long_cycle):
                    new_config.drx_long_cycle_offset = long_cycle_offset
                else:
                    self.log.error(
                            ("The cDRX long cycle offset must be in the "
                             "range 0 to (long cycle  - 1). Setting "
                             "long cycle offset to 0"))
                    new_config.drx_long_cycle_offset = 0

            except ValueError:
                self.log.error(("cDRX long cycle and long cycle offset "
                                "must be integers. Disabling cDRX mode."))
                new_config.drx_connected_mode = False
        else:
            self.log.warning(("DRX mode was not configured properly. "
                              "Please provide the following 5 values: "
                              "1) DRX on duration timer "
                              "2) Inactivity timer "
                              "3) Retransmission timer "
                              "4) Long DRX cycle duration "
                              "5) Long DRX cycle offset "
                              "Example: drx_2_6_16_20_0"))

        # Setup LTE RRC status change function and timer for LTE idle test case
        values = self.consume_parameter(parameters,
                                        self.PARAM_RRC_STATUS_CHANGE_TIMER, 1)
        if not values:
            self.log.info(
                    "The test name does not include the '{}' parameter. Disabled "
                    "by default.".format(self.PARAM_RRC_STATUS_CHANGE_TIMER))
            self.simulator.set_lte_rrc_state_change_timer(False)
        else:
            timer = int(values[1])
            self.simulator.set_lte_rrc_state_change_timer(True, timer)
            self.rrc_sc_timer = timer

        # Channel Control Indicator
        values = self.consume_parameter(parameters, self.PARAM_CFI, 1)

        if not values:
            self.log.warning('The {} parameter was not provided. Setting '
                             'CFI to BESTEFFORT.'.format(self.PARAM_CFI))
            new_config.cfi = 'BESTEFFORT'
        else:
            new_config.cfi = values[1]

        # PHICH group size
        values = self.consume_parameter(parameters, self.PARAM_PHICH, 1)

        if not values:
            self.log.warning('The {} parameter was not provided. Setting '
                             'PHICH group size to 1 by default.'.format(
                                     self.PARAM_PHICH))
            new_config.phich = '1'
        else:
            if values[1] == '16':
                new_config.phich = '1/6'
            elif values[1] == '12':
                new_config.phich = '1/2'
            elif values[1] in ['1/6', '1/2', '1', '2']:
                new_config.phich = values[1]
            else:
                raise ValueError('The {} parameter can only be followed by 1,'
                                 '2, 1/2 (or 12) and 1/6 (or 16).'.format(
                                         self.PARAM_PHICH))

        # Paging cycle duration
        values = self.consume_parameter(parameters, self.PARAM_PAGING, 1)

        if not values:
            self.log.warning('The {} parameter was not provided. Setting '
                             'paging cycle duration to 1280 ms by '
                             'default.'.format(self.PARAM_PAGING))
            new_config.paging_cycle = 1280
        else:
            try:
                new_config.paging_cycle = int(values[1])
            except ValueError:
                raise ValueError(
                        'The {} parameter has to be followed by the paging cycle '
                        'duration in milliseconds.'.format(self.PARAM_PAGING))

        # Get uplink power

        ul_power = self.get_uplink_power_from_parameters(parameters)

        # Power is not set on the callbox until after the simulation is
        # started. Saving this value in a variable for later
        self.sim_ul_power = ul_power

        # Get downlink power

        dl_power = self.get_downlink_power_from_parameters(parameters)

        # Power is not set on the callbox until after the simulation is
        # started. Saving this value in a variable for later
        self.sim_dl_power = dl_power

        # Setup the base station with the obtained configuration and then save
        # these parameters in the current configuration object
        self.simulator.configure_bts(new_config)
        self.primary_config.incorporate(new_config)

        # Now that the band is set, calibrate the link if necessary
        self.load_pathloss_if_required()

    def calibrated_downlink_rx_power(self, bts_config, rsrp):
        """ LTE simulation overrides this method so that it can convert from
        RSRP to total signal power transmitted from the basestation.

        Args:
            bts_config: the current configuration at the base station
            rsrp: desired rsrp, contained in a key value pair
        """

        power = self.rsrp_to_signal_power(rsrp, bts_config)

        self.log.info(
                "Setting downlink signal level to {} RSRP ({} dBm)".format(
                        rsrp, power))

        # Use parent method to calculate signal level
        return super(LteSimulation,
                     self).calibrated_downlink_rx_power(bts_config, power)

    def downlink_calibration(self, rat=None, power_units_conversion_func=None):
        """ Computes downlink path loss and returns the calibration value.

        See base class implementation for details.

        Args:
            rat: ignored, replaced by 'lteRsrp'
            power_units_conversion_func: ignored, replaced by
                self.rsrp_to_signal_power

        Returns:
            Downlink calibration value and measured DL power. Note that the
            phone only reports RSRP of the primary chain
        """

        return super().downlink_calibration(
                rat='lteDbm',
                power_units_conversion_func=self.rsrp_to_signal_power)

    def rsrp_to_signal_power(self, rsrp, bts_config):
        """ Converts rsrp to total band signal power

        RSRP is measured per subcarrier, so total band power needs to be
        multiplied by the number of subcarriers being used.

        Args:
            rsrp: desired rsrp in dBm
            bts_config: a base station configuration object
        Returns:
            Total band signal power in dBm
        """

        bandwidth = bts_config.bandwidth

        if bandwidth == 20:  # 100 RBs
            power = rsrp + 30.79
        elif bandwidth == 15:  # 75 RBs
            power = rsrp + 29.54
        elif bandwidth == 10:  # 50 RBs
            power = rsrp + 27.78
        elif bandwidth == 5:  # 25 RBs
            power = rsrp + 24.77
        elif bandwidth == 3:  # 15 RBs
            power = rsrp + 22.55
        elif bandwidth == 1.4:  # 6 RBs
            power = rsrp + 18.57
        else:
            raise ValueError("Invalid bandwidth value.")

        return power

    def maximum_downlink_throughput(self):
        """ Calculates maximum achievable downlink throughput in the current
            simulation state.

        Returns:
            Maximum throughput in mbps.

        """

        return self.bts_maximum_downlink_throughtput(self.primary_config)

    def bts_maximum_downlink_throughtput(self, bts_config):
        """ Calculates maximum achievable downlink throughput for a single
        base station from its configuration object.

        Args:
            bts_config: a base station configuration object.

        Returns:
            Maximum throughput in mbps.

        """
        if bts_config.mimo_mode == MimoMode.MIMO_1x1:
            streams = 1
        elif bts_config.mimo_mode == MimoMode.MIMO_2x2:
            streams = 2
        elif bts_config.mimo_mode == MimoMode.MIMO_4x4:
            streams = 4
        else:
            raise ValueError('Unable to calculate maximum downlink throughput '
                             'because the MIMO mode has not been set.')

        bandwidth = bts_config.bandwidth
        rb_ratio = bts_config.dl_rbs / self.total_rbs_dictionary[bandwidth]
        mcs = bts_config.dl_mcs

        max_rate_per_stream = None

        tdd_subframe_config = bts_config.dlul_config
        duplex_mode = self.get_duplex_mode(bts_config.band)

        if duplex_mode == DuplexMode.TDD:
            if self.dl_256_qam:
                if mcs == 27:
                    if bts_config.tbs_pattern_on:
                        max_rate_per_stream = self.tdd_config_tput_lut_dict[
                                'TDD_CONFIG3'][tdd_subframe_config][bandwidth][
                                        'DL']
                    else:
                        max_rate_per_stream = self.tdd_config_tput_lut_dict[
                                'TDD_CONFIG2'][tdd_subframe_config][bandwidth][
                                        'DL']
            else:
                if mcs == 28:
                    if bts_config.tbs_pattern_on:
                        max_rate_per_stream = self.tdd_config_tput_lut_dict[
                                'TDD_CONFIG4'][tdd_subframe_config][bandwidth][
                                        'DL']
                    else:
                        max_rate_per_stream = self.tdd_config_tput_lut_dict[
                                'TDD_CONFIG1'][tdd_subframe_config][bandwidth][
                                        'DL']

        elif duplex_mode == DuplexMode.FDD:
            if (not self.dl_256_qam and bts_config.tbs_pattern_on
                        and mcs == 28):
                max_rate_per_stream = {
                        3: 9.96,
                        5: 17.0,
                        10: 34.7,
                        15: 52.7,
                        20: 72.2
                }.get(bandwidth, None)
            if (not self.dl_256_qam and bts_config.tbs_pattern_on
                        and mcs == 27):
                max_rate_per_stream = {
                        1.4: 2.94,
                }.get(bandwidth, None)
            elif (not self.dl_256_qam and not bts_config.tbs_pattern_on
                  and mcs == 27):
                max_rate_per_stream = {
                        1.4: 2.87,
                        3: 7.7,
                        5: 14.4,
                        10: 28.7,
                        15: 42.3,
                        20: 57.7
                }.get(bandwidth, None)
            elif self.dl_256_qam and bts_config.tbs_pattern_on and mcs == 27:
                max_rate_per_stream = {
                        3: 13.2,
                        5: 22.9,
                        10: 46.3,
                        15: 72.2,
                        20: 93.9
                }.get(bandwidth, None)
            elif self.dl_256_qam and bts_config.tbs_pattern_on and mcs == 26:
                max_rate_per_stream = {
                        1.4: 3.96,
                }.get(bandwidth, None)
            elif (self.dl_256_qam and not bts_config.tbs_pattern_on
                  and mcs == 27):
                max_rate_per_stream = {
                        3: 11.3,
                        5: 19.8,
                        10: 44.1,
                        15: 68.1,
                        20: 88.4
                }.get(bandwidth, None)
            elif (self.dl_256_qam and not bts_config.tbs_pattern_on
                  and mcs == 26):
                max_rate_per_stream = {
                        1.4: 3.96,
                }.get(bandwidth, None)

        if not max_rate_per_stream:
            raise NotImplementedError(
                    "The calculation for tbs pattern = {} "
                    "and mcs = {} is not implemented.".format(
                            "FULLALLOCATION"
                            if bts_config.tbs_pattern_on else "OFF", mcs))

        return max_rate_per_stream * streams * rb_ratio

    def maximum_uplink_throughput(self):
        """ Calculates maximum achievable uplink throughput in the current
            simulation state.

        Returns:
            Maximum throughput in mbps.

        """

        return self.bts_maximum_uplink_throughtput(self.primary_config)

    def bts_maximum_uplink_throughtput(self, bts_config):
        """ Calculates maximum achievable uplink throughput for the selected
        basestation from its configuration object.

        Args:
            bts_config: an LTE base station configuration object.

        Returns:
            Maximum throughput in mbps.

        """

        bandwidth = bts_config.bandwidth
        rb_ratio = bts_config.ul_rbs / self.total_rbs_dictionary[bandwidth]
        mcs = bts_config.ul_mcs

        max_rate_per_stream = None

        tdd_subframe_config = bts_config.dlul_config
        duplex_mode = self.get_duplex_mode(bts_config.band)

        if duplex_mode == DuplexMode.TDD:
            if self.ul_64_qam:
                if mcs == 28:
                    if bts_config.tbs_pattern_on:
                        max_rate_per_stream = self.tdd_config_tput_lut_dict[
                                'TDD_CONFIG3'][tdd_subframe_config][bandwidth][
                                        'UL']
                    else:
                        max_rate_per_stream = self.tdd_config_tput_lut_dict[
                                'TDD_CONFIG2'][tdd_subframe_config][bandwidth][
                                        'UL']
            else:
                if mcs == 23:
                    if bts_config.tbs_pattern_on:
                        max_rate_per_stream = self.tdd_config_tput_lut_dict[
                                'TDD_CONFIG4'][tdd_subframe_config][bandwidth][
                                        'UL']
                    else:
                        max_rate_per_stream = self.tdd_config_tput_lut_dict[
                                'TDD_CONFIG1'][tdd_subframe_config][bandwidth][
                                        'UL']

        elif duplex_mode == DuplexMode.FDD:
            if mcs == 23 and not self.ul_64_qam:
                max_rate_per_stream = {
                        1.4: 2.85,
                        3: 7.18,
                        5: 12.1,
                        10: 24.5,
                        15: 36.5,
                        20: 49.1
                }.get(bandwidth, None)
            elif mcs == 28 and self.ul_64_qam:
                max_rate_per_stream = {
                        1.4: 4.2,
                        3: 10.5,
                        5: 17.2,
                        10: 35.3,
                        15: 53.0,
                        20: 72.6
                }.get(bandwidth, None)

        if not max_rate_per_stream:
            raise NotImplementedError(
                    "The calculation fir mcs = {} is not implemented.".format(
                            "FULLALLOCATION"
                            if bts_config.tbs_pattern_on else "OFF", mcs))

        return max_rate_per_stream * rb_ratio

    def allocation_percentages_to_rbs(self, bw, tm, dl, ul):
        """ Converts usage percentages to number of DL/UL RBs

        Because not any number of DL/UL RBs can be obtained for a certain
        bandwidth, this function calculates the number of RBs that most
        closely matches the desired DL/UL percentages.

        Args:
            bw: the bandwidth for the which the RB configuration is requested
            tm: the transmission in which the base station will be operating
            dl: desired percentage of downlink RBs
            ul: desired percentage of uplink RBs
        Returns:
            a tuple indicating the number of downlink and uplink RBs
        """

        # Validate the arguments
        if (not 0 <= dl <= 100) or (not 0 <= ul <= 100):
            raise ValueError("The percentage of DL and UL RBs have to be two "
                             "positive between 0 and 100.")

        # Get min and max values from tables
        max_rbs = self.total_rbs_dictionary[bw]
        min_dl_rbs = self.min_dl_rbs_dictionary[bw]
        min_ul_rbs = self.min_ul_rbs_dictionary[bw]

        def percentage_to_amount(min_val, max_val, percentage):
            """ Returns the integer between min_val and max_val that is closest
            to percentage/100*max_val
            """

            # Calculate the value that corresponds to the required percentage.
            closest_int = round(max_val * percentage / 100)
            # Cannot be less than min_val
            closest_int = max(closest_int, min_val)
            # RBs cannot be more than max_rbs
            closest_int = min(closest_int, max_val)

            return closest_int

        # Calculate the number of DL RBs

        # Get the number of DL RBs that corresponds to
        #  the required percentage.
        desired_dl_rbs = percentage_to_amount(min_val=min_dl_rbs,
                                              max_val=max_rbs,
                                              percentage=dl)

        if tm == TransmissionMode.TM3 or tm == TransmissionMode.TM4:

            # For TM3 and TM4 the number of DL RBs needs to be max_rbs or a
            # multiple of the RBG size

            if desired_dl_rbs == max_rbs:
                dl_rbs = max_rbs
            else:
                dl_rbs = (math.ceil(desired_dl_rbs / self.rbg_dictionary[bw]) *
                          self.rbg_dictionary[bw])

        else:
            # The other TMs allow any number of RBs between 1 and max_rbs
            dl_rbs = desired_dl_rbs

        # Calculate the number of UL RBs

        # Get the number of UL RBs that corresponds
        # to the required percentage
        desired_ul_rbs = percentage_to_amount(min_val=min_ul_rbs,
                                              max_val=max_rbs,
                                              percentage=ul)

        # Create a list of all possible UL RBs assignment
        # The standard allows any number that can be written as
        # 2**a * 3**b * 5**c for any combination of a, b and c.

        def pow_range(max_value, base):
            """ Returns a range of all possible powers of base under
              the given max_value.
          """
            return range(int(math.ceil(math.log(max_value, base))))

        possible_ul_rbs = [
            2**a * 3**b * 5**c for a in pow_range(max_rbs, 2)
            for b in pow_range(max_rbs, 3)
            for c in pow_range(max_rbs, 5)
            if 2**a * 3**b * 5**c <= max_rbs] # yapf: disable

        # Find the value in the list that is closest to desired_ul_rbs
        differences = [abs(rbs - desired_ul_rbs) for rbs in possible_ul_rbs]
        ul_rbs = possible_ul_rbs[differences.index(min(differences))]

        # Report what are the obtained RB percentages
        self.log.info("Requested a {}% / {}% RB allocation. Closest possible "
                      "percentages are {}% / {}%.".format(
                              dl, ul, round(100 * dl_rbs / max_rbs),
                              round(100 * ul_rbs / max_rbs)))

        return dl_rbs, ul_rbs

    def calibrate(self, band):
        """ Calculates UL and DL path loss if it wasn't done before

        Before running the base class implementation, configure the base station
        to only use one downlink antenna with maximum bandwidth.

        Args:
            band: the band that is currently being calibrated.
        """

        # Save initial values in a configuration object so they can be restored
        restore_config = self.BtsConfig()
        restore_config.mimo_mode = self.primary_config.mimo_mode
        restore_config.transmission_mode = self.primary_config.transmission_mode
        restore_config.bandwidth = self.primary_config.bandwidth

        # Set up a temporary calibration configuration.
        temporary_config = self.BtsConfig()
        temporary_config.mimo_mode = MimoMode.MIMO_1x1
        temporary_config.transmission_mode = TransmissionMode.TM1
        temporary_config.bandwidth = max(
                self.allowed_bandwidth_dictionary[int(band)])
        self.simulator.configure_bts(temporary_config)
        self.primary_config.incorporate(temporary_config)

        super().calibrate(band)

        # Restore values as they were before changing them for calibration.
        self.simulator.configure_bts(restore_config)
        self.primary_config.incorporate(restore_config)

    def start_traffic_for_calibration(self):
        """
            If TBS pattern is set to full allocation, there is no need to start
            IP traffic.
        """
        if not self.primary_config.tbs_pattern_on:
            super().start_traffic_for_calibration()

    def stop_traffic_for_calibration(self):
        """
            If TBS pattern is set to full allocation, IP traffic wasn't started
        """
        if not self.primary_config.tbs_pattern_on:
            super().stop_traffic_for_calibration()

    def get_duplex_mode(self, band):
        """ Determines if the band uses FDD or TDD duplex mode

        Args:
            band: a band number
        Returns:
            an variable of class DuplexMode indicating if band is FDD or TDD
        """

        if 33 <= int(band) <= 46:
            return DuplexMode.TDD
        else:
            return DuplexMode.FDD

    def get_measured_ul_power(self, samples=5, wait_after_sample=3):
        """ Calculates UL power using measurements from the callbox and the
        calibration data.

        Args:
            samples: the numble of samples to average
            wait_after_sample: time in seconds to wait in between samples

        Returns:
            the ul power at the UE antenna ports in dBs
        """
        ul_power_sum = 0
        samples_left = samples

        while samples_left > 0:
            ul_power_sum += self.simulator.get_measured_pusch_power()
            samples_left -= 1
            time.sleep(wait_after_sample)

        # Got enough samples, return calibrated average
        if self.dl_path_loss:
            return ul_power_sum / samples + self.ul_path_loss
        else:
            self.log.warning('No uplink calibration data. Returning '
                             'uncalibrated values as measured by the '
                             'callbox.')
            return ul_power_sum / samples

    def send_sms(self, sms_message):
        """ Sets the SMS message for the simulation. """
        self.simulator.send_sms(sms_message)
