# Copyright 2011 Dan Smith <dsmith@danplanet.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 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/>.
import time
import logging
import re

from chirp.drivers import yaesu_clone
from chirp import chirp_common, directory, errors, bitwise, util
from chirp.settings import RadioSetting, RadioSettingGroup, \
    RadioSettingValueInteger, RadioSettingValueList, \
    RadioSettingValueBoolean, RadioSettingValueString, \
    RadioSettings

LOG = logging.getLogger(__name__)

MEM_FORMAT = """
struct flag_struct {
  u8 unknown1f:5,
     skip:1,
     mask:1,
     used:1;
};

struct mem_struct {
  u8 showname:1,
     unknown1:3,
     unknown2:2,
     unknown3:2;
  u8 ishighpower:1,
     power:2,
     unknown4:1,
     tuning_step:4;
  u8 codememno:4,
     codeorpage:2,
     duplex:2;
  u8 tmode:2,
     tone:6;
  u8 unknown5:1,
     dtcs:7;
  u8 unknown6:6,
     mode:2;
  bbcd freq[3];
  bbcd offset[3];
  u8 name[4];
};

#seekto 0x000C;
struct {
  u8 extendedrx_flg;    // Seems to be set to 03 when extended rx is enabled
  u8 extendedrx;        // Seems to be set to 01 when extended rx is enabled
} extendedrx_struct;    // UNFINISHED!!

#seekto 0x001A;
struct flag_struct flag[100];

#seekto 0x079C;
struct flag_struct flag_repeat[100];

#seekto 0x00AA;
struct mem_struct memory[100];
struct mem_struct special[11];

#seekto 0x08C7;
struct {
  u8 sub_display;
  u8 unknown1s;
  u8 apo;
  u8 timeout;
  u8 lock;
  u8 rxsave;
  u8 lamp;
  u8 bell;
  u8 cwid[16];
  u8 unknown2s;
  u8 artsmode;
  u8 artsbeep;
  u8 unknown3s;
  u8 unknown4s;
  struct {
    u8 header[3];
    u8 mem_num;
    u8 digits[16];
  } autodial[8];
  struct {
    u8 header[3];
    u8 mem_num;
    u8 digits[32];
  } autodial9_ro;
  bbcd pagingcodec_ro[2];
  bbcd pagingcodep[2];
  struct {
    bbcd digits[2];
  } pagingcode[6];
  u8 code_dec_c_en:1,
     code_dec_p_en:1,
     code_dec_1_en:1,
     code_dec_2_en:1,
     code_dec_3_en:1,
     code_dec_4_en:1,
     code_dec_5_en:1,
     code_dec_6_en:1;
  u8 pagingspeed;
  u8 pagingdelay;
  u8 pagingbell;
  u8 paginganswer;

  #seekto 0x0E30;
  u8 squelch;       // squelch
  u8 unknown0c;
  u8 rptl:1,        // repeater input tracking
     amod:1,        // auto mode
     scnl:1,        // scan lamp
     resm:1,        // scan resume mode 0=5sec, 1=carr
     ars:1,         // automatic repeater shift
     keybeep:1,     // keypad beep
     lck:1,         // lock
     unknown1c:1;
  u8 lgt:1,
     pageamsg:1,
     unknown2c:1,
     bclo:1,        // Busy channel lock out
     unknown3c:2,
     cwid_en:1,     // CWID off/on
     tsav:1;        // TX save
  u8 unknown4c:4,
     artssped:1,    // ARTS/SPED: 0=15s, 1=25s
     unknown5c:1,
     rvhm:1,        // RVHM: 0=home, 1=rev
     mon:1;         // MON: 0=mon, 1=tcal
} settings;

#seekto 0x080E;
struct mem_struct vfo_mem[10];


"""

# 10 VFO memories: A145, A220, A380, A430, A800,
#                  B145, B220, B380, B430, B800

DUPLEX = ["", "-", "+"]
MODES = ["FM", "AM", "WFM"]
SKIP_VALUES = ["", "S"]
TMODES = ["", "Tone", "TSQL", "DTCS"]
TUNING_STEPS = [5.0, 10.0, 12.5, 15.0, 20.0, 25.0, 50.0]
TONES = list(chirp_common.OLD_TONES)

