#!/usr/bin/python -tt
import warnings
import traceback
import sys
import threading
import time
import iguanaIR
import struct

#output "constants"
FATAL  = 0
ERROR  = 1
WARN   = 2
ALWAYS = 2.5
NORMAL = 3
INFO   = 4
DEBUG  = 5

msgPrefixes = [
    "FATAL: ",
    "ERROR: ",
    "WARNING: ",
    "",
    "INFO: ",
    "DEBUG: "
]

# constants
SPACE = 0
PULSE = 0

#local variables
currentLevel = NORMAL
logFile = None

def dieCleanly(level = None):
    """Exit the application with proper cleanup."""

    #TODO: perform application cleanup

    if level == None:
        level = ERROR

    #exit with appropriate value
    if level == FATAL:
        sys.exit(1)
    sys.exit(0)


def message(level, msg):
    """Print a message to a certain debug level"""
    retval = None

    if level <= currentLevel or level == ALWAYS:
        out = sys.stdout

        # if logfile is open print to it instead
        if logFile == "-":
            out = sys.log
        elif level <= WARN:
            out = sys.stderr

        retval = msgPrefixes[int(level + 0.5)] + msg
        out.write(retval)
        retval = len(retval)

    if level <= FATAL:
        dieCleanly(level)

    return retval


def printUsage(msg = None):
    usage = "Usage: " + sys.argv[0] + " [OPTION]..." + """

-h
--help : Print this usage message.

-l
--log-file : Specify a log to receive all messages.

-q
--quiet : Decrease verbosity.

-v
--verbose : Increase verbosity.
"""

    if msg != None:
        message(FATAL, msg + usage)
    message(ALWAYS, usage)
    dieCleanly(ALWAYS)


index = 1
while index < len(sys.argv):
    arg = sys.argv[index]
    if arg == "-h" or arg == "--help":
        printUsage()
    elif arg == "-l" or arg == "--log-file":
        index += 1
        logFile = sys.argv[index]
        if logFile == "-":
            logFile = None
    elif arg == "-q" or arg == "--quiet":
        if currentLevel > FATAL:
            currentLevel -= 1
    elif arg == "-v" or arg == "--verbose":
        currentLevel += 1
    else:
        printUsage("Unknown argument: " + arg + "\n")
    index += 1

# open the log file if specified
if logFile != None:
    sys.log = open(logFile, "a", 1)
    logFile = "-"

def deviceTransaction(type, data = ''):
    retval = False
    req = iguanaIR.createRequest(type, data)
    if not iguanaIR.writeRequest(req, conn):
        print 'Failed to write packet.'
    else:
        resp = iguanaIR.readResponse(conn, 10000)
        if iguanaIR.responseIsError(resp):
            print 'Error response.'
        else:
            retval = True
    return retval

def collectSignals(data, minCount = -1, maxCount = -1, state = None):
    done = False

    while (maxCount == -1 or \
           (len(data) < maxCount and not done)) and \
          (state is None or not state['done']):

        resp = iguanaIR.readResponse(conn, 10000)
        if iguanaIR.responseIsError(resp):
            print 'Error response.'
        else:
            more = iguanaIR.removeData(resp)
            more = struct.unpack('I' * (len(more) / 4), more)

            # lock the list while we add things too it
            if state is not None:
                state['lock'].acquire()

            # populate next from the existing list
            try:
                next = int(data.pop(-1))
            except IndexError:
                if state is not None:
                    next = state['gap']
                else:
                    next = 0

            for pulse in more:
                # if we changed states output the old value
                if pulse & ~iguanaIR.IG_PULSE_BIT < 200:
                    message(WARN, "Transitions appear to be too short....\n")
                elif pulse & iguanaIR.IG_PULSE_BIT != next & iguanaIR.IG_PULSE_BIT:
                    data.append(next)
                    next = 0

                if next == 0:
                    next = pulse
                # cut off when a space exceeds the maxPause
                elif next & ~iguanaIR.IG_PULSE_BIT <= 1000000:
                    next += pulse & ~iguanaIR.IG_PULSE_BIT
                elif minCount != -1 and len(data) > minCount:
                    done = True

            # put next back onto the end of the list
            data.append(next)

            # allow the consumer back in
            if state is not None:
                state['lock'].release()

def rawCodesToStrings(codes, spaceFirst = True,
                      useHeader = True, padding = 0):
    strings = []

    next = 0
    for pulse in codes:
        header = ''
        if useHeader:
            header += '%s ' % ('space', 'pulse')[next]
            next ^= 1

        format = '%d'
        if padding:
            format = '%%%dd' % padding
        strings.append(header + format % pulse)

    return strings

