#!/usr/bin/python

import struct
import sys

# Documentation for Entry fields
ENTRYDOC = {
        "var": "The variable code (BXXYYY)",
        "desc": "The variable description (max 64 chars)",
        "unit": "The measurement unit of the variable (max 64 chars)",
        "scale": "The scale of the variable.  When the variable is represented as an integer, it is multiplied by 10**scale",
        "ref": "The reference value for the variable.  When the variable is represented as an integer, and after scaling, it is added this value",
        "len": "The length in digits of the integer representation of this variable (after scaling and changing reference value)",
        "bit_ref": "The reference value for bit-encoding.  When the variable is encoded in a bit string, it is added this value",
        "bit_len": "The length in bits of the variable when encoded in a bit string (after scaling and changing reference value)",
        "bufr_unit": "The measurement unit of the variable when encoded in BUFR. (max 24 chars)",
        "bufr_scale": "The scale of the variable when encoded in BUFR.",
}

def unit_is_string(unit):
    # if unit.startswith("CODE TABLE"): return True
    return unit in ["CCITTIA5", "CHARACTER"]

class Entry(object):
    "A B table entry"
    FIELDS=["var", "desc", "unit", "scale", "ref", "len", "bit_ref", "bit_len", "bufr_unit", "bufr_scale"]
    NUMFIELDS=["scale", "ref", "len", "bit_ref", "bit_len", "bufr_scale"]

    def __init__(self):
        self.var = "B00000"
        self.desc = ""
        self.unit = ""
        self.scale = 0
        self.ref = -10
        self.len = 0
        self.bit_ref = -10
        self.bit_len = 0
        self.bufr_unit = ""
        self.bufr_scale = 0
        ## Minimum unscaled value the field can have
        #self.imin 
        ## Maximum unscaled value the field can have
        #int imax;
        ## Minimum scaled value the field can have
        #double dmin;
        ## Maximum scaled value the field can have
        #double dmax;

    BUFRFMT='x6sx64sx24sx3sx12sx3s'
    def read_bufr(self, s):
        "Read from the bufr part of the B table"
        self.var, self.desc, self.unit, self.scale, self.bit_ref, self.bit_len = struct.unpack(self.BUFRFMT, s[:118])
        if self.var[0] == '0':
            self.var = 'B' + self.var[1:]
        self.scale = int(self.scale)
        self.bit_ref = int(self.bit_ref)
        self.bit_len = int(self.bit_len)
        self.desc = self.desc.strip()
        self.unit = self.unit.strip()
        self.bufr_unit = self.unit
        self.bufr_scale = self.scale

        # Compute character length from bit lenght
        if unit_is_string(self.unit):
            self.len = self.bit_len / 8
        else:
            tlen = 1 << self.bit_len
            self.len = 0
            while tlen != 0:
                tlen = tlen / 10
                self.len += 1

    CREXFMT='24sx2sx10s'
    def read_crex(self, s):
        self.unit, self.scale, self.len = struct.unpack(self.CREXFMT, s[:38])
        self.unit = self.unit.strip()
        self.scale = int(self.scale)
        self.len = int(self.len)

    FMT=" %-6.6s %-64.64s %-24.24s %3.3s %12.12s %3.3s %-24.24s %2.2s %10.10s"
    def encode(self):
        var = self.var
        if var[0] == 'B':
            var = '0' + var[1:]
        args = map(str, (var, self.desc, self.unit, self.scale, self.bit_ref, self.bit_len, self.unit, self.scale, self.len))
        return self.FMT % tuple(args)

    def dump(self):
        print "Var:", self.var
        print "Desc:", self.desc
        if self.unit == self.bufr_unit:
            print "Unit:", self.unit
        else:
            print "BUFR unit:", self.bufr_unit
            print "CREX unit:", self.unit
        if self.is_string():
            print "Format: %d chars:" % self.len
        elif self.scale == 0:
            print "Format: %d digits" % self.len
        elif self.scale > 0:
            fmtdesc = "#" * min(self.len - self.scale, 99)
            fmtdesc += '.'
            fmtdesc += "#" * min(self.scale, 99)
            print "Format:", fmtdesc
        elif self.scale < 0:
            fmtdesc = "#" * min(self.len, 99)
            fmtdesc += "0" * min(-self.scale, 99)
            print "Format:", fmtdesc

    def is_string(self):
        "True if the variable is a string; false if it is a numeric value"
        return unit_is_string(self.unit)

    def check(self):
        "Consistency checks"
        if unit_is_string(self.bufr_unit) != unit_is_string(self.unit):
            return "CREX is_string (%s - %s) is different than BUFR is_string (%s - %s)" % (
                    self.unit, str(unit_is_string(self.unit)),
                    self.bufr_unit, str(unit_is_string(self.bufr_unit)))
        return None