# CHARSET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ ()+-=*/???|0123456789"
# the = displays as an underscored dash on radio
# the first ? is an uppercase delta - \xA7
# the second ? is an uppercase gamma - \xD1
# the third ? is an uppercase sigma - \xCF
NUMERIC_CHARSET = list("0123456789")
CHARSET = [str(x) for x in range(0, 10)] + \
    [chr(x) for x in range(ord("A"), ord("Z")+1)] + \
    list(" ()+-=*/" + ("\x00" * 3) + "|") + NUMERIC_CHARSET
DTMFCHARSET = NUMERIC_CHARSET + list("ABCD*#")

POWER_LEVELS = [chirp_common.PowerLevel("Hi", watts=5.0),
                chirp_common.PowerLevel("L3", watts=2.5),
                chirp_common.PowerLevel("L2", watts=1.0),
                chirp_common.PowerLevel("L1", watts=0.1)]
SPECIALS = ["L1", "U1", "L2", "U2", "L3", "U3", "L4", "U4", "L5", "U5", "UNK"]


@directory.register
class FT50Radio(yaesu_clone.YaesuCloneModeRadio):
    """Yaesu FT-50"""
    BAUD_RATE = 9600
    VENDOR = "Yaesu"
    MODEL = "FT-50"

    _model = ""
    _memsize = 3723
    _block_lengths = [10, 16, 112, 16, 16, 1776, 1776, 1]
    # _block_delay = 0.15
    _block_size = 8

    @classmethod
    def get_prompts(cls):
        rp = chirp_common.RadioPrompts()
        rp.pre_download = _(
            "1. Turn radio off.\n"
            "2. Connect cable to MIC/SP jack.\n"
            "3. Press and hold [PTT] &amp; Knob while turning the\n"
            "     radio on.\n"
            "4. <b>After clicking OK</b>, press the [PTT] switch to send"
            " image.\n")
        rp.pre_upload = _(
            "1. Turn radio off.\n"
            "2. Connect cable to MIC/SP jack.\n"
            "3. Press and hold [PTT] &amp; Knob while turning the\n"
            "     radio on.\n"
            "4. Press the [MONI] switch (\"WAIT\" will appear on the LCD).\n"
            "5. Press OK.\n")
        return rp

    def get_features(self):
        rf = chirp_common.RadioFeatures()
        rf.memory_bounds = (1, 100)
        rf.valid_duplexes = DUPLEX
        rf.valid_tmodes = TMODES
        rf.valid_power_levels = POWER_LEVELS
        rf.valid_tuning_steps = TUNING_STEPS
        rf.valid_power_levels = POWER_LEVELS
        rf.valid_characters = "".join(CHARSET)
        rf.valid_name_length = 4
        rf.valid_modes = MODES
        # Specials not yet implemented
        # rf.valid_special_chans = SPECIALS
        rf.valid_bands = [(76000000, 200000000),
                          (300000000, 540000000),
                          (590000000, 999000000)]
        # rf.can_odd_split = True
        rf.has_ctone = False
        rf.has_bank = False
        rf.has_settings = True
        rf.has_dtcs_polarity = False

        return rf

    def _checksums(self):
        return [yaesu_clone.YaesuChecksum(0x0000, 0xE89)]

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

    def get_raw_memory(self, number):
        return repr(self._memobj.memory[number - 1])

    def get_memory(self, number):
        mem = chirp_common.Memory()
        _mem = self._memobj.memory[number-1]
        _flg = self._memobj.flag[number-1]
        mem.number = number

        # if not _flg.visible:
        #    mem.empty = True
        if not _flg.used:
            mem.empty = True
            return mem

        for i in _mem.name:
            mem.name += CHARSET[i & 0x7F]
        mem.name = mem.name.rstrip()

        mem.freq = chirp_common.fix_rounded_step(int(_mem.freq) * 1000)
        mem.duplex = DUPLEX[_mem.duplex]
        mem.offset = chirp_common.fix_rounded_step(int(_mem.offset) * 1000)
        mem.rtone = mem.ctone = TONES[_mem.tone]
        mem.tmode = TMODES[_mem.tmode]
        mem.mode = MODES[_mem.mode]
        mem.tuning_step = TUNING_STEPS[_mem.tuning_step]
        mem.dtcs = chirp_common.DTCS_CODES[_mem.dtcs]
        # Power is stored as 2 bits to describe the 3 low power levels
        # High power is determined by a different bit.
        if not _mem.ishighpower:
            mem.power = POWER_LEVELS[3 - _mem.power]
        else:
            mem.power = POWER_LEVELS[0]
        mem.skip = SKIP_VALUES[_flg.skip]

        return mem

    def set_memory(self, mem):
        _mem = self._memobj.memory[mem.number-1]
        _flg = self._memobj.flag[mem.number-1]
        _flg_repeat = self._memobj.flag_repeat[mem.number-1]

        if mem.empty:
            _flg.used = False
            return

        if (len(mem.name) == 0):
            _mem.name = [0x24] * 4
            _mem.showname = 0
        else:
            _mem.showname = 1
            for i in range(0, 4):
                _mem.name[i] = CHARSET.index(mem.name.ljust(4)[i])

        _mem.freq = int(mem.freq / 1000)
        _mem.duplex = DUPLEX.index(mem.duplex)
        _mem.offset = int(mem.offset / 1000)
        _mem.mode = MODES.index(mem.mode)
        _mem.tuning_step = TUNING_STEPS.index(mem.tuning_step)
        if mem.power:
            if (mem.power == POWER_LEVELS[0]):
                # low power level is not changed when high power is selected
                _mem.ishighpower = 0x01
                if (_mem.power == 3):
                    # Set low power to L3 (0x02) if it is
                    # set to 3 (new object default)
                    LOG.debug("SETTING DEFAULT?")
                    _mem.power = 0x02
            else:
                _mem.ishighpower = 0x00
                _mem.power = 3 - POWER_LEVELS.index(mem.power)
        else:
            _mem.ishighpower = 0x01
            _mem.power = 0x02
        _mem.tmode = TMODES.index(mem.tmode)
        try:
            _mem.tone = TONES.index(mem.rtone)
        except ValueError:
            raise errors.UnsupportedToneError(
                ("This radio does not support tone %s" % mem.rtone))
        _mem.dtcs = chirp_common.DTCS_CODES.index(mem.dtcs)

        _flg.skip = SKIP_VALUES.index(mem.skip)

        # initialize new channel to safe defaults
        if not mem.empty and not _flg.used:
            _flg.used = True
            _flg.mask = True        # Mask = True to be visible on radio
            _mem.unknown1 = 0x00
            _mem.unknown2 = 0x00
            _mem.unknown3 = 0x00
            _mem.unknown4 = 0x00
            _mem.unknown5 = 0x00
            _mem.unknown6 = 0x00
            _mem.codememno = 0x02   # Not implemented in chirp
            _mem.codeorpage = 0x00  # Not implemented in chirp

        # Duplicate flags to repeated part in memory
        _flg_repeat.skip = _flg.skip
        _flg_repeat.mask = _flg.mask
        _flg_repeat.used = _flg.used

    def _decode_cwid(self, inarr):
        LOG.debug("@_decode_chars, type: %s" % type(inarr))
        LOG.debug(inarr)
        outstr = ""
        for i in inarr:
            if i == 0xFF:
                break
            outstr += CHARSET[i & 0x7F]
        LOG.debug(outstr)
        return outstr.rstrip()

    def _encode_cwid(self, instr, length=16):
        LOG.debug("@_encode_chars, type: %s" % type(instr))
        LOG.debug(instr)
        outarr = []
        instr = str(instr)
        for i in range(0, length):
            if i < len(instr):
                outarr.append(CHARSET.index(instr[i]))
            else:
                outarr.append(0xFF)
        return outarr

    def get_settings(self):
        _settings = self._memobj.settings
        basic = RadioSettingGroup("basic", "Basic")
        dtmf = RadioSettingGroup("dtmf", "DTMF Code & Paging")
        arts = RadioSettingGroup("arts", "ARTS")
        autodial = RadioSettingGroup("autodial", "AutoDial")
        top = RadioSettings(basic, autodial, arts, dtmf)

        rs = RadioSetting(
                "squelch", "Squelch",
                RadioSettingValueInteger(0, 15, _settings.squelch))
        basic.append(rs)

        rs = RadioSetting(
                "keybeep", "Keypad Beep",
                RadioSettingValueBoolean(_settings.keybeep))
        basic.append(rs)

        rs = RadioSetting(
                "scnl", "Scan Lamp",
                RadioSettingValueBoolean(_settings.scnl))
        basic.append(rs)

        options = ["off", "30m", "1h", "3h", "5h", "8h"]
        rs = RadioSetting(
                "apo", "APO time (hrs)",
                RadioSettingValueList(options, options[_settings.apo]))
        basic.append(rs)

        options = ["off", "1m", "2.5m", "5m", "10m"]
        rs = RadioSetting(
                "timeout", "Time Out Timer",
                RadioSettingValueList(options, options[_settings.timeout]))
        basic.append(rs)

        options = ["key", "dial", "key+dial", "ptt",
                   "key+ptt", "dial+ptt", "all"]
        rs = RadioSetting(
                "lock", "Lock mode",
                RadioSettingValueList(options, options[_settings.lock]))
        basic.append(rs)

        options = ["off", "0.2", "0.3", "0.5", "1.0", "2.0"]
        rs = RadioSetting(
                "rxsave", "RX Save (sec)",
                RadioSettingValueList(options, options[_settings.rxsave]))
        basic.append(rs)

        options = ["5sec", "key", "tgl"]
        rs = RadioSetting(
                "lamp", "Lamp mode",
                RadioSettingValueList(options, options[_settings.lamp]))
        basic.append(rs)

        options = ["off", "1", "3", "5", "8", "rpt"]
        rs = RadioSetting(
                "bell", "Bell Repetitions",
                RadioSettingValueList(options, options[_settings.bell]))
        basic.append(rs)

        rs = RadioSetting(
                "cwid_en", "CWID Enable",
                RadioSettingValueBoolean(_settings.cwid_en))
        arts.append(rs)

        cwid = RadioSettingValueString(
                0, 16, self._decode_cwid(_settings.cwid.get_value()))
        cwid.set_charset(CHARSET)
        rs = RadioSetting("cwid", "CWID", cwid)
        arts.append(rs)

        options = ["off", "rx", "tx", "trx"]
        rs = RadioSetting(
                "artsmode", "ARTS Mode",
                RadioSettingValueList(
                    options, options[_settings.artsmode]))
        arts.append(rs)

        options = ["off", "in range", "always"]
        rs = RadioSetting(
                "artsbeep", "ARTS Beep",
                RadioSettingValueList(options, options[_settings.artsbeep]))
        arts.append(rs)

        for i in range(0, 8):
            dialsettings = _settings.autodial[i]
            dialstr = ""
            for c in dialsettings.digits:
                if c < len(DTMFCHARSET):
                    dialstr += DTMFCHARSET[c]
            dialentry = RadioSettingValueString(0, 16, dialstr)
            dialentry.set_charset(DTMFCHARSET + list(" "))
            rs = RadioSetting("autodial" + str(i+1),
                              "AutoDial " + str(i+1), dialentry)
            autodial.append(rs)

        dialstr = ""
        for c in _settings.autodial9_ro.digits:
            if c < len(DTMFCHARSET):
                dialstr += DTMFCHARSET[c]
        dialentry = RadioSettingValueString(0, 32, dialstr)
        dialentry.set_mutable(False)
        rs = RadioSetting("autodial9_ro", "AutoDial 9 (read only)", dialentry)
        autodial.append(rs)

        options = ["50ms", "100ms"]
        rs = RadioSetting(
                "pagingspeed", "Paging Speed",
                RadioSettingValueList(options, options[_settings.pagingspeed]))
        dtmf.append(rs)

        options = ["250ms", "450ms", "750ms", "1000ms"]
        rs = RadioSetting(
                "pagingdelay", "Paging Delay",
                RadioSettingValueList(options, options[_settings.pagingdelay]))
        dtmf.append(rs)

        options = ["off", "1", "3", "5", "8", "rpt"]
        rs = RadioSetting(
                "pagingbell", "Paging Bell Repetitions",
                RadioSettingValueList(options, options[_settings.pagingbell]))
        dtmf.append(rs)

        options = ["off", "ans", "for"]
        rs = RadioSetting(
                "paginganswer", "Paging Answerback",
                RadioSettingValueList(options,
                                      options[_settings.paginganswer]))
        dtmf.append(rs)

        rs = RadioSetting(
                "code_dec_c_en", "Paging Code C Decode Enable",
                RadioSettingValueBoolean(_settings.code_dec_c_en))
        dtmf.append(rs)

        _str = str(bitwise.bcd_to_int(_settings.pagingcodec_ro))
        code = RadioSettingValueString(0, 3, _str)
        code.set_charset(NUMERIC_CHARSET + list(" "))
        code.set_mutable(False)
        rs = RadioSetting("pagingcodec_ro", "Paging Code C (read only)", code)
        dtmf.append(rs)

        rs = RadioSetting(
                "code_dec_p_en", "Paging Code P Decode Enable",
                RadioSettingValueBoolean(_settings.code_dec_p_en))
        dtmf.append(rs)

        _str = str(bitwise.bcd_to_int(_settings.pagingcodep))
        code = RadioSettingValueString(0, 3, _str)
        code.set_charset(NUMERIC_CHARSET + list(" "))
        rs = RadioSetting("pagingcodep", "Paging Code P", code)
        dtmf.append(rs)

        for i in range(0, 6):
            num = str(i+1)
            name = "code_dec_" + num + "_en"
            rs = RadioSetting(
                    name, "Paging Code " + num + " Decode Enable",
                    RadioSettingValueBoolean(getattr(_settings, name)))
            dtmf.append(rs)

            _str = str(bitwise.bcd_to_int(_settings.pagingcode[i].digits))
            code = RadioSettingValueString(0, 3, _str)
            code.set_charset(NUMERIC_CHARSET + list(" "))
            rs = RadioSetting("pagingcode" + num, "Paging Code " + num, code)
            dtmf.append(rs)

        return top

    def set_settings(self, uisettings):
        for element in uisettings:
            if not isinstance(element, RadioSetting):
                self.set_settings(element)
                continue
            if not element.changed():
                continue
            try:
                setting = element.get_name()
                _settings = self._memobj.settings
                if re.match(r'autodial\d', setting):
                    # set autodial fields
                    dtmfstr = str(element.value).strip()
                    newval = []
                    for i in range(0, 16):
                        if i < len(dtmfstr):
                            newval.append(DTMFCHARSET.index(dtmfstr[i]))
                        else:
                            newval.append(0xFF)
                    LOG.debug(newval)
                    idx = int(setting[-1:]) - 1
                    _settings = self._memobj.settings.autodial[idx]
                    _settings.digits = newval
                    continue
                if (setting == "pagingcodep"):
                    bitwise.int_to_bcd(_settings.pagingcodep,
                                       int(element.value))
                    continue
                if re.match(r'pagingcode\d', setting):
                    idx = int(setting[-1:]) - 1
                    bitwise.int_to_bcd(_settings.pagingcode[idx].digits,
                                       int(element.value))
                    continue
                newval = element.value
                oldval = getattr(_settings, setting)
                if setting == "cwid":
                    newval = self._encode_cwid(newval)
                LOG.debug("Setting %s(%s) <= %s" % (setting, oldval, newval))
                setattr(_settings, setting, newval)
            except Exception:
                LOG.debug(element.get_name())
                raise

    @classmethod
    def match_model(cls, filedata, filename):
        return len(filedata) == cls._memsize

    def sync_out(self):
        self.update_checksums()
        return _clone_out(self)


