# Copyright 2016 Leo Barring <leo.barring@protonmail.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/>.

from chirp import chirp_common, directory, memmap, \
                  bitwise, settings, errors, util
from struct import pack
import logging

LOG = logging.getLogger(__name__)

SUPPORT_NONSPLIT_DUPLEX_ONLY = False
SUPPORT_SPLIT_BUT_DEFAULT_TO_NONSPLIT_ALWAYS = True
UNAMBIGUOUS_CROSS_MODES_ONLY = True

# With this setting enabled, some CHIRP settings are stored in
# thought-to-be junk/padding data of the channel memories.
# Enabling this feature while using CHIRP with an actual radio
# and any effects thereof is entirely the responsibility of the user.
ENABLE_DANGEROUS_EXPERIMENTAL_FEATURES = False

MEM_FORMAT = """
// data fields are generally written 0xff if they are unset
struct {

#seekto 0x0000;
    struct {
        struct {
            // 0-3
            bbcd rx_freq[4];

            // 4-7
            bbcd tx_freq[4];

            // 8-9 A-B
            struct {
                u8 digital:1,
                   invert:1,
                   high:6;
                u8 low;
            } tone[2];
            // tx_squelch on 0, rx_squelch on 1

            // C
            // the duplex sign is not used for memories,
            // but is kept for interface consistency
            u8 duplex_sign:2,
               compander:1,
               txpower:1,
               modulation_width:1,
               txrx_reverse:1,
               bcl:2;

            // D
            u8 scrambler_type:3,
               use_scrambler:1,
               opt_signal:2,
               ptt_id_edge:2;

            // E-F
            // u8 _unknown_000E[2];
            %s
        } data[128];
        struct {
            // 0-5, alt 8-D
            char entry[6];

            // 6-8, alt E-F
            char _unknown_0806[2];
        } names[128];

#seekto 0x0c20;
        bit present[128];

#seekto 0x0c30;
        bit priority[128];
    } channel_memory;

#seekto 0x0c00;
    struct {
        // 0-3
        bbcd rx_freq[4];

        // 4-7
        bbcd tx_freq[4]; // actually offset, but kept for name consistency

        // 8
        struct {
            u8 digital:1,
               invert:1,
               high:6;
            u8 low;
        } tone[2]; // tx_squelch on 0, rx_squelch on 1

        // C
        u8 duplex_sign:2,
           compander:1,
           txpower:1,
           modulation_width:1,
           txrx_reverse:1,
           bcl:2;

        // D
        u8 scrambler_type:3,
           use_scrambler:1,
           opt_signal:2,
           ptt_id_edge:2;

        // E-F
        // u8 _unknown_0C0E[2];
        %s

    } vfo_data[2];

#seekto 0xc40;
    struct {
        // 0-5
        char model_string[6]; // typically PX888D, unknown if rw or ro

        // 6-7
        u8 _unknown_0C46[2];

        // 8-9
        struct {
            bbcd lower_freq[2];
            bbcd upper_freq[2];
        } band_limits[2];
    } model_information;

#seekto 0x0c50;
    char radio_information_string[16];

#seekto 0x0c60;
    struct {
        // 0
        u8 ptt_cancel_sq:1,
           dis_ptt_id:1,
           workmode_b:2,
           use_roger_beep:1,
           msk_reverse:1,
           workmode_a:2;

        // 1
        u8 backlight_color:2,
           backlight_mode:2,
           dual_single_watch:1,
           auto_keylock:1,
           scan_mode:2;

        // 2
        u8 rx_stun:1,
           tx_stun:1,
           boot_message_mode:2,
           battery_save:1,
           key_beep:1,
           voice_announce:2;

        // 3
        bbcd squelch_level;

        // 4
        bbcd tx_timeout;

        // 5
        u8 allow_keypad:1,
           relay_without_disable_tail:1,
           _unknown_0C65:1,
           call_channel_active:1,
           vox_gain:4;

        // 6
        bbcd vox_delay;

        // 7
        bbcd vfo_step;

        // 8
        bbcd ptt_id_type;

        // 9
        u8 keypad_lock:1,
            _unknown_0C69_1:1,
            side_button_hold_mode:2,
            dtmf_sidetone:1,
            _unknown_0C69_2:1,
            side_button_click_mode:2;

        // A
        u8 roger_beep:4,
           main_watch:1,
           _unknown_0C6A:3;

        // B
        u8 channel_a;

        // C
        u8 channel_b;

        // D
        u8 priority_channel;

        // E
        u8 wait_time;

        // F
        u8 _unknown_0C6F;

        // 0-7 on next block
        u8 _unknown_0C70[8];

        // 8-D on next block
        char boot_message[6];
    } opt_settings;

#seekto 0x0c80;
    struct {
        // these fields are used for all ptt id forms (msk/dtmf/5t)
        // (only one can be active and stored at a time)
        // and different constraints are applied depending
        // on the ptt id type
        u8 entry[7];
        u8 length;
    } ptt_id_data[2];
    // 0 is BOT, 1 is EOT

#seekto 0x0c90;
    struct {
        // 0
        u8 _unknown_0C90;

        // 1
        u8 _unknown_0C91_1:3,
           channel_stepping:1,
           unknown_0C91_2:1,
           receive_range:2,
           unknown_0C91_3:1;

        // 2-3
        u8 _unknown_0C92[2];

        // 4-7
        u8 vfo_freq[4];

        // 8-F and two more blocks
        struct {
            u8 entry[4];
        } memory[10];
    } fm_radio;

#seekto 0x0cc0;
    struct {
        char id_code[4];
        struct {
            char entry[4];
        } phone_book[9];
    } msk_settings;

#seekto 0x0cf0;
    struct {
        // 0-3
        bbcd rx_freq[4];

        // 4-7
        bbcd tx_freq[4];

        // 8
        struct {
            u8 digital:1,
               invert:1,
               high:6;
            u8 low;
        } tone[2];
        // tx_squelch on 0, rx_squelch on 1


        // C
        // the duplex sign is not used for the CALL,
        // channel but is kept for interface consistency
        u8 duplex_sign:2,
           compander:1,
           txpower:1,
           modulation_width:1,
           txrx_reverse:1,
           bcl:2;

        // D
        u8 scrambler_type:3,
           use_scrambler:1,
           opt_signal:2,
           ptt_id_edge:2;

        // E-F
        // u8 _unknown_0CFE[2];
        %s
    } call_channel;

#seekto 0x0d00;
    struct {

        // DTMF codes are stored as hex half-bytes,
        // 0-9 A-D are mapped straight
        // DTMF '*' is HEX E, DTMF '#' is HEX F

        // 0x0d00
        struct {
            u8 digit_length;       // 0x05 to 0x14 corresponding to 50-200ms
            u8 inter_digit_pause;  // same
            u8 first_digit_length; // same
            u8 first_digit_delay;  // 0x02 to 0x14 corresponding to 100-1000ms
        } timing;

#seekto 0x0d30;
        u8 _unknown_0D30[2]; // 0-1
        u8 group_code;       // 2
        u8 reset_time;       // 3
        u8 alert_transpond;  // 4
        u8 id_code[4];       // 5-8
        u8 _unknown_0D39[4]; // 9-C
        u8 id_code_length;   // D
        u8 _unknown_0d3e[2]; // E-F

// 0x0d40
        u8 tx_stun_code[4];
        u8 _unknown_0D44[4];
        u8 tx_stun_code_length;
        u8 cancel_tx_stun_code_length;
        u8 cancel_tx_stun_code[4];
        u8 _unknown_0D4E[2];

// 0x0d50
        u8 rxtx_stun_code[4];
        u8 _unknown_0D54[4];
        u8 rxtx_stun_code_length;
        u8 cancel_rxtx_stun_code_length;
        u8 cancel_rxtx_stun_code[4];
        u8 _unknown_0D5E[2];

// 0x0d60
        struct {
            u8 entry[5];
            u8 _unknown_0D65[3];
            u8 length;
            u8 _unknown_0D69[7];
        } phone_book[9];
    } dtmf_settings;

#seekto 0x0e00;
    struct {
        u8 delay;
        u8 _unknown_0E01[5];
        u8 alert_transpond;
        u8 reset_time;
        u8 tone_standard;
        u8 id_code[3];

#seekto 0x0e20;
        struct {
            u8 period;
            u8 group_code:4,
               repeat_code:4;
        } tone_settings[4];
        // the order is ZVEI1 ZVEI2 CCIR1 CCITT

#seekto 0x0e40;
        // 5-Tone tone standard frequency table
        // unknown use, changing the values does not seem to have
        // any effect on the produced sound, but the values are not
        // overwritten either.
        il16 tone_frequency_table[16];

// 0xe60
        u8 tx_stun_code[5];
        u8 _unknown_0E65[3];
        u8 tx_stun_code_length;
        u8 cancel_tx_stun_code_length;
        u8 cancel_tx_stun_code[5];
        u8 _unknown_0E6F;

// 0xe70
        u8 rxtx_stun_code[5];
        u8 _unknown_0E75[3];
        u8 rxtx_stun_code_length;
        u8 cancel_rxtx_stun_code_length;
        u8 cancel_rxtx_stun_code[5];
        u8 _unknown_0E7F;

// 0xe80
        struct {
            u8 entry[3];
        } phone_book[9];
    } five_tone_settings;
} mem;"""

