#!/usr/bin/env python
# coding: utf
'''
Simple input box for pygame

Usage:
    box=Input(…)                                    # declare an input box
    while <main loop>:
        …
        if box.is_active() and <event goes to box>: # currently in input state
            box.edit(event)                         # pass event to the box
        else:
            <parse eventsd normally>
        …
        if box.is_done():                           # input is finished …
            if box.is_cancelled():                  # …with cancel
                <"no input is needed" action>
            elif box.is_failed():                   # …unsuccessfully
                <unsuccessfull input action>
            else:                                   # …succsessfuly
                <succsessful input action>
            box.deactivate()                        # hide box and resore state
        …
        if <input is needed>:                       # we need to input something in program
            box.activate(…)                         # store old state and turn on the box
        …
        if box.is_active(): box.draw(surface,pos)   # show box after all surface changes
        pygame.display.flip()                       # display the picture
        if box.is_active(): box.undraw()            # hide box before further surface changes
'''

import pygame
__VERSION__=0.05

ACTIVE,DONE,FAILED,CANCEL,SHOWN,FIRST=(1<<i for i in xrange(6))

class Input:
    # Default values for some properties
    BorderWidth=3
    Status=0
    CursorWidth=2
    Margin=0
    TextColor=pygame.Color("Black")
    PromptColor=pygame.Color("grey50")
    CursorColor=pygame.Color("grey75")
    OverColor=pygame.Color("tomato")
    PaperColor=pygame.Color("ivory")
    Prompt=""
    DefaultText=""
    CheckType=str
    RetryIncorrect=True
    TextLength=0
    FontID=None
    Font=None
    FontSize=24
    PromptGap=None
    Size=None
    BackingStore=None
    RepeatStore=None
    RepeatDefault=(500,100)

    def __update__(self, *pargs, **nargs):
        '''Update box properties.
        Positional parameters: [Prompt, [DefaultText]].
        Named parameters: all class data fields.
        
        - Size supercedes FontSize, default FontSize is 24
        - setting DefaultText also sets CheckType (so use 0., not "0" for float input)
        '''
        if len(pargs)>0:
            self.Prompt=pargs[0]
        if len(pargs)>1:
            self.DefaultText=unicode(pargs[1])
            self.CheckType=type(pargs[1])
        for prop in nargs:
            if hasattr(self,prop):
                setattr(self,prop,nargs[prop])
        if not self.FontID:
            self.FontID=pygame.font.match_font("sans")
        if self.PromptGap is None and self.Prompt:
            self.PromptGap=" "
        if "CheckType" not in nargs and "DefaultText" in nargs:
            self.CheckType=type(nargs["DefaultText"])
        if "Size" in nargs:
            self.FontSize=self.Size[1]-2*self.BorderWidth
            self.Font=pygame.font.Font(self.FontID, self.FontSize)
        elif not self.Size:
            self.Font=pygame.font.Font(self.FontID, self.FontSize)
            self.Size=self.Font.size(self.Prompt+self.PromptGap+"W"*max(self.TextLength,len(self.DefaultText)+1,2))
            self.Size=self.Size[0]+2*self.BorderWidth,self.Size[1]+2*self.BorderWidth
        self.Paper=pygame.Surface(self.Size)
        # TODO background image/transparency for Paper
        self.Paper.fill(self.PaperColor)
        pr=self.Font.render(self.Prompt, True, self.PromptColor)
        self.Paper.blit(pr, (self.BorderWidth,self.BorderWidth+self.FontSize-self.Font.get_height()))
        self.Text=unicode(self.DefaultText)
        self.Cursor=len(self.Text)

    def __init__(self, *pargs, **nargs):
        '''Create a text input entity. Call __update__() next.'''
        self.__update__(*pargs, **nargs)

    def __sawtoothed__(self, block, side, mult=3):
        '''Create a sawtoothed mark for left (False) or right (True) side'''
        w,h=block.get_size()
        nw=mult*self.BorderWidth
        n=(h/nw)|1
        x,d=side and (w-1,-nw) or (0,nw)
        return [(x+d*(i%2),h*i/n) for i in xrange(n)]

    def value(self):
        '''Check if input is correct and return it, return None if it is not'''
        try:
            return self.CheckType(self.Text)
        except:
            return None

    def render(self):
        '''Return paper surface with current prompt and text printed on'''
        ret=self.Paper.copy()
        wl=self.Font.size(self.Prompt+self.PromptGap)[0]
        ib=ret.subsurface((wl,0,ret.get_width()-wl,ret.get_height()))
        ia=ret.subsurface((wl+self.BorderWidth,self.BorderWidth,ret.get_width()-wl-2*self.BorderWidth,ret.get_height()-2*self.BorderWidth))
        pr=self.Font.render(self.Text, True, self.TextColor)
        w=self.Font.size(self.Text[:self.Cursor])[0]
        while self.Margin and w-self.Font.size(self.Text[:self.Margin])[0]<self.CursorWidth:
            self.Margin-=1
        while w-self.Font.size(self.Text[:self.Margin])[0]>ia.get_width()-self.CursorWidth:
            self.Margin+=1
        Margin=-self.Font.size(self.Text[:self.Margin])[0]
        ia.blit(pr,(Margin,self.FontSize-self.Font.get_height()))
        pygame.draw.line(ia, self.CursorColor, (w+Margin,2), (w+Margin,ia.get_height()-2),self.CursorWidth)
        if Margin<0:
            pygame.draw.polygon(ib, self.OverColor, self.__sawtoothed__(ib, False))
        if Margin+pr.get_width()>ia.get_width()-self.CursorWidth:
            pygame.draw.polygon(ib, self.OverColor, self.__sawtoothed__(ib, True))
        return ret

    def draw(self,scr,pos):
        '''Draw input box on surface scr at position pos
        backuping an underlying part of surface'''
        self.BackingStore=(self.Paper.copy(),scr,pos)
        self.BackingStore[0].blit(self.BackingStore[1],(0,0),(self.BackingStore[2],self.Size))
        self.BackingStore[1].blit(self.render(),self.BackingStore[2])
        self.Status|=SHOWN

    def undraw(self):
        '''Remove the box from the surface it was drawn
        restoring underlying part of surface'''
        if self.BackingStore:
            self.BackingStore[1].blit(self.BackingStore[0],self.BackingStore[2])
        self.Status&=~SHOWN

    def activate(self, *pargs, **nargs):
        '''Enable input from the box.
        If either pargs or nargs is given, call __update__().
        Return False if no activation was needed, True otherwise.

        Calling __update__() means resetting every field,
        so use `inputbox.activate("<any prompt>")' to replace
        last entered value with the default one.
        '''
        if self.Status&ACTIVE and not pargs and not nargs:
            return False
        if pargs or nargs:
            self.__update__(*pargs, **nargs)
        self.Cursor=len(self.Text)
        self.Margin=0
        self.Status=ACTIVE|FIRST
        self.RepeatStore=pygame.key.get_repeat()
        pygame.key.set_repeat(*self.RepeatDefault)
        return True

    def deactivate(self):
        '''Disable input from the box'''
        if self.Status:
            self.Status=0
            pygame.key.set_repeat(*self.RepeatStore)

    def is_active(self): return self.Status&ACTIVE
    def is_done(self): return self.Status&DONE
    def is_failed(self): return self.Status&FAILED
    def is_cancelled(self): return self.Status&CANCEL
    def is_shown(self): return self.Status&SHOWN

    def is_success(self):
        return self.is_done() and not (self.is_failed() or self.is_cancelled())

    def edit(self,ev):
        '''Proceed event for editing input box.
        Supported keys:
            <any unicode symbol>:   input character
            <backspace>:            delete character under the cursor
            <esc>:                  restore default value
            <esc><esc>:             force unsuccsessful input'''
        if ev.type is pygame.KEYDOWN:
            if ev.key == pygame.K_BACKSPACE:
                if self.Cursor>0:
                    self.Text=self.Text[:self.Cursor-1]+self.Text[self.Cursor:]
                    self.Cursor-=1
            elif ev.key == pygame.K_DELETE:
                if self.Cursor<len(self.Text):
                    self.Text=self.Text[:self.Cursor]+self.Text[self.Cursor+1:]
            elif ev.unicode >= u' ':
                if self.Status&FIRST:
                    self.Text=ev.unicode
                    self.Cursor=1
                else:
                    self.Text=self.Text[:self.Cursor]+ev.unicode+self.Text[self.Cursor:]
                    self.Cursor+=1
            elif ev.key == pygame.K_ESCAPE:
                if self.Text==self.DefaultText:
                     self.Status|=CANCEL|DONE
                self.Text=self.DefaultText
                self.Margin=0
            elif ev.key == pygame.K_RETURN:
                if self.value() is None:
                    if self.RetryIncorrect:
                        # TODO signal an error
                        self.Text=self.DefaultText
                        self.Margin=0
                    else:
                        self.Status|=DONE|FAILED
                else:
                    self.Status|=DONE
            elif ev.key == pygame.K_HOME:
                self.Cursor=0
            elif ev.key == pygame.K_END:
                self.Cursor=len(self.Text)
            elif ev.key == pygame.K_RIGHT:
                self.Cursor=min(self.Cursor+1,len(self.Text))
            elif ev.key == pygame.K_LEFT:
                self.Cursor=max(self.Cursor-1,0)
            self.Status&=~FIRST

