# Copyright 2024:
# * Pavel Moravec, OK2MOP <moravecp.cz@gmail.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

import logging

from chirp import (
    bitwise,
    chirp_common,
    directory,
    errors,
    memmap,
    util,
)
from chirp.settings import (
    RadioSetting,
    RadioSettingGroup,
    RadioSettingSubGroup,
    #    RadioSettings,
    RadioSettingValueBoolean,
    #    RadioSettingValueInteger,
    RadioSettingValueList,
    RadioSettingValueString,
    RadioSettingValueMap,
    InvalidValueError
)

from chirp.drivers import (
    baofeng_uv17Pro,
    mml_jc8810,
    baofeng_common as bfc
)

import struct

LOG = logging.getLogger(__name__)

REMOVE_GROUPS = ['ani']
REMOVE_SETTINGS = ['ani', 'skey2_sp', 'skey2_lp', 'skey3_sp',
                   'skey3_lp', 'rxendtail']

MEM_FORMAT_KDH110D = """
#seekto 0xB200;
struct {
  char name[10];      //      10-character Custom CH Names (Talkpod A36plus)
  u8 unused[6];
} customnames[30];

// vfo settings, 32 bytes
struct vfo {
  u8 freq[8];               // vfo freq
  ul16 rxtone;              // coded RX CTCSS/DTC tone
  ul16 txtone;              // coded TX CTCSS/DTC tone
  u8 unknown0[2];
  u8 unknown1:2,
     sftd:2,                // offset direction, 0 == OFF, 1 == +, 2 == -
     scode:4;               // scode 0-15
  u8 unknown2;
  u8 unknown3:3,
     scramble:1,            // 0b0 == OFF, 0b1 == ON
     unknown4:2,
     txpower:2;             // TX power, 0b0 == HIGH, 0b1 == Middle, 0b2 == LOW
  u8 unknown5:1,
     widenarr:1,            // Bandwidth, 0b0 == WIDE, 0b1 == NARROW
     voicepri:2,            // Voice privacy encryption, 0b00 == OFF, 0b01 == ENCRY1, 2 == ENCRY2, 0b11 == ENCRY3
     unknown6:2,
     rxmod:1,               // RX Modulation, 0b0 == FM, 0b1 == AM
     unknown7:1;
  u8 unknown8;
  u8 step;                  // vfo tuning step size index: 0 == 2.5K, ... 6 == 50K, 7 == 8.33K: 0x8013, 0x8033
  u8 offset[6];             // TX freq offset: 0x814, 0x834
  u8 unknown9[6];
};

#seekto 0x8000; // vfo a & b
struct {
  struct vfo a;
  struct vfo b;
} vfo;

#seekto 0xD000; // radio operationg mode
struct {
  u8 radio_mode;
} opmode;

"""

def _enter_programming_mode(radio):
    serial = radio.pipe
    mml_jc8810._enter_programming_mode(radio)

    try:
        ident = serial.read(8)
        serial.write(radio._cryptsetup)
        ack = serial.read(1)
        if ack != mml_jc8810.CMD_ACK:
            raise errors.RadioError("Error setting up encryption")
    except Exception:
        raise errors.RadioError("Error communicating with radio")
    if ident not in radio._fingerprint2:
        LOG.debug(util.hexprint(ident))
        raise errors.RadioError("Radio returned unknown secondary"
                                " identification string")


def _exit_programming_mode(radio):
    mml_jc8810._exit_programming_mode(radio)


def _read_block(radio, block_addr, block_size):
    block_data = mml_jc8810._read_block(radio, block_addr, block_size)
    if block_addr >= 0xf000:
        return block_data
    else:
        return baofeng_uv17Pro._crypt(1, block_data)