# various magic numbers and strings, apart from the memory format
if ENABLE_DANGEROUS_EXPERIMENTAL_FEATURES:
    LOG.warn("ENABLE_DANGEROUS_EXPERIMENTAL_FEATURES"
             "AND/OR DANGEROUS FEATURES ENABLED")
    MEM_FORMAT = MEM_FORMAT % (
            "u8 _unknown_000E_1: 6,\n"
            "   experimental_duplex_mode_indicator: 1,\n"
            "   experimental_cross_mode_indicator: 1;\n"
            "u8 _unknown_000F;",
            "u8 _unknown_0C0E_1: 6,\n"
            "   experimental_duplex_mode_indicator: 1,\n"
            "   experimental_cross_mode_indicator: 1;\n"
            "u8 _unknown_0C0F;",
            "u8 _unknown_0CFE_1: 6,\n"
            "   experimental_duplex_mode_indicator: 1,\n"
            "   experimental_cross_mode_indicator: 1;\n"
            "u8 _unknown_0CFF;")
    # we don't need these settings anymore, because it's exactly what the
    # experimental features are about
    SUPPORT_SPLIT_BUT_DEFAULT_TO_NONSPLIT_ALWAYS = False
    SUPPORT_NONSPLIT_DUPLEX_ONLY = False
    UNAMBIGUOUS_CROSS_MODES_ONLY = False

else:
    MEM_FORMAT = MEM_FORMAT % (
            "u8 _unknown_000E[2];",
            "u8 _unknown_0C0E[2];",
            "u8 _unknown_0CFE[2];")

FILE_MAGIC = [0xc40, 0xc50,
              b'\x50\x58\x38\x38\x38\x44\x00\xff'
              b'\x13\x40\x17\x60\x40\x00\x48\x00']
HANDSHAKE_OUT = b'XONLINE'
HANDSHAKE_IN = [b'PX888D\x00\xff']

LOWER_READ_BOUND = 0
UPPER_READ_BOUND = 0x1000
LOWER_WRITE_BOUND = 0
UPPER_WRITE_BOUND = 0x0fc0
BLOCKSIZE = 64

OFF_INT = ["Off"] + [str(x+1) for x in range(100)]
OFF_ON = ["Off", "On"]
INACTIVE_ACTIVE = ["Inactive", "Active"]
NO_YES = ["No", "Yes"]
YES_NO = ["Yes", "No"]

BANDS = [(134000000, 176000000),  # VHF
         (400000000, 480000000)]  # UHF

SPECIAL_CHANNELS = {'VFO-A': -2, 'VFO-B': -1, 'CALL': 0}
SPECIAL_NUMBERS = {-2: 'VFO-A', -1: 'VFO-B', 0: 'CALL'}

DUPLEX_MODES = ['', '+', '-', 'split']
if SUPPORT_NONSPLIT_DUPLEX_ONLY:
    DUPLEX_MODES = ['', '+', '-']

TONE_MODES = ["", "Tone", "TSQL", "DTCS", "Cross"]

CROSS_MODES = ["Tone->Tone",
               "DTCS->",
               "->DTCS",
               "Tone->DTCS",
               "DTCS->Tone",
               "->Tone",
               "DTCS->DTCS",
               "Tone->"]
if UNAMBIGUOUS_CROSS_MODES_ONLY:
    CROSS_MODES = ["Tone->Tone",
                   "DTCS->",
                   "->DTCS",
                   "Tone->DTCS",
                   "DTCS->Tone",
                   "->Tone",
                   "DTCS->DTCS"]

MODES = ["NFM", "FM"]

# Only the 'High' power level is quantified in the manual for
# the radio (4W for VHF, 5W for UHF), a web search turned
# up numbers for the 'Low' power level (0.5W for VHF and
# 0.7W for UHF), but they are not official to my knowledge
# and should be taken with a grain of salt or two.
# Numbers used in code is the averages
POWER_LEVELS = [chirp_common.PowerLevel("Low", watts=0.6),
                chirp_common.PowerLevel("High", watts=4.5)]

SKIP_MODES = ["", "S"]
BCL_MODES = ["Off", "Carrier", "QT/DQT"]
SCRAMBLER_MODES = OFF_INT[0:9]
PTT_ID_EDGES = ["Off", "BOT", "EOT", "Both"]
OPTSIGN_MODES = ["None", "DTMF", "5-Tone", "MSK"]

VFO_STRIDE = ['5 kHz', '6.25 kHz', '10 kHz', '12.5 kHz', '25 kHz']
AB = ['A', 'B']
WATCH_MODES = ['Single watch', 'Dual watch']
AB_MODES = ['VFO', 'Memory index', 'Memory name', 'Memory frequency']
SCAN_MODES = ["Time", "Carrier", "Seek"]
WAIT_TIMES = [("0.3s", 6), ("0.5s", 10)] +\
             [("%ds" % t, t*20) for t in range(1, 13)]

BUTTON_MODES = ["Send call list data",
                "Emergency alarm",
                "Send 1750 Hz signal",
                "Open squelch"]
BOOT_MESSAGE_TYPES = ["Off", "Battery voltage", "Custom message"]
TALKBACK = ['Off', 'Chinese', 'English']
BACKLIGHT_COLORS = list(zip(["Blue", "Orange", "Purple"], range(1, 4)))
VOX_GAIN = OFF_INT[0:10]
VOX_DELAYS = ['1s', '2s', '3s', '4s']
TRANSMIT_ALARMS = ['Off', '30s', '60s', '90s', '120s',
                   '150s', '180s', '210s', '240s', '270s']

DATA_MODES = ['MSK', 'DTMF', '5-Tone']

ASCIIPART = ''.join([chr(x) for x in range(0x20, 0x7f)])
DTMF = "0123456789ABCD*#"
HEXADECIMAL = "0123456789ABCDEF"

ROGER_BEEP = OFF_INT[0:11]
BACKLIGHT_MODES = ["Off", "Auto", "On"]

TONE_RESET_TIME = ['Off'] + ['%ds' % x for x in range(1, 256)]
DTMF_TONE_RESET_TIME = TONE_RESET_TIME[0:16]

DTMF_GROUPS = list(zip(["Off", "A", "B", "C", "D", "*", "#"], [255] +
                       list(range(10, 16))))
FIVE_TONE_STANDARDS = ['ZVEI1', 'ZVEI2', 'CCIR1', 'CCITT']

