# Copyright 2010 Vernon Mauery <vernon@mauery.org>
# Copyright 2016 Angus Ainslie <angus@akkea.ca>
#
# 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 3 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, errors, util, directory
from chirp import bitwise, memmap
from chirp.settings import RadioSettingGroup, RadioSetting, RadioSettings
from chirp.settings import RadioSettingValueInteger, RadioSettingValueString
from chirp.settings import RadioSettingValueList, RadioSettingValueBoolean
import time
import struct
import sys
import logging

LOG = logging.getLogger(__name__)

# TH-D72 memory map
# 0x0000..0x0200: startup password and other stuff
# 0x0200..0x0400: current channel and other settings
#   0x244,0x246: last menu numbers
#   0x249: last f menu number
# 0x0400..0x0c00: APRS settings and likely other settings
# 0x0c00..0x1500: memory channel flags
# 0x1500..0x5380: 0-999 channels
# 0x5380..0x54c0: 0-9 scan channels
# 0x54c0..0x5560: 0-9 wx channels
# 0x5560..0x5e00: ?
# 0x5e00..0x7d40: 0-999 channel names
# 0x7d40..0x7de0: ?
# 0x7de0..0x7e30: wx channel names
# 0x7e30..0x7ed0: ?
# 0x7ed0..0x7f20: group names
# 0x7f20..0x8b00: ?
# 0x8b00..0x9c00: last 20 APRS entries
# 0x9c00..0xe500: ?
# 0xe500..0xe7d0: startup bitmap
# 0xe7d0..0xe800: startup bitmap filename
# 0xe800..0xead0: gps-logger bitmap
# 0xe8d0..0xeb00: gps-logger bipmap filename
# 0xeb00..0xff00: ?
# 0xff00..0xffff: stuff?

# memory channel
# 0 1 2 3  4 5     6            7     8     9    a          b c d e   f
# [freq ]  ? mode  tmode/duplex rtone ctone dtcs cross_mode [offset]  ?

mem_format = """
#seekto 0x0000;
struct {
  ul16 version;
  u8   shouldbe32;
  u8   efs[11];
  u8   unknown0[3];
  u8   radio_custom_image;
  u8   gps_custom_image;
  u8   unknown1[7];
  u8   passwd[6];
} frontmatter;

#seekto 0x02c0;
struct {
  ul32 start_freq;
  ul32 end_freq;
} prog_vfo[6];

#seekto 0x0300;
struct {
  char power_on_msg[8];
  u8 unknown0[8];
  u8 unknown1[2];
  u8 lamp_timer;
  u8 contrast;
  u8 battery_saver;
  u8 APO;
  u8 unknown2;
  u8 key_beep;
  u8 unknown3[8];
  u8 unknown4;
  u8 balance;
  u8 unknown5[23];
  u8 lamp_control;
} settings;

#seekto 0x0c00;
struct {
  u8 disabled:4,
     prog_vfo:4;
  u8 skip;
} flag[1032];

#seekto 0x1500;
struct {
  ul32 freq;
  u8 unknown1;
  u8 mode;
  u8 tone_mode:4,
     duplex:4;
  u8 rtone;
  u8 ctone;
  u8 dtcs;
  u8 cross_mode;
  ul32 offset;
  u8 unknown2;
} memory[1032];

#seekto 0x5e00;
struct {
    char name[8];
} channel_name[1000];

#seekto 0x7de0;
struct {
    char name[8];
} wx_name[10];

#seekto 0x7ed0;
struct {
    char name[8];
} group_name[10];
"""

THD72_SPECIAL = {}

for i in range(0, 10):
    THD72_SPECIAL["L%i" % i] = 1000 + (i * 2)
    THD72_SPECIAL["U%i" % i] = 1000 + (i * 2) + 1
for i in range(0, 10):
    THD72_SPECIAL["WX%i" % (i + 1)] = 1020 + i
THD72_SPECIAL["C VHF"] = 1030
THD72_SPECIAL["C UHF"] = 1031

THD72_SPECIAL_REV = {}
for k, v in THD72_SPECIAL.items():
    THD72_SPECIAL_REV[v] = k