def countLengths(codes):
    # create a raw count of different lengths
    counts = {}
    for pulse in codes:
        length = pulse & ~iguanaIR.IG_PULSE_BIT
        if length in counts:
            counts[length] += 1
        else:
            counts[length] = 1
    return counts

def binCounts(counts, fudge = 0.20):
    newCounts = {}
    for count in counts:
        for newCount in newCounts:
            if newCount * (1 - fudge) <= count and \
               newCount * (1 + fudge) >= count:
                total = newCounts[newCount] + counts[count]
                avg = (newCounts[newCount] * newCount + \
                       counts[count] * count) / float(total)
                # remove the old one then add the new one
                del newCounts[newCount]
                newCounts[avg] = total
                message(DEBUG, '%d is in %d\n' % (count, avg))
                break
        else:
            newCounts[count] = counts[count]
    return newCounts

def printCodes(codes):
    text = [ '' ]
    prev = None
    for code in codes:
        # trim out repeats
        if prev is not None:
            if len(prev) == len(code):
                for x in range(len(prev)):
                    if prev[x] != code[x]:
                        break
                else:
                    text[0] += '.'
                    continue
        prev = code
        
        length = 0
        for pulse in code:
            if pulse > length:
                length = pulse

        width = len(text[0])
        x = 0
        for more in rawCodesToStrings(code, False, code == codes[0],
                                      len('%s' % length) + 1):
            if x == len(text):
                if code == codes[0]:
                    text.append('')
                else:
                    text.append(('space', 'pulse')[x % 2])

            if len(text[x]) < width:
                text[x] += ' ' * (width - len(text[x]))

            text[x] += more
            x += 1

    print "\n".join(text)

# divide a stream of signals into codes, discarding little blips
def divideCodes(data, gap, gapFudge = 0.20):
    codes = []
    code = []
    first = True
    for pulse in data:
        # determine type and length
        isSpace = (pulse & iguanaIR.IG_PULSE_BIT) == 0
        pulse &= ~iguanaIR.IG_PULSE_BIT

        # divide on gaps
        if isSpace and pulse >= gap * (1 - gapFudge):
            if code and len(code) > 2:
                codes.append(code)
            # add the gap as the first part of the next code
            code = [pulse]
        else:
            code.append(pulse)
            appended = True

    codes.append(code)
    return codes

def sanitizeCodes(codes, counts, smallest, fudge = 0.20):
    for code in codes:
        for x in range(len(code)):
            for count in counts:
                if count * (1 - fudge) <= code[x] and \
                   count * (1 + fudge) >= code[x]:
                    code[x] = count
                    break
            #print '%s %s' % (code[x], smallest)
            code[x] = round(code[x] / float(smallest))

# decode signals that start with the gap
class decoder:
    name = 'DecoderBaseClass'
    header = []

    def decodeReset(self):
        pass

    def decodeStep(self, pos):
        return 'decodeStep no implemented.'

    def decodeDone(self):
        return None

    def decode(self, code):
        msg = None

        # prepare to decode a signal
        self.retval = 0
        self.code = code
        self.decodeReset()

        # check the space, then header, then body
        for pos in range(1, len(self.code)):
            if pos == 0:
                continue
            elif pos < len(self.header) + 1:
                if self.header[pos - 1] != self.code[pos]:
                    msg = 'Bad header.'
                    break
            else:
                msg = self.decodeStep(pos)
                if msg is not None:
                    break

        # if no errors so far call done
        if msg is None:
            msg = self.decodeDone()

        if msg is not None:
            message(INFO,
                    'decode(%s): %s (%d at %d)\n' % (self.name, msg,
                                                     self.code[pos], pos))
            return None

        return self.retval

class RC5(decoder):
    """
Decodes signals such as the following:

RC5:
1  1 1  1 1  1 1  1 1  1 1  1 1  1 1  1 1  1 1  1 1  1 1  1 1  1 1
p  s p  p s  p s  p s  s p  p s  s p  p s  p s  s p  s p  p s  p s

   1000 1010 0 1100
   8    A    0 C

   1 1001 0100 1100
   194c
"""
    name = 'RC5'
    header = [1]

    def decodeReset(self):
        self.prevType = None

    def decodeStep(self, pos):
        if self.code[pos] != 1 and self.code[pos] != 2:
            return 'Not in range !(1 <= %d <= 2).' % self.code[pos]

        if self.prevType is None:
            self.prevType = pos % 2
            if self.code[pos] != 1:
                return 'Bad first half.'
        else:
            type = pos % 2
            if self.prevType == type:
                return 'Back-to-back %d.' % type
            else:
                if self.prevType == 0:
                    self.retval = (self.retval << 1) | 0x1
                else:
                    self.retval = (self.retval << 1) | 0x0

                if self.code[pos] == 2:
                    self.prevType = type
                else:
                    self.prevType = None
        return None

    def decodeDone(self):
        if self.prevType is not None:
            if self.prevType == 0:
                return 'Back-to-back final space.'
            else:
                self.retval = (self.retval << 1) | 0x0

        return None