def __main():
    import random
    _=random.randint
    pygame.init()
    Size=(760,180)
    Scr=pygame.display.set_mode(Size)
    Scr.fill(pygame.Color("Black"))
    #inp=Input("", "Default text", FontSize=36)
    #inp=Input("Float input:", "123", FontSize=36, CheckType=float)
    r=20
    for i in xrange(100):
        pos=_(r,Size[0]-r),_(r,Size[1]-r)
        col=_(10,255),_(10,255),_(10,255)
        pygame.draw.circle(Scr,col,pos,r)
    x,y=Size[0]/12,Size[1]/5
    inp=Input("Input",Size=(Size[0]-2*x,Size[1]-2*y))
    cont,verbose=True,False
    defaults,defcnt=(("String",""),("Int",0),("Float",0.)),0
    while cont:
        for ev in pygame.event.get():
            if verbose:
                print ev
            if ev.type is pygame.QUIT:
                cont=False
            if ev.type is pygame.KEYDOWN and inp.is_active():
                inp.edit(ev)
            elif ev.type is pygame.KEYDOWN:
                if ev.key in (pygame.K_KP_ENTER, pygame.K_RETURN, 13):
                    if not inp.is_active():
                        inp.activate(*defaults[defcnt])
                        defcnt=(defcnt+1)%len(defaults)
                elif ev.key == pygame.K_F1:
                    verbose=True
                elif ev.key is pygame.K_ESCAPE:
                    if not inp.is_active():
                        cont=False

        if inp.is_done():
            if inp.is_failed():
                print "Data incorrect"
            elif inp.is_cancelled():
                print "Input is cancelled"
            else:
                print "Result: '{0}'".format(inp.value())
            inp.deactivate()

        if inp.is_active(): inp.draw(Scr,(x,y))
        pygame.display.flip()
        if inp.is_active(): inp.undraw()
    quit()

if __name__ == "__main__":
    __main()
