"""
データ探索を行う
"""

import os
from glob import glob
import csv
import pandas as pd
import numpy as np
from collections import Counter
import re
from pathlib import Path

LAG_NUM = 5

def bname(object_name):
    return os.path.basename(object_name.rstrip(os.sep))

def make_data(path,bssid_list):
    # WAYPOINT: 1列目TYPE_WAYPOINT,2列名x,3列目y
    # wifi: 0列目にtimestamp,1列目にTYPE_WIFI,3列目にbssid,4列目にRSSI,5列目に周波数
    # beacon: 0列目にtimestamp,1列目にTYPE_BEACON,8列目にdistance,9列目にmac_address
    way_point_list = []
    wifi_list = []
    beacon_list = []
    # wifi特徴量,floor,x,y
    train_data = pd.DataFrame()
    with open(path,'r',encoding="utf-8") as f:
        for row in csv.reader(f, delimiter="\t", doublequote=True):
            # 教師データ取得
            if row[1] == 'TYPE_WAYPOINT':
                way_point_list.append([int(row[0]),row[2],row[3]])
            # 特徴量 wifi 取得
            elif row[1] == 'TYPE_WIFI':
                wifi_list.append([int(row[0]),row[3],row[4],row[5]])
    if len(wifi_list) == 0:
        return train_data
    # 施設の再頻出wifiデータのみ使用
    wifi_list = [wifi_data for wifi_data in wifi_list if wifi_data[1] in bssid_list]
    for way_point in way_point_list:
        # waypointと一番近い過去LAG_NUM地点までのwifiのデータ取得
        target_wifi_list =\
             get_lag_data(LAG_NUM,way_point,wifi_list)
        if len(target_wifi_list) == 0:
            continue
        index, value = zip(*target_wifi_list)
        target_wifi_series = pd.Series(value, index=index)
        target_wifi_series['x'] = way_point[1]
        target_wifi_series['y'] = way_point[2]
        # 重複したINDEXを削除
        target_wifi_series = target_wifi_series[np.logical_not(target_wifi_series.index.duplicated())]
        # bssid_listを列名,floor,x,y の訓練データ作成 wifi_listは開いていれる
        train_data = train_data.append(target_wifi_series,ignore_index=True)
    return train_data

def get_time_boundary(wifi_list,target_timestamp,is_future=False):
    """
    lag_num分のまとまったwifi_listを取得
    """
    wifi_timestamp_list = np.asarray(wifi_list)[:,0].astype(np.int64)
    lag_list = []
    pre_d = wifi_timestamp_list[0]
    pre_index = 0
    lag_count = 0
    for i,d in enumerate(wifi_timestamp_list):
        # 過去から取得の場合は、時刻が対象時刻を越えた時 または 指定回数ラグデータを取得した時
        if (not is_future) and (d > target_timestamp) or (lag_count >= LAG_NUM):
            break
        # 未来から取得の場合はlag_num分まで取得
        if is_future and (lag_count >= LAG_NUM):
            break
        # 未来から取得の場合は自身の時刻よりも未来でないといけない
        elif is_future and (target_timestamp > d):
            continue
        if d != pre_d:
            lag_count += 1
            lag_list.append(wifi_list[pre_index:i])
            pre_index = i
        pre_d = d
    # 未来の場合反転
    if is_future:
        return lag_list[::-1]
    return lag_list

def get_lag_data(lag_num,way_point,wifi_list):
    """
    時刻が近い順にラグデータを取得
    """
    # 過去のラグリストを取得
    lag_list = get_time_boundary(wifi_list,way_point[0])
    if len(lag_list) < lag_num:
        # 過去にラグリストが作れないなら、未来から作る
        lag_list = get_time_boundary(wifi_list,way_point[0],is_future=True)
    target_wifi_list = []
    for i,lags in enumerate(lag_list):
        for lag in lags:
            target_wifi_list.append([lag[1]+'_lag_'+str(len(lag_list)-i),float(lag[2])])
    return target_wifi_list

def get_bssid_list(floor_folders):
    """
    BSSIDのリスト作成
    """
    bssid_list = []
    # フロアのパスを取得
    for floor_folder in floor_folders:
        print(f'{bname(floor_folder)}のBSSID/MACを探索中')
        for survey_path in glob(f'{floor_folder}/*'):
            with open(survey_path,'r',encoding="utf-8") as f:
                for row in csv.reader(f, delimiter="\t", doublequote=True):
                    if row[1] == 'TYPE_WIFI':
                        bssid_list.append(row[3])
    bssid_tuple, _ = zip(*Counter(bssid_list).most_common(1500))
    return list(bssid_tuple)

def get_floor_num(floor):
    # 数字が含まれていない or 2文字以下の場合スキップ
    if len(floor) < 2 or (not bool(re.search(r'\d', floor))):
        return -99
    FLOOR = ['F','L']
    BASEMENT = ['B']
    floor_num = int(re.sub(r"\D", "", floor))
    floor_str = re.sub(r"\d", "", floor)
    if floor_str in FLOOR:
        return floor_num - 1
    elif floor_str in BASEMENT:
        return -floor_num
    else:
        return -99

def get_target_facilities():
    """
    submitに使用する施設を取得
    """
    with open('./test_facilities.csv','r',encoding="utf-8") as f:
        for row in csv.reader(f, delimiter=",", doublequote=True):
            return row

def create_df_columns(bssid_list):
    columns = []
    for id in bssid_list:
        for n in range(LAG_NUM):
            columns.append(id+f'_lag_{str(n+1)}')
    return columns

if __name__== "__main__":
    DIR = './download_data/indoor-location-navigation'
    train_folders = glob(f'{DIR}/train/*')
    print(f'施設数: {len(train_folders)}')
    skip = True
    target_facilities = get_target_facilities()
    for facility_id in train_folders:
        if bname(facility_id) not in target_facilities:
            print(f'{bname(facility_id)}は載っていないため、スキップ')
            continue
        # if bname(facility_id) == '5cd56b99e2acfd2d33b5f491':
        #     skip = False
        print(f'施設ID {bname(facility_id)} を探索')
        # if skip:
        #     continue
        floor_folders = glob(f'{facility_id}/*')
        print(f'階層一覧: {[bname(p) for p in floor_folders]}')
        bssid_list = get_bssid_list(floor_folders)
        columns = create_df_columns(bssid_list)
        train_data_parent = pd.DataFrame(columns=[*columns,'floor','x','y'])
        # 各フロア の調査データから訓練データ作成
        for floor_paths in [(glob(f'{p}/*')) for p in floor_folders]:
            floor_num = get_floor_num(Path(floor_paths[0]).parts[-2])
            if floor_num == -99:
                continue
            # 各調査データから特徴量作成
            for survey in floor_paths:
                print(f'ターゲットファイル: {bname(survey)}')
                # if bname(survey) != '5d8f0954b6e29d0006fb8c0d.txt':
                #     continue
                train_data = make_data(survey,bssid_list)
                train_data['floor'] = floor_num
                train_data_parent = pd.concat([train_data_parent,train_data],axis=0)
                print(train_data_parent)
        train_data_parent.to_csv(f'./got_data_lag_wifi_1000columns/{bname(facility_id)}.csv')
    print('全て終了')


import pika
from pika.exchange_type import ExchangeType

def callback(ch, method, properties, body):
    print(f" [x] Received {body.decode()}")

connection_params = pika.ConnectionParameters(host='localhost')
connection = pika.BlockingConnection(connection_params)
channel = connection.channel()
channel.exchange_declare(exchange='altexexchange', exchange_type=ExchangeType.fanout)
channel.exchange_declare(exchange='mainexchange', exchange_type=ExchangeType.direct, arguments={'alternate-exchange': 'altexexchange'})
message = 'Hello World!'
channel.basic_publish(exchange='mainexchange', routing_key='test', body=message)
print(f" [x] Sent '{message}'")
connection.close()

rabbitMq/dead-exchange - move to del timeout/producer.py


import copy
import random
import numpy as np
from collections import defaultdict

__author__ = 'Riccardo Guidotti'

def entropy_cluster(cluster_info, N):
    n = cluster_info['n']
    if n == 0:
        return 0.0
    item_freq = cluster_info['item_freq']
    entropy_cluster_val = 0
    for item in range(0, len(item_freq)):
        p = 1.0 * item_freq[item] / n
        if p == 0 or p == 1.0:
            entropy_item = 0.0
        else:
            entropy_item = - ((p * np.log2(p)) + ((1-p) * np.log2(1-p)))
        entropy_cluster_val += entropy_item

    return entropy_cluster_val / N

def entropy(clusters_entropy):
    return np.sum(clusters_entropy.values())

def cluster_info_init(basket, nitems):
    cluster_info = dict()

    cluster_info['n'] = 1
    cluster_info['item_freq'] = [0] * nitems

    for item in range(0, len(basket)):
        if basket[item]:
            cluster_info['item_freq'][item] += 1

    return cluster_info

def add_basket(cluster_info, basket):

    cluster_info['n'] += 1

    for item in range(0, len(basket)):
        if basket[item]:
            cluster_info['item_freq'][item] += 1

    return cluster_info

