#copyright ReportLab Inc. 2000-2016
#see license.txt for license details
from __future__ import print_function
__version__='3.3.0'
__all__ = ('USPS_4State',)

from reportlab.lib.colors import black
from reportlab.graphics.barcode.common import Barcode
from reportlab.lib.utils import asNative

def nhex(i):
    'normalized hex'
    r = hex(i)
    r = r[:2]+r[2:].lower()
    if r.endswith('l'): r = r[:-1]
    return r

class USPS_4State(Barcode):
    ''' USPS 4-State OneView (TM) barcode. All info from USPS-B-3200A
    '''
    _widthSize = 1
    _heightSize = 1
    _fontSize = 11
    _humanReadable = 0
    if True:
        tops = dict(
            F = (0.0625,0.0825),
            T = (0.0195,0.0285),
            A = (0.0625,0.0825),
            D = (0.0195,0.0285),
            )
        bottoms = dict(
            F = (-0.0625,-0.0825),
            T = (-0.0195,-0.0285),
            D = (-0.0625,-0.0825),
            A = (-0.0195,-0.0285),
            )
        dimensions = dict(
            width = (0.015, 0.025),
            pitch = (0.0416, 0.050),
            hcz = (0.125,0.125),
            vcz = (0.028,0.028),
            )
    else:
        tops = dict(
            F = (0.067,0.115),
            T = (0.021,0.040),
            A = (0.067,0.115),
            D = (0.021,0.040),
            )
        bottoms = dict(
            F = (-0.067,-0.115),
            D = (-0.067,-0.115), 
            T = (-0.021,-0.040),
            A = (-0.021,-0.040),
            )
        dimensions = dict(
            width = (0.015, 0.025),
            pitch = (0.0416,0.050),
            hcz = (0.125,0.125),
            vcz = (0.040,0.040),
            )

    def __init__(self,value='01234567094987654321',routing='',**kwd):
        self._init()
        value = str(value) if isinstance(value,int) else asNative(value)
        if not routing:
            #legal values for combined tracking + routing
            if len(value) in (20,25,29,31):
                value, routing = value[:20], value[20:]
            else:
                raise ValueError('value+routing length must be 20, 25, 29 or 31 digits not %d' % len(value))
        elif len(routing) not in (5,9,11):
            raise ValueError('routing length must be 5, 9 or 11 digits not %d' % len(routing))
        self._tracking = value
        self._routing = routing
        self._setKeywords(**kwd)

    def _init(self):
        self._bvalue = None
        self._codewords = None
        self._characters = None
        self._barcodes = None

    def scale(kind,D,s):
        V = D[kind]
        return 72*(V[0]*(1-s)+s*V[1])
    scale = staticmethod(scale)

    def tracking(self,tracking):
        self._init()
        self._tracking = tracking
    tracking = property(lambda self: self._tracking,tracking)

    def routing(self,routing):
        self._init()
        self._routing = routing
    routing = property(lambda self: self._routing,routing)

    def widthSize(self,value):
        self._sized = None
        self._widthSize = min(max(0,value),1)
    widthSize = property(lambda self: self._widthSize,widthSize)

    def heightSize(self,value):
        self._sized = None
        self._heightSize = value
    heightSize = property(lambda self: self._heightSize,heightSize)

    def fontSize(self,value):
        self._sized = None
        self._fontSize = value
    fontSize = property(lambda self: self._fontSize,fontSize)

    def humanReadable(self,value):
        self._sized = None
        self._humanReadable = value
    humanReadable = property(lambda self: self._humanReadable,humanReadable)

    def binary(self):
        '''convert the 4 state string values to binary
        >>> print(nhex(USPS_4State('01234567094987654321','').binary))
        0x1122103b5c2004b1
        >>> print(nhex(USPS_4State('01234567094987654321','01234').binary))
        0xd138a87bab5cf3804b1
        >>> print(nhex(USPS_4State('01234567094987654321','012345678').binary))
        0x202bdc097711204d21804b1
        >>> print(nhex(USPS_4State('01234567094987654321','01234567891').binary))
        0x16907b2a24abc16a2e5c004b1
        '''
        value = self._bvalue
        if not value:
            routing = self.routing
            n = len(routing)
            try:
                if n==0:
                    value = 0
                elif n==5:
                    value = int(routing)+1
                elif n==9:
                    value = int(routing)+100001
                elif n==11:
                    value = int(routing)+1000100001
                else:
                    raise ValueError
            except:
                raise ValueError('Problem converting %s, routing code must be 0, 5, 9 or 11 digits' % routing)

            tracking = self.tracking
            svalue = tracking[0:2]
            try:
                value *= 10
                value += int(svalue[0])
                value *= 5
                value += int(svalue[1])
            except:
                raise ValueError('Problem converting %s, barcode identifier must be 2 digits' % svalue)

            i = 2
            for name,nd in (('special services',3), ('customer identifier',6), ('sequence number',9)):
                j = i
                i += nd
                svalue = tracking[j:i]
                try:
                    if len(svalue)!=nd: raise ValueError
                    for j in range(nd):
                        value *= 10
                        value += int(svalue[j])
                except:
                    raise ValueError('Problem converting %s, %s must be %d digits' % (svalue,name,nd))
            self._bvalue = value
        return value
    binary = property(binary)

    def codewords(self):
        '''convert binary value into codewords
        >>> print(USPS_4State('01234567094987654321','01234567891').codewords)
        (673, 787, 607, 1022, 861, 19, 816, 1294, 35, 602)
        '''
        if not self._codewords:
            value = self.binary
            A, J = divmod(value,636)
            A, I = divmod(A,1365)
            A, H = divmod(A,1365)
            A, G = divmod(A,1365)
            A, F = divmod(A,1365)
            A, E = divmod(A,1365)
            A, D = divmod(A,1365)
            A, C = divmod(A,1365)
            A, B = divmod(A,1365)
            assert 0<=A<=658, 'improper value %s passed to _2codewords A-->%s' % (hex(int(value)),A)
            self._fcs = _crc11(value)
            if self._fcs&1024: A += 659
            J *= 2
            self._codewords = tuple(map(int,(A,B,C,D,E,F,G,H,I,J)))
        return self._codewords
    codewords = property(codewords)


    def table1(self):
        self.__class__.table1 = _initNof13Table(5,1287)
        return self.__class__.table1
    table1 = property(table1)

    def table2(self):
        self.__class__.table2 = _initNof13Table(2,78)
        return self.__class__.table2
    table2 = property(table2)

    def characters(self):
        ''' convert own codewords to characters
        >>> print(' '.join(hex(c)[2:] for c in USPS_4State('01234567094987654321','01234567891').characters))
        dcb 85c 8e4 b06 6dd 1740 17c6 1200 123f 1b2b
        '''
        if not self._characters:
            codewords = self.codewords
            fcs = self._fcs
            C = []
            aC = C.append
            table1 = self.table1
            table2 = self.table2
            for i in range(10):
                cw = codewords[i]
                if cw<=1286:
                    c = table1[cw]
                else:
                    c = table2[cw-1287]
                if (fcs>>i)&1:
                    c = ~c & 0x1fff
                aC(c)
            self._characters = tuple(C)
        return self._characters
    characters = property(characters)

    def barcodes(self):
        '''Get 4 state bar codes for current routing and tracking
        >>> print(USPS_4State('01234567094987654321','01234567891').barcodes)
        AADTFFDFTDADTAADAATFDTDDAAADDTDTTDAFADADDDTFFFDDTTTADFAAADFTDAADA
        '''
        if not self._barcodes:
            C = self.characters
            B = []
            aB = B.append
            bits2bars = self._bits2bars
            for dc,db,ac,ab in self.table4:
                aB(bits2bars[((C[dc]>>db)&1)+2*((C[ac]>>ab)&1)])
            self._barcodes = ''.join(B)
        return self._barcodes
    barcodes = property(barcodes)

    table4 = ((7, 2, 4, 3), (1, 10, 0, 0), (9, 12, 2, 8), (5, 5, 6, 11),
                (8, 9, 3, 1), (0, 1, 5, 12), (2, 5, 1, 8), (4, 4, 9, 11),
                (6, 3, 8, 10), (3, 9, 7, 6), (5, 11, 1, 4), (8, 5, 2, 12),
                (9, 10, 0, 2), (7, 1, 6, 7), (3, 6, 4, 9), (0, 3, 8, 6),
                (6, 4, 2, 7), (1, 1, 9, 9), (7, 10, 5, 2), (4, 0, 3, 8),
                (6, 2, 0, 4), (8, 11, 1, 0), (9, 8, 3, 12), (2, 6, 7, 7),
                (5, 1, 4, 10), (1, 12, 6, 9), (7, 3, 8, 0), (5, 8, 9, 7),
                (4, 6, 2, 10), (3, 4, 0, 5), (8, 4, 5, 7), (7, 11, 1, 9),
                (6, 0, 9, 6), (0, 6, 4, 8), (2, 1, 3, 2), (5, 9, 8, 12),
                (4, 11, 6, 1), (9, 5, 7, 4), (3, 3, 1, 2), (0, 7, 2, 0),
                (1, 3, 4, 1), (6, 10, 3, 5), (8, 7, 9, 4), (2, 11, 5, 6),
                (0, 8, 7, 12), (4, 2, 8, 1), (5, 10, 3, 0), (9, 3, 0, 9),
                (6, 5, 2, 4), (7, 8, 1, 7), (5, 0, 4, 5), (2, 3, 0, 10),
                (6, 12, 9, 2), (3, 11, 1, 6), (8, 8, 7, 9), (5, 4, 0, 11),
                (1, 5, 2, 2), (9, 1, 4, 12), (8, 3, 6, 6), (7, 0, 3, 7),
                (4, 7, 7, 5), (0, 12, 1, 11), (2, 9, 9, 0), (6, 8, 5, 3),
                (3, 10, 8, 2))

    _bits2bars = 'T','D','A','F'
    horizontalClearZone = property(lambda self: self.scale('hcz',self.dimensions,self.widthScale))
    verticalClearZone = property(lambda self: self.scale('vcz',self.dimensions,self.heightScale))

    @property
    def barWidth(self):
        if '_barWidth' in self.__dict__:
            return self.__dict__['_barWidth']
        return self.scale('width',self.dimensions,self.widthScale)

    @barWidth.setter
    def barWidth(self,value):
        n, x = self.dimensions['width']
        self.__dict__['_barWidth'] = 72*min(max(value/72.0,n),x)

    @property
    def pitch(self):
        if '_pitch' in self.__dict__:
            return self.__dict__['_pitch']
        return self.scale('pitch',self.dimensions,self.widthScale)

    @pitch.setter
    def pitch(self,value):
        n, x = self.dimensions['pitch']
        self.__dict__['_pitch'] = 72*min(max(value/72.0,n),x)

    @property
    def barHeight(self):
        if '_barHeight' in self.__dict__:
            return self.__dict__['_barHeight']
        return self.scale('F',self.tops,self.heightScale) - self.scale('F',self.bottoms,self.heightScale)

    @barHeight.setter
    def barHeight(self,value):
        n = self.tops['F'][0] - self.bottoms['F'][0]
        x = self.tops['F'][1] - self.bottoms['F'][1]
        value = self.__dict__['_barHeight'] = 72*min(max(value/72.0,n),x)
        self.heightSize = (value - n)/(x-n)

    widthScale = property(lambda self: min(1,max(0,self.widthSize)))
    heightScale = property(lambda self: min(1,max(0,self.heightSize)))

    def width(self):
        self.computeSize()
        return self._width
    width = property(width)

    def height(self):
        self.computeSize()
        return self._height
    height = property(height)

    def computeSize(self):
        if not getattr(self,'_sized',None):
            ws = self.widthScale
            hs = self.heightScale
            barHeight = self.barHeight
            barWidth = self.barWidth
            pitch = self.pitch
            hcz = self.horizontalClearZone
            vcz = self.verticalClearZone
            self._width = 2*hcz + barWidth + 64*pitch
            self._height = 2*vcz+barHeight
            if self.humanReadable:
                self._height += self.fontSize*1.2+vcz
            self._sized = True

    def wrap(self,aW,aH):
        self.computeSize()
        return self.width, self.height

    def _getBarVInfo(self,y0=0):
        vInfo = {}
        hs = self.heightScale
        for b in ('T','D','A','F'):
            y = self.scale(b,self.bottoms,hs)+y0
            vInfo[b] = y,self.scale(b,self.tops,hs)+y0 - y
        return vInfo

    def draw(self):
        self.computeSize()
        hcz = self.horizontalClearZone
        vcz = self.verticalClearZone
        bw = self.barWidth
        x = hcz
        y0 = vcz+self.barHeight*0.5
        dw = self.pitch
        vInfo = self._getBarVInfo(y0)
        for b in self.barcodes:
            yb, hb = vInfo[b]
            self.rect(x,yb,bw,hb)
            x += dw
        self.drawHumanReadable()

    def value(self):
        tracking = self.tracking
        routing = self.routing
        routing = routing and (routing,) or ()
        return ' '.join((tracking[0:2],tracking[2:5],tracking[5:11],tracking[11:])+routing)
    value = property(value,lambda self,value: self.__dict__.__setitem__('tracking',value))

    def drawHumanReadable(self):
        if self.humanReadable:
            hcz = self.horizontalClearZone
            vcz = self.verticalClearZone
            fontName = self.fontName
            fontSize = self.fontSize
            y = self.barHeight+2*vcz+0.2*fontSize
            self.annotate(hcz,y,self.value,fontName,fontSize)

    def annotate(self,x,y,text,fontName,fontSize,anchor='middle'):
        Barcode.annotate(self,x,y,text,fontName,fontSize,anchor='start')