def _write_block(radio, block_addr, block_size):
    # Unfortunately we cannot use the original method as data is mmaped here
    serial = radio.pipe

    cmd = struct.pack(">cHb", b'W', block_addr, block_size)
    data = radio.get_mmap()[block_addr:block_addr + block_size]
    if block_addr < 0xf000:
        data = baofeng_uv17Pro._crypt(1, data)

    LOG.debug("Writing Data:")
    LOG.debug(util.hexprint(cmd + data))

    try:
        serial.write(cmd + data)
        if serial.read(1) != mml_jc8810.CMD_ACK:
            raise Exception("No ACK")
    except Exception:
        raise errors.RadioError("Failed to send block "
                                "to radio at %04x" % block_addr)


def do_download(radio):
    LOG.debug("download")
    _enter_programming_mode(radio)

    data = b""

    status = chirp_common.Status()
    status.msg = "Cloning from radio"

    status.cur = 0
    status.max = radio._memsize

    for addr in range(0, radio._memsize, radio.BLOCK_SIZE):
        status.cur = addr + radio.BLOCK_SIZE
        radio.status_fn(status)

        block = _read_block(radio, addr, radio.BLOCK_SIZE)
        data += block

        LOG.debug("Address: %04x" % addr)
        LOG.debug(util.hexprint(block))

    _exit_programming_mode(radio)

    return memmap.MemoryMapBytes(data)


def do_upload(radio):
    status = chirp_common.Status()
    status.msg = "Uploading to radio"

    _enter_programming_mode(radio)

    status.cur = 0
    status.max = radio._memsize

    for start_addr, end_addr in radio._ranges:
        for addr in range(start_addr, end_addr, radio.BLOCK_SIZE_UP):
            status.cur = addr + radio.BLOCK_SIZE_UP
            radio.status_fn(status)
            _write_block(radio, addr, radio.BLOCK_SIZE_UP)

    _exit_programming_mode(radio)