def remove_basket(cluster_info, basket):

    cluster_info['n'] -= 1

    if cluster_info['n'] == 0:
        return None

    for item in range(0, len(basket)):
        if basket[item]:
            cluster_info['item_freq'][item] -= 1

    return cluster_info

class Coolcat:

    def __init__(self):
        self.clustering = list()
        self.iter_count = 0
        self.cluster_id = 0

    def fit(self, baskets, nbaskets, nitems, k, random_sample=float('infinity')):

        self.clustering = list()
        self.iter_count = 0
        self.cluster_id = 0

        self.nbaskets = nbaskets
        self.nitems = nitems
        self.k = k

        self.random_sample = random_sample

        if self.random_sample > self.nbaskets:
            self.random_sample = self.nbaskets

        # print 'initialization'
        clusters, clusters_info, clusters_entropy, seeds_set = self._initialization(baskets)

        # print 'allocation'
        clusters, clusters_info, clusters_entropy, clusters_reverse = self._allocation(
                baskets, clusters, clusters_info, clusters_entropy, seeds_set)

        # print 'refinement'
        clusters = self._refinement(baskets, clusters, clusters_info, clusters_entropy, clusters_reverse)

        # print clusters

        for cluster in clusters.values():
            res = dict()
            for bid in cluster:
                res[bid] = baskets[bid]
            self.clustering.append({
                'cluster': res,
                'centroid': None
            })

        return self

    def _initialization(self, baskets):

        self.iter_count += 1

        baskets_sample_keys = random.sample(baskets.keys(), self.random_sample)

        b1b2_dist = defaultdict(dict)

        max_dist = -float('infinity')
        best_couple = None

        for b1 in baskets_sample_keys:
            cluster_info = cluster_info_init(baskets[b1], self.nitems)
            for b2 in baskets_sample_keys:
                if b1 == b2:
                    continue

                cluster_info = add_basket(cluster_info, baskets[b2])
                dist = entropy_cluster(cluster_info, self.nbaskets)
                b1b2_dist[b1][b2] = dist

                if dist > max_dist:
                    max_dist = dist
                    best_couple = (b1, b2)

                cluster_info = remove_basket(cluster_info, baskets[b2])

        clusters = defaultdict(list)
        clusters_info = dict()
        clusters_entropy = dict()

        cid = self.cluster_id
        clusters[cid].append(best_couple[0])
        clusters_info[cid] = cluster_info_init(baskets[best_couple[0]], self.nitems)
        clusters_entropy[cid] = 0.0
        self.cluster_id += 1

        cid = self.cluster_id
        clusters[cid].append(best_couple[1])
        clusters_info[cid] = cluster_info_init(baskets[best_couple[1]], self.nitems)
        clusters_entropy[cid] = 0.0
        self.cluster_id += 1

        seeds_set = [best_couple[0], best_couple[1]]
        seeds_set = set(seeds_set)

        while len(seeds_set) < self.k:

            max_dist = -float('infinity')
            best_candidate = None
            for s in seeds_set:
                min_dist = float('infinity')
                best_candidate_b = None

                for b in b1b2_dist[s]:
                    if b not in seeds_set:

                        dist = b1b2_dist[b][s]

                        if dist > 0 and dist < min_dist:
                            min_dist = dist
                            best_candidate_b = b

                if min_dist > max_dist:
                    max_dist = min_dist
                    best_candidate = best_candidate_b

            seeds_set.add(best_candidate)
            cid = self.cluster_id
            clusters[cid].append(best_candidate)
            clusters_info[cid] = cluster_info_init(baskets[best_candidate], self.nitems)
            clusters_entropy[cid] = 0.0
            self.cluster_id += 1

        return clusters, clusters_info, clusters_entropy, seeds_set

    def _allocation(self, baskets, clusters, clusters_info, clusters_entropy, seeds_set):

        self.iter_count += 1

        clusters_reverse = dict()

        for b in baskets:
            if b not in seeds_set:
                min_entropy = float('infinity')
                best_cid = None
                best_cluster_info = None
                for cid in clusters:
                    cluster_info = copy.deepcopy(clusters_info[cid])
                    cluster_info = add_basket(cluster_info, baskets[b])
                    new_entropy_cluster = entropy_cluster(cluster_info, self.nbaskets)
                    if new_entropy_cluster < min_entropy:
                        min_entropy = new_entropy_cluster
                        best_cid = cid
                        best_cluster_info = cluster_info

                clusters[best_cid].append(b)
                clusters_info[best_cid] = best_cluster_info
                clusters_entropy[best_cid] = min_entropy
                clusters_reverse[b] = best_cid
            else:
                for cid in clusters:
                    if b in clusters[cid]:
                        clusters_reverse[b] = cid
                        break

        return clusters, clusters_info, clusters_entropy, clusters_reverse

    def _refinement(self, baskets, clusters, clusters_info, clusters_entropy, clusters_reverse):

        while True:
            moved = False
            self.iter_count += 1

            for b in baskets:
                current_entropy = entropy(clusters_entropy)

                original_cid = clusters_reverse[b]

                original_cluster_info = copy.deepcopy(clusters_info[original_cid])
                original_cluster_entropy = clusters_entropy[original_cid]

                clusters_info_tmp = remove_basket(clusters_info[original_cid], baskets[b])
                if clusters_info_tmp is None:
                    clusters_info[original_cid] = original_cluster_info
                    clusters_entropy[original_cid] = original_cluster_entropy
                    continue

                clusters_entropy[original_cid] = clusters_info_tmp
                clusters_entropy[original_cid] = entropy_cluster(clusters_info[original_cid], self.nbaskets)

                for cid in clusters:
                    if cid == original_cid:
                        continue

                    cid_cluster_info = copy.deepcopy(clusters_info[cid])
                    cid_cluster_entropy = clusters_entropy[cid]

                    clusters_info[cid] = add_basket(clusters_info[cid], baskets[b])
                    clusters_entropy[cid] = entropy_cluster(clusters_info[cid], self.nbaskets)

                    new_entropy = entropy(clusters_entropy)

                    if new_entropy < current_entropy:
                        moved = True
                    else:
                        clusters_info[cid] = cid_cluster_info
                        clusters_entropy[cid] = cid_cluster_entropy

                if not moved:
                    clusters_info[original_cid] = original_cluster_info
                    clusters_entropy[original_cid] = original_cluster_entropy

            if not moved:
                break

        return clusters


import torch
import torch.nn as nn
import torch.optim as optim
import pdb
import random
import numpy as np
import quadprog
import miosqp
import scipy as sp
import scipy.sparse as spa
from .common import MLP, ResNet18