TMODES = {
    0x08: "Tone",
    0x04: "TSQL",
    0x02: "DTCS",
    0x01: "Cross",
    0x00: "",
}
TMODES_REV = {
    "": 0x00,
    "Cross": 0x01,
    "DTCS": 0x02,
    "TSQL": 0x04,
    "Tone": 0x08,
}

MODES = {
    0x00: "FM",
    0x01: "NFM",
    0x02: "AM",
}

MODES_REV = {
    "FM": 0x00,
    "NFM": 0x01,
    "AM": 0x2,
}

DUPLEX = {
    0x00: "",
    0x01: "+",
    0x02: "-",
    0x04: "split",
}
DUPLEX_REV = {
    "": 0x00,
    "+": 0x01,
    "-": 0x02,
    "split": 0x04,
}


EXCH_R = "R\x00\x00\x00\x00"
EXCH_W = "W\x00\x00\x00\x00"

DEFAULT_PROG_VFO = (
    (136000000, 174000000),
    (410000000, 470000000),
    (118000000, 136000000),
    (136000000, 174000000),
    (320000000, 400000000),
    (400000000, 524000000),
)
# index of PROG_VFO used for setting memory.unknown1 and memory.unknown2
# see http://chirp.danplanet.com/issues/1611#note-9
UNKNOWN_LOOKUP = (0, 7, 4, 0, 4, 7)


def get_prog_vfo(frequency):
    for i, (start, end) in enumerate(DEFAULT_PROG_VFO):
        if start <= frequency < end:
            return i
    raise ValueError("Frequency is out of range.")