# should mimic the defaults in the memedit MemoryEditor somewhat
#                          0   1   2   3   4   5   6   7
SANE_MEMORY_DEFAULT = b"\x14\x61\x00\x00\x14\x61\x00\x00" + \
                      b"\xff\xff\xff\xff\xc8\x00\xff\xff"
#                          8   9   A   B   C   D   E   F


# these two option sets are listed differently like this in the stock software,
# so I'm keeping them separate for now if they are in fact identical
# in behaviour, that should probably be amended
DTMF_ALERT_TRANSPOND = list(zip(['Off', 'Call alert',
                                 'Transpond-alert',
                                 'Transpond-ID code'],
                                [255] + list(range(1, 4))))
FIVE_TONE_ALERT_TRANSPOND = list(zip(['Off', 'Alert tone',
                                      'Transpond', 'Transpond-ID code'],
                                     [255] + list(range(1, 4))))

BFM_BANDS = ['87.5-108 MHz', '76.0-91.0 MHz', '76.0-108.0 MHz', '65.0-76.0 MHz']
BFM_STRIDE = ['100 kHz', '50 kHz']


def piperead(pipe, amount):
    """read some data, catch exceptions, validate length of data read"""
    try:
        d = pipe.read(amount)
    except Exception as e:
        raise errors.RadioError(
                "Tried to read %d bytes, but got an exception: %s" %
                (amount, repr(e)))
    if d is None:
        raise errors.RadioError(
                "Tried to read %d bytes, but read operation returned <None>." %
                (amount))
    if d is None or len(d) != amount:
        raise errors.RadioError(
                "Tried to read %d bytes, but got %d bytes instead." %
                (amount, len(d)))
    return d


def pipewrite(pipe, data):
    """write some data, catch exceptions, validate length of data written"""
    try:
        n = pipe.write(data)
    except Exception as e:
        raise errors.RadioError(
                "Tried to write %d bytes, but got an exception: %s." %
                (len(data), repr(e)))
    if n is None:
        raise errors.RadioError(
                "Tried to write %d bytes, but operation returned <None>." %
                (len(data)))
    if n != len(data):
        raise errors.RadioError(
                "Tried to write %d bytes, but wrote %d bytes instead." %
                (len(data), n))


def attempt_initial_handshake(pipe):
    """try to do the initial handshake"""
    pipewrite(pipe, HANDSHAKE_OUT)
    x = piperead(pipe, len(HANDSHAKE_IN[0]))
    if x in HANDSHAKE_IN:
        return True
    LOG.debug("Handshake failed: received: %s expected one of: %s" %
              (repr(x), repr(HANDSHAKE_IN)))
    return False


def initial_handshake(pipe, tries):
    """do an initial handshake attempt up to tries times"""
    x = False
    for i in range(tries):
        x = attempt_initial_handshake(pipe)
        if x:
            break
    if not x:
        raise errors.RadioError("Initial handshake failed all ten tries.")


def mk_writecommand(addr):
    """makes a write command from an address specification"""
    return pack('>cHc', b'W', addr, b'@')


def mk_readcommand(addr):
    """makes a read command from an address specification"""
    return pack('>cHc', b'R', addr, b'@')


def expect_ack(pipe):
    x = piperead(pipe, 1)
    if x != b'\x06':
        LOG.debug(
                "Did not get ACK. received: %s, expected: '\\x06'" %
                repr(x))
        raise errors.RadioError("Did not get ACK when expected.")


def end_communications(pipe):
    """tell the radio that we are done"""
    pipewrite(pipe, b'E')
    expect_ack(pipe)


def read_block(pipe, addr):
    """read and return a chunk of data at specified address"""
    r = mk_readcommand(addr)
    w = mk_writecommand(addr)
    pipewrite(pipe, r)
    x = piperead(pipe, len(w))
    if x != w:
        raise errors.RadioError("Received data not following protocol.")
    block = piperead(pipe, BLOCKSIZE)
    return block


def write_block(pipe, addr, block):
    """write a chunk of data at specified address"""
    w = mk_writecommand(addr)
    pipewrite(pipe, w)
    pipewrite(pipe, block)
    expect_ack(pipe)


def show_progress(radio, blockaddr, upper, msg):
    """relay read/write information to the user through the gui"""
    if radio.status_fn:
        status = chirp_common.Status()
        status.cur = blockaddr
        status.max = upper
        status.msg = msg
        radio.status_fn(status)


def do_download(radio):
    """download from the radio to the memory map"""
    initial_handshake(radio.pipe, 10)
    memory = memmap.MemoryMapBytes(b'\xff'*0x1000)
    for blockaddr in range(LOWER_READ_BOUND, UPPER_READ_BOUND, BLOCKSIZE):
        LOG.debug("Reading block "+str(blockaddr))
        block = read_block(radio.pipe, blockaddr)
        memory.set(blockaddr, block)
        show_progress(radio, blockaddr, UPPER_READ_BOUND,
                      "Reading radio memory... %04x" % blockaddr)
    end_communications(radio.pipe)
    return memory


def do_upload(radio):
    """upload from the memory map to the radio"""
    memory = radio.get_mmap()
    initial_handshake(radio.pipe, 10)
    for blockaddr in range(LOWER_WRITE_BOUND, UPPER_WRITE_BOUND, BLOCKSIZE):
        LOG.debug("Writing block "+str(blockaddr))
        block = memory[blockaddr:blockaddr+BLOCKSIZE]
        write_block(radio.pipe, blockaddr, block)
        show_progress(radio, blockaddr, UPPER_WRITE_BOUND,
                      "Writing radio memory...  % 04x" % blockaddr)
    end_communications(radio.pipe)


def parse_tone(t):
    """
    parse the tone (ctss, dtcs) part of the mmap
    into more easily handled data types
    """
    # [ mode, value, polarity ]
    if int(t.high) == 0x3f and int(t.low) == 0xff:
        return [None, None, None]
    elif bool(t.digital):
        t = ['DTCS',
             (int(t.high) & 0x0f)*100 +
             ((int(t.low) & 0xf0) >> 4)*10 +
             (int(t.low) & 0x0f),
             ['N', 'R'][bool(t.invert)]]
        if t[1] not in chirp_common.DTCS_CODES:
            return [None, None, None]
    else:
        t = ['Tone',
             ((int(t.high) & 0xf0) >> 4)*100 +
             (int(t.high) & 0x0f)*10 +
             ((int(t.low) & 0xf0) >> 4) +
             (int(t.low) & 0x0f)/10.0,
             None]
        if t[1] not in chirp_common.TONES:
            return [None, None, None]
    return t


def unparse_tone(t):
    """parse tone data back into the format used by the radio"""
    # [ mode, value, polarity ]
    if t[0] == 'Tone':
        tint = int(t[1]*10)
        t0, tint = tint % 10, tint // 10
        t1, tint = tint % 10, tint // 10
        t2, tint = tint % 10, tint // 10
        high = (tint << 4) | t2
        low = (t1 << 4) | t0
        digital = False
        invert = False
        return digital, invert, high, low
    elif t[0] == 'DTCS':
        tint = int(t[1])
        t0, tint = tint % 10, tint // 10
        t1, tint = tint % 10, tint // 10
        high = tint
        low = (t1 << 4) | t0
        digital = True
        invert = t[2] == 'R'
        return digital, invert, high, low
    return None


def decode_halfbytes(data, mapping, length):
    """
    construct a string from a datatype
    where each half-byte maps to a character
    """
    s = ''
    for i in range(length):
        if i & 1 == 0:
            s += mapping[(int(data[i >> 1]) & 0xf0) >> 4]
        else:
            s += mapping[int(data[i >> 1]) & 0x0f]
    return s


def encode_halfbytes(data, datapad, mapping, fillvalue, fieldlen):
    """encode data from a string where each character maps to a half-byte"""
    if len(data) & 1:
        # pad to an even length
        data += datapad
    o = [fillvalue] * fieldlen
    for i in range(0, len(data), 2):
        v = (mapping.index(data[i]) << 4) | mapping.index(data[i+1])
        o[i >> 1] = v
    return bytearray(util.byte_to_int(b) for b in o)