class Net(nn.Module):
    def __init__(self,
                 n_inputs,
                 n_outputs,
                 n_tasks,
                 args):
        super(Net, self).__init__()
        nl, nh = args.n_layers, args.n_hiddens
        self.is_cifar = ('cifar10' in args.data_file)
        if self.is_cifar:
            self.net = ResNet18(n_outputs, bias=args.bias)
        else:
            self.net = MLP([n_inputs] + [nh] * nl + [n_outputs])
        self.ce = nn.CrossEntropyLoss()
        self.n_outputs = n_outputs
        self.opt = optim.SGD(self.parameters(), args.lr)
        self.n_memories = args.n_memories                  # number of memories per task
        self.n_sampled_memories = args.n_sampled_memories  # number of sampled memories per task
        self.n_constraints = args.n_constraints
        self.gpu = args.cuda
        self.batch_size=args.batch_size
        self.n_iter = args.n_iter

        # we allocate buffer for each class, not each task
        self.sampled_class_data = {}
        self.sampled_class_labs = {}
        self.sampled_class_features = {}

        # allocate counters
        self.observed_tasks = []
        self.old_task = -1
        self.n_task=0
        self.n_old_task=0
        self.sample_size_list=[]
        self.class_buffer_size=0
        self.n_class=0
        self.n_old_class=0
        self.total_task = args.total_task
        self.class_per_task=args.class_per_task

        # initialized for MOF
        self.mean_features = [0 for i in range(self.total_task*self.class_per_task)]
        self.class_nums  = [0 for i in range(self.total_task*self.class_per_task)]

    def forward(self, x, t=0):
        output = self.net(x)
        return output

    def dist_matrix(self,x,y=None):
        if y is None:
            y=x.clone()
        n = x.size(0)
        m = y.size(0)
        d = x.size(1)
        x = x.unsqueeze(1).expand(n, m, d)
        y = y.unsqueeze(0).expand(n, m, d)
        dist = torch.pow(x - y, 2).sum(2)
        return dist

    def update_effective_data(self,effective_new_data,effective_new_labs,class_data,class_labs):

        if effective_new_data is None:
            effective_new_data=class_data
            effective_new_labs=class_labs
        else:
            effective_new_data=torch.cat((effective_new_data,class_data),dim=0)
            effective_new_labs=torch.cat((effective_new_labs,class_labs),dim=0)

        return effective_new_data,effective_new_labs

    # we only select represetative points to update model
    def update_MOF(self,pretrained,x,y):

        self.eval()

        start_index=self.n_old_class
        end_index=self.n_class

        if pretrained==None:
            new_features = self.net.feature_extractor(x).data
        else:
            new_features = pretrained.feature_extractor(x).data

        effective_new_data= None
        effective_new_labs= None

        for class_id in range(start_index,end_index):
            mask=torch.eq(y,class_id)
            class_data=x[mask]
            class_labs=y[mask]
            class_features=new_features[mask]
            class_size=class_labs.size(0)
            self.class_nums[class_id]+=class_size

            # we assume that the new batch data size will not exceed the buffer size
            if class_id not in self.sampled_class_labs.keys():
                self.sampled_class_data[class_id]=class_data
                self.sampled_class_labs[class_id]=class_labs
                self.sampled_class_features[class_id]=class_features
                self.mean_features[class_id]=torch.mean(new_features[mask],dim=0,keepdim=True)

                effective_new_data,effective_new_labs=self.update_effective_data(effective_new_data,effective_new_labs,class_data,class_labs)

            else:
                buffer_size=self.sampled_class_labs[class_id].size(0)
                total_size=class_size+buffer_size
                lack=self.class_buffer_size-buffer_size
                ratio=class_size/total_size

                self.sampled_class_data[class_id]=torch.cat((self.sampled_class_data[class_id],class_data),dim=0)
                self.sampled_class_labs[class_id]=torch.cat((self.sampled_class_labs[class_id],class_labs),dim=0)
                self.sampled_class_features[class_id]=torch.cat((self.sampled_class_features[class_id],class_features),dim=0)

                self.mean_features[class_id]= self.mean_features[class_id]*(1-ratio)+torch.mean(class_features,dim=0,keepdim=True)*ratio

                # print('updated mean features')
                # print(self.mean_features[class_id])
                # mean features is two dimensional

                if total_size>self.class_buffer_size:

                    dist=self.dist_matrix(self.sampled_class_features[class_id], self.mean_features[class_id]).squeeze()

                    sorted_inds=torch.argsort(dist,descending=False)[:self.class_buffer_size]
                    effective_inds=sorted_inds[sorted_inds>=buffer_size]
                    class_effective_new_data=self.sampled_class_data[class_id][effective_inds]
                    class_effective_new_labs=self.sampled_class_labs[class_id][effective_inds]

                    effective_new_data,effective_new_labs=self.update_effective_data(effective_new_data,effective_new_labs,class_effective_new_data,class_effective_new_labs)

                    self.sampled_class_data[class_id]=self.sampled_class_data[class_id][sorted_inds]
                    self.sampled_class_labs[class_id]=self.sampled_class_labs[class_id][sorted_inds]
                    self.sampled_class_features[class_id]=self.sampled_class_features[class_id][sorted_inds]

                else:
                    effective_new_data,effective_new_labs=self.update_effective_data(effective_new_data,effective_new_labs,class_data,class_labs)

        self.train()

        return effective_new_data, effective_new_labs

    def observe(self, x, t, y, pretrained=None):

        if t!=self.old_task:
            # update the counter and list
            self.old_task=t
            self.observed_tasks.append(t)
            self.n_old_task=self.n_task
            self.n_task+=1
            self.n_old_class=self.n_old_task*self.class_per_task
            self.n_class=self.n_task*self.class_per_task
            self.class_buffer_size=int(self.n_sampled_memories/self.n_class)

            if self.n_task>1:
                remainder=self.n_constraints%self.n_old_class
                quotient=int(self.n_constraints/self.n_old_class)
                self.sample_size_list=[quotient for _ in range(self.n_old_class)]
                for i in range(remainder):
                    self.sample_size_list[i]=self.sample_size_list[i]+1

                for index in range(self.n_old_class):
                    old_size=self.sampled_class_labs[index].size(0)
                    new_size=min(old_size,self.class_buffer_size)
                    self.sampled_class_data[index]=self.sampled_class_data[index][:new_size]
                    self.sampled_class_labs[index]=self.sampled_class_labs[index][:new_size]

        effective_new_data,effective_new_labs=self.update_MOF(pretrained,x,y)
        effective_size=effective_new_labs.size(0)
        print('effective size: %d'%effective_size)

        for iter_i in range(self.n_iter):

            if effective_size!=0:
                self.zero_grad()
                loss = self.ce(self.forward(effective_new_data), effective_new_labs)
                loss.backward()
                self.opt.step()

            if self.n_task==1 and self.class_nums[0]>=30:

                equal_sample_size=int(self.n_constraints/self.n_class)
                batch_x=None
                batch_y=None

                # sample from each new class
                for class_id in range(self.n_class):
                    buffer_size=self.sampled_class_labs[class_id].size(0)
                    sample_size=min(buffer_size,equal_sample_size)
                    random_inds=random.sample(range(0,buffer_size),sample_size)

                    if batch_x is None:
                        batch_x=self.sampled_class_data[class_id][random_inds]
                        batch_y=self.sampled_class_labs[class_id][random_inds]
                    else:
                        batch_x=torch.cat((batch_x,self.sampled_class_data[class_id][random_inds]),dim=0)
                        batch_y=torch.cat((batch_y,self.sampled_class_labs[class_id][random_inds]),dim=0)

                self.zero_grad()
                loss = self.ce(self.forward(batch_x), batch_y)
                loss.backward()
                self.opt.step()

            #----update model on the old data----#
            elif self.n_task>1:

                batch_x=None
                batch_y=None

                # sample from each class
                for index in range(self.n_old_class):
                    sample_size=self.sample_size_list[index]
                    buffer_size=self.sampled_class_labs[index].size(0)
                    random_inds=random.sample(range(0,buffer_size),sample_size)

                    if batch_x is None:
                        batch_x=self.sampled_class_data[index][random_inds]
                        batch_y=self.sampled_class_labs[index][random_inds]
                    else:
                        batch_x=torch.cat((batch_x,self.sampled_class_data[index][random_inds]),dim=0)
                        batch_y=torch.cat((batch_y,self.sampled_class_labs[index][random_inds]),dim=0)

                self.zero_grad()
                loss = self.ce(self.forward(batch_x), batch_y)
                loss.backward()
                self.opt.step()


from chat.models import ChatRoom
from competitions import design
from competitions.forms import CreateCompetitionForm, EditCompetitionForm, TimeUnit
from competitions.models import Competition, Entry, ThumbsUp
from datetime import datetime, timedelta
from django.conf import settings
from django.contrib.auth.decorators import login_required
from django.core.paginator import Paginator
from django.core.urlresolvers import reverse
from django.http import HttpResponseRedirect
from django.shortcuts import render_to_response, get_object_or_404
from django.template import RequestContext
from main.common import json_login_required, json_post_required, json_failure, \
    json_response, get_obj_from_request, get_val
from main.uploadsong import upload_song, handle_project_upload
from workshop.models import Project, ProjectVersion, SampleDependency

@json_login_required
@json_post_required
def ajax_submit_entry(request):
    compo = get_obj_from_request(request.POST, 'compo', Competition)
    if compo is None:
        return json_failure(design.competition_not_found)

    # make sure it's still submission time
    now = datetime.now()
    if now >= compo.submit_deadline:
        return json_failure(design.past_submission_deadline)

    if now <= compo.start_date:
        return json_failure(design.competition_not_started)

    title = request.POST.get('entry-title','')
    comments = request.POST.get('entry-comments', '')
    mp3_file = request.FILES.get('entry-file-mp3')
    source_file = request.FILES.get('entry-file-source')
    is_open_source = request.POST.get('entry-open-source', False)

    entries = Entry.objects.filter(owner=request.user, competition=compo)
    resubmitting = entries.count() > 0

    # make sure files are small enough
    if not resubmitting and mp3_file is None:
        return json_failure(design.mp3_required)

    if mp3_file is not None and mp3_file.size > settings.FILE_UPLOAD_SIZE_CAP:
        return json_failure(design.mp3_too_big)

    if source_file is not None:
        if source_file.size > settings.FILE_UPLOAD_SIZE_CAP:
            return json_failure(design.source_file_too_big)

    if title == '':
        return json_failure(design.entry_title_required)

    if mp3_file is not None:
        band = request.user.get_profile().solo_band

        if resubmitting:
            entry = entries[0]
            project = Project.objects.get(latest_version__song=entry.song)
            new_version_number = project.latest_version.version + 1
            filename_appendix = "_" + str(new_version_number)
        else:
            filename_appendix = ""

        result = upload_song(request.user,
            file_mp3_handle=mp3_file,
            file_source_handle=source_file,
            max_song_len=settings.COMPO_ENTRY_MAX_LEN,
            band=band,
            song_title=title,
            song_album=compo.title,
            song_comments=comments,
            filename_appendix=filename_appendix)

        if not result['success']:
            return json_failure(result['reason'])

        song = result['song']
        song.is_open_source = is_open_source
        song.is_open_for_comments = True
        song.save()

        # make a new version and attach that to the entry
        if resubmitting:

            # create new version
            version = ProjectVersion()
            version.project = project
            version.song = song
            version.version = new_version_number
            version.saveNewVersion()

            old_length = entry.song.length
            buffer_time = 0
        else:
            # create the project
            project = Project()
            project.band = band
            project.save()

            # create the first version
            version = ProjectVersion()
            version.project = project
            version.song = song
            version.version = 1
            version.saveNewVersion()

            # subscribe the creator
            project.subscribers.add(request.user)
            project.save()

            # create new entry
            entry = Entry()
            entry.competition = compo
            entry.owner = request.user

            old_length = 0
            buffer_time = settings.LISTENING_PARTY_BUFFER_TIME

        entry.song = song
        entry.save()

        # update competition dates based on this newfound length
        vote_period_delta = timedelta(seconds=compo.vote_period_length)
        if compo.have_listening_party:
            compo.listening_party_end_date += timedelta(seconds=(song.length-old_length+buffer_time))
            compo.vote_deadline = compo.listening_party_end_date + vote_period_delta
        else:
            compo.vote_deadline = compo.submit_deadline + vote_period_delta
        compo.save()

        chatroom = compo.chat_room
        chatroom.end_date = compo.listening_party_end_date + timedelta(hours=1)
        chatroom.save()
    else:
        # only providing source and possibly renaming.
        entry = entries[0]
        song = entry.song

        if source_file is not None:
            handle_project_upload(source_file, request.user, song)

        song.title = title
        song.is_open_source = is_open_source
        song.comments = comments
        song.save()

    return json_response({'success': True})