@directory.register
class THD72Radio(chirp_common.CloneModeRadio):

    BAUD_RATE = 9600
    VENDOR = "Kenwood"
    MODEL = "TH-D72 (clone mode)"
    HARDWARE_FLOW = sys.platform == "darwin"  # only OS X driver needs hw flow

    mem_upper_limit = 1022
    _memsize = 65536
    _model = ""  # FIXME: REMOVE
    _dirty_blocks = []

    _LCD_CONTRAST = ["Level %d" % x for x in range(1, 16)]
    _LAMP_CONTROL = ["Manual", "Auto"]
    _LAMP_TIMER = ["Seconds %d" % x for x in range(2, 11)]
    _BATTERY_SAVER = ["OFF", "0.03 Seconds", "0.2 Seconds", "0.4 Seconds",
                      "0.6 Seconds", "0.8 Seconds", "1 Seconds", "2 Seconds",
                      "3 Seconds", "4 Seconds", "5 Seconds"]
    _APO = ["OFF", "15 Minutes", "30 Minutes", "60 Minutes"]
    _AUDIO_BALANCE = ["Center", "A +50%", "A +100%", "B +50%", "B +100%"]
    _KEY_BEEP = ["OFF", "Radio & GPS", "Radio Only", "GPS Only"]

    def get_features(self):
        rf = chirp_common.RadioFeatures()
        rf.memory_bounds = (0, 1031)
        rf.valid_bands = [(118000000, 174000000),
                          (320000000, 524000000)]
        rf.has_cross = True
        rf.can_odd_split = True
        rf.has_dtcs_polarity = False
        rf.has_tuning_step = False
        rf.has_bank = False
        rf.has_settings = True
        rf.valid_tuning_steps = []
        rf.valid_modes = list(MODES_REV.keys())
        rf.valid_tmodes = list(TMODES_REV.keys())
        rf.valid_duplexes = list(DUPLEX_REV.keys())
        rf.valid_skips = ["", "S"]
        rf.valid_characters = chirp_common.CHARSET_ALPHANUMERIC
        rf.valid_name_length = 8
        return rf

    def process_mmap(self):
        self._memobj = bitwise.parse(mem_format, self._mmap)
        self._dirty_blocks = []

    def _detect_baud(self):
        for baud in [9600, 19200, 38400, 57600]:
            self.pipe.baudrate = baud
            try:
                self.pipe.write("\r\r")
            except:
                break
            self.pipe.read(32)
            try:
                id = self.get_id()
                LOG.info("Radio %s at %i baud" % (id, baud))
                return True
            except errors.RadioError:
                pass

        raise errors.RadioError("No response from radio")

    def get_special_locations(self):
        return sorted(THD72_SPECIAL.keys())

    def add_dirty_block(self, memobj):
        block = memobj._offset / 256
        if block not in self._dirty_blocks:
            self._dirty_blocks.append(block)
        self._dirty_blocks.sort()
        print("dirty blocks: ", self._dirty_blocks)

    def get_channel_name(self, number):
        if number < 999:
            name = str(self._memobj.channel_name[number].name) + '\xff'
        elif number >= 1020 and number < 1030:
            number -= 1020
            name = str(self._memobj.wx_name[number].name) + '\xff'
        else:
            return ''
        return name[:name.index('\xff')].rstrip()

    def set_channel_name(self, number, name):
        name = name[:8] + '\xff' * 8
        if number < 999:
            self._memobj.channel_name[number].name = name[:8]
            self.add_dirty_block(self._memobj.channel_name[number])
        elif number >= 1020 and number < 1030:
            number -= 1020
            self._memobj.wx_name[number].name = name[:8]
            self.add_dirty_block(self._memobj.wx_name[number])

    def get_raw_memory(self, number):
        return repr(self._memobj.memory[number]) + \
            repr(self._memobj.flag[number])

    def get_memory(self, number):
        if isinstance(number, str):
            try:
                number = THD72_SPECIAL[number]
            except KeyError:
                raise errors.InvalidMemoryLocation("Unknown channel %s" %
                                                   number)

        if number < 0 or number > (max(THD72_SPECIAL.values()) + 1):
            raise errors.InvalidMemoryLocation(
                "Number must be between 0 and 999")

        _mem = self._memobj.memory[number]
        flag = self._memobj.flag[number]

        mem = chirp_common.Memory()
        mem.number = number

        if number > 999:
            mem.extd_number = THD72_SPECIAL_REV[number]
        if flag.disabled == 0xf:
            mem.empty = True
            return mem

        mem.name = self.get_channel_name(number)
        mem.freq = int(_mem.freq)
        mem.tmode = TMODES[int(_mem.tone_mode)]
        mem.rtone = chirp_common.TONES[_mem.rtone]
        mem.ctone = chirp_common.TONES[_mem.ctone]
        mem.dtcs = chirp_common.DTCS_CODES[_mem.dtcs]
        mem.duplex = DUPLEX[int(_mem.duplex)]
        mem.offset = int(_mem.offset)
        mem.mode = MODES[int(_mem.mode)]

        if number < 999:
            mem.skip = chirp_common.SKIP_VALUES[int(flag.skip)]
            mem.cross_mode = chirp_common.CROSS_MODES[_mem.cross_mode]
        if number > 999:
            mem.cross_mode = chirp_common.CROSS_MODES[0]
            mem.immutable = ["number", "bank", "extd_number", "cross_mode"]
            if number >= 1020 and number < 1030:
                mem.immutable += ["freq", "offset", "tone", "mode",
                                  "tmode", "ctone", "skip"]  # FIXME: ALL
            else:
                mem.immutable += ["name"]

        return mem

    def set_memory(self, mem):
        LOG.debug("set_memory(%d)" % mem.number)
        if mem.number < 0 or mem.number > (max(THD72_SPECIAL.values()) + 1):
            raise errors.InvalidMemoryLocation(
                "Number must be between 0 and 999")

        # weather channels can only change name, nothing else
        if mem.number >= 1020 and mem.number < 1030:
            self.set_channel_name(mem.number, mem.name)
            return

        flag = self._memobj.flag[mem.number]
        self.add_dirty_block(self._memobj.flag[mem.number])

        # only delete non-WX channels
        was_empty = flag.disabled == 0xf
        if mem.empty:
            flag.disabled = 0xf
            return
        flag.disabled = 0

        _mem = self._memobj.memory[mem.number]
        self.add_dirty_block(_mem)
        if was_empty:
            self.initialize(_mem)

        _mem.freq = mem.freq

        if mem.number < 999:
            self.set_channel_name(mem.number, mem.name)

        _mem.tone_mode = TMODES_REV[mem.tmode]
        _mem.rtone = chirp_common.TONES.index(mem.rtone)
        _mem.ctone = chirp_common.TONES.index(mem.ctone)
        _mem.dtcs = chirp_common.DTCS_CODES.index(mem.dtcs)
        _mem.cross_mode = chirp_common.CROSS_MODES.index(mem.cross_mode)
        _mem.duplex = DUPLEX_REV[mem.duplex]
        _mem.offset = mem.offset
        _mem.mode = MODES_REV[mem.mode]

        prog_vfo = get_prog_vfo(mem.freq)
        flag.prog_vfo = prog_vfo
        _mem.unknown1 = _mem.unknown2 = UNKNOWN_LOOKUP[prog_vfo]

        if mem.number < 999:
            flag.skip = chirp_common.SKIP_VALUES.index(mem.skip)

    def sync_in(self):
        self._detect_baud()
        self._mmap = self.download()
        self.process_mmap()

    def sync_out(self):
        self._detect_baud()
        if len(self._dirty_blocks):
            self.upload(self._dirty_blocks)
        else:
            self.upload()

    def read_block(self, block, count=256):
        self.pipe.write(struct.pack("<cBHB", "R", 0, block, 0))
        r = self.pipe.read(5)
        if len(r) != 5:
            raise Exception("Did not receive block response")

        cmd, _zero, _block, zero = struct.unpack("<cBHB", r)
        if cmd != "W" or _block != block:
            raise Exception("Invalid response: %s %i" % (cmd, _block))

        data = ""
        while len(data) < count:
            data += self.pipe.read(count - len(data))

        self.pipe.write(chr(0x06))
        if self.pipe.read(1) != chr(0x06):
            raise Exception("Did not receive post-block ACK!")

        return data

    def write_block(self, block, map):
        self.pipe.write(struct.pack("<cBHB", "W", 0, block, 0))
        base = block * 256
        self.pipe.write(map[base:base + 256])

        ack = self.pipe.read(1)

        return ack == chr(0x06)

    def download(self, raw=False, blocks=None):
        if blocks is None:
            blocks = range(self._memsize / 256)
        else:
            blocks = [b for b in blocks if b < self._memsize / 256]

        if self.command("0M PROGRAM") != "0M":
            raise errors.RadioError("No response from self")

        allblocks = range(self._memsize / 256)
        self.pipe.baudrate = 57600
        try:
            self.pipe.setRTS()
        except AttributeError:
            self.pipe.rts = True
        self.pipe.read(1)
        data = ""
        LOG.debug("reading blocks %d..%d" % (blocks[0], blocks[-1]))
        total = len(blocks)
        count = 0
        for i in allblocks:
            if i not in blocks:
                data += 256 * '\xff'
                continue
            data += self.read_block(i)
            count += 1
            if self.status_fn:
                s = chirp_common.Status()
                s.msg = "Cloning from radio"
                s.max = total
                s.cur = count
                self.status_fn(s)

        self.pipe.write("E")

        if raw:
            return data
        return memmap.MemoryMap(data)

    def upload(self, blocks=None):
        if blocks is None:
            blocks = range((self._memsize / 256) - 2)
        else:
            blocks = [b for b in blocks if b < self._memsize / 256]

        if self.command("0M PROGRAM") != "0M":
            raise errors.RadioError("No response from self")

        self.pipe.baudrate = 57600
        try:
            self.pipe.setRTS()
        except AttributeError:
            self.pipe.rts = True
        self.pipe.read(1)
        LOG.debug("writing blocks %d..%d" % (blocks[0], blocks[-1]))
        total = len(blocks)
        count = 0
        for i in blocks:
            r = self.write_block(i, self._mmap)
            count += 1
            if not r:
                raise errors.RadioError("self NAK'd block %i" % i)
            if self.status_fn:
                s = chirp_common.Status()
                s.msg = "Cloning to radio"
                s.max = total
                s.cur = count
                self.status_fn(s)

        self.pipe.write("E")
        # clear out blocks we uploaded from the dirty blocks list
        self._dirty_blocks = [b for b in self._dirty_blocks if b not in blocks]

    def command(self, cmd, timeout=0.5):
        start = time.time()

        data = ""
        LOG.debug("PC->D72: %s" % cmd)
        self.pipe.write(cmd + "\r")
        while not data.endswith("\r") and (time.time() - start) < timeout:
            data += self.pipe.read(1)
        LOG.debug("D72->PC: %s" % data.strip())
        return data.strip()

    def get_id(self):
        r = self.command("ID")
        if r.startswith("ID "):
            return r.split(" ")[1]
        else:
            raise errors.RadioError("No response to ID command")

    def initialize(self, mmap):
        mmap.set_raw("\x00\xc8\xb3\x08\x00\x01\x00\x08"
                     "\x08\x00\xc0\x27\x09\x00\x00\x00")

    def _get_settings(self):
        top = RadioSettings(self._get_display_settings(),
                            self._get_audio_settings(),
                            self._get_battery_settings())
        return top

    def set_settings(self, settings):
        _mem = self._memobj
        for element in settings:
            if not isinstance(element, RadioSetting):
                self.set_settings(element)
                continue
            if not element.changed():
                continue
            try:
                if element.has_apply_callback():
                    LOG.debug("Using apply callback")
                    try:
                        element.run_apply_callback()
                    except NotImplementedError as e:
                        LOG.error("thd72: %s", e)
                    continue

                # Find the object containing setting.
                obj = _mem
                bits = element.get_name().split(".")
                setting = bits[-1]
                for name in bits[:-1]:
                    if name.endswith("]"):
                        name, index = name.split("[")
                        index = int(index[:-1])
                        obj = getattr(obj, name)[index]
                    else:
                        obj = getattr(obj, name)

                try:
                    old_val = getattr(obj, setting)
                    LOG.debug("Setting %s(%r) <= %s" % (
                        element.get_name(), old_val, element.value))
                    setattr(obj, setting, element.value)
                except AttributeError as e:
                    LOG.error("Setting %s is not in the memory map: %s" %
                              (element.get_name(), e))
            except Exception as e:
                LOG.debug(element.get_name())
                raise

    def get_settings(self):
        try:
            return self._get_settings()
        except:
            import traceback
            LOG.error("Failed to parse settings: %s", traceback.format_exc())
            return None

    @classmethod
    def apply_power_on_msg(cls, setting, obj):
        message = setting.value.get_value()
        setattr(obj, "power_on_msg", cls._add_ff_pad(message, 8))

    def apply_lcd_contrast(cls, setting, obj):
        rawval = setting.value.get_value()
        val = cls._LCD_CONTRAST.index(rawval) + 1
        obj.contrast = val

    def apply_lamp_control(cls, setting, obj):
        rawval = setting.value.get_value()
        val = cls._LAMP_CONTROL.index(rawval)
        obj.lamp_control = val

    def apply_lamp_timer(cls, setting, obj):
        rawval = setting.value.get_value()
        val = cls._LAMP_TIMER.index(rawval) + 2
        obj.lamp_timer = val

    def _get_display_settings(self):
        menu = RadioSettingGroup("display", "Display")
        display_settings = self._memobj.settings

        val = RadioSettingValueString(
            0, 8, str(display_settings.power_on_msg).rstrip("\xFF"))
        rs = RadioSetting("display.power_on_msg", "Power on message", val)
        rs.set_apply_callback(self.apply_power_on_msg, display_settings)
        menu.append(rs)

        val = RadioSettingValueList(
            self._LCD_CONTRAST,
            self._LCD_CONTRAST[display_settings.contrast - 1])
        rs = RadioSetting("display.contrast", "LCD Contrast",
                          val)
        rs.set_apply_callback(self.apply_lcd_contrast, display_settings)
        menu.append(rs)

        val = RadioSettingValueList(
            self._LAMP_CONTROL,
            self._LAMP_CONTROL[display_settings.lamp_control])
        rs = RadioSetting("display.lamp_control", "Lamp Control",
                          val)
        rs.set_apply_callback(self.apply_lamp_control, display_settings)
        menu.append(rs)

        val = RadioSettingValueList(
            self._LAMP_TIMER,
            self._LAMP_TIMER[display_settings.lamp_timer - 2])
        rs = RadioSetting("display.lamp_timer", "Lamp Timer",
                          val)
        rs.set_apply_callback(self.apply_lamp_timer, display_settings)
        menu.append(rs)

        return menu

    def apply_battery_saver(cls, setting, obj):
        rawval = setting.value.get_value()
        val = cls._BATTERY_SAVER.index(rawval)
        obj.battery_saver = val

    def apply_APO(cls, setting, obj):
        rawval = setting.value.get_value()
        val = cls._APO.index(rawval)
        obj.APO = val

    def _get_battery_settings(self):
        menu = RadioSettingGroup("battery", "Battery")
        battery_settings = self._memobj.settings

        val = RadioSettingValueList(
            self._BATTERY_SAVER,
            self._BATTERY_SAVER[battery_settings.battery_saver])
        rs = RadioSetting("battery.battery_saver", "Battery Saver",
                          val)
        rs.set_apply_callback(self.apply_battery_saver, battery_settings)
        menu.append(rs)

        val = RadioSettingValueList(
            self._APO,
            self._APO[battery_settings.APO])
        rs = RadioSetting("battery.APO", "Auto Power Off",
                          val)
        rs.set_apply_callback(self.apply_APO, battery_settings)
        menu.append(rs)

        return menu

    def apply_balance(cls, setting, obj):
        rawval = setting.value.get_value()
        val = cls._AUDIO_BALANCE.index(rawval)
        obj.balance = val

    def apply_key_beep(cls, setting, obj):
        rawval = setting.value.get_value()
        val = cls._KEY_BEEP.index(rawval)
        obj.key_beep = val

    def _get_audio_settings(self):
        menu = RadioSettingGroup("audio", "Audio")
        audio_settings = self._memobj.settings

        val = RadioSettingValueList(
            self._AUDIO_BALANCE,
            self._AUDIO_BALANCE[audio_settings.balance])
        rs = RadioSetting("audio.balance", "Balance",
                          val)
        rs.set_apply_callback(self.apply_balance, audio_settings)
        menu.append(rs)

        val = RadioSettingValueList(
            self._KEY_BEEP,
            self._KEY_BEEP[audio_settings.key_beep])
        rs = RadioSetting("audio.key_beep", "Key Beep",
                          val)
        rs.set_apply_callback(self.apply_key_beep, audio_settings)
        menu.append(rs)

        return menu

    @staticmethod
    def _add_ff_pad(val, length):
        return val.ljust(length, "\xFF")[:length]

    @classmethod
    def _strip_ff_pads(cls, messages):
        result = []
        for msg_text in messages:
            result.append(str(msg_text).rstrip("\xFF"))
        return result