def decode_ffstring(data):
    """decode a string delimited by 0xff"""
    s = ''
    for b in data:
        if int(b) == 0xff:
            break
        s += chr(int(b))
    return s


def encode_ffstring(data, fieldlen):
    """right-pad to specified length with 0xff bytes"""
    extra = fieldlen-len(data)
    if extra > 0:
        data += '\xff'*extra
    return bytearray([util.byte_to_int(x) for x in data])


def decode_dtmf(data, length):
    """decode a field containing dtmf data into a string"""
    if length == 0xff:
        return ''
    return decode_halfbytes(data, DTMF, length)


def encode_dtmf(data, length, fieldlen):
    """encode a string containing dtmf characters into a data field"""
    return encode_halfbytes(data, '0', DTMF, b'\xff', fieldlen)


def decode_5tone(data):
    """decode a field containing 5-tone data into a string"""
    if (int(data[2]) & 0x0f) != 0:
        return ''
    return decode_halfbytes(data, HEXADECIMAL, 5)


def encode_5tone(data, fieldlen):
    """encode a string containing 5-tone characters into a data field"""
    return encode_halfbytes(data, '0', HEXADECIMAL, b'\xff', fieldlen)


def decode_freq(data):
    """decode frequency data for the broadcast FM radio memories"""
    data_out = ''
    if data[0] != 0xff:
        data_out = chirp_common.format_freq(
                int(decode_halfbytes(data, "0123456789", len(data)))*100000)
    return data_out


def encode_freq(data, fieldlen):
    """encode frequency data for the broadcast FM radio memories"""
    data_out = bytearray(b'\xff')*fieldlen
    if data != '':
        data_out = encode_halfbytes((('%%0%di' % (fieldlen << 1)) %
                                     int(chirp_common.parse_freq(data)/10)),
                                    '', '0123456789', '', fieldlen)
    return data_out


def sbyn(s, n):
    """setting by name"""
    return list(filter(lambda x: x.get_name() == n, s))[0]


# These helper classes provide a direct link between the value
# of the widget shown in the ui, and the setting in the memory
# map of the radio, lessening the need to write large chunks
# of code, first for populating the ui from the memory map,
# then secondly for parsing the values back.
# By supplying the memory map entry to the setting instance,
# it is possible to automatically 1) initialize the value of
# the setting, as well as 2) automatically update the memory
# value when the user changes it in the ui, without adding
# any code outside the class.
class MappedIntegerSettingValue(settings.RadioSettingValueInteger):
    """"
    Integer setting, with the possibility to add translation
    functions between memory map <-> integer setting
    """
    def __init__(self, val_mem, minval, maxval, step=1,
                 int_from_mem=lambda x: int(x),
                 mem_from_int=lambda x: x,
                 autowrite=True):
        """
        val_mem      - memory map entry for the value
        minval       - the minimum value allowed
        maxval       - maximum value allowed
        step         - value stepping
        int_from_mem - function to convert memory entry to integer
        mem_from_int - function to convert integer to memory entry
        autowrite    - automatically write the memory map entry
                       when the value is changed
        """
        self._val_mem = val_mem
        self._int_from_mem = int_from_mem
        self._mem_from_int = mem_from_int
        self._autowrite = autowrite
        settings.RadioSettingValueInteger.__init__(
                self,
                minval, maxval, self._int_from_mem(val_mem), step)

    def set_value(self, x):
        settings.RadioSettingValueInteger.set_value(self, x)
        if self._autowrite:
            self.write_mem()

    def write_mem(self):
        if self.get_mutable() and self._mem_from_int is not None:
            self._val_mem.set_value(self._mem_from_int(
                settings.RadioSettingValueInteger.get_value(self)))


class MappedListSettingValue(settings.RadioSettingValueMap):
    """Mapped list setting"""
    def __init__(self, val_mem, options, autowrite=True):
        """
        val_mem      - memory map entry for the value
        options      - either a list of strings options to present,
                       mapped to integers 0...n
                       in the memory map entry, or a list of tuples
                       ("option description", memory map value)
        int_from_mem - function to convert memory entry to integer
        mem_from_int - function to convert integer to memory entry
        autowrite    - automatically write the memory map entry when
                       the value is changed
        """
        self._val_mem = val_mem
        self._autowrite = autowrite
        options = list(options)
        if not isinstance(options[0], tuple):
            options = list(zip(options, range(len(options))))
        settings.RadioSettingValueMap.__init__(
                self,
                options, mem_val=int(val_mem))

    def set_value(self, value):
        settings.RadioSettingValueMap.set_value(self, value)
        if self._autowrite:
            self.write_mem()

    def write_mem(self):
        if self.get_mutable():
            self._val_mem.set_value(
                    settings.RadioSettingValueMap.get_mem_val(self))


class MappedCodedStringSettingValue(settings.RadioSettingValueString):
    """
    generic base class for a number of mapped presented-as-strings
    values which may need conversion between mem and string,
    and may store a length value in a separate mem field
    """
    def __init__(self, val_mem, len_mem, min_length, max_length,
                 charset=ASCIIPART, padchar=' ', autowrite=True,
                 str_from_mem=lambda mve, lve: str(mve[0:int(lve)]),
                 mem_val_from_str=lambda s, fl: s[0:fl],
                 mem_len_from_int=lambda l: l):
        """
        val_mem          - memory map entry for the value
        len_mem          - memory map entry for the length (or None)
        min_length       - length that the string will be right-padded to
        max_length       - maximum length of the string, set as maxlength
                           for the RadioSettingValueString
        charset          - the allowed charset
        padchar          - the character that will be used to pad short
                           strings, if not in the charset, charset[0] is used
        autowrite        - automatically call write_mem when the ui value
                           change
        str_from_mem     - function to convert from memory entry to string
                           value, form:
                           func(value_entry, length_entry or none) -> string
        mem_val_from_str - function to convert from string value to
                           memory-fitting value, form:
                           func(string, value_entry_length) ->
                           value to store in value entry
        mem_len_from_int - function to convert from string length to
                           memory-fitting value, form:
                           func(stringlength) -> value to store in length entry
        """
        self._min_length = min_length
        self._val_mem = val_mem
        self._len_mem = len_mem
        self._padchar = padchar
        if padchar not in charset:
            self._padchar = charset[0]
        self._autowrite = autowrite
        self._str_from_mem = str_from_mem
        self._mem_val_from_str = mem_val_from_str
        self._mem_len_from_int = mem_len_from_int
        settings.RadioSettingValueString.__init__(
                self,
                0, max_length,
                self._str_from_mem(self._val_mem, self._len_mem),
                charset=charset, autopad=False)

    def set_value(self, value):
        """
        Set the value of the string, pad if below minimum length,
        unless it's '' to provide a distinction between
        uninitialized/reset data and needs-to-be-padded data
        """
        while len(value) < self._min_length and len(value) != 0:
            value += self._padchar
        settings.RadioSettingValueString.set_value(self, value)
        if self._autowrite:
            self.write_mem()

    def write_mem(self):
        """update the memory"""
        if not self.get_mutable() or self._mem_val_from_str is None:
            return
        v = self.get_value()
        l = len(v)
        self._val_mem.set_value(self._mem_val_from_str(v, len(self._val_mem)))
        if self._len_mem is not None and self._mem_len_from_int is not None:
            self._len_mem.set_value(self._mem_len_from_int(l))


class MappedFFStringSettingValue(MappedCodedStringSettingValue):
    """
    Mapped string setting, tailored for the puxing px888k,
    which uses 0xff terminated strings.
    """
    def __init__(self, val_mem, min_length, max_length,
                 charset=ASCIIPART, padchar=' ', autowrite=True):
        MappedCodedStringSettingValue.__init__(
                self,
                val_mem, None, min_length, max_length,
                charset=charset, padchar=padchar, autowrite=autowrite,
                str_from_mem=lambda mve, lve: decode_ffstring(mve),
                mem_val_from_str=lambda s, fl: encode_ffstring(s, fl),
                mem_len_from_int=None)