def max_vote_count(entry_count):
    """
    given how many entrants there are, compute how many votes each person gets.
    """
    x = int(entry_count / 3)
    if x < 1:
        x = 1

    return x

def song_to_dict(song, user):
    d = song.to_dict(chains=['owner', 'studio', 'band', 'comment_node'])
    if song.studio is not None and d.has_key('studio'):
        if song.studio.logo_16x16 is not None:
            d['studio']['logo_16x16'] = song.studio.logo_16x16.url
        else:
            d['studio']['logo_16x16'] = design.unknown_studio_icon

        if user.is_authenticated():
            profile = user.get_profile()

            d['studio']['missing'] = song.studio not in profile.studios.all()

            def sample_to_dict(x):
                d = x.to_dict()
                d['missing'] = x.uploaded_sample is None
                return d

            d['samples'] = [sample_to_dict(x) for x in SampleDependency.objects.filter(song=song)]

            owned_plugins = profile.plugins.all()
            def plugin_to_dict(x):
                d = x.to_dict()
                d['missing'] = x not in owned_plugins
                return d

            d['plugins'] = [plugin_to_dict(x) for x in song.plugins.all()]

    return d

def ajax_compo(request, compo_id):
    compo_id = int(compo_id)
    try:
        compo = get_object_or_404(Competition, pk=compo_id)
    except Competition.DoesNotExist:
        return json_failure(design.competition_not_found)

    data = {
        'user': {
            'is_authenticated': request.user.is_authenticated(),
        },
        'compo': compo_to_dict(compo, request.user),
        'party': {
            'buffer_time': settings.LISTENING_PARTY_BUFFER_TIME,
        }
    }

    if request.user.is_authenticated():
        max_votes = max_vote_count(compo.entry_set.count())
        used_votes = ThumbsUp.objects.filter(owner=request.user, entry__competition=compo)

        data['user'].update(request.user.get_profile().to_dict());
        data['votes'] = {
            'max': max_votes,
            'used': [x.to_dict() for x in used_votes],
            'left': max_votes - used_votes.count(),
        }
        user_entries = Entry.objects.filter(competition=compo, owner=request.user)
        data['submitted'] = (user_entries.count() > 0)

    def entry_to_dict(entry):
        data = entry.to_dict(chains=['owner.solo_band'])
        data['song'] = song_to_dict(entry.song, request.user)
        if request.user.is_authenticated() and entry.owner == request.user:
            version = ProjectVersion.objects.get(song=entry.song)
            data['project_id'] = version.project.pk
            data['version_number'] = version.version
        return data

    # entries. if competition is closed, sort by vote count.
    data['entries'] = [entry_to_dict(x) for x in compo.entry_set.all()]

    return json_response(data)

@json_login_required
def ajax_unbookmark(request, id):
    id = int(id)
    comp = get_object_or_404(Competition, id=id)
    prof = request.user.get_profile()
    prof.competitions_bookmarked.remove(comp)
    prof.save()
    data = {'success': True}
    return json_response(data)

@json_login_required
def ajax_bookmark(request, id):
    comp = get_object_or_404(Competition, id=int(id))
    prof = request.user.get_profile()
    prof.competitions_bookmarked.add(comp)
    prof.save()
    data = {'success': True}
    return json_response(data)

def compo_to_dict(compo, user):
    data = compo.to_dict(chains=['host'])

    data['have_theme'] = compo.theme != ''
    data['have_rules'] = compo.rules != ''

    if compo.rulesVisible():
        data['rules'] = compo.rules
    if compo.themeVisible():
        data['theme'] = compo.theme

    if user.is_authenticated():
        # see if the user entered the compo
        entries = Entry.objects.filter(owner=user, competition=compo)
        if entries.count() > 0:
            data['user_entered'] = True
            entry = entries[0]
            data['vote_count'] = ThumbsUp.objects.filter(entry=entry).count()
    return data

def compoRequest(request, compos):
    page_number = get_val(request.GET, 'page', 1)
    paginator = Paginator(compos, settings.ITEMS_PER_PAGE)

    # build the json object
    data = {
        'compos': [compo_to_dict(x, request.user) for x in paginator.page(page_number).object_list],
        'page_count': paginator.num_pages,
        'page_number': page_number,
        'user': {
            'is_authenticated': request.user.is_authenticated(),
        },
    }

    return json_response(data)

def ajax_available(request):
    compos = Competition.objects

    # don't show bookmarked items
    if request.user.is_authenticated():
        pkeys = request.user.get_profile().competitions_bookmarked.values_list('pk')
        compos = compos.exclude(pk__in=pkeys)

    # we have to sort on the server due to paging
    compos = compos.order_by('-start_date')

    return compoRequest(request, compos)

@json_login_required
def ajax_owned(request):
    # only show bookmarked items
    # we have to sort on the server due to paging
    compos = request.user.get_profile().competitions_bookmarked.order_by('-start_date')
    return compoRequest(request, compos)

@login_required
def create(request):
    prof = request.user.get_profile()
    err_msg = ""
    if request.method == 'POST':
        form = CreateCompetitionForm(request.POST)
        if form.is_valid() and request.user.is_authenticated():
            # create and save the Competition
            comp = Competition()
            comp.title = form.cleaned_data.get('title')
            comp.host = request.user

            comp.preview_theme = form.cleaned_data.get('preview_theme', False)
            if form.cleaned_data.get('have_theme', False):
                comp.theme = form.cleaned_data.get('theme', '')

            comp.preview_rules = form.cleaned_data.get('preview_rules', False)
            if form.cleaned_data.get('have_rules', False):
                comp.rules = form.cleaned_data.get('rules', '')

            tz_offset = form.cleaned_data.get('tz_offset')
            tz_delta = timedelta(hours=tz_offset)

            comp.start_date = form.cleaned_data.get('start_date') + tz_delta
            comp.submit_deadline = form.cleaned_data.get('submission_deadline_date') + tz_delta

            # calculate vote deadline
            quantity = int(form.cleaned_data.get('vote_time_quantity'))
            quantifier = int(form.cleaned_data.get('vote_time_measurement'))

            comp.vote_period_length = quantity * TimeUnit.seconds_multiplier[quantifier]
            vote_period_delta = timedelta(seconds=comp.vote_period_length)

            comp.have_listening_party = form.cleaned_data.get('have_listening_party', True)
            if comp.have_listening_party:
                if form.cleaned_data.get('party_immediately'):
                    comp.listening_party_start_date = comp.submit_deadline
                else:
                    comp.listening_party_start_date = form.cleaned_data.get('listening_party_date') + tz_delta
                # initialize end date to start date. we make modifications to it
                # when entries are submitted.
                comp.listening_party_end_date = comp.listening_party_start_date

                # this changes whenever listening_party_end_date changes.
                comp.vote_deadline = comp.listening_party_end_date + vote_period_delta
            else:
                comp.vote_deadline = comp.submit_deadline + vote_period_delta

            # create a chatroom for it
            chatroom = ChatRoom()
            chatroom.permission_type = ChatRoom.OPEN
            # open the chat room an hour before the competition
            chatroom.start_date = comp.start_date - timedelta(hours=1)
            # chat room is open an hour before and after competition
            chatroom.end_date = comp.listening_party_end_date + timedelta(hours=1)
            chatroom.save()
            comp.chat_room = chatroom;

            comp.save()

            # automatically bookmark it
            prof.competitions_bookmarked.add(comp)
            prof.save()

            return HttpResponseRedirect(reverse("arena.home"))
    else:
        initial = {
            'have_theme': True,
            'have_rules': True,
            'preview_rules': True,
            'have_listening_party': True,
            'party_immediately': True,
            'vote_time_quantity': 1,
            'vote_time_measurement': TimeUnit.WEEKS,
        }
        form = CreateCompetitionForm(initial=initial)

    return render_to_response('arena/create.html', locals(),
        context_instance=RequestContext(request))