#    def irange(self):
#        "Range of int-converted values"
#        if self.is_string:
#            return 0, 0
#        if self.len >= 10:
#            return -2**31, 2**31
#
#    def drange(self):
#        "Range of float, unconverted values"
#        if self.is_string:
#            return 0.0, 0.0
#
#		if (i->len >= 10)
#		{
#			i->imin = INT_MIN;
#			i->imax = INT_MAX;
#		} else {
#			/*
#			// We subtract 2 because 2^bit_len-1 is the
#			// BUFR missing value
#			int bufr_min = i->bit_ref;
#			int bufr_max = exp2(i->bit_len) + i->bit_ref - 2;
#			// We subtract 2 because 10^len-1 is the
#			// CREX missing value
#			int crex_min = -(int)(exp10(i->len) - 1.0);
#			int crex_max = (int)(exp10(i->len) - 2.0);
#			// Actually, we cannot subtract 2 because RADAR BUFR
#			// messages have 255 subsets, and the delayed
#			// replication field is 8 bits, so 255 is the missing
#			// value, and if we disallow it here we cannot import
#			// radars anymore.
#			*/
#			/*
#			 * If the unit is the same between BUFR and CREX, take
#			 * the most restrictive extremes.
#			 *
#			 * If the unit is different, take the most permissive
#			 * extremes, to make sure to fit values in both units
#			 */
#			/*
#			if (strcmp(i->unit, i->bufr_unit) == 0)
#			{
#				i->imin = bufr_min > crex_min ? bufr_min : crex_min;
#				i->imax = bufr_max < crex_max ? bufr_max : crex_max;
#			} else {
#				i->imin = bufr_min < crex_min ? bufr_min : crex_min;
#				i->imax = bufr_max > crex_max ? bufr_max : crex_max;
#			}
#			*/
#			/*
#			i->imin = i->bit_ref;
#			i->imax = exp2(i->bit_len) + i->bit_ref - 2;
#			*/
#			i->imin = -(int)(exp10(i->len) - 1.0);
#			i->imax = (int)(exp10(i->len) - 1.0);
#		}
#		i->dmin = dba_var_decode_int(i->imin, i);
#		i->dmax = dba_var_decode_int(i->imax, i);


def read_btable(fd):
    for idx, line in enumerate(fd):
        entry = Entry()
        entry.read_bufr(line)
        if len(line) >= 157:
            entry.read_crex(line[119:])
        msg = entry.check()
        if msg is not None:
            print >>sys.stderr, "Line %d: %s" % (idx+1, msg)
        yield entry

import readline

class Completer(object):
    def __init__(self, func):
        self.func = func
        self.opts = []

    def __call__(self, text, state):
        if state == 0:
            self.opts = list(self.func(text))
        if state >= len(self.opts):
            return None
        return self.opts[state]

class App(object):
    def __init__(self, tablefname):
        self.entries = list(read_btable(open(tablefname)))
        self.entry = None

    def get_entry(self, b):
        for e in self.entries:
            if e.var == b:
                return e
        return None

    def comp_field(self, text):
        for field in self.entry.FIELDS:
            if field.startswith(text):
                yield field

    def comp_val(self, text):
        res = set()
        for e in self.entries:
            val = str(getattr(e, self.field, ""))
            if val.startswith(text):
                res.add(val)
        return sorted(res)

    def main(self, var):
        readline.parse_and_bind("tab: complete")

        self.entry = self.get_entry(var)
        if self.entry is None:
            self.entry = Entry()
            self.entry.var = var

        while True:
            print "Current entry value:"
            self.entry.dump()
            readline.set_completer(Completer(self.comp_field))
            self.field = raw_input("field to set (? for help, q to quit): ")
            if self.field == '?':
                print
                for f in self.entry.FIELDS:
                    print "%s: %s" % (f, ENTRYDOC[f])
                print
                continue
            elif self.field == 'q':
                break
            elif self.field == "":
                print
                continue
            elif self.field not in self.entry.FIELDS:
                print "Invalid field (try ? for help)."
                print
                continue
            else:
                readline.set_completer(Completer(self.comp_val))
                print "%s: %s" % (self.field, ENTRYDOC[self.field])
                print "current value:", getattr(self.entry, self.field)
                val = raw_input("value to set: ").strip()
                try:
                    if self.field in self.entry.NUMFIELDS:
                        val = int(val)
                    setattr(self.entry, self.field, val)
                except Exception, e:
                    print e
                print

        print self.entry.encode()

app = App(sys.argv[1])
app.main(sys.argv[2])