class MappedDTMFStringSettingValue(MappedCodedStringSettingValue):
    """
    Mapped string setting, tailored for the puxing px888k
    field pairs (value and length) storing DTMF codes
    """
    def __init__(self, val_mem, len_mem, min_length, max_length,
                 autowrite=True):
        MappedCodedStringSettingValue.__init__(
                self,
                val_mem, len_mem, min_length, max_length,
                charset=DTMF, padchar='0', autowrite=autowrite,
                str_from_mem=lambda mve, lve: decode_dtmf(mve, lve),
                mem_val_from_str=lambda s, fl: encode_dtmf(s, len(s), fl))


class MappedFiveToneStringSettingValue(MappedCodedStringSettingValue):
    """
    Mapped string setting, tailored for the puxing px888k
    fields storing 5-Tone codes
    """
    def __init__(self, val_mem, autowrite=True):
        MappedCodedStringSettingValue.__init__(
                self,
                val_mem, None, 0, 5, charset=HEXADECIMAL, padchar='0',
                autowrite=autowrite,
                str_from_mem=lambda mve, lve: decode_5tone(mve),
                mem_val_from_str=lambda s, fl: encode_5tone(s, fl),
                mem_len_from_int=None)


class MappedFreqStringSettingValue(MappedCodedStringSettingValue):
    """
    Mapped string setting, tailored for the puxing px888k
    fields for the broadcast FM radio frequencies
    """
    def __init__(self, val_mem, autowrite=True):
        MappedCodedStringSettingValue.__init__(
                self,
                val_mem, None, 0, 128, charset=ASCIIPART, padchar=' ',
                autowrite=autowrite,
                str_from_mem=lambda mve, lve: decode_freq(mve),
                mem_val_from_str=lambda s, fl: encode_freq(s, fl))


# These functions lessen the amount of boilerplate on the form
# x = RadioSetting("AAA", "BBB", SomeKindOfRadioSettingValue( ... ))
# to
# x = some_kind_of_setting("AAA", "BBB", ... )
def integer_setting(k, n, *args, **kwargs):
    return settings.RadioSetting(
            k, n,
            MappedIntegerSettingValue(*args, **kwargs))


def list_setting(k, n, *args, **kwargs):
    return settings.RadioSetting(
            k, n,
            MappedListSettingValue(*args, **kwargs))


def ff_string_setting(k, n, *args, **kwargs):
    return settings.RadioSetting(
            k, n,
            MappedFFStringSettingValue(*args, **kwargs))


def dtmf_string_setting(k, n, *args, **kwargs):
    return settings.RadioSetting(
            k, n,
            MappedDTMFStringSettingValue(*args, **kwargs))


def five_tone_string_setting(k, n, *args, **kwargs):
    return settings.RadioSetting(
            k, n,
            MappedFiveToneStringSettingValue(*args, **kwargs))


def frequency_setting(k, n, *args, **kwargs):
    return settings.RadioSetting(
            k, n,
            MappedFreqStringSettingValue(*args, **kwargs))


@directory.register
class Puxing_PX888K_Radio(chirp_common.CloneModeRadio):
    """Puxing PX-888K"""
    VENDOR = "Puxing"
    MODEL = "PX-888K"
    BAUD_RATE = 9600
    NEEDS_COMPAT_SERIAL = False

    @classmethod
    def match_model(cls, filedata, filename):
        if len(filedata) == UPPER_READ_BOUND:
            if filedata[FILE_MAGIC[0]:FILE_MAGIC[1]] == FILE_MAGIC[2]:
                return True
        return False

    def get_features(self):
        rf = chirp_common.RadioFeatures()
        rf.has_bank_index = False
        rf.has_dtcs = True
        rf.has_ctone = True
        rf.has_rx_dtcs = True
        rf.has_dtcs_polarity = True
        rf.has_mode = True
        rf.has_offset = True
        rf.has_name = True
        rf.has_bank = False
        rf.has_bank_names = False
        rf.has_tuning_step = False
        rf.has_cross = True
        rf.has_infinite_number = False
        rf.has_nostep_tuning = False
        rf.has_comment = False
        rf.has_settings = True
        if SUPPORT_NONSPLIT_DUPLEX_ONLY:
            rf.can_odd_split = False
        else:
            rf.can_odd_split = True

        rf.valid_modes = MODES
        rf.valid_tmodes = TONE_MODES
        rf.valid_duplexes = DUPLEX_MODES
        rf.valid_bands = BANDS
        rf.valid_skips = SKIP_MODES
        rf.valid_power_levels = POWER_LEVELS
        rf.valid_characters = ASCIIPART
        rf.valid_name_length = 6
        rf.valid_cross_modes = CROSS_MODES
        rf.memory_bounds = (1, 128)
        rf.valid_special_chans = list(SPECIAL_CHANNELS.keys())
        rf.valid_tuning_steps = [5.0, 6.25, 10.0, 12.5, 25.0]
        return rf

    def sync_in(self):
        self._mmap = do_download(self)
        self.process_mmap()

    def process_mmap(self):
        self._memobj = bitwise.parse(MEM_FORMAT, self._mmap)

    def sync_out(self):
        do_upload(self)

    def _set_sane_defaults(self, data):
        # thanks thayward!
        data.set_raw(SANE_MEMORY_DEFAULT)

    def _uninitialize(self, data, n):
        if isinstance(data, bitwise.arrayDataElement):
            data.set_value(b"\xff"*n)
        else:
            data.set_raw(b"\xff"*n)

    def _get_memory_structs(self, number):
        """
        fetch the correct data structs no matter
        if its regular or special channels,
        no matter if they're referred by name or channel index
        """
        index = 2501
        i = -42
        designator = 'INVALID'
        isregular = False
        iscall = False
        isvfo = False
        _data = None
        _name = None
        _present = None
        _priority = None
        if number in SPECIAL_NUMBERS.keys():
            index = number
            # special by index
            designator = SPECIAL_NUMBERS[number]
        elif number in SPECIAL_CHANNELS.keys():
            # special by name
            index = SPECIAL_CHANNELS[number]
            designator = number
        elif number > 0:
            # regular by number
            index = number
            designator = number

        if index < 0:
            isvfo = True
            _data = self._memobj.mem.vfo_data[index+2]
        elif index == 0:
            iscall = True
            _data = self._memobj.mem.call_channel
        elif index > 0:
            isregular = True
            i = number - 1
            _data = self._memobj.mem.channel_memory.data[i]
            _name = self._memobj.mem.channel_memory.names[i].entry
            _present = self._memobj.mem.channel_memory.present[
                    (i & 0x78) | (7-(i & 0x07))]
            _priority = self._memobj.mem.channel_memory.priority[
                    (i & 0x78) | (7-(i & 0x07))]

        if _data == bytearray(0xff)*16:
            self._set_sane_defaults(_data)

        return (index, designator,
                _data, _name, _present, _priority,
                isregular, isvfo, iscall)

    def get_raw_memory(self, number):
        x = self._get_memory_structs(number)
        return repr(x[2])

    def get_memory(self, number):
        mem = chirp_common.Memory()
        (index, designator,
            _data, _name, _present, _priority,
            isregular, isvfo, iscall) = self._get_memory_structs(number)

        mem.number = index

        # handle empty channels
        if isregular:
            if bool(_present):
                mem.empty = False
                mem.name = str(decode_ffstring(_name))
                mem.skip = SKIP_MODES[1-int(_priority)]
            else:
                mem.empty = True
                mem.name = ''
                return mem
        else:
            mem.empty = False
            mem.name = ''
            mem.extd_number = designator

        # get frequency data
        mem.freq = int(_data.rx_freq)*10
        mem.offset = int(_data.tx_freq)*10

        # interpret frequency data
        # only the vfo channels support duplex,
        # memory channels operate in split mode all the time
        if isvfo:
            mem.duplex = DUPLEX_MODES[int(_data.duplex_sign)]
            if mem.duplex == '-':
                mem.offset = mem.freq - mem.offset
            elif mem.duplex == '':
                mem.offset = 0
            elif mem.duplex == '+':
                mem.offset = mem.offset - mem.freq
        else:
            if mem.freq == mem.offset:
                mem.duplex = ''
                mem.offset = 0
            elif SUPPORT_NONSPLIT_DUPLEX_ONLY or \
                    SUPPORT_SPLIT_BUT_DEFAULT_TO_NONSPLIT_ALWAYS:
                if mem.freq > mem.offset:
                    mem.offset = mem.freq - mem.offset
                    mem.duplex = '-'
                elif mem.freq < mem.offset:
                    mem.offset = mem.offset - mem.freq
                    mem.duplex = '+'
            else:
                mem.duplex = 'split'

        # get tone data
        txtone = parse_tone(_data.tone[0])
        rxtone = parse_tone(_data.tone[1])
        chirp_common.split_tone_decode(mem, txtone, rxtone)