def competition(request, id):
    competition = get_object_or_404(Competition, id=int(id))
    return render_to_response('arena/competition.html', locals(),
        context_instance=RequestContext(request))

@login_required
def edit(request, comp_id_str):
    comp_id = int(comp_id_str)
    comp = get_object_or_404(Competition, id=comp_id)
    prof = request.user.get_profile()
    err_msg = ""

    # the person has to be the competition owner
    if not request.user.is_authenticated() or request.user.id != comp.host.id:
        err_msg = design.only_host_can_edit_compo
        return render_to_response('arena/cannot_edit_competition.html',
            locals(), context_instance=RequestContext(request))

    if request.method == 'POST':
        form = EditCompetitionForm(request.POST)
        if form.is_valid():
            # save edits to competition
            comp.title = form.cleaned_data.get('title')

            comp.preview_theme = form.cleaned_data.get('preview_theme', False)
            if form.cleaned_data.get('have_theme', False):
                comp.theme = form.cleaned_data.get('theme', '')
            else:
                comp.theme = ""

            comp.preview_rules = form.cleaned_data.get('preview_rules', False)
            if form.cleaned_data.get('have_rules', False):
                comp.rules = form.cleaned_data.get('rules', '')
            else:
                comp.rules = ""

            tz_offset = form.cleaned_data.get('tz_offset')
            tz_delta = timedelta(hours=tz_offset)

            comp.start_date = form.cleaned_data.get('start_date') + tz_delta
            comp.submit_deadline = form.cleaned_data.get('submission_deadline_date') + tz_delta

            # calculate vote deadline
            quantity = int(form.cleaned_data.get('vote_time_quantity'))
            quantifier = int(form.cleaned_data.get('vote_time_measurement'))

            comp.vote_period_length = quantity * TimeUnit.seconds_multiplier[quantifier]
            vote_period_delta = timedelta(seconds=comp.vote_period_length)

            if form.cleaned_data.get('have_listening_party', True):
                if form.cleaned_data.get('party_immediately'):
                    comp.changeListeningPartyStartDate(comp.submit_deadline)
                else:
                    comp.changeListeningPartyStartDate(form.cleaned_data.get('listening_party_date') + tz_delta)
            else:
                comp.changeListeningPartyStartDate(None)
            comp.save()

            chatroom = comp.chat_room
            # open the chat room an hour before the competition
            chatroom.start_date = comp.start_date - timedelta(hours=1)
            # chat room is open an hour before and after competition
            chatroom.end_date = comp.listening_party_end_date + timedelta(hours=1)
            chatroom.save()

            return HttpResponseRedirect(reverse("arena.compete", args=[comp.id]))
    else:
        # compute vote time
        for unit in sorted(TimeUnit.seconds_multiplier.keys(), reverse=True):
            seconds_mult = TimeUnit.seconds_multiplier[unit]
            if comp.vote_period_length >= seconds_mult:
                vote_time_quantity = comp.vote_period_length / seconds_mult
                vote_time_measurement = unit
                break
        initial = {
            'title': comp.title,
            'have_theme': comp.theme != "",
            'preview_theme': comp.preview_theme,
            'theme': comp.theme,
            'have_rules': comp.rules != "",
            'preview_rules': comp.preview_rules,
            'rules': comp.rules,
            'start_date': comp.start_date,
            'submission_deadline_date': comp.submit_deadline,
            'have_listening_party': comp.have_listening_party,
            'party_immediately': comp.listening_party_start_date == comp.submit_deadline,
            'listening_party_date': comp.listening_party_start_date,
            'vote_time_quantity': vote_time_quantity,
            'vote_time_measurement': vote_time_measurement,
        }
        form = EditCompetitionForm(initial=initial)

    return render_to_response('arena/edit.html', locals(),
        context_instance=RequestContext(request))

@json_login_required
def ajax_vote(request, entry_id):
    data = {'success': False}

    try:
        entry_id = int(entry_id)
    except ValueError:
        entry_id = 0
    entry = get_object_or_404(Entry, id=entry_id)

    # can't vote for yourself
    if entry.owner == request.user:
        data['reason'] = design.cannot_vote_for_yourself
        return json_response(data)

    # how many thumbs up should they have
    max_votes = max_vote_count(entry.competition.entry_set.count())
    used_votes = ThumbsUp.objects.filter(owner=request.user, entry__competition=entry.competition).count()

    if used_votes < max_votes:
        # OK! spend a vote on this entry
        vote = ThumbsUp()
        vote.owner = request.user
        vote.entry = entry
        vote.save()

        data['success'] = True
    else:
        data['reason'] = design.no_votes_left

    return json_response(data)

@json_login_required
def ajax_unvote(request, entry_id):
    data = {'success': False}

    try:
        entry_id = int(entry_id)
    except ValueError:
        entry_id = 0
    entry = get_object_or_404(Entry, id=entry_id)

    votes = ThumbsUp.objects.filter(owner=request.user, entry=entry)
    if votes.count() > 0:
        votes[0].delete()

    data['success'] = True
    return json_response(data)


import pygame
from pygame.image import load_extended
import ticks
import gui
import yaml
import math
import languages

_font_code: pygame.font.Font
_font_victory: pygame.font.Font

def init():
    global _font_code, _font_victory
    _font_code = pygame.font.Font("src/res/font/JetBrainsMono-Bold.ttf", 25)
    _font_victory = pygame.font.Font("src/res/font/JetBrainsMono-Bold.ttf", 60)

def draw_text(screen: pygame.Surface, rect: pygame.Rect, data: str, colour, font: pygame.font.Font):
    font_surface = font.render(data, True, colour)
    screen.blit(font_surface, rect)

def fill_rect(screen: pygame.Surface, rect: pygame.Rect, colour):
    c = pygame.Color(colour)
    s = pygame.Surface(rect.size)  # the size of your rect
    s.set_alpha(c.a)  # alpha level
    s.fill(c)  # this fills the entire surface
    screen.blit(s, rect.topleft)

class SlicedSprite:
    def __init__(self, image: pygame.Surface, corner_size: int = 32):
        self.image = image
        self.corner = corner_size
        self.size = image.get_size()

    def draw(self, screen: pygame.Surface, rect: pygame.Rect, sizes: tuple, gap: int = 0):
        sizes = (max(sizes[0], 0), max(sizes[1], 1))
        # Top left
        screen.blit(
            self.image, rect,
            pygame.Rect(0, 0, self.corner, self.corner)
        )
        # Top
        top = pygame.transform.scale(
            self.image.subsurface(
                pygame.Rect(
                    self.corner, 0, self.size[0] - 2 * self.corner, self.corner
                )
            ).copy(),
            (sizes[0], self.corner)
        )
        screen.blit(top, (rect.x + self.corner, rect.y))
        # Top right
        screen.blit(
            self.image, (rect.x + sizes[0] + self.corner, rect.y),
            pygame.Rect(self.size[0] - self.corner, 0, self.corner, self.corner)
        )
        # Left
        left = pygame.transform.scale(
            self.image.subsurface(
                pygame.Rect(
                    0, self.corner, self.corner, self.size[1] - 2 * self.corner
                )
            ).copy(),
            (self.corner, sizes[1])
        )
        screen.blit(left, (rect.x, rect.y + self.corner))
        # Centre
        centre = pygame.transform.scale(
            self.image.subsurface(
                pygame.Rect(
                    self.corner, self.corner, self.size[0] - 2 * self.corner, self.size[1] - 2 * self.corner
                )
            ).copy(),
            (sizes[0], sizes[1] - gap)
        )
        screen.blit(centre, (rect.x + self.corner, rect.y + self.corner))
        # Right
        right = pygame.transform.scale(
            self.image.subsurface(
                pygame.Rect(
                    self.size[0] - self.corner, self.corner, self.corner, self.size[1] - 2 * self.corner
                )
            ).copy(),
            (self.corner, sizes[1] - gap)
        )
        screen.blit(right, (rect.x + sizes[0] + self.corner, rect.y + self.corner))
        # Bottom left
        screen.blit(
            self.image, (rect.x, rect.y + sizes[1] + self.corner),
            pygame.Rect(0, self.size[1] - self.corner, self.corner, self.corner)
        )
        # Bottom
        bottom = pygame.transform.scale(
            self.image.subsurface(
                pygame.Rect(
                    self.corner, self.size[1] - self.corner, self.size[0] - 2 * self.corner, self.corner
                )
            ).copy(),
            (sizes[0], self.corner)
        )
        screen.blit(bottom, (rect.x + self.corner, rect.y + sizes[1] + self.corner))
        # Bottom right
        screen.blit(
            self.image, (rect.x + sizes[0] + self.corner, rect.y + sizes[1] + self.corner),
            pygame.Rect(self.size[0] - self.corner, self.size[1] - self.corner, self.corner, self.corner)
        )