def _clone_out(radio):
    try:
        return __clone_out(radio)
    except Exception as e:
        raise errors.RadioError("Failed to communicate with the radio: %s" % e)


def __clone_out(radio):
    pipe = radio.pipe
    pipe.timeout = 1
    block_lengths = radio._block_lengths
    total_written = 0

    def _status():
        status = chirp_common.Status()
        status.msg = "Cloning to radio"
        status.max = sum(block_lengths)
        status.cur = total_written
        radio.status_fn(status)

    start = time.time()

    blocks = 0
    pos = 0
    for block in radio._block_lengths:
        blocks += 1
        data = radio.get_mmap()[pos:pos + block]
        # LOG.debug("Sending block: %s" % util.hexprint(data))

        recvd = b""
        # Radio echos every block received
        for byte in data:
            time.sleep(0.01)
            pipe.write(bytes([byte]))
            # flush & sleep so don't loose ack
            pipe.flush()
            time.sleep(0.015)
            recvd += pipe.read(1)  # chew the echo
        # LOG.debug("Echo was %s" % util.hexprint(recvd))
        LOG.debug("Bytes sent: %i" % len(data))

        # Radio does not ack last block
        if (blocks < 8):
            buf = pipe.read(block)
            LOG.debug("ACK attempt: " + util.hexprint(buf))
            if buf and buf[0] != yaesu_clone.CMD_ACK:
                buf = pipe.read(block)
            if not buf or buf[-1] != yaesu_clone.CMD_ACK:
                raise errors.RadioError("Radio did not ack block %i" % blocks)

        total_written += len(data)
        _status()
        pos += block

    pipe.read(pos)  # Chew the echo if using a 2-pin cable

    LOG.debug("Clone completed in %i seconds" % (time.time() - start))