@directory.register
class KDHUV110D(mml_jc8810.JC8810base):
    """KDH BT8000 / KSUN UV-110D"""
    VENDOR = "KSUN"
    MODEL = "UV-110D_BT"
    BAUD_RATE = 57600

    # ==========
    # Notice to developers:
    # The BT8000 support in this driver is currently based upon v0.05
    # firmware.
    # ==========

    POWER_LEVELS = [chirp_common.PowerLevel("High", watts=8.00),
                    chirp_common.PowerLevel("Low", watts=1.00),
                    chirp_common.PowerLevel("Medium", watts=4.00)]

    SKEY_LIST = ["FM Radio",
                 "TX Power Level",
                 "Scan",
                 "Search",
                 "NOAA Weather",
                 "SOS",
                 # "Flashlight", # Not used in BT model
                 ]

    AB = ['Off', 'A', 'B']

    VALID_BANDS = [(108000000, 136000000),  # AM mode
                   #                   (136000000, 174000000),
                   #                   (200000000, 260000000),
                   #                   (350000000, 390000000),
                   (136000000, 260000000),
                   (260000000, 330000000),  # TX inhibited
                   (330000000, 520000000)]

    self = (118000000, 136000000)

    _has_bt_denoise = True
    _has_am_switch = True

    _magic = b"PROGRAMBT80U"
    _fingerprint = [b"\x01\x36\x01\x80\x04\x00\x05\x20",
                    b"\x01\x00\x01\x80\x04\x00\x05\x20",
                    ]  # fw 0.0.5 and newer for BT80
    _fingerprint2 = [b"\x02\x00\x02\x60\x01\x03\x30\x04",
                     ]  # fw 0.0.5 and newer for BT80
    _cryptsetup = (b'SEND \x01\x01\x00\x00\x00\x00\x00\x00\x00\x00' +
                   b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')

    _ranges = [
        (0x0000, 0x2000),
        (0x8000, 0x8040),
        (0x9000, 0x9040),
        (0xA000, 0xA140),
        (0xB000, 0xB440),
        (0xD000, 0xD040) # Radio mode hidden setting
    ]
    
    # Radio modes (in general) are: 0xff - default setting, 0 (?), 0xa5 (GMRS), 
    # 0x66 (PMR), 0x56 (Super mode), 0x28 (?)
    
    _calibration = (0xF000, 0xF250)  # Calibration data
    _memsize = 0xF250  # Including calibration data
    _aninames = 0

    def sync_in(self):
        """Download from radio"""
        try:
            data = do_download(self)
        except errors.RadioError:
            # Pass through any real errors we raise
            raise
        except Exception:
            # If anything unexpected happens, make sure we raise
            # a RadioError and log the problem
            LOG.exception('Unexpected error during download')
            raise errors.RadioError('Unexpected error communicating '
                                    'with the radio')
        self._mmap = data
        self.process_mmap()

    def sync_out(self):
        """Upload to radio"""
        try:
            do_upload(self)
        except Exception:
            # If anything unexpected happens, make sure we raise
            # a RadioError and log the problem
            LOG.exception('Unexpected error during upload')
            raise errors.RadioError('Unexpected error communicating '
                                    'with the radio')

    def remove_extras(self, settings):
        for element in settings:
            name = element.get_name()
            if not isinstance(element, RadioSetting):
                settings.remove(element)
                if name in REMOVE_GROUPS:
                    LOG.debug("Deleting group: " + name + " from list")
                else:
                    settings.append(self.remove_extra_items(element))
        settings.sort()

    def remove_extra_items(self, group):
        tmp = RadioSettingGroup(group.get_name(), group.get_shortname())
        for element in group:
            name = element.get_name()
            if not isinstance(element, RadioSetting):
                if name in REMOVE_GROUPS:
                    LOG.debug("Deleting subgroup: " + name + " from list")
                else:
                    tmp.append(self.remove_extra_items(element))
            elif name not in REMOVE_SETTINGS:
                tmp.append(element)
        return tmp

    def get_settings(self):
        _settings = self._memobj.settings
        _radio_mode = self._memobj.opmode.radio_mode
        group = super().get_settings()
        self.remove_extras(group)
        spec = RadioSettingGroup("bt800", "Model Specific")

        # UNLOCK SW CH - does it actually do anything?
        # rs = RadioSettingValueBoolean(_settings.unlock_sw_ch)
        # rset = RadioSetting("unlock_sw_ch",
        #                    "Override KB Lock for Channel Keys (?)", rs)
        # spec.append(rset)

        if self.MODEL in "RT-900_BT":
            # secret radio mode setting (no menu). PTT + 8 for Super Mode
            rs = RadioSettingValueMap(self._RADIO_MODE_MAP, _radio_mode)
            rset = RadioSetting("opmode.radio_mode", "Radio Mode", rs)
            rset.set_warning(_(
              'This should only be used to change the operating MODE of your '
              'radio if you understand the legalities and implications of '
              'doing so. The change may enable the radio to transmit on '
              'frequencies it is not Type Accepted to do and my be in '
              'violation of FCC and other governing agency regulations.\n\n'
              'It may make your saved image files incompatible with the radio '
              'and non-usable until you change the radio MODE back to the '
              'MODE in effect when the image file was saved. After the '
              'changed image is uploaded, the radio may have to turned OFF '
              'and back ON to have the MODE changes take full effect.\n'
              'DO NOT attempt to edit any settings until uploading to and '
              'downloading from the radio with the new operating MODE.'))
            spec.append(rset)

        if self._has_bt_denoise:
            # Menu 46: Noise reduction
            rs = RadioSettingValueBoolean(_settings.unknown_9031)
            rset = RadioSetting("unknown_9031", "Noise reduction", rs)
            spec.append(rset)

            # Menu 47: Bluetooth
            rs = RadioSettingValueBoolean(_settings.unknown_901d)
            rset = RadioSetting("unknown_901d", "Bluetooth", rs)
            spec.append(rset)

        # Menu 23: PF2 Short
        rs = RadioSettingValueList(self.SKEY_LIST, current_index=_settings.ani)
        rset = RadioSetting("ani", "PF2 Key (Short Press)", rs)
        spec.append(rset)

        # Menu 24: PF2 Long - one byte shift in settings data
        rs = RadioSettingValueList(
            self.SKEY_LIST, current_index=_settings.skey2_sp)
        rset = RadioSetting("skey2_sp", "PF2 Key (Long Press)", rs)
        spec.append(rset)

        # Menu 25: PF3 Short
        rs = RadioSettingValueList(
            self.SKEY_LIST, current_index=_settings.skey2_lp)
        rset = RadioSetting("skey2_lp", "PF3 Key (Short Press)", rs)
        spec.append(rset)

        if self.MODEL not in "RT-900_BT":
        # Menu 50: AM/FM Mode
            if self._has_am_switch:
                rs = RadioSettingValueBoolean(_settings.skey3_lp)
                rset = RadioSetting("skey3_lp", "AM Mode", rs)
                spec.append(rset)
        group.append(spec)

        if self.MODEL in "RT-900_BT":
            # Menu 7: TDR - Dual freq standby
            rs = RadioSettingValueBoolean(_settings.tdr)
            rset = RadioSetting("tdr", "TDR - Dual frequency standby", rs)
            spec.append(rset)

            # VFO A/B settings
            abblock = RadioSettingGroup("abblock", "VFO A/B Channel")
            spec.append(abblock)
            vfo = self._memobj.vfo
            # Menu 42: TX-A/B
            rs = RadioSettingValueList(
                self.AB, current_index=_settings.unknown_9018)
            rset = RadioSetting("unknown_9018", "TX-A/B", rs)
            abblock.append(rset)

            # Menu 21: VFO A/B BCL (Busy lock)
            rs = RadioSettingValueBoolean(_settings.unknown_900f)
            rset = RadioSetting("unknown_900f", "VFO A/B Channel BCL", rs)
            abblock.append(rset)

            # VFO A channel sub menu
            achannel = RadioSettingSubGroup("achannel", "VFO A Channel")
            abblock.append(achannel)

            # VFO A Freq
            def freq_validate(value):
                _radio_mode = self._memobj.opmode.radio_mode
                _vhf_lower = 0.0
                _uhf_upper = 0.0
                if _radio_mode in [0x00, 0x56, 0xff]:
                    _vhf_lower = 18.0
                else:
                    _vhf_lower = 108.0
                _vhf_upper = 299.99875
                _uhf_lower = 300.0
                if _radio_mode in [0x00, 0x56]:
                    _uhf_upper = 999.99875
                else:
                    _uhf_upper = 519.99875

                value = chirp_common.parse_freq(value)
                msg = ("Can't be less than %i.0000")
                if value < _vhf_lower * 1000000:
                    raise InvalidValueError(msg % _vhf_lower)
                msg = ("Can't be between %i.9975-%i.0000")
                if _vhf_upper * 1000000 <= value and value < _uhf_lower * 1000000:
                    raise InvalidValueError(msg % (_vhf_upper - 1, _uhf_lower))
                msg = ("Can't be greater than %i.9975")
                if value > _uhf_upper * 1000000:
                    raise InvalidValueError(msg % (_uhf_upper - 1))

                return chirp_common.format_freq(value)

            def apply_freq(setting, obj):
                value = chirp_common.parse_freq(str(setting.value)) / 10
                for i in range(7, -1, -1):
                    obj.freq[i] = value % 10
                    value /= 10

            rs = RadioSettingValueString(0, 10,
                                            bfc.bcd_decode_freq(vfo.a.freq))
            rs.set_validate_callback(freq_validate)
            rset = RadioSetting("vfo.a.freq", "Frequency", rs)
            rset.set_apply_callback(apply_freq, vfo.a)
            abblock.append(rset)

            # Menu 01: A Step
            rs = RadioSettingValueList(
                self._step_list, current_index = vfo.a.step)
            rset = RadioSetting("vfo.a.step", "Tuning Step", rs)
            abblock.append(rset)

            # Menu 02: TX Power
            rs = RadioSettingValueList(
                [str(x) for x in self.POWER_LEVELS],
                    current_index = vfo.a.txpower)
            rset = RadioSetting("vfo.a.txpower", "TX Power", rs)
            abblock.append(rset)

            # Menu 05: Wide/Narrow Band
            rs = RadioSettingValueList(
                self._bandwidth_list, current_index = vfo.a.widenarr)
            rset = RadioSetting("vfo.a.widenarr", "Bandwidth", rs)
            abblock.append(rset)

            def convert_bytes_to_offset(bytes):
                real_offset = 0
                for byte in bytes:
                    real_offset = (real_offset * 10) + byte
                return chirp_common.format_freq(real_offset * 1000)

            def apply_offset(setting, obj):
                value = chirp_common.parse_freq(str(setting.value)) / 1000
                for i in range(5, -1, -1):
                    obj.offset[i] = value % 10
                    value /= 10

            # Menu 12,13: RX ctcss/dtsc
            rs = RadioSettingValueList(
                self._code_list, current_index = self._code_list.index(
                    self.decode(vfo.a.rxtone)))
            rset = RadioSetting("vfo.a.rxtone", "RX CTCSS/DCS", rs)
            abblock.append(rset)

            # Menu 14,15: TX ctcss/dtsc
            rs = RadioSettingValueList(
                self._code_list, current_index = self._code_list.index(
                    self.decode(vfo.a.txtone)))
            rset = RadioSetting("vfo.a.txtone", "TX CTCSS/DCS", rs)
            abblock.append(rset)

            # Menu 16: Voice Privacy (encryption)
            rs = RadioSettingValueList(
                self._voicepri_list, current_index = vfo.a.voicepri)
            rset = RadioSetting("vfo.a.voicepri",
                                "Voice Privary - Subtone Encryption", rs)
            abblock.append(rset)

            # Menu 26: Offset
            rs = RadioSettingValueString(
                        0, 10, convert_bytes_to_offset(vfo.a.offset))
            rset = RadioSetting("vfo.a.offset", "Offset (MHz)", rs)
            rset.set_apply_callback(apply_offset, vfo.a)
            abblock.append(rset)

            # Menu 27: Offset direction
            rs = RadioSettingValueList(
                self._offset_list, current_index = vfo.a.sftd)
            rset = RadioSetting("vfo.a.sftd", "Offset Direction", rs)
            abblock.append(rset)

            # Menu 29: S-Code DTMF 1-15
            rs = RadioSettingValueList(
                self._scode_list, current_index = vfo.a.scode)
            rset = RadioSetting("vfo.a.scode", "S-CODE", rs)
            abblock.append(rset)

            # Menu 45: Scramble (On/Off: bool)
            rs = RadioSettingValueBoolean(vfo.a.scramble)
            rset = RadioSetting("vfo.a.scramble", "Scramble", rs)
            abblock.append(rset)

            # Menu 50: RX Modulation
            if self._has_am_switch:
                rs = RadioSettingValueList(
                    self._rx_modulation_list, current_index = vfo.a.rxmod)
                rset = RadioSetting("vfo.a.rxmod", "RX Modulation", rs)
                abblock.append(rset)

            # VFO B channel sub menu
            bchannel = RadioSettingSubGroup("bchannel", "VFO B Channel")
            abblock.append(bchannel)

            # VFO B Freq
            rs = RadioSettingValueString(0, 10,
                                            bfc.bcd_decode_freq(vfo.b.freq))
            rs.set_validate_callback(freq_validate)
            rset = RadioSetting("vfo.b.freq", "Frequency", rs)
            rset.set_apply_callback(apply_freq, vfo.b)
            abblock.append(rset)

            # Menu 01: B Step
            rs = RadioSettingValueList(
                self._step_list, current_index = vfo.b.step)
            rset = RadioSetting("vfo.b.step", "Tuning Step", rs)
            abblock.append(rset)

            # Menu 02: TX Power
            rs = RadioSettingValueList(
                [str(x) for x in self.POWER_LEVELS],
                    current_index = vfo.b.txpower)
            rset = RadioSetting("vfo.b.txpower", "TX Power", rs)
            abblock.append(rset)

            # Menu 05: Wide/Narrow Band
            rs = RadioSettingValueList(
                self._bandwidth_list, current_index = vfo.b.widenarr)
            rset = RadioSetting("vfo.b.widenarr", "Bandwidth", rs)
            abblock.append(rset)

            # Menu 12,13: RX ctcss/dtsc
            rs = RadioSettingValueList(
                self._code_list, current_index = self._code_list.index(
                    self.decode(vfo.b.rxtone)))
            rset = RadioSetting("vfo.b.rxtone", "RX CTCSS/DCS", rs)
            abblock.append(rset)

            # Menu 14,15: TX ctcss/dtsc
            rs = RadioSettingValueList(
                self._code_list, current_index = self._code_list.index(
                    self.decode(vfo.b.txtone)))
            rset = RadioSetting("vfo.b.txtone", "TX CTCSS/DCS", rs)
            abblock.append(rset)

            # Menu 16: Voice Privacy (encryption)
            rs = RadioSettingValueList(
                self._voicepri_list, current_index = vfo.b.voicepri)
            rset = RadioSetting("vfo.b.voicepri",
                                "Voice Privary - Subtone Encryption", rs)
            abblock.append(rset)

            # Menu 26: Offset
            rs = RadioSettingValueString(
                        0, 10, convert_bytes_to_offset(vfo.b.offset))
            rset = RadioSetting("vfo.b.offset", "Offset (MHz)", rs)
            rset.set_apply_callback(apply_offset, vfo.b)
            abblock.append(rset)

            # Menu 27: Offset direction
            rs = RadioSettingValueList(
                self._offset_list, current_index = vfo.b.sftd)
            rset = RadioSetting("vfo.b.sftd", "Offset Direction", rs)
            abblock.append(rset)

            # Menu 29: S-Code DTMF 1-15
            rs = RadioSettingValueList(
                self._scode_list, current_index = vfo.b.scode)
            rset = RadioSetting("vfo.b.scode", "S-CODE", rs)
            abblock.append(rset)

            # Menu 45: Scramble (On/Off: bool)
            rs = RadioSettingValueBoolean(vfo.b.scramble)
            rset = RadioSetting("vfo.b.scramble", "Scramble", rs)
            abblock.append(rset)

            # Menu 50: RX Modulation
            if self._has_am_switch:
                rs = RadioSettingValueList(
                    self._rx_modulation_list, current_index = vfo.b.rxmod)
                rset = RadioSetting("vfo.b.rxmod", "RX Modulation", rs)
                abblock.append(rset)

        return group

    def get_memory(self, number):
        mem = super().get_memory(number)
        if mem.freq == 0:
            mem.empty = True
        return mem

    def process_mmap(self):
        mem_format = mml_jc8810.MEM_FORMAT % self._mem_params + \
                     MEM_FORMAT_KDH110D
        self._memobj = bitwise.parse(mem_format, self._mmap)


@directory.register
class RT900BT(KDHUV110D):
    # ==========
    # Notice to developers:
    # The RT-900 BT support in this driver is currently based upon v1.15
    # firmware.
    # ==========

    """Radtel RT-900 BT"""
    VENDOR = "Radtel"
    MODEL = "RT-900_BT"
    BAUD_RATE = 57600
    _AIRBAND = (108000000, 136975000)
    _MIL_AIRBAND = (225000000, 399950000)
    _AIRBANDS = [_AIRBAND] + [_MIL_AIRBAND]
    _RADIO_MODE_MAP = [("Default", 0xff), ("GMRS", 0xa5), \
                        ("PMR", 0x66), ("Super", 0x56), \
                        ("Factory", 0x00), ("unknown Mode 2", 0x28)]
    POWER_LEVELS = [chirp_common.PowerLevel("High", watts=8.00),
                chirp_common.PowerLevel("Middle", watts=4.00),
                chirp_common.PowerLevel("Low", watts=1.00)]

    _upper = 512  # fw 1.06_512 expands from 256 to 512 channels
    _mem_params = (_upper,  # number of channels
                   mml_jc8810.JC8810base._aninames,  # number of aninames
                   )
    _ranges = [
        (0x0000, 0x4000),
        (0x8000, 0x8040),
        (0x9000, 0x9040),
        (0xA000, 0xA140),
        (0xB000, 0xB440),
        (0xD000, 0xD040) # Radio mode hidden setting
    ]
    # for firmware V1.5P. was missing 10.0 K step
    #  _steps = [2.5, 5.0, 6.25, 12.5, 20.0, 25.0, 50.0, 8.33]
    # for firmware V1.6P with 8.33 and 10 K step
    _steps = [2.5, 5.0, 6.25, 10.0, 12.5, 20.0, 25.0, 50.0, 8.33]
    _step_list = ['%#.3g K' % x for x in _steps]
    _bandwidth_list = ["Wide (25 KHz)", "Narrow (12.5 KHz)"]
    _offset_list = ["Off", "+", "-"]
    _rx_modulation_list = ["FM", "AM"]
    _scode_list = ["%s" % x for x in range(1, 16)]
    _voicepri_list = ["Off", "ENCRY1", "ENCRY2", "ENCRY3"]

    _code_list_ctcss = ["%2.1fHz" % x for x in sorted(chirp_common.TONES)]
    _code_list_ctcss.insert(0, "Off")
    _dcs = tuple(sorted(chirp_common.DTCS_CODES + (645,)))
    _code_list_dcsn = ["D%03iN" % x for x in _dcs]
    _code_list_dcsi = ["D%03iI" % x for x in _dcs]
    _code_list = _code_list_ctcss + _code_list_dcsn + _code_list_dcsi

    def decode(self, code):
        if code in [0, 0xffff]:
            tone = 'Off'  # Off
        elif code >= 0x0258: # CTCSS
            tone = "%2.1fHz" % (int(code) / 10.0)
        elif code <= 0x0258:  # DCS
            if code > 0x69:  # inverse
                index = code - 0x6a
                dtcs_pol = 'I'
            else:  # normal
                index = code - 1
                dtcs_pol = 'N'
            tone = 'D' + "%03i" % (self._dcs[index]) + dtcs_pol
        else:
            msg = "Invalid tone code from radio: %s" % hex(code)
            LOG.exception(msg)
            raise InvalidValueError(msg)

        return tone

    def encode(self, tone):
        if tone == "Off":
            code = 0
        elif tone.endswith('Hz'):  # CTCSS
            code = int(float(tone[0:tone.index('Hz')]) * 10)
        elif tone.startswith('D'):  # DCS
            index = self._dcs.index(int(tone[1:4]))
            if tone.endswith('I'):  # inverse
                code = index + 0x6a 
            elif tone.endswith('N'):  # normal
                code = index + 1
            else:
                msg = "Unknown CTCSS/DTC tone: %s" % tone
                LOG.exception(msg)
                raise InvalidValueError(msg)
        return code

    def get_features(self):
        rf = super().get_features()
        rf.valid_bands = [(18000000, 1000000000)]
        rf.valid_modes = ["FM", "NFM", "AM", "NAM"]  # 25 kHz, 12.5 kHz, AM, NAM
        rf.valid_tuning_steps = self._steps
        return rf

    def get_memory(self, number):
        mem = super().get_memory(number)
        _mem = self._memobj.memory[mem.number - 1]
        if chirp_common.in_range(mem.freq, self._AIRBANDS) or \
               _mem.unknown5 == 0b1:
            mem.mode = _mem.narrow and "NAM" or 'AM'
            mem.duplex = 'off'  # AM mode is RX Only

        # base class swaps these so swap Middle and Low back to match radio
        if _mem.txpower == 1:
            mem.power = self.POWER_LEVELS[1]
        elif _mem.txpower == 2:
            mem.power = self.POWER_LEVELS[2]
        return mem

    def set_memory(self, mem):
        _mem = self._memobj.memory[mem.number - 1]
        super().set_memory(mem)

        match mem.mode:
            case 'AM':
                _mem.unknown5 = 0b1
                _mem.narrow = 0b0
            case "NAM":
                _mem.unknown5 = 0b1
                _mem.narrow = 0b1
            case 'FM':
                _mem.unknown5 = 0b0
                _mem.narrow = 0b0
            case 'NFM':
                _mem.unknown5 = 0b0
                _mem.narrow = 0b1

        # base class swaps these so swap Middle and Low back to match radio
        if mem.power == self.POWER_LEVELS[1]:
            _mem.txpower = 1
        elif mem.power == self.POWER_LEVELS[2]:
            _mem.txpower = 2

    def validate_memory(self, mem):
        msgs = []
        in_range = chirp_common.in_range
        AM_mode = 'AM' in mem.mode

        if in_range(mem.freq, self._AIRBANDS) and not AM_mode:
            msgs.append(chirp_common.ValidationWarning(
                _('Frequency in this range requires AM mode')))
        elif not in_range(mem.freq, self._AIRBANDS) and AM_mode:
            msgs.append(chirp_common.ValidationWarning(
                _('Frequency in this range should NOT be AM mode')))
        return msgs + super().validate_memory(mem)

    def set_settings(self, settings):
        _settings = self._memobj.settings
        for element in settings:
            if not isinstance(element, RadioSetting):
                self.set_settings(element)
                continue
            else:
                try:
                    name = element.get_name()
                    if "." in name:
                        bits = name.split(".")
                        obj = self._memobj
                        for bit in bits[:-1]:
                            if "/" in bit:
                                bit, index = bit.split("/", 1)
                                index = int(index)
                                obj = getattr(obj, bit)[index]
                            else:
                                obj = getattr(obj, bit)
                        setting = bits[-1]
                    else:
                        obj = _settings
                        setting = element.get_name()
                    if element.has_apply_callback():
                        LOG.debug("Using apply callback")
                        element.run_apply_callback()
                    elif setting == "fmradio":
                        setattr(obj, setting, not int(element.value))
                    elif setting in ["rxtone", "txtone"]:
                        setattr(obj, setting,
                                self.encode(self._code_list[int(element.value)]))
                    elif element.value.get_mutable():
                        LOG.debug("Setting %s = %s" % (setting, element.value))
                        setattr(obj, setting, element.value)
                except Exception as e:
                    LOG.debug(element.get_name(), e)
                    raise


@directory.register
class RT900(RT900BT):
    # ==========
    # Notice to developers:
    # The BT8000 support in this driver is currently based upon stock
    # factory firmware.
    # ==========

    """Radtel RT-900 (without Bluetooth)"""
    VENDOR = "Radtel"
    MODEL = "RT-900"
    BAUD_RATE = 57600
    _has_bt_denoise = False
    #_has_am_switch = False  # Stock firmware did not have yet - now has
    #_upper = 512  # fw 1.04P expands from 256 to 512 channels

    SKEY_LIST = KDHUV110D.SKEY_LIST + ["Flashlight"]

    def get_features(self):
        rf = super().get_features()
        # So far no firmware update to test new range availability yet
        # rf.valid_bands = [(18000000, 1000000000)]
        return rf