texture_atlas: pygame.Surface = load_extended("src/res/textures/atlas.png")
texture_res = 32
robot_atlas: pygame.Surface = load_extended("src/res/textures/robot.png")
coin_atlas: pygame.Surface = load_extended("src/res/textures/coin.png")
entity_res = 256
entity_ground = 192
click_threshold = 150
step_delay = 200
cursor_h = 16
win_rot_speed = 0.03
editor_scroll_speed = 40
render_zoom_speed = .2

# Each sprite uses a different dark colour against white bold text, and
# two adjacent sprites have very distinct colours. This was made with
# colour blindness in mind, to help create greater contrast between the
# code blocks.
code_block_textures: list = [
    SlicedSprite(load_extended("src/res/textures/blocks/block_red.png")),
    SlicedSprite(load_extended("src/res/textures/blocks/block_blue.png")),
    SlicedSprite(load_extended("src/res/textures/blocks/block_yellow.png")),
    SlicedSprite(load_extended("src/res/textures/blocks/block_green.png")),
    SlicedSprite(load_extended("src/res/textures/blocks/block_orange.png")),
    SlicedSprite(load_extended("src/res/textures/blocks/block_pink.png")),
]

class Level:
    def __init__(self, filename: str):
        text: str
        with open(filename, encoding='utf-8') as file:
            text = file.read()
        data = yaml.safe_load(text)
        # Available blocks
        self.blocks = data["blocks"]
        # Map data
        self.map = data["map"]
        # Map surface
        self.height = len(self.map)
        self.width = 0
        if self.height > 0:
            for row in self.map:
                if len(row) != len(self.map[0]):
                    raise ValueError("Invalid map, inconsistent row lengths.")
            self.width = len(self.map[0])
        self.level_map = pygame.Surface((self.width * texture_res, self.height * texture_res), pygame.SRCALPHA)

        player = data["player"]
        self.start_x = player["start-x"]
        self.start_y = player["start-y"]
        self.start_dir = player["start-dir"]

        stars = data["stars"]
        self.min_coins = stars["coins"]
        self.min_blocks = stars["blocks"]

        # Level number
        self.number = data["number"]

        for i in range(self.height):
            for j in range(self.width):
                tile = self.map[i][j]
                area = pygame.Rect(tile * texture_res, 0, texture_res, texture_res)
                dest = (j * texture_res, i * texture_res)
                self.level_map.blit(texture_atlas, dest, area)

    def get_block(self, x: int, y: int) -> int:
        if x < 0 or x >= self.width or y < 0 or y >= self.height:
            return 0
        return self.map[y][x]

class Game:
    def __init__(self):
        # Code
        self.code = None
        self.gen = None
        self.steps = 1
        self.code_start = 0
        # Blocks (source)
        self.blocks = []
        # UI interactions
        self.block_dragged = None
        self.block_offset = (0, 0)
        self.click_start = -1
        self.click_start_pos = None
        self.click_type = 0  # 1 = pan, 2 = drag block
        # 3D angles
        self.yaw = math.pi / 4
        self.pitch = math.pi / 6
        self.scroll = 1
        self.zoom = 1
        self.old_view = (0, 0)  # (yaw, pitch)
        # UI integrations
        self.elem = None  # Container for rendering game scene
        self.level = None  # Current level object
        self.enabled = False  # false = in title screen
        self.coins = []
        # Game
        self.robot_x = 0
        self.robot_y = 0
        self.robot_dir = 0
        # Levels
        self.state = 0
        self.coin_counter = 0
        # Level stats
        self.unlocked_level = 1
        self.levels = []

    def enable(self, document: gui.DocumentXML, level: Level, screen: pygame.Surface):
        self.elem = document.ids["game"]
        self.level = level
        self.enabled = True
        lvl_size = max(self.level.width, self.level.height) * texture_res

        self.zoom = min(self.elem.rect.w / 2, self.elem.rect.h / 2) / lvl_size
        self.scroll = 1
        self.yaw = math.pi / 4
        self.pitch = math.pi / 6

        self.code = Code(document)

        self.robot_x = level.start_x
        self.robot_y = level.start_y
        self.robot_dir = level.start_dir

        btnlist: gui.Element = document.ids["blocklist"]
        btnlist.children.clear()
        self.blocks = []

        self.coins = []
        for i, row in enumerate(self.level.map):
            for j, col in enumerate(row):
                if col == 4:
                    self.coins.append((j, i))

        for i, b in enumerate(level.blocks):
            block = defined_blocks[b]
            self.blocks.append(Blocklist(document, *block))
        document.calc_draw(screen.get_clip())
        for b in self.blocks:
            b.update()
        document.calc_draw(screen.get_clip())
        document.hover_element = document.trace_element(pygame.mouse.get_pos())

    def disable(self):
        self.elem = None
        self.level = None
        self.enabled = False
        self.state = 0
        self.gen = None
        self.blocks = []

    def handle_event(self, _: pygame.Surface, event: pygame.event.Event):
        if not self.enabled:
            return
        mpos = pygame.mouse.get_pos()

        if event.type == pygame.MOUSEBUTTONDOWN:
            if event.button == 1:
                self.click_start = ticks.get_time()
                self.click_start_pos = mpos
                if self.elem.rect.collidepoint(mpos):
                    self.click_type = 1  # drag/pan
                    self.old_view = (self.yaw, self.pitch)
                else:
                    for b in self.blocks:
                        b: Blocklist
                        if b.block.get_box().collidepoint(mpos):
                            self.click_type = 2
                            self.block_dragged = b.get_new_block(pygame.Rect(mpos[0], mpos[1], 0, 0))
                            self.block_offset = (mpos[0] - b.block.pos.x, mpos[1] - b.block.pos.y)
                            break
                    else:
                        ret = self.code.trace_block(mpos)
                        rect: pygame.Rect = self.code.elem.rect
                        if ret is not None:
                            self.click_type = 3
                            self.block_dragged = ret[1]
                            self.block_offset = (mpos[0] - ret[1].pos.x - rect.x, mpos[1] - ret[1].pos.y - rect.y)
                            self.code.remove_block(ret[0])
            elif event.button == 4:
                if self.elem.rect.collidepoint(mpos):
                    self.scroll = min(self.scroll + render_zoom_speed, 4)
                elif self.code.elem.rect.collidepoint(mpos):
                    self.code.scroll = min(self.code.scroll + editor_scroll_speed, 0)
            elif event.button == 5:
                if self.elem.rect.collidepoint(mpos):
                    self.scroll = max(self.scroll - render_zoom_speed, 0.5)
                elif self.code.elem.rect.collidepoint(mpos):
                    self.code.scroll -= editor_scroll_speed
        elif event.type == pygame.MOUSEBUTTONUP and event.button == 1:
            click_duration = ticks.get_time() - self.click_start
            if self.click_type == 2:
                if self.code.elem.rect.collidepoint(mpos):
                    self.code.place_block(self.block_dragged)
                elif click_duration < click_threshold:
                    self.code.place_block(self.block_dragged)
            elif self.click_type == 3:
                if self.code.elem.rect.collidepoint(mpos):
                    self.code.place_block(self.block_dragged)
            self.click_type = 0
            self.click_start = -1
        elif event.type == pygame.VIDEORESIZE:
            lvl_size = max(self.level.width, self.level.height) * texture_res
            self.zoom = min(self.elem.rect.w / 2, self.elem.rect.h / 2) / lvl_size

    def lose(self):
        self.gen = None
        self.state = 2

    def win(self):
        self.unlocked_level = max(self.unlocked_level, self.level.number + 1)
        self.gen = None
        self.state = 1

        level_stats = (
            True,
            self.code.get_block_count() <= self.level.min_blocks,
            self.coin_counter >= self.level.min_coins
        ).count(True)
        level_idx = self.level.number - 1
        if level_idx < len(self.levels):
            self.levels[level_idx] = max(level_stats, self.levels[level_idx])
        elif level_idx == len(self.levels):
            self.levels.append(level_stats)

    def update(self):
        if not self.enabled:
            return
        mpos = pygame.mouse.get_pos()
        now = ticks.get_time()
        for b in self.blocks:
            b.update()
        self.code.update()
        if self.gen is not None:
            if self.steps * step_delay < now - self.code_start:
                try:
                    move = next(self.gen)
                    self.move_bot(move)
                    self.steps += 1
                except StopIteration:
                    self.gen = None
        if self.state > 0:
            self.update_position(self.yaw + win_rot_speed)
        if self.click_start > -1:
            click_duration = ticks.get_time() - self.click_start
            if self.click_type == 1:
                n_yaw = (mpos[0] - self.click_start_pos[0]) / 100 + self.old_view[0]
                n_pitch = (mpos[1] - self.click_start_pos[1]) / 400 + self.old_view[1]
                self.update_position(n_yaw, n_pitch, None)
            elif self.click_type == 2:
                if click_duration >= click_threshold:
                    self.block_dragged: Codeblock
                    self.block_dragged.pos = pygame.Rect(
                        mpos[0] - self.block_offset[0],
                        mpos[1] - self.block_offset[1],
                        0, 0
                    )
                    self.block_dragged.update(None, self.block_dragged.pos.topleft)
                    if self.code.elem.rect.collidepoint(mpos):
                        self.code.cursor_closest(mpos)
            elif self.click_type == 3:
                self.block_dragged: Codeblock
                self.block_dragged.pos = pygame.Rect(
                    mpos[0] - self.block_offset[0],
                    mpos[1] - self.block_offset[1],
                    0, 0
                )
                self.block_dragged.update(None, self.block_dragged.pos.topleft)
                if self.code.elem.rect.collidepoint(mpos):
                    self.code.cursor_closest(mpos)

    def update_position(self, yaw=None, pitch=None, zoom=None):
        if yaw:
            self.yaw = yaw % (math.pi * 2)
        if pitch:
            self.pitch = min(max(pitch, math.pi / 12), math.pi / 4)
        if zoom:
            self.zoom = zoom

    def run_code(self):
        self.code_start = ticks.get_time()
        self.gen = self.code.exec()
        self.steps = 1

        self.robot_x = self.level.start_x
        self.robot_y = self.level.start_y
        self.robot_dir = self.level.start_dir

        self.state = 0
        self.coins = []
        self.coin_counter = 0
        for i, row in enumerate(self.level.map):
            for j, col in enumerate(row):
                if col == 4:
                    self.coins.append((j, i))

    def move_bot(self, move):
        old_pos = self.robot_x, self.robot_y
        if move == "forward":
            pos_offset = (
                (0, -1),
                (-1, 0),
                (0, 1),
                (1, 0),
            )[self.robot_dir]
            self.robot_x += pos_offset[0]
            self.robot_y += pos_offset[1]
        elif move == "left":
            self.robot_dir = (self.robot_dir + 1) % 4
        elif move == "right":
            self.robot_dir = (self.robot_dir - 1) % 4

        block = self.level.get_block(self.robot_x, self.robot_y)

        collected = None
        for c in self.coins:
            if (self.robot_x, self.robot_y) == c:
                collected = c
        if collected is not None:
            self.coins.remove(collected)
            self.coin_counter += 1

        if block == 0:
            self.robot_x, self.robot_y = old_pos
        elif block == 1:
            self.lose()
        elif block == 3:
            self.win()

    def draw(self, screen: pygame.Surface):
        if not self.enabled:
            return
        self.code.render(screen)
        self.render_scene(screen)
        dest = screen.subsurface(self.elem.rect)
        for b in self.blocks:
            b: Blocklist
            b.draw(screen)
        if self.click_start > -1:
            click_duration = ticks.get_time() - self.click_start
            if self.click_type == 2:
                if click_duration >= click_threshold:
                    self.block_dragged: Codeblock
                    self.block_dragged.draw(screen)
            elif self.click_type == 3:
                self.block_dragged: Codeblock
                self.block_dragged.draw(screen)
        if self.state == 1:
            text = languages.get_str("level.win")
            text_width = _font_victory.size(text)[0]
            pos = pygame.Rect((self.elem.rect.w - text_width) / 2, 10, 0, 0)
            draw_text(dest, pos, text, 0xFF00FFFF, _font_victory)
        elif self.state == 2:
            text = languages.get_str("level.lose")
            text_width = _font_victory.size(text)[0]
            pos = pygame.Rect((self.elem.rect.w - text_width) / 2, 10, 0, 0)
            draw_text(dest, pos, text, 0xFF00FFFF, _font_victory)

    def render_scene(self, screen: pygame.Surface):
        true_zoom = self.zoom * self.scroll
        dest = screen.subsurface(self.elem.rect)
        # Transform axis vectors
        x_axis = pygame.math.Vector2(texture_res * true_zoom, 0)
        y_axis = pygame.math.Vector2(0, texture_res * true_zoom)
        x_axis = x_axis.rotate(math.degrees(-self.yaw))
        y_axis = y_axis.rotate(math.degrees(-self.yaw))
        x_axis.y *= math.sin(self.pitch)
        y_axis.y *= math.sin(self.pitch)
        # Transform rendered map
        map_render = pygame.transform.rotozoom(self.level.level_map, math.degrees(self.yaw), true_zoom)
        map_render = pygame.transform.scale(
            map_render,
            (int(map_render.get_width()),
             int(map_render.get_height() * math.sin(self.pitch)))
        )
        # pos_x = self.elem.rect.x + (self.elem.rect.w / 2)
        # pos_y = self.elem.rect.y + (self.elem.rect.h / 2)
        pos_x = self.elem.rect.w / 2
        pos_y = self.elem.rect.h / 2
        # Scale robot sprite
        angle = ((self.yaw + self.robot_dir * math.pi / 2 + math.pi * 9 / 8) % (math.pi * 2)) * 8 / (math.pi * 2)
        bot_render = robot_atlas.subsurface(
            pygame.Rect(
                int(angle) * entity_res,
                0,
                entity_res,
                entity_res
            )
        ).copy()
        coin_state = int(ticks.get_time() / 250 - self.yaw * 4 / (math.pi * 2)) % 4
        # coin_state = int(ticks.get_time() / 250) % 4
        coin_render = coin_atlas.subsurface(
            pygame.Rect(
                coin_state * entity_res,
                0,
                entity_res,
                entity_res
            )
        ).copy()
        # Entities
        entities = []
        for c in self.coins:
            entities.append(
                (pygame.transform.rotozoom(coin_render, 0, true_zoom / (entity_res / 32)), *c)
            )
        entities.append((
            pygame.transform.rotozoom(bot_render, 0, true_zoom / (entity_res / 32)),
            self.robot_x,
            self.robot_y,
        ))
        to_render = []
        for e in entities:
            ent_pos_real = (e[1] - self.level.width / 2 + .5, e[2] - self.level.height / 2 + .5)
            ent_pos = x_axis * ent_pos_real[0] + y_axis * ent_pos_real[1] + pygame.math.Vector2(pos_x, pos_y)
            to_render.append((e[0], ent_pos.x, ent_pos.y))
        to_render.sort(key=lambda x: x[2])
        # Rendering
        dest.blit(map_render, (pos_x - map_render.get_width() / 2, pos_y - map_render.get_height() / 2))

        for e in to_render:
            # print(e)
            dest.blit(
                e[0],
                (
                    e[1] - (entity_res * true_zoom) / (entity_res / 16),
                    e[2] - (entity_ground * true_zoom) / (entity_res / 32)
                )
            )

