# 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,
    #    RadioSettings,
    RadioSettingValueBoolean,
    #    RadioSettingValueInteger,
    RadioSettingValueList,
    #    RadioSettingValueString,
)

from chirp.drivers import (
    baofeng_uv17Pro,
    mml_jc8810
)

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];

#seekto 0xD000;
u8 radio_mode;

"""

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 = ['A', 'B']

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

    self = (118000000, 138000000)

    _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
        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._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)

        # 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)

        rs = RadioSettingValueList(
            self.AB, current_index=_settings.unknown_9018)
        rset = RadioSetting("unknown_9018", "A/B switch", rs)
        spec.append(rset)

        group.append(spec)
        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 = mml_jc8810.RT470XRadio._AIRBAND
    _AIRBAND = (108000000, 137000000)
    _MIL_AIRBAND = (225000000, 399950000)
    _AIRBANDS = [_AIRBAND] + [_MIL_AIRBAND]
    _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
    ]

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

    def get_memory(self, number):
        mem = super().get_memory(number)
        if chirp_common.in_range(mem.freq, self._AIRBANDS):
            mem.mode = 'AM'
        return mem

    def validate_memory(self, mem):
        msgs = []
        in_range = chirp_common.in_range
        AM_mode = mem.mode == 'AM'
        if in_range(mem.freq, self._AIRBANDS) and not AM_mode:
            msgs.append(chirp_common.ValidationWarning(
                _('Frequency in this range requires AM mode')))
        if not in_range(mem.freq, self._AIRBANDS) and AM_mode:
            msgs.append(chirp_common.ValidationWarning(
                _('Frequency in this range must 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 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
