#!/usr/bin/python

import re


class CameraFrame(object):
  STX = ''
  ETX = ''

  _PCT_C_RE = re.compile(r'%(\d*)c')
  _PCT_D_RE = re.compile(r'%(\d*)d')
  _PCT_S_RE = re.compile(r'%s')

  def __init__(self, desc, confirm_format, control_format, reply_format,
               checksum=False):
    self.desc = desc
    self.confirm_format = confirm_format
    self.control_format = control_format
    self.reply_format = reply_format
    self.checksum = checksum
    self.confirm_re = self._FormatToRE(confirm_format, checksum)
    self.control_re = self._FormatToRE(control_format, checksum)
    self.reply_re = self._FormatToRE(reply_format, checksum)

  def _Checksum(self, cmd):
    cksum = sum([ord(x) for x in cmd]) % 0x100
    if cksum == 0:
      cksum = 1
    if cksum == 0x0D:
      cksum = 0x0E
    return cksum

  def EncodeConfirmation(self, args):
    cmd = self.confirm_format % args
    if self.checksum:
      cksum = self._Checksum(cmd)
      cksum = ('%02X' % cksum).decode('hex')
      cmd = cmd + cksum
    return self.STX + cmd + self.ETX

  def EncodeControl(self, args):
    cmd = self.control_format % args
    if self.checksum:
      cksum = self._Checksum(cmd)
      cksum = ('%02X' % cksum).decode('hex')
      cmd = cmd + cksum
    return self.STX + cmd + self.ETX

  def DecodeReply(self, cmd):
    """Attempt to decode cmd and return values, failure raises ValueError."""
    if self.STX:
      if cmd[0] != self.STX:
        raise ValueError('Incorrect framing')
      cmd = cmd[1:]
    if self.ETX:
      if cmd[-1] != self.ETX:
        raise ValueError('Incorrect framing')
      cmd = cmd[:-1]

    result = self.reply_re.match(cmd)
    if result is None:
      raise ValueError('No match')

    # Replies don't have checksums.
    #if self.checksum:
    #  cksum = self._Checksum(cmd[:-1])
    #  if cksum != ord(cmd[-1]):
    #    raise ValueError('Checksum mismatch (%02X/%02X)' %
    #                     (cksum, ord(cmd[-1])))

    return result

  def _FormatToRE(self, fmt, checksum):
    """Convert a printf style format string into a regular expression."""
    if not fmt:
      return

    if checksum:
      fmt = fmt + '%c'

    def percent_c_repl(match):
      count = match.group(1)
      if count:
        return r'(.{%s})' % count
      return r'(.)'
    fmt = self._PCT_C_RE.sub(percent_c_repl, fmt)

    def percent_d_repl(match):
      count = match.group(1)
      if count:
        return r'(\d{%s})' % count.lstrip('0')
      return r'(\d)'
    fmt = self._PCT_D_RE.sub(percent_d_repl, fmt)

    def percent_s_repl(match):
      return r'(.*)'
    fmt = self._PCT_S_RE.sub(percent_s_repl, fmt)

    return re.compile(fmt)


class CCP(CameraFrame):
  STX = '\x02'
  ETX = '\x03'


class PT(CameraFrame):
  STX=''
  ETX='\r'


class HE100(object):
  commands = (CCP('model number', 'QID', None, 'OID:%2c'),
              CCP('software version', 'QSV', None, 'OSV:%s'), # Unsure of rlen
              CCP('AWC/AWB', None, 'OWS', 'OWS'),
              CCP('ABC/ABB', None, 'OAS', 'OAS'),
              CCP('AWC mode', 'QAW', 'OAW:%c', 'OAW:%c'),
              CCP('detail', 'QDT', 'ODT:%c', 'ODT:%c'),
              CCP('gain up', 'QGU', 'OGU:%c', 'OGU:%c'),
              CCP('shutter', 'QSH', 'OSH:%c', 'OSH:%c'),
              CCP('synchro scan', 'QMS', 'OMS:%3c', 'OMS:%3c'), # Unsure of fmt
              CCP('field/frame', 'QFF', None, 'OFF:%c'),
              CCP('v.resolution', None, 'OFR:%c', 'OFR:%c'),
              CCP('iris auto/manual', 'QRS', 'ORS:%c', 'ORS:%c'),
              CCP('manual iris volume', 'QRV', 'ORV:%3c', 'ORV:%3c'), # fmt?
              CCP('picture level', 'QSD:48', 'OSD:48:%2c', 'OSD:48:%2c'), # fmt?
              CCP('light peak/avg', 'QPA', None, 'OPA:%2c'), # fmt?
              CCP('light peak/avg', None, 'OPV:%2c', 'OPV:%2c'), # fmt?
              CCP('light area', 'QAR', None, 'OAR:%c'),
              CCP('light area', None, 'ORA:%c', 'ORA:%c'),
              CCP('nega/posi', 'QNP', 'ONP:%c', 'ONP:%c'),
              CCP('r pedestal', 'QRD', 'ORD:%2c', 'ORD:%2c'), # fmt?
              CCP('b pedestal', 'QBD', 'OBD:%2c', 'OBD:%2c'), # fmt?
              CCP('r gain', 'QGR', 'OGR:%2c', 'OGR:%2c'), # typo? fmt?
              CCP('b gain', 'QGB', 'OGB:%2c', 'OGB:%2c'), # typo? fmt?
              CCP('t pedestal', 'QTD', 'OTD:%2c', 'OTD:%2c'), # fmt?
              CCP('h phase', 'QHP', 'OHP:%3c', 'OHP:%3c'), # fmt?
              CCP('sc coarse', 'QSC', 'OSC:%c', 'OSC:%c'),
              CCP('sc fine', 'QSN', 'OSN:%3c', 'OSN:%3c'), # fmt?
              CCP('chroma level', 'QCG', 'OCG:%2c', 'OCG:%2c'),
              CCP('scene file', 'QSF', 'OSF:%c', 'OSF:%c'),
              CCP('scene file', None, 'XSF:%c', 'XSF:%c'),
              CCP('gamma', 'QSD:00', 'OSD:00:%2c', 'OSD:00:%2c'), # fmt?...
              CCP('knee point', 'QSD:08', 'OSD:08:%2c', 'OSD:08:%2c'),
              CCP('white clip', 'QSD:09', 'OSD:09:%2c', 'OSD:09:%2c'),
              CCP('h.dtl level h', 'QSD:0A', 'OSD:0A:%2c', 'OSD:0A:%2c'),