# Source of blocks, from which they spawn and get dragged out of
class Blocklist:
    def __init__(self, document: gui.DocumentXML, block_name: str, colour: int = 0, container: bool = False, times=4):
        btnlist: gui.Element = document.ids["blocklist"]
        self.elem = gui.Space(document, "space", { "margin": "60px" })
        btnlist.add_child(self.elem)
        self.name = block_name
        self.colour = colour
        self.container = container
        self.times = times
        if container:
            self.sprite = code_block_textures[colour]
            self.block = CodeContainer(code_block_textures[colour], block_name, pygame.Rect(0, 0, 0, 0), times)
        else:
            self.sprite = code_block_textures[colour]
            self.block = Codeblock(code_block_textures[colour], block_name, pygame.Rect(0, 0, 0, 0))

    def get_new_block(self, rect: pygame.Rect):
        if self.container:
            return CodeContainer(self.sprite, self.name, rect, self.times)
        else:
            return Codeblock(self.sprite, self.name, rect)

    def update(self):
        self.block.pos = self.elem.rect
        self.block.pos = pygame.Rect(self.elem.rect.move(-48, -48))
        self.elem.length = f"{self.block.get_box().w}px"

    def draw(self, screen: pygame.Surface):
        self.block.draw(screen)

defined_blocks = {
    "left": ("left", 0, False),
    "forward": ("forward", 1, False),
    "right": ("right", 2, False),
    "repeat4": ("repeat4", 3, True, 4),
    "repeat8": ("repeat8", 4, True, 8),
}

class Codeblock:
    def __init__(self, sprite: SlicedSprite, name: str, pos: pygame.Rect):
        self.sprite = sprite
        self.name = name
        self.pos = pos

    def draw(self, screen: pygame.Surface):
        text = languages.get_str("level.blocks." + self.name)
        size = _font_code.size(text)
        size = (size[0] - 32, size[1] - 32)
        self.sprite.draw(screen, self.pos, size)
        draw_text(
            screen,
            pygame.Rect(self.pos.x + 16, self.pos.y + 16, 0, 0),
            text,
            0xFFFFFFFF,
            _font_code
        )

    def get_box(self, off: tuple = (0, 0)) -> pygame.Rect:
        size = _font_code.size(languages.get_str("level.blocks." + self.name))
        return pygame.Rect(
            self.pos.x + off[0],
            self.pos.y + off[1],
            size[0] + 32,
            size[1] + 32
        )

    def update(self, cursors, off: tuple):
        pass

