#!/usr/bin/python3

# FRA Challenge fall of 2021 by PG, www.fra.se

# Tested on Ubuntu
# python 3.8.10
# PIL 7.0.0


import PIL
from PIL import Image, ImageFont, ImageDraw
import tty, termios, sys, select

import math, time, random

import json, codecs, hashlib

import socket

RESOLUTION={'X':150, 'Y':50}
IMAGE_RESOLUTION={'X':RESOLUTION['X']*3, 'Y':RESOLUTION['Y']*7}


COLOR_BLACK=0
COLOR_RED=1
COLOR_GREEN=2
COLOR_YELLOW=3
COLOR_BLUE=4
COLOR_PURPLE=5
COLOR_CYAN=6
COLOR_WHITE=7

ANSI_TOP_LEFT='\033[;H'
ANSI_CLEAR='\033[2J'
ANSI_RESET='\033[0m'
ANSI_COLORS={
    COLOR_BLACK:'\033[30m',
    COLOR_RED:'\033[31m',
    COLOR_GREEN:'\033[32m',
    COLOR_YELLOW:'\033[33m',
    COLOR_BLUE:'\033[34m',
    COLOR_PURPLE:'\033[35m',
    COLOR_CYAN:'\033[36m',
    COLOR_WHITE:'\033[37m',
}


class Object:
    def __init__(self, x, y):
        self.x=x
        self.y=y
        self.rot=0
        self.rs=0
        self.sx=self.sy=self.speed=0
        self.MAX_ROTATION_SPEED=0
        self.MAX_SPEED=0
        self.MAX_THRUST=0
        self.SPEED_REDUCTION=1.01
        
        self.img=Image.new(mode='RGB', size=(IMAGE_RESOLUTION['X'], IMAGE_RESOLUTION['Y']))

    def update(self, x, y, rot):
        self.x=x
        self.y=y
        self.rot=rot

    def pixel2char(self, intensity):
        c=' .,-ouwdpbO0AGMZBW#@'
        
        return c[int(len(c)*intensity/256)]

    def image2ascii(self, image, offset=(0, 0) ):
        if image.size!=(IMAGE_RESOLUTION['X'], IMAGE_RESOLUTION['Y']):
            tmp_image=Image.new(mode='RGB', size=(IMAGE_RESOLUTION['X'], IMAGE_RESOLUTION['Y']))
            tmp_image.paste(image, box=(0, 0))
            rotated_image=tmp_image.rotate(self.rot)
            resized_image=rotated_image.resize((RESOLUTION['X'], RESOLUTION['Y']))
            pixels=resized_image.load()
        else:
            rotated_image=image.rotate(self.rot)
            resized_image=rotated_image.resize((RESOLUTION['X'], RESOLUTION['Y']))
            pixels=resized_image.load()

        for y in range(RESOLUTION['Y']):
            for x in range(RESOLUTION['X']):
                r,g,b=pixels[x, y]
                intensity=max(r, g, b)

                col=0

                if r>0:
                    col|=1
                if g>0:
                    col|=2
                if b>0:
                    col|=4

                color=col

                pixel=self.pixel2char(intensity)
                if pixel!=' ':
                    tx=int(x+offset[0])
                    ty=int(y+offset[1])
                    
                    if tx>0 and ty>0 and tx<RESOLUTION['X']-1 and ty<RESOLUTION['Y']:
                        self.buf[ty][tx]=(pixel, color)


    def game2image(self, game_x, game_y, center_x, center_y):
        image_x=(game_x-center_x)
        image_y=(game_y-center_y)

        return image_x, image_y
        return image_x-RESOLUTION['X']/2, image_y-RESOLUTION['Y']/2


    def render(self, buf, center_x, center_y):
        self.buf=buf

        self.img=Image.new(mode='RGB', size=(IMAGE_RESOLUTION['X'], IMAGE_RESOLUTION['Y']))
        d=ImageDraw.Draw(self.img)
        d.ellipse((IMAGE_RESOLUTION['X']/2-30, IMAGE_RESOLUTION['Y']/2-30,\
                   IMAGE_RESOLUTION['X']/2+30, IMAGE_RESOLUTION['Y']/2+30), outline='blue')

        # blit ascii chars with correct offset
        self.image2ascii(self.img, self.game2image(self.x, self.y, center_x, center_y) )

        return self.buf