def _crc11(value):
    '''
    >>> usps = [USPS_4State('01234567094987654321',x).binary for x in ('','01234','012345678','01234567891')]
    >>> print(' '.join(nhex(x) for x in usps))
    0x1122103b5c2004b1 0xd138a87bab5cf3804b1 0x202bdc097711204d21804b1 0x16907b2a24abc16a2e5c004b1
    >>> print(' '.join(nhex(_crc11(x)) for x in usps))
    0x51 0x65 0x606 0x751
    '''
    hexbytes = nhex(int(value))[2:]
    hexbytes = '0'*(26-len(hexbytes))+hexbytes
    gp = 0x0F35
    fcs = 0x07FF
    data = int(hexbytes[:2],16)<<5
    for b in range(2,8):
        if (fcs ^ data)&0x400:
            fcs = (fcs<<1)^gp
        else:
            fcs = fcs<<1
        fcs &= 0x7ff
        data <<= 1

    for x in range(2,2*13,2):
        data = int(hexbytes[x:x+2],16)<<3
        for b in range(8):
            if (fcs ^ data)&0x400:
                fcs = (fcs<<1)^gp
            else:
                fcs = fcs<<1
            fcs &= 0x7ff
            data <<= 1
    return fcs

def _ru13(i):
    '''reverse unsigned 13 bit number
    >>> print(_ru13(7936), _ru13(31), _ru13(47), _ru13(7808))
    31 7936 7808 47
    '''
    r = 0
    for x in range(13):
        r <<= 1
        r |= i & 1
        i >>= 1
    return r

def _initNof13Table(N,lenT):
    '''create and return table of 13 bit values with N bits on
    >>> T = _initNof13Table(5,1287)
    >>> print(' '.join('T[%d]=%d' % (i, T[i]) for i in (0,1,2,3,4,1271,1272,1284,1285,1286)))
    T[0]=31 T[1]=7936 T[2]=47 T[3]=7808 T[4]=55 T[1271]=6275 T[1272]=6211 T[1284]=856 T[1285]=744 T[1286]=496
    '''
    T = lenT*[None]
    l = 0
    u = lenT-1
    for c in range(8192):
        bc = 0
        for b in range(13):
            bc += (c&(1<<b))!=0
        if bc!=N: continue
        r = _ru13(c)
        if r<c: continue    #we already looked at this pair
        if r==c:
            T[u] = c
            u -= 1
        else:
            T[l] = c
            l += 1
            T[l] = r
            l += 1
    assert l==(u+1), 'u+1(%d)!=l(%d) for %d of 13 table' % (u+1,l,N) 
    return T

def _test():
    import doctest
    return doctest.testmod()

if __name__ == "__main__":
    _test()