class CodeContainer(Codeblock):
    def __init__(self, sprite: SlicedSprite, name: str, pos: pygame.Rect, times):
        super().__init__(sprite, name, pos)
        self.children = []
        self.height = 0
        self.times = times

    def draw(self, screen: pygame.Surface):
        for i, b in enumerate(self.children):
            b: Codeblock
            b.draw(screen)

        text = languages.get_str("level.blocks." + self.name)
        size = _font_code.size(text)
        size = (size[0] - 32, size[1] + self.height)
        self.sprite.draw(screen, self.pos, size, self.height)
        draw_text(
            screen,
            pygame.Rect(self.pos.x + 16, self.pos.y + 16, 0, 0),
            text,
            0xFFFFFFFF,
            _font_code
        )

    def get_block_count(self):
        count = 0
        for b in self.children:
            if type(b) is CodeContainer:
                b: CodeContainer
                count += b.get_block_count()
            count += 1
        return count

    def update(self, cursors, off: tuple):
        current_y = 0
        size = _font_code.size(languages.get_str("level.blocks." + self.name))
        off = (off[0] + 32, off[1] + 32 + size[1])
        for i, b in enumerate(self.children):
            b: Codeblock
            if cursors is not None and len(cursors) > 0 and cursors[0] == i:
                if len(cursors) == 1:
                    current_y += cursor_h
                n_off = (off[0], off[1] + current_y)
                b.update(cursors[1:], n_off)
            else:
                n_off = (off[0], off[1] + current_y)
                b.update(None, n_off)
            b.pos = pygame.Rect(n_off[0], n_off[1], 0, 0)
            current_y += b.get_box().h
        if cursors is not None and len(cursors) > 0 and cursors[0] == len(self.children):
            current_y += cursor_h
        self.height = current_y

    def draw_cursor(self, screen: pygame.Surface, cursors, off: tuple):
        current_y = 0
        size = _font_code.size(languages.get_str("level.blocks." + self.name))
        off = (off[0] + 32, off[1] + 32 + size[1])
        for i, b in enumerate(self.children):
            b: Codeblock
            if cursors is not None and len(cursors) > 0 and cursors[0] == i:
                if len(cursors) == 1:
                    fill_rect(screen, pygame.Rect(off[0], off[1] + current_y, 1000, cursor_h), 0xFFFFFF1F)
                    current_y += cursor_h
                n_off = (off[0], off[1] + current_y)
                if type(b) is CodeContainer:
                    b: CodeContainer
                    b.draw_cursor(screen, cursors[1:], n_off)
            current_y += b.get_box().h
        if cursors is not None and len(cursors) > 0 and cursors[0] == len(self.children):
            fill_rect(screen, pygame.Rect(off[0], off[1] + current_y, 1000, cursor_h), 0xFFFFFF1F)
            current_y += cursor_h

    def trace_block(self, pos: tuple, off: tuple):
        for i, b in enumerate(self.children):
            b: Codeblock
            if b.get_box(off).collidepoint(pos):
                if type(b) is CodeContainer:
                    b: CodeContainer
                    ret = b.trace_block(pos, off)
                    if ret is None:
                        return [i], b
                    else:
                        return [i, *ret[0]], ret[1]
                else:
                    return [i], b
        return None

    def cursor_closest(self, pos: tuple, off: pygame.Rect, stack: list):
        vpos = pygame.Vector2(pos)
        size = _font_code.size(languages.get_str("level.blocks." + self.name))
        closest = [*stack, 0]
        closest_dis = pygame.Vector2(
            self.pos.x + off.x,
            self.pos.y + 32 + size[1] + off.y
        ).distance_squared_to(vpos)
        for i, b in enumerate(self.children):
            b: Codeblock
            block_pos = (b.pos.x + off.x, b.pos.y + off.y)
            this_dis = pygame.Vector2(block_pos).distance_squared_to(vpos)
            if closest_dis is None or this_dis < closest_dis:
                closest = [*stack, i]
                closest_dis = this_dis
            if type(b) is CodeContainer:
                b: CodeContainer
                this_cur, this_dis = b.cursor_closest(pos, off, [*stack, i])
                if this_dis < closest_dis:
                    closest = this_cur
                    closest_dis = this_dis
        if len(self.children) > 0:
            last_b = self.children[-1]
            block_pos = (last_b.pos.x + off.x, last_b.pos.y + off.y + last_b.get_box().h)
            last_dis = pygame.Vector2(block_pos).distance_squared_to(vpos)
            if last_dis < closest_dis:
                closest = [*stack, len(self.children)]
                closest_dis = last_dis

        # self.set_cursor(closest)
        return closest, closest_dis

    def get_box(self, off: tuple = (0, 0)) -> pygame.Rect:
        size = _font_code.size(languages.get_str("level.blocks." + self.name))
        return pygame.Rect(
            self.pos.x + off[0],
            self.pos.y + off[1],
            size[0] + 32,
            size[1] + 64 + self.height
        )

    def exec(self):
        for b in self.children:
            b: Codeblock
            if type(b) is CodeContainer:
                b: CodeContainer
                for i in range(b.times):
                    yield from b.exec()
            else:
                yield b.name

class Code:
    def __init__(self, document: gui.DocumentXML):
        self.elem: gui.Element = document.ids["code"]
        self.scroll = 0
        self.blocks = []
        self.cursors = [0]  # For nested blocks

    def get_block_count(self):
        count = 0
        for b in self.blocks:
            if type(b) is CodeContainer:
                b: CodeContainer
                count += b.get_block_count()
            count += 1
        return count

    def place_block(self, block: Codeblock):
        context = self.blocks
        for c in self.cursors[:-1]:
            context = context[c].children
        context.insert(self.cursors[-1], block)
        self.cursors[-1] += 1

    def remove_block(self, index: list):
        context = self.blocks
        for c in index[:-1]:
            context = context[c].children
        if index[-1] < self.cursors[-1]:
            self.cursors[-1] -= 1
        context.pop(index[-1])

    def trace_block(self, pos: tuple):
        for i, b in enumerate(self.blocks):
            b: Codeblock
            rect: pygame.Rect = self.elem.rect
            if b.get_box(rect.topleft).collidepoint(pos):
                if type(b) is CodeContainer:
                    b: CodeContainer
                    ret = b.trace_block(pos, rect.topleft)
                    if ret is None:
                        return [i], b
                    else:
                        return [i, *ret[0]], ret[1]
                else:
                    return [i], b
        return None

    def cursor_closest(self, pos: tuple):
        vpos = pygame.Vector2(pos)
        closest = [0]
        closest_dis = None
        for i, b in enumerate(self.blocks):
            b: Codeblock
            block_pos = (b.pos.x + self.elem.rect.x, b.pos.y + self.elem.rect.y)
            this_dis = pygame.Vector2(block_pos).distance_squared_to(vpos)
            if closest_dis is None or this_dis < closest_dis:
                closest = [i]
                closest_dis = this_dis
            if type(b) is CodeContainer:
                b: CodeContainer
                this_cur, this_dis = b.cursor_closest(pos, self.elem.rect, [i])
                if this_dis < closest_dis:
                    closest = this_cur
                    closest_dis = this_dis
        if len(self.blocks) > 0:
            last_b = self.blocks[-1]
            block_pos = (last_b.pos.x + self.elem.rect.x, last_b.pos.y + self.elem.rect.y + last_b.get_box().h)
            last_dis = pygame.Vector2(block_pos).distance_squared_to(vpos)
            if last_dis < closest_dis:
                closest = [len(self.blocks)]
                closest_dis = last_dis

        self.set_cursor(closest)
        return closest, closest_dis

    def update(self):
        current_y = self.scroll
        for i, b in enumerate(self.blocks):
            b: Codeblock
            if i == self.cursors[0]:
                if len(self.cursors) == 1:
                    current_y += cursor_h
                b.update(self.cursors[1:], (0, current_y))
            else:
                b.update(None, (0, current_y))
            b.pos = pygame.Rect(0, current_y, 0, 0)
            current_y += b.get_box().h

    def draw_cursor(self, screen: pygame.Surface):
        current_y = self.scroll
        for i, b in enumerate(self.blocks):
            b: Codeblock
            if i == self.cursors[0]:
                if len(self.cursors) == 1:
                    fill_rect(screen, pygame.Rect(0, current_y, 1000, cursor_h), 0xFFFFFF1F)
                    current_y += cursor_h
                if type(b) is CodeContainer:
                    b: CodeContainer
                    b.draw_cursor(screen, self.cursors[1:], (0, current_y))
            current_y += b.get_box().h
        if self.cursors is not None and len(self.cursors) > 0 and self.cursors[0] == len(self.blocks):
            fill_rect(screen, pygame.Rect(0, current_y, 1000, cursor_h), 0xFFFFFF1F)
            current_y += cursor_h

    def set_cursor(self, path: list):
        self.cursors = path
        self.update()

    def render(self, screen: pygame.Surface):
        dest = screen.subsurface(self.elem.rect)
        for i, b in enumerate(self.blocks):
            b: Codeblock
            b.draw(dest)
        self.draw_cursor(dest)

    def exec(self):
        for b in self.blocks:
            b: Codeblock
            if type(b) is CodeContainer:
                b: CodeContainer
                for i in range(b.times):
                    yield from b.exec()
            else:
                yield b.name

    # def reset(self):
    #     pass
    #
    # def step(self):
    #     pass