# A better version will be downloaded...
class Rock(Object):
    def __init__(self, x, y, rs=0, points=[], colors=[]):
        return 

class Player(Object):
    def __init__(self, x, y, rs=0, points=[], colors=[]):
        return 

class Flower(Object):
    def __init__(self, x, y, rs=0, points=[], colors=[]):
        return 


class ClientModel:
    def __init__(self, socket):
        self.s=socket

        self.objects={}


    def poll_data(self):
        data=[]
        code=[]
        try:
            while 1: 
                data+=[self.s.recv(8192).decode('utf-8')]
        except socket.timeout:
            pass


        if len(data):
            for d in ''.join(data).split('\n'):

                    if not len(d):
                        continue
                    j=json.loads(d)

                    if j['encoding']=='xor':
                        key=open('xor.key', 'r').read(1)
                        jd=''.join([chr(i ^ ord(key)) for i in codecs.decode(j['data'], 'hex')])
                    elif j['encoding']=='long_xor':
                        key=open('long_xor.key', 'r').readline().strip()
                        jd=''.join([chr(i ^ ord(k)) for i,k in zip(codecs.decode(j['data'], 'hex'), key*500)])
                    else:
                        jd=j['data']

                    if hashlib.md5(bytes(jd, 'utf-8')).hexdigest()==j['md5']:
                        if j['msg_type']=='update':
                            
                            data=json.loads(jd)

                            if data['id'] not in self.objects:
                                if data['type']=='Rock':
                                    self.objects[data['id']]=Rock(data['x'], data['y'], colors=data['colors'], points=data['points'])
                                elif data['type']=='Player':
                                    self.objects[data['id']]=Player(data['x'], data['y'])
                                elif data['type']=='Flower':
                                    self.objects[data['id']]=Flower(data['x'], data['y'])

                            self.objects[data['id']].update(data['x'], data['y'], data['rot'])
                            if data['id']==1:
                                self.player=self.objects[1]

                        elif j['msg_type']=='code':
                            code+=[jd]
                    else:
                        print("md5 wrong:", j)
                        sys.exit(0)
        return code

    def update(self):
        return self.poll_data()


class ClientView:
    def __init__(self, model):
        self.model=model

    def render(self):
        buf=[ [[' ', COLOR_BLACK] for i in range(RESOLUTION['X'])] for j in range(RESOLUTION['Y'])]

        for num, obj in self.model.objects.items():
            buf=obj.render(buf, self.model.player.x, self.model.player.y)

        out=[ANSI_TOP_LEFT,]
        current_color=-1
        for row in buf:
            for char, color in row:
                if current_color!=color:
                    out+=[ANSI_COLORS[color]]
                    current_color=color
                out+=[char]
            out+=['\n']
        print(''.join(out))



old_settings = termios.tcgetattr(sys.stdin)

HOST='localhost'
f=open('port', 'r')
PORT=int(f.readline().strip())
f.close()
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((HOST, PORT))
s.settimeout(0.0001)


try:
    tty.setcbreak(sys.stdin.fileno())

    M=ClientModel(s)
    V=ClientView(M)

    last_draw=time.time()
    print(ANSI_CLEAR)
    while 1:

        # nonblocking input
        if select.select([sys.stdin], [], [], 0) == ([sys.stdin], [], []):
            c=sys.stdin.read(1)
           
            if c=='q' or c=='\033':
                break # player pressed esc
            
            s.send(bytes(c, 'ascii'))

         
        for code in M.update():
            exec(code) # bad idea!
        
        if time.time()-last_draw>0.06: # fps=1/0.06
            V.render()
            last_draw=time.time()

        time.sleep(0.01)


finally:
    print(ANSI_RESET+"bye")
    termios.tcsetattr(sys.stdin, termios.TCSADRAIN, old_settings)
    s.close()