if __name__ == "__main__":
    import sys
    import serial
    import detect
    import getopt

    def fixopts(opts):
        r = {}
        for opt in opts:
            k, v = opt
            r[k] = v
        return r

    def usage():
        print("Usage: %s <-i input.img>|<-o output.img> -p port "
              "[[-f first-addr] [-l last-addr] | [-b list,of,blocks]]" %
              sys.argv[0])
        sys.exit(1)

    opts, args = getopt.getopt(sys.argv[1:], "i:o:p:f:l:b:")
    opts = fixopts(opts)
    first = last = 0
    blocks = None
    if '-i' in opts:
        fname = opts['-i']
        download = False
    elif '-o' in opts:
        fname = opts['-o']
        download = True
    else:
        usage()
    if '-p' in opts:
        port = opts['-p']
    else:
        usage()

    if '-f' in opts:
        first = int(opts['-f'], 0)
    if '-l' in opts:
        last = int(opts['-l'], 0)
    if '-b' in opts:
        blocks = [int(b, 0) for b in opts['-b'].split(',')]
        blocks.sort()

    ser = serial.Serial(port=port, baudrate=9600, timeout=0.25)
    r = THD72Radio(ser)
    memmax = r._memsize
    if not download:
        memmax -= 512

    if blocks is None:
        if first < 0 or first > (r._memsize - 1):
            raise errors.RadioError("first address out of range")
        if (last > 0 and last < first) or last > memmax:
            raise errors.RadioError("last address out of range")
        elif last == 0:
            last = memmax
        first /= 256
        if last % 256 != 0:
            last += 256
        last /= 256
        blocks = range(first, last)

    if download:
        data = r.download(True, blocks)
        file(fname, "wb").write(data)
    else:
        r._mmap = file(fname, "rb").read(r._memsize)
        r.upload(blocks)
    print("\nDone")