######################################################################
        if ENABLE_DANGEROUS_EXPERIMENTAL_FEATURES:
            # override certain settings based on flags
            # that we have set in junk areas of the memory
            # or basically, we BELIEVE this to be junk memory,
            # hence why it's experimental and dangerous
            if bool(_data.experimental_cross_mode_indicator) is False:
                if mem.tmode == 'Tone':
                    mem.cross_mode = 'Tone->'
                elif mem.tmode == 'TSQL':
                    mem.cross_mode = 'Tone->Tone'
                elif mem.tmode == 'DTCS':
                    mem.cross_mode = 'DTCS->DTCS'
                mem.tmode = 'Cross'
######################################################################

        # transmit mode and power level
        mem.mode = MODES[bool(_data.modulation_width)]
        mem.power = POWER_LEVELS[_data.txpower]

        # extra channel settings
        mem.extra = settings.RadioSettingGroup(
                "extra",
                "extra",
                list_setting("Busy channel lockout",
                             "BCL",
                             _data.bcl,
                             BCL_MODES),
                list_setting("Swap transmit and receive frequencies",
                             "Tx Rx freq swap",
                             _data.txrx_reverse,
                             OFF_ON),
                list_setting("Use compander",
                             "Use compander",
                             _data.compander,
                             OFF_ON),
                list_setting("Use scrambler", "Use scrambler",
                             _data.use_scrambler,
                             NO_YES),
                list_setting("Scrambler selection",
                             "Voice Scrambler",
                             _data.scrambler_type,
                             SCRAMBLER_MODES),
                list_setting("Send ID code before and/or after transmitting",
                             "PTT ID",
                             _data.ptt_id_edge,
                             PTT_ID_EDGES),
                list_setting("Optional signal before/after transmission, " +
                             "this setting overrides the PTT ID setting",
                             "Opt Signal",
                             _data.opt_signal,
                             OPTSIGN_MODES))

######################################################################
        if ENABLE_DANGEROUS_EXPERIMENTAL_FEATURES:
            # override certain settings based on flags
            # that we have set in junk areas of the memory
            # or basically, we BELIEVE this to be junk memory,
            # hence why it's experimental and dangerous
            if bool(_data.experimental_duplex_mode_indicator) is False:
                # if this flag is set, this means that we in the gui
                # have set the duplex mode to something
                # the channel does not really support,
                # such as split modes for vfo channels,
                # and non-split modes for the memory channels
                mem.duplex = DUPLEX_MODES[int(_data.duplex_sign)]
                mem.freq = int(_data.rx_freq)*10
                mem.offset = int(_data.tx_freq)*10
                if isvfo:
                    # we want split, so we have to reconstruct it
                    # from -/0/+ modes
                    if mem.duplex == '-':
                        mem.offset = mem.freq - mem.offset
                    elif mem.duplex == '':
                        mem.offset = mem.freq
                    elif mem.duplex == '+':
                        mem.offset = mem.freq + mem.offset
                    mem.duplex = 'split'
                else:
                    # we want -/0/+, so we have to reconstruct it
                    # from split modes
                    if mem.freq > mem.offset:
                        mem.offset = mem.freq - mem.offset
                        mem.duplex = '-'
                    elif mem.freq < mem.offset:
                        mem.offset = mem.offset - mem.freq
                        mem.duplex = '+'
                    else:
                        mem.offset = 0
                        mem.duplex = ''
######################################################################

        return mem

    def set_memory(self, mem):
        (index, designator,
         _data, _name, _present, _priority,
         isregular, isvfo, iscall) = self._get_memory_structs(mem.number)

        # handle empty channels
        if mem.empty:
            if isregular:
                _present.set_value(False)
                _priority.set_value(False)
                self._uninitialize(_data, 16)
                self._uninitialize(_name, 6)
            else:
                raise errors.InvalidValueError(
                        "Can't remove CALL and/or VFO channels!")
            return

        # handle regular channel stuff like name and present+priority flags
        if isregular:
            if not bool(_present):
                self._set_sane_defaults(_data)
            _name.set_value(
                    encode_ffstring(self.filter_name(mem.name.rstrip()),
                                    len(_name)))
            _present.set_value(True)
            _priority.set_value(1-SKIP_MODES.index(mem.skip))

        # frequency data
        rxf = int(mem.freq/10)
        txf = int(mem.offset/10)

        _data.rx_freq.set_value(rxf)

        if isvfo:
            # fake split modes on write, for channels
            # that do not support it, which are some
            # (the two vfo channels)
            if mem.duplex == 'split':
                for band in BANDS:
                    rb = mem.freq in range(band[0], band[1])
                    tb = mem.offset in range(band[0], band[1])
                    if rb != tb:
                        raise errors.InvalidValueError(
                                "VFO frequencies should be on the same band")
                if rxf < txf:
                    _data.duplex_sign.set_value(1)
                    _data.tx_freq.set_value(txf - rxf)
                elif rxf > txf:
                    _data.duplex_sign.set_value(2)
                    _data.tx_freq.set_value(rxf-txf)
                else:
                    _data.duplex_sign.set_value(0)
                    _data.tx_freq.set_value(0)
            else:
                _data.duplex_sign.set_value(DUPLEX_MODES.index(mem.duplex))
                _data.tx_freq.set_value(txf)

######################################################################
            if ENABLE_DANGEROUS_EXPERIMENTAL_FEATURES:
                if mem.duplex == 'split':
                    _data.experimental_duplex_mode_indicator.set_value(0)
                else:
                    _data.experimental_duplex_mode_indicator.set_value(1)
######################################################################

        else:
            # fake duplex modes on write, for channels
            # that do not support it, which are most
            # (all the memory channels)
            if mem.duplex == '' or mem.duplex is None:
                _data.tx_freq.set_value(rxf)
            elif mem.duplex == '+':
                _data.tx_freq.set_value(rxf + txf)
            elif mem.duplex == '-':
                _data.tx_freq.set_value(rxf - txf)
            else:
                _data.tx_freq.set_value(txf)

######################################################################
            if ENABLE_DANGEROUS_EXPERIMENTAL_FEATURES:
                if mem.duplex != 'split':
                    _data.experimental_duplex_mode_indicator.set_value(0)
                else:
                    _data.experimental_duplex_mode_indicator.set_value(1)
######################################################################

        # tone data
        tonedata = chirp_common.split_tone_encode(mem)
        for i in range(2):
            dihl = unparse_tone(tonedata[i])
            if dihl is not None:
                _data.tone[i].digital.set_value(dihl[0])
                _data.tone[i].invert.set_value(dihl[1])
                _data.tone[i].high.set_value(dihl[2])
                _data.tone[i].low.set_value(dihl[3])
            else:
                _data.tone[i].digital.set_value(1)
                _data.tone[i].invert.set_value(1)
                _data.tone[i].high.set_value(0x3f)
                _data.tone[i].low.set_value(0xff)