class SpaceEncoded(decoder):
    name = 'SpaceEncoder'
    #header = [8,4,1]
    header = [17,8,1]
    #header = [17,4,1]

    def decodeStep(self, pos):
        if pos % 2 == 0:
            if self.code[pos] == 1:
                self.retval = (self.retval << 1) | 0x0
            elif self.code[pos] == 3:
                self.retval = (self.retval << 1) | 0x1
            else:
                return 'Bad space length.'
        else:
            if self.code[pos] != 1:
                return 'Bad pulse length.'
        return None

def handleTransmissions(binned):
    # compute the smallest to divide the codes down
    smallest = None
    for signal in binned:
        if smallest is None or signal < smallest:
            smallest = signal

    while True:
        lock.acquire()

        codes = divideCodes(data, gap, 0.60)
        lastCode = codes[-1][:]
        sanitizeCodes(codes, binned, smallest)
        for x in range(len(codes)):
            value = None
            for encoding in ('RC5', 'SpaceEncoded'):
                value = eval('%s().decode(codes[x])' % encoding)
                if value is not None and value > 0:
                    message(NORMAL, "%s: %x\n" % (encoding, value))
                    break
            else:
                if x < len(codes) - 1:
                    message(WARN, "Unrecognized code: %s\n" % codes[x])
                else:
                    data[:] = lastCode
                    break
        else:
            data[:] = []
        lock.release()

        time.sleep(1)
        if data:
            print data

"""
value = RC5().decode([104, 1.0, 1.0, 1.0, 1.0,
                      2.0, 1.0, 1.0, 2.0, 2.0,
                      2.0, 2.0, 1.0, 1.0, 2.0,
                      1.0, 1.0, 2.0, 1.0, 1.0])
if value is not None:
    print '0x%x' % value
sys.exit(0)
"""

# connect to device 0
conn = iguanaIR.connect('0')
# turn the receiver on
deviceTransaction(iguanaIR.IG_DEV_RECVON)

data = []
collectSignals(data, 50, 1000)
binned = binCounts(countLengths(data), 0.40)
minCount = 2
maxGap = 100000
counts = {}
gap = 0
for size in binned:
    if binned[size] >= minCount:
        counts[size] = binned[size]
        # largest at this point is probably the gap
        if size > gap and size < maxGap:
            gap = size
message(NORMAL, 'Gap determined to be: %d\n' % gap)

# start the collector
lock = threading.Lock()
event = threading.Event()
state = { 'lock'  : lock,
          'done'  : False,
          'gap'   : int(gap),
          'event' : event }
reader = threading.Thread(target = collectSignals,
                          args = (data,),
                          kwargs = { 'state' : state })
reader.start()

try:
    handleTransmissions(binned)
finally:
    state['done'] = True
    reader.join()

"""
List of things supported by WinLirc.

# RC5
The remote uses the RC5 protocol.

# RC6
The remote uses the RC6 protocol.

# RCMM
The remote uses the RC-MM protocol (transmitting not supported).

# SHIFT_ENC
Obsolete flag, now a synonym for RC5. The position of the pulse (before or after the space) determines whether the bit is a one or a zero.

# SPACE_ENC
A one and a zero can be distinguished by the length of the spaces.

# REVERSE
Reverses the bit order of the pre_data, the post_data and the codes (e.g., 0x123 becomes 0xC48). If this flag is present, the least significant bit is sent first.

# NO_HEAD_REP
The header is not sent when a signal (the button is held down) is repeated even though there is no special repeat code.

# NO_FOOT_REP
The foot is not sent when a signal is repeated (the button is held down) even though there is no special repeat code.

# CONST_LENGTH
The total signal length is always constant. The gap length now represents the length of the entire signal, and the actual gap at the end of the signal is adjusted accordingly.

# RAW_CODES
The codes are in raw format.

# REPEAT_HEADER
Send the header when the signal is repeated even though the remote has a special repeat code.

# SPECIAL_TRANSMITTER
Use the transmitter type specified for this remote. (Not supported by LIRC). 

"""
