#!/usr/bin/env python
#
# (c) Copyright 2018 by Coinkite Inc. This file is part of Coldcard <coldcardwallet.com>
# and is covered by GPLv3 license found in COPYING.
#
# Simulate the hardware of a Coldcard. Particularly the OLED display (128x32) and 
# the numberpad. 
#
# This is a normal python3 program, not micropython. It communicates with a running
# instance of micropython that simulates the micropython that would be running in the main
# chip.
#
import os, sys, tty, pty, termios, time, pdb
import subprocess
import sdl2.ext
from PIL import Image
from select import select
import fcntl
from binascii import b2a_hex, a2b_hex

MPY_UNIX = 'l-port/micropython'

UNIX_SOCKET_PATH = '/tmp/ckcc-simulator.sock'

# top-left coord of OLED area; size is 1:1 with real pixels... 128x64 pixels
OLED_ACTIVE = (50, 78)

# keypad touch buttons
KEYPAD_LEFT = 52
KEYPAD_TOP = 216
KEYPAD_PITCH = 73


class OLEDSimulator:

    def __init__(self, factory):
        self.movie = None

        s = factory.create_software_sprite( (128,64), bpp=32)
        self.sprite = s
        s.x, s.y = OLED_ACTIVE
        s.depth = 100

        self.fg = sdl2.ext.prepare_color('#ccf', s)
        self.bg = sdl2.ext.prepare_color('#111', s)
        sdl2.ext.fill(s, self.bg)

        self.mv = sdl2.ext.PixelView(self.sprite)

    def render(self, window, buf):
        # take a full-screen update of the OLED contents and display
        assert len(buf) == 1024, len(buf)

        for y in range(0, 64, 8):
            line = buf[y*128//8:]
            for x in range(128):
                val = buf[(y*128//8) + x]
                mask = 0x01
                for i in range(8):
                    self.mv[y+i][x] = self.fg if (val & mask) else self.bg
                    mask <<= 1

        if self.movie is not None:
            self.new_frame()

    def snapshot(self):
        fn = time.strftime('../snapshot-%j-%H%M%S.png')
        sdl2.SDL_SaveBMP(self.sprite.surface, fn.encode('ascii'))
        print("Snapshot saved: %s" % fn.split('/', 1)[1])

    def movie_start(self):
        self.movie = []
        self.last_frame = time.time() - 0.1
        print("Movie recording started.")
        self.new_frame()

    def movie_end(self):
        fn = time.strftime('../movie-%j-%H%M%S.gif')
        from PIL import Image, ImageSequence

        dt0, img = self.movie[0]

        img.save(fn, save_all=True, append_images=[fr for _,fr in self.movie[1:]],
                        duration=[max(dt, 20) for dt,_ in self.movie], loop=50)

        print("Movie saved: %s (%d frames)" % (fn.split('/', 1)[1], len(self.movie)))

        self.movie = None

    def new_frame(self):
        from PIL import Image
        import tempfile

        dt = int((time.time() - self.last_frame) * 1000)
        self.last_frame = time.time()

        with tempfile.NamedTemporaryFile() as tmp:
            sdl2.SDL_SaveBMP(self.sprite.surface, tmp.name.encode('ascii'))
            tmp.file.seek(0)
            img = Image.open(tmp.file)
            img = img.convert('P')
            self.movie.append((dt, img))
    

def start():
    print('''\nColdcard Simulator: Commands (over simulated window):
  - Control-Q to quit
  - Z to snapshot screen.
  - S/E to start/end movie recording
''')
    sdl2.ext.init()

    factory = sdl2.ext.SpriteFactory(sdl2.ext.SOFTWARE)
    bg = factory.from_image("background.png")
    oled = OLEDSimulator(factory)
    
    # for genuine/caution lights
    led_red = factory.from_image("led-red.png")
    led_green = factory.from_image("led-green.png")

    window = sdl2.ext.Window("Coldcard Simulator", size=bg.size, position=(100, 100))
    window.show()

    ico = factory.from_image('program-icon.png')
    sdl2.SDL_SetWindowIcon(window.window, ico.surface)

    spriterenderer = factory.create_sprite_render_system(window)

    spriterenderer.render(bg)
    spriterenderer.render(oled.sprite)
    spriterenderer.render(led_red)
    genuine_state = False

    # capture exec path and move into intended working directory
    mpy_exec = os.path.realpath('l-port/coldcard-mpy')
    env = os.environ.copy()
    env['MICROPYPATH'] = ':' + os.path.realpath('../shared')


    oled_r, oled_w = os.pipe()      # fancy OLED display
    led_r, led_w = os.pipe()        # genuine LED
    numpad_r, numpad_w = os.pipe()  # keys

    # manage unix socket cleanup for client
    try:
        os.unlink('/tmp/ckcc-simulator.sock')
    except: pass
    import atexit
    atexit.register(os.unlink, '/tmp/ckcc-simulator.sock')

    os.chdir('./work')
    cc_cmd = ['../coldcard-mpy', '-i', '../sim_boot.py',
                        str(oled_w), str(numpad_r), str(led_w)] \
                        + sys.argv[1:]
    xterm = subprocess.Popen(['xterm', '-title', 'Coldcard Simulator REPL',
                                '-geom', '132x40+450+40', '-e'] + cc_cmd,
                                env=env,
                                stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL,
                                pass_fds=[oled_w, numpad_r, led_w], shell=False)


    # reopen as binary streams
    oled_rx = open(oled_r, 'rb', closefd=0, buffering=0)
    led_rx = open(led_r, 'rb', closefd=0, buffering=0)
    numpad_tx = open(numpad_w, 'wb', closefd=0, buffering=0)

    # setup no blocking
    for r in [oled_rx, led_rx]:
        fl = fcntl.fcntl(r, fcntl.F_GETFL)
        fcntl.fcntl(r, fcntl.F_SETFL, fl | os.O_NONBLOCK)

    running = True
    pressed = set()

    def send_event(ch, is_down):
        before = len(pressed)

        if is_down:
            pressed.add(ch)
        else:
            pressed.discard(ch)

        if len(pressed) != before:
            numpad_tx.write(b''.join(pressed) + b'\n')


    while running:
        events = sdl2.ext.get_events()
        for event in events:
            if event.type == sdl2.SDL_QUIT:
                running = False
                break

            if event.type == sdl2.SDL_KEYUP or event.type == sdl2.SDL_KEYDOWN:
                try:
                    ch = chr(event.key.keysym.sym)
                    #print('0x%0x => %s  mod=0x%x'%(event.key.keysym.sym, ch, event.key.keysym.mod))
                except:
                    # things like 'shift' by itself
                    #print('0x%0x' % event.key.keysym.sym)
                    if 0x4000004f <= event.key.keysym.sym <= 0x40000052:
                        # arrow keys
                        ch = '9785'[event.key.keysym.sym - 0x4000004f]
                    else:
                        ch = '\0'

                # remap ESC/Enter 
                if ch == '\x1b':
                    ch = 'x'
                elif ch == '\x0d':
                    ch = 'y'

                if ch == 'q' and event.key.keysym.mod == 0x40:
                    # control-Q
                    running = False
                    break

                if ch in 'zse':
                    if event.type == sdl2.SDL_KEYDOWN:
                        if ch == 'z':
                            oled.snapshot()
                        if ch == 's':
                            oled.movie_start()
                        if ch == 'e':
                            oled.movie_end()
                    continue

                if ch not in '0123456789xy':
                    if ch.isprintable():
                        print("Invalid key: '%s'" % ch)
                    continue
                
                # need this to kill key-repeat

                ch = ch.encode('ascii')
                send_event(ch, event.type == sdl2.SDL_KEYDOWN)

            if event.type == sdl2.SDL_MOUSEBUTTONDOWN:
                #print('xy = %d, %d' % (event.button.x, event.button.y))
                col = ((event.button.x - KEYPAD_LEFT) // KEYPAD_PITCH)
                row = ((event.button.y - KEYPAD_TOP) // KEYPAD_PITCH)
                #print('rc= %d,%d' % (row,col))
                if not (0 <= row < 4): continue
                if not (0 <= col < 3): continue
                ch = '123456789x0y'[(row*3) + col]
                send_event(ch.encode('ascii'), True)

            if event.type == sdl2.SDL_MOUSEBUTTONUP:
                for ch in list(pressed):
                    send_event(ch, False)

        rs, ws, es = select([oled_rx, led_rx], [], [], .001)
        for r in rs:

            # Cheating: 1024 is size of OLED update, don't change.
            c = r.read(1024*1000)
            if not c:
                break
        
            if r is oled_rx:
                c = c[-1024:]
                oled.render(window, c)
                spriterenderer.render(oled.sprite)
                window.refresh()
            elif r is led_rx:
                genuine_state = (c[0] == 1)
                #print("Genuine LED: %r" % genuine_state)
                spriterenderer.render(bg)
                spriterenderer.render(oled.sprite)
                spriterenderer.render(led_green if genuine_state else led_red)
                window.refresh()
            else:
                pass

        if xterm.poll() != None:
            print("\r\n<xterm stopped: %s>\r\n" % xterm.poll())
            break

    xterm.kill()
    

if __name__ == '__main__':
    start()