######################################################################
        if ENABLE_DANGEROUS_EXPERIMENTAL_FEATURES:
            if mem.tmode == 'Cross' and mem.cross_mode in ['Tone->',
                                                           'Tone->Tone',
                                                           'DTCS->DTCS']:
                _data.experimental_cross_mode_indicator.set_value(0)
            else:
                _data.experimental_cross_mode_indicator.set_value(1)
######################################################################

        # transmit mode and power level
        _data.modulation_width.set_value(MODES.index(mem.mode))
        if str(mem.power) == 'High':
            _data.txpower.set_value(1)
        else:
            _data.txpower = 0

    def get_settings(self):
        _model = self._memobj.mem.model_information
        _settings = self._memobj.mem.opt_settings
        _ptt_id_data = self._memobj.mem.ptt_id_data
        _msk_settings = self._memobj.mem.msk_settings
        _dtmf_settings = self._memobj.mem.dtmf_settings
        _5tone_settings = self._memobj.mem.five_tone_settings
        _broadcast = self._memobj.mem.fm_radio

        # for safety reasons we are showing these as read-only
        model_unit_settings = [
            integer_setting("vhflo", "VHF lower bound",
                            _model.band_limits[0].lower_freq,
                            134, 176,
                            int_from_mem=lambda x:int(int(x)/10),
                            mem_from_int=None),
            integer_setting("vhfhi", "VHF upper bound",
                            _model.band_limits[0].upper_freq,
                            134, 176,
                            int_from_mem=lambda x:int(int(x)/10),
                            mem_from_int=None),
            integer_setting("uhflo", "UHF lower bound",
                            _model.band_limits[1].lower_freq,
                            400, 480,
                            int_from_mem=lambda x:int(int(x)/10),
                            mem_from_int=None),
            integer_setting("uhfhi", "UHF upper bound",
                            _model.band_limits[1].upper_freq,
                            400, 480,
                            int_from_mem=lambda x:int(int(x)/10),
                            mem_from_int=None),
            ff_string_setting("model", "Model string",
                              _model.model_string,
                              0, 6)

        ]
        for s in model_unit_settings:
            s.value.set_mutable(False)
        model_unit_settings.append(ff_string_setting(
            "info", "Unit Information",
            self._memobj.mem.radio_information_string,
            0, 16))

        # tx/rx related settings
        radio_channel_settings = [
            list_setting("vfostep", "VFO step size",
                         _settings.vfo_step,
                         VFO_STRIDE),
            list_setting("abwatch", "Main watch",
                         _settings.main_watch,
                         AB),
            list_setting("watchmade", "Watch mode",
                         _settings.main_watch,
                         WATCH_MODES),
            list_setting("amode", "A mode",
                         _settings.workmode_a,
                         AB_MODES),
            list_setting("bmode", "B mode",
                         _settings.workmode_b,
                         AB_MODES),
            integer_setting("achan", "A channel index",
                            _settings.channel_a,
                            1, 128,
                            int_from_mem=lambda i:i+1,
                            mem_from_int=lambda i:i-1),
            integer_setting("bchan", "B channel index",
                            _settings.channel_b,
                            1, 128,
                            int_from_mem=lambda i:i+1,
                            mem_from_int=lambda i:i-1),
            integer_setting("pchan", "Priority channel index",
                            _settings.priority_channel,
                            1, 128, int_from_mem=lambda i:i+1,
                            mem_from_int=lambda i:i-1),
            list_setting("cactive", "Call channel active?",
                         _settings.call_channel_active,
                         NO_YES),
            list_setting("scanm", "Scan mode",
                         _settings.scan_mode,
                         SCAN_MODES),
            list_setting("swait", "Wait time",
                         _settings.wait_time,
                         WAIT_TIMES),
            # it is unclear what this option below does,
            # possibly squelch tail elimination?
            list_setting("tail", "Relay without disable tail (?)",
                         _settings.relay_without_disable_tail,
                         NO_YES),
            list_setting("batsav", "Battery saving mode",
                         _settings.battery_save,
                         OFF_ON),
            ]

        # user interface related settings
        interface_settings = [
            list_setting("sidehold", "Side button hold action",
                         _settings.side_button_hold_mode,
                         BUTTON_MODES),
            list_setting("sideclick", "Side button click action",
                         _settings.side_button_click_mode,
                         BUTTON_MODES),
            list_setting("bootmt", "Boot message type",
                         _settings.boot_message_mode,
                         BOOT_MESSAGE_TYPES),
            ff_string_setting("bootm", "Boot message",
                              _settings.boot_message,
                              0, 6),
            list_setting("beep", "Key beep",
                         _settings.key_beep,
                         OFF_ON),
            list_setting("talkback", "Menu talkback",
                         _settings.voice_announce,
                         TALKBACK),
            list_setting("sidetone", "DTMF sidetone",
                         _settings.dtmf_sidetone,
                         OFF_ON),
            list_setting("roger", "Roger beep",
                         _settings.use_roger_beep,
                         ROGER_BEEP),
            list_setting("backlm", "Backlight mode",
                         _settings.backlight_mode,
                         BACKLIGHT_MODES),
            list_setting("backlc", "Backlight color",
                         _settings.backlight_color,
                         BACKLIGHT_COLORS),
            integer_setting("squelch", "Squelch level",
                            _settings.squelch_level,
                            0, 9),
            list_setting("voxg", "Vox gain",
                         _settings.vox_gain,
                         VOX_GAIN),
            list_setting("voxd", "Vox delay",
                         _settings.vox_delay,
                         VOX_DELAYS),
            list_setting("txal", "Trinsmit time alarm",
                         _settings.tx_timeout,
                         TRANSMIT_ALARMS),
            ]

        # settings related to tone/data sending and interpretation
        data_general_settings = [
            list_setting("disptt", "Display PTT ID",
                         _settings.dis_ptt_id,
                         NO_YES),
            list_setting("pttidt", "PTT ID signal type",
                         _settings.ptt_id_type,
                         DATA_MODES)
            ]

        data_msk_settings = [
            ff_string_setting("bot", "MSK PTT ID (BOT)",
                              _ptt_id_data[0].entry,
                              0, 6, autowrite=False),
            ff_string_setting("eot", "MSK PTT ID (EOT)",
                              _ptt_id_data[1].entry,
                              0, 6, autowrite=False),
            ff_string_setting("id", "MSK ID code",
                              _msk_settings.id_code,
                              0, 4, charset=HEXADECIMAL),
            list_setting("mskr", "MSK reverse",
                         _settings.msk_reverse,
                         NO_YES)
            ]

        data_dtmf_settings = [
            dtmf_string_setting("bot", "DTMF PTT ID (BOT)",
                                _ptt_id_data[0].entry,
                                _ptt_id_data[0].length,
                                0, 8, autowrite=False),
            dtmf_string_setting("eot", "DTMF PTT ID (EOT)",
                                _ptt_id_data[1].entry,
                                _ptt_id_data[1].length,
                                0, 8, autowrite=False),
            dtmf_string_setting("id",  "DTMF ID code",
                                _dtmf_settings.id_code,
                                _dtmf_settings.id_code_length,
                                3, 8),

            integer_setting("time",   "Digit time (ms)",
                            _dtmf_settings.timing.digit_length,
                            50,  200, step=10,
                            int_from_mem=lambda x:x*10,
                            mem_from_int=lambda x:int(x/10)),
            integer_setting("pause",  "Inter digit time (ms)",
                            _dtmf_settings.timing.digit_length,
                            50,  200, step=10,
                            int_from_mem=lambda x:x*10,
                            mem_from_int=lambda x:int(x/10)),
            integer_setting("time1",  "First digit time (ms)",
                            _dtmf_settings.timing.digit_length,
                            50,  200, step=10,
                            int_from_mem=lambda x:x*10,
                            mem_from_int=lambda x:int(x/10)),
            integer_setting("pause1", "First digit delay (ms)",
                            _dtmf_settings.timing.digit_length,
                            100, 1000, step=50,
                            int_from_mem=lambda x:x*50,
                            mem_from_int=lambda x:int(x/50)),

            list_setting("arst", "Auto reset time",
                         _dtmf_settings.reset_time,
                         DTMF_TONE_RESET_TIME),
            list_setting("grp", "Group code",
                         _dtmf_settings.group_code,
                         DTMF_GROUPS),
            dtmf_string_setting("stunt", "TX Stun code",
                                _dtmf_settings.tx_stun_code,
                                _dtmf_settings.tx_stun_code_length,
                                3, 8),
            dtmf_string_setting("cstunt", "TX Stun cancel code",
                                _dtmf_settings.cancel_tx_stun_code,
                                _dtmf_settings.cancel_tx_stun_code_length,
                                3, 8),
            dtmf_string_setting("stunrt", "RX/TX Stun code",
                                _dtmf_settings.rxtx_stun_code,
                                _dtmf_settings.rxtx_stun_code_length,
                                3, 8),
            dtmf_string_setting("cstunrt", "RX/TX Stun cancel code",
                                _dtmf_settings.cancel_rxtx_stun_code,
                                _dtmf_settings.cancel_rxtx_stun_code_length,
                                3, 8),
            list_setting("altr", "Alert/Transpond",
                         _dtmf_settings.alert_transpond,
                         DTMF_ALERT_TRANSPOND),
            ]

        data_5tone_settings = [
            five_tone_string_setting("bot",
                                     "5-Tone PTT ID (BOT)",
                                     _ptt_id_data[0].entry,
                                     autowrite=False),
            five_tone_string_setting("eot",
                                     "5-Tone PTT ID (EOT)",
                                     _ptt_id_data[1].entry,
                                     autowrite=False),
            five_tone_string_setting("id",
                                     "5-tone ID code",
                                     _5tone_settings.id_code),
            list_setting("arst", "Auto reset time",
                         _5tone_settings.reset_time,
                         TONE_RESET_TIME),
            five_tone_string_setting("stunt",
                                     "TX Stun code",
                                     _5tone_settings.tx_stun_code),
            five_tone_string_setting("cstunt",
                                     "TX Stun cancel code",
                                     _5tone_settings.cancel_tx_stun_code),
            five_tone_string_setting("stunrt",
                                     "RX/TX Stun code",
                                     _5tone_settings.rxtx_stun_code),
            five_tone_string_setting("cstunrt",
                                     "RX/TX Stun cancel code",
                                     _5tone_settings.cancel_rxtx_stun_code),
            list_setting("altr", "Alert/Transpond",
                         _5tone_settings.alert_transpond,
                         FIVE_TONE_ALERT_TRANSPOND),
            list_setting("std", "5-Tone standard",
                         _5tone_settings.tone_standard,
                         FIVE_TONE_STANDARDS),
            ]
        for i in range(4):
            s = ['z1', 'z2', 'c1', 'ct'][i]
            l = FIVE_TONE_STANDARDS[i]
            data_5tone_settings.append(
                settings.RadioSettingGroup(
                    s, '%s settings' % l,
                    integer_setting("%speriod" % s, "%s Period (ms)" % l,
                                    _5tone_settings.tone_settings[i].period,
                                    20, 255),
                    list_setting("%sgrp" % s, "%s Group code" % l,
                                 _5tone_settings.tone_settings[i].group_code,
                                 HEXADECIMAL),
                    list_setting("%srpt" % s, "%s Repeat code" % l,
                                 _5tone_settings.tone_settings[i].repeat_code,
                                 HEXADECIMAL)))

        data_msk_call_list = []
        data_dtmf_call_list = []
        data_5tone_call_list = []
        for i in range(9):
            j = i+1
            data_msk_call_list.append(
                ff_string_setting("ce%d" % i,
                                  "MSK call entry %d" % j,
                                  _msk_settings.phone_book[i].entry,
                                  0, 4,
                                  charset=HEXADECIMAL))

            data_dtmf_call_list.append(
                dtmf_string_setting("ce%d" % i,
                                    "DTMF call entry %d" % j,
                                    _dtmf_settings.phone_book[i].entry,
                                    _dtmf_settings.phone_book[i].length,
                                    0, 10))

            data_5tone_call_list.append(
                five_tone_string_setting("ce%d" % i,
                                         "5-Tone call entry %d" % j,
                                         _5tone_settings.phone_book[i].entry)),

        data_settings = data_general_settings
        data_settings.extend([
                settings.RadioSettingGroup("MSK_s", "MSK settings",
                                           *data_msk_settings),
                settings.RadioSettingGroup("MSK_c", "MSK call list",
                                           *data_msk_call_list),
                settings.RadioSettingGroup("DTMF_s", "DTMF settings",
                                           *data_dtmf_settings),
                settings.RadioSettingGroup("DTMF_c", "DTMF call list",
                                           *data_dtmf_call_list),
                settings.RadioSettingGroup("5-Tone_s", "5-tone settings",
                                           *data_5tone_settings),
                settings.RadioSettingGroup("5-Tone_c", "5-tone call list",
                                           *data_5tone_call_list)
            ])

        # settings related to the various ways the radio can be locked down
        locking_settings = [
            list_setting("autolock", "Automatic timed keypad lock",
                         _settings.auto_keylock,
                         OFF_ON),
            list_setting("lockon", "Current status of keypad lock",
                         _settings.keypad_lock,
                         INACTIVE_ACTIVE),
            list_setting("nokeypad", "Disable keypad",
                         _settings.allow_keypad,
                         YES_NO),
            list_setting("rxstun", "Disable receiver (rx stun)",
                         _settings.rx_stun,
                         NO_YES),
            list_setting("txstun", "Disable transmitter (tx stun)",
                         _settings.tx_stun,
                         NO_YES),
            ]

        # broadcast FM radio settings
        broadcast_settings = [
            list_setting("band", "Frequency interval",
                         _broadcast.receive_range,
                         BFM_BANDS),
            list_setting("stride", "VFO step",
                         _broadcast.channel_stepping,
                         BFM_STRIDE),
            frequency_setting("vfo", "VFO frequency (MHz)",
                              _broadcast.vfo_freq)
        ]

        for i in range(10):
            broadcast_settings.append(
                frequency_setting("bcd%d" % i, "Memory %d frequency" % i,
                                  _broadcast.memory[i].entry))

        return settings.RadioSettings(
            settings.RadioSettingGroup("model", "Model/Unit information",
                                       *model_unit_settings),
            settings.RadioSettingGroup("radio", "Radio/Channel settings",
                                       *radio_channel_settings),
            settings.RadioSettingGroup("interface", "Interface",
                                       *interface_settings),
            settings.RadioSettingGroup("data", "Data",
                                       *data_settings),
            settings.RadioSettingGroup("locking", "Locking",
                                       *locking_settings),
            settings.RadioSettingGroup("broadcast",
                                       "Broadcast FM radio settings",
                                       *broadcast_settings)
        )

    def set_settings(self, s, parent=''):
        # The helper classes take care of all settings except these below,
        # since it is a single instance of settings having interdependencies,
        # i.e., which value that gets written to the memory depends on
        # the value of another setting. The value of the ptt id type setting
        # decides which of the msk/dtmf/5-tone ptt id strings are
        # actually written to memory.
        ds = sbyn(s, 'data')
        idts = sbyn(ds, 'pttidt').value
        idtv = idts.get_value()
        cs = sbyn(ds, idtv+'_s')
        tss = [sbyn(cs, e).value for e in ['bot', 'eot']]

        for ts in tss:
            ts.write_mem()
