from flask import request, render_template, abort, send_from_directory
import flask  # For creating a web application interface
from Crypto.Hash import SHA256
import sys
sys.path.append("/root/projs/APathThroughDust")
from util.DBCommand import getDBConnection, RegUser2DB, getPubKey
import os
import util.rsaSignVerify as rsautil

app = flask.Flask(__name__)

# Welcome page
@app.route('/', methods=['GET'])
def welcome():
    return render_template("welcome.html")

# User register
@app.route('/register', methods=['GET', 'POST'])
def register():
    if request.method == 'POST':
        username = request.form['username']
        usertype = request.form['usertype']
        duration = request.form['duration']
        cost = 0
        '''
        print(f"Username: {username}")
        print(f"User Type: {usertype}")
        print(f"Duration: {duration}")
        '''
        if not username or not usertype or not duration:
            abort(400)  # 触发400错误，跳转到错误页面
        elif username.__len__() > 15:
            render_template("用户名过长，请重试")
            return render_template("register.heml")
        if usertype == 'personal':
            cost = int(duration)
        else:
            cost = int(duration) * 1000
        return render_template('ConfirmRegister.html', username=username, usertype=usertype, duration=duration,
                               cost=cost)
    return render_template('register.html')

'''
确定注册（缴费）只能跳转，不能直接访问
生成密钥对
向数据库写入用户信息
    昵称--VARCHAR(50)
    注册时间--date
    公钥--BINARY(1500)
    用户类型--INT
        1：个人用户
        2：企业用户
    有效时间--INT

提示用户下载私钥（privatekeyURL）
'''

@app.route('/confirmReg', methods=['POST'])
def confirm():
    keypem, pubkeypem = rsautil.rsaKeyPairGen()
    username = request.form['username']
    usertype = request.form['usertype']
    duration = request.form['duration']
    if (not RegUser2DB(username, usertype, duration, pubkeypem)):
        return "注册出现错误"

    # 将私钥写入临时文件
    filePath = 'Resources/TmpKeys/' + username + 'privatekey.pom'  # 私钥临时文件的相对地址
    keyfile = open(filePath, 'wb')
    keyfile.write(keypem)
    keyfile.close()

    temp_url = 'localhost:5000/tempkeys/' + username + 'privatekey.pom'
    return render_template("postRegister.html", privatekeyURL=temp_url)

'''
私钥下载页面
'''
@app.route('/tempkeys/<filename>', methods=['GET'])
def serve_temp_file(filename):
    # 临时目录
    #print('进入私钥下载流程')
    relativeFilePathtoProj = 'Resources/TmpKeys/' + filename
    abPath=os.path.abspath("/root/projs/APathThroughDust")
    # 发送文件
    return send_from_directory(abPath+'/', relativeFilePathtoProj, as_attachment=True)

''''
实名认证页面
向保存实名信息的数据库写入信息
    昵称--VARCHAR(50),
    真实姓名 VARCHAR(50),
    真实姓名+身份证的SHA256哈希值 BINARY(32),

'''
@app.route('/RealNameAuth',methods=['GET', 'POST'])
def realNameAuthentication():
    if request.method=='GET':
        return render_template('realNameAuth.html')

    #从表单中提取信息
    print('收到实名认证POST请求')
    nickName = request.form['nickname']
    realName = request.form['real_name']
    idNum    = request.form['id_number']
    sig      = request.form['signature']

    #print(nickName)
    #print(realName)
    #print(idNum)

    #确认昵称已经注册
    if(not is_nickname_registered(nickName)):
        return "还未注册，请返回主界面注册"
    pubKey = getPubKey(nickName)
    if(rsautil.verify_sign(realName+"+"+idNum,sig,pubKey)):
        __insert_user_real_name(nickName,realName,idNum)
        return "注册成功"
    return "签名错误，请确认私钥无误后重新提交实名认证申请"

# 确认昵称是否被注册
def is_nickname_registered(nickname):
    # 连接到MySQL数据库
    connection = getDBConnection()

    try:
        with connection.cursor() as cursor:
            # 查询昵称是否存在
            sql = "SELECT COUNT(*) as count FROM ChainUser WHERE nickname = %s"
            cursor.execute(sql, (nickname,))
            result = cursor.fetchone()

            count = result['count']
            if count > 0:
                return True
            else:
                return False

    finally:
        # 关闭数据库连接
        connection.close()

# 向实名信息数据库中新增行
def __insert_user_real_name(nickname, real_name, id_number):
    # 计算真实姓名和身份证号的SHA256哈希值
    h = SHA256.new()
    h.update(bytearray(real_name + id_number))
    hash_value = h.digest()

    # 连接到MySQL数据库
    connection = getDBConnection()

    try:
        with connection.cursor() as cursor:
            # 插入用户实名信息
            sql = "INSERT INTO UserRealName (nickname, real_name, hash_value) VALUES (%s, %s, %s)"
            cursor.execute(sql, (nickname, real_name, hash_value))

        # 提交事务
        connection.commit()
        print("用户实名信息已成功插入数据库！")

    finally:
        # 关闭数据库连接
        connection.close()

# Running the Web App
app.run(host='0.0.0.0', port=5000)


# Create your views here.

from halfmakery import forms
from django.shortcuts import render, redirect
from halfmakery.models import Approach, Milestone, Task, Attempt, Address, Comment, Category, Subcategory, Idea
from django.contrib.auth.models import User

# Interaction with Blockchains.

def get_recipients_and_total(txid, currency='BTC'):
    import requests, json
    r = requests.get('https://blockchain.info/tx-index/%s?format=json' % txid)
    c = json.loads(r.content)
    addrs = [d['addr'] for d in c['out'] if 'addr' in d]
    addresses = Address.objects.all().filter(address__in=addrs)
    recipients = []
    satoshis = 0
    for address in addresses:
        recipients.append(address.user.id)
        satoshis += int(c['out'][addrs.index(address.address)]['value'])
    return (recipients, satoshis)

# Limit of most people's short term memory
MAX_MILESTONES_COUNT = False

def approach_add(request, template_name='halfmakery/add_approach_tpl.html'):
    """ This view will be used to add
        approaches, depending on the POST and GET variables we get."""
    form = forms.ApproachForm(request.POST or None)
    validity = False
    if form.is_valid():
        validity = True
        approach = form.save(commit=False)
        approach.user = request.user
        approach.save()
        return redirect('/approach/'+str(approach.id))
    return render(request, template_name, {'form': form,
                                           'valid': validity,
                                           'form_action': '/approach/' })

def approach_view(request, approach_id, template_name='halfmakery/approach_tpl.html'):
    """ This view will be used to edit
        approaches, depending on the POST and GET variables we get."""
    approach = Approach.objects.get(id=approach_id)
    # approach.user = request.user
    form = forms.ApproachForm(request.POST or None, instance=approach)
    if form.is_valid():
        form.save()

    milestone_form = forms.MilestoneForm(initial={'approach': approach_id})
    milestones = Milestone.objects.all().filter(approach_id=approach_id).order_by('priority')

    def comment_proc(request, approach, approach_id):
        edit_comment_id = request.GET.get('comment', False)
        if edit_comment_id: edit_comment_id = int(edit_comment_id);
        comments = Comment.objects.all().filter(approach_id=approach_id, milestone_id=None, task_id=None, attempt_id=None)
        comment_editing_form = forms.CommentForm(request.POST or None, initial={'approach': approach})
        if comment_editing_form.is_valid():
            comment_editing_form.save()
        for comment in comments:
            if comment.id == edit_comment_id:
                comment_editing_form = forms.CommentForm(instance=comment)
        if request.GET.get('target',False) == 'content':
            comment_editing_form = forms.CommentForm(None, initial={'approach': approach})
        comment_editing_form.fields['txid'].widget.attrs['placeholder'] = 'Optional.'
        return comments, edit_comment_id, comment_editing_form

    comments, edit_comment_id, comment_editing_form = comment_proc(request, approach, approach_id)

    milestone_list = []
    for item in milestones:
        task_count = Task.objects.all().filter(milestone=item).count()
        coin_count = float(sum([i.satoshis for i in Comment.objects.all().filter(milestone=item,currency=1)]))/(10**8)
        milestone_list.append((item, task_count, coin_count))

    # / Comments
    return render(request, template_name, {'form': form,
                                           'approach': approach,
                                           'milestones': milestone_list,
                                           'milestone_form': milestone_form,
                                           'milestones_limit_reached': (milestones.count() >= MAX_MILESTONES_COUNT) and (MAX_MILESTONES_COUNT > 0),
                                           'req': request,
                                           'form_action': '/approach/'+str(approach_id),
                                           'comments': comments,
                                           'edit_comment_id': edit_comment_id,
                                           'comment_editing_form': comment_editing_form,
                                           'level_id': approach_id,
                                           'info': request.GET.get('info',False)})

def approach_action(request, approach_id, action):
    """ This view will be used to delete, and execute actions of its depenents
        approaches, depending on the POST and GET variables we get."""

    # Delete Approach
    if action == 'delete':
        approach = Approach.objects.get(id=approach_id)
        approach.delete()
        return redirect('/')

    # Create Milestone
    elif action == 'milestone':
        form = forms.MilestoneForm(request.POST or None)
        if (Milestone.objects.all().filter(approach_id=approach_id).count() < MAX_MILESTONES_COUNT) or (MAX_MILESTONES_COUNT == False):
            if form.is_valid():
                milestone = form.save(commit=False)
                milestone.user = request.user
                priorities = [m.priority for m in Milestone.objects.all().filter(approach_id=approach_id)]
                if priorities:
                    milestone.priority = max(priorities) + 1
                else:
                    milestone.priority = 0
                milestone.save()
        else:
            """MAX_MILESTONES_COUNT reached."""
            pass

    # Create Comment
    if action == 'comment':
        form = forms.CommentForm(request.POST or None)
        if form.is_valid():
            comment = form.save(commit=False)
            comment.user = request.user
            try:
                recipients, total = get_recipients_and_total(comment.txid)
                __this_txid_is_used__ = Comment.objects.all().filter(txid=comment.txid)
                if not __this_txid_is_used__:
                    comment.satoshis = total
                    comment.save()
                    comment.recipients = recipients
                else:
                    return redirect('/approach/%s?info=used_txid' % approach_id)
            except:
                # Optional Txid.
                comment.satoshis = 0
                comment.save()
                return redirect('/approach/%s?info=invalid_txid' % approach_id)

    return redirect('/approach/%s' % approach_id)

def milestone_action(request, approach_id, milestone, milestone_id, action, template_name='halfmakery/approach_tpl.html'):
    """ Delete a Milestone. """

    if action == 'delete':
        milestone = Milestone.objects.get(id=milestone_id)
        milestone.delete()
        return redirect('/approach/'+str(approach_id))

    if action == 'task':
        form = forms.TaskForm(request.POST or None)
        if form.is_valid():
            task = form.save(commit=False)
            task.user = request.user
            task.save()
        return redirect('/approach/%s/milestone/%s' % (approach_id, milestone_id))

    # Create Comment
    if action == 'comment':
        form = forms.CommentForm(request.POST or None)
        if form.is_valid():
            comment = form.save(commit=False)
            comment.user = request.user
            try:
                recipients, total = get_recipients_and_total(comment.txid)
                __this_txid_is_used__ = Comment.objects.all().filter(txid=comment.txid)
                if not __this_txid_is_used__:
                    comment.satoshis = total
                    comment.save()
                    comment.recipients = recipients
                    return redirect('/approach/%s/milestone/%s' % (approach_id, milestone_id))
                else:
                    return redirect('/approach/%s/milestone/%s?info=used_txid' % (approach_id, milestone_id))
            except:
                # Optional Txid
                comment.satoshis = 0
                comment.save()
                return redirect('/approach/%s/milestone/%s?info=invalid_txid' % (approach_id, milestone_id))

    return redirect('/approach/%s' % approach_id)

def milestone_view(request, approach_id, milestone, milestone_id, template_name='halfmakery/milestone_tpl.html'):
    milestone = Milestone.objects.get(id=milestone_id)
    form = forms.MilestoneForm(request.POST or None, instance=milestone)
    if form.is_valid():
        form.save()

    task_form = forms.TaskForm(initial={'milestone': milestone_id})
    tasks = Task.objects.all().filter(milestone_id=milestone_id).order_by('-priority')

    def comment_proc(request, approach_id, milestone, milestone_id):
        edit_comment_id = request.GET.get('comment', False)
        if edit_comment_id: edit_comment_id = int(edit_comment_id);
        comments = Comment.objects.all().filter(approach_id=approach_id, milestone_id=milestone_id, task_id=None, attempt_id=None)
        approach = Approach.objects.get(id=approach_id)
        comment_editing_form = forms.CommentForm(request.POST or None, initial={'approach': approach, 'milestone': milestone})
        if comment_editing_form.is_valid():
            comment_editing_form.save()
        for comment in comments:
            if comment.id == edit_comment_id:
                comment_editing_form = forms.CommentForm(instance=comment)
        if request.GET.get('target',False) == 'content':
            comment_editing_form = forms.CommentForm(None, initial={'approach': approach})
        comment_editing_form.fields['txid'].widget.attrs['placeholder'] = 'Optional.'
        return comments, edit_comment_id, comment_editing_form

    comments, edit_comment_id, comment_editing_form = comment_proc(request, approach_id, milestone, milestone_id)

    user_tasks = {}
    for item in tasks:
        attempt_count = Attempt.objects.all().filter(task=item).count()
        try:
            user_tasks[item.user].append((item, attempt_count))
        except:
            user_tasks[item.user] = []
            user_tasks[item.user].append((item, attempt_count))

    return render(request, template_name, {'form': form,
                                           'user_tasks': user_tasks,
                                           'task_form': task_form,
                                           'return_link': '/approach/%s' % (approach_id,),
                                           'form_action': '/approach/%s/milestone/%s' % (approach_id, milestone_id),
                                           'comments': comments,
                                           'edit_comment_id': edit_comment_id,
                                           'comment_editing_form': comment_editing_form,
                                           'milestone': milestone,
                                           'level_id': milestone_id,
                                           'info': request.GET.get('info',False)})

def task_action(request, approach_id, milestone, milestone_id, task, task_id, action):

    if action == 'delete':
        task = Task.objects.get(id=task_id)
        task.delete()

    if action == 'attempt':
        form = forms.AttemptForm(request.POST or None)
        if form.is_valid():
            attempt = form.save(commit=False)
            attempt.user = request.user
            attempt.save()
        return redirect('/approach/%s/milestone/%s/task/%s' % (approach_id, milestone_id, task_id))

    # Create Comment
    if action == 'comment':
        form = forms.CommentForm(request.POST or None)
        if form.is_valid():
            comment = form.save(commit=False)
            comment.user = request.user
            try:
                recipients, total = get_recipients_and_total(comment.txid)
                __this_txid_is_used__ = Comment.objects.all().filter(txid=comment.txid)
                if not __this_txid_is_used__:
                    comment.satoshis = total
                    comment.save()
                    comment.recipients = recipients
                    return redirect('/approach/%s/milestone/%s/task/%s' % (approach_id, milestone_id, task_id))
                else:
                    return redirect('/approach/%s/milestone/%s/task/%s?info=used_txid' % (approach_id, milestone_id, task_id))
            except:
                # Optional Txid.
                comment.satoshis = 0
                comment.save()
                return redirect('/approach/%s/milestone/%s/task/%s?info=invalid_txid' % (approach_id, milestone_id, task_id))

    return redirect('/approach/%s/milestone/%s' % (approach_id, milestone_id))

def test(request, template_name='halfmakery/test_tpl.html'):
    return render(request, template_name, {'req': request})

def task_view(request, approach_id, milestone, milestone_id, task, task_id, template_name='halfmakery/task_tpl.html'):

    task = Task.objects.get(id=task_id)
    form = forms.TaskForm(request.POST or None, instance=task)

    if form.is_valid():
        form.save()

    attempt_form = forms.AttemptForm(initial={'task': task_id})
    attempts = Attempt.objects.all().filter(task_id=task_id)

    def comment_proc(request, approach_id, milestone, milestone_id, task, task_id):
        edit_comment_id = request.GET.get('comment', False)
        if edit_comment_id: edit_comment_id = int(edit_comment_id);
        comments = Comment.objects.all().filter(approach_id=approach_id, milestone_id=milestone_id, task_id=task_id, attempt_id=None)
        approach = Approach.objects.get(id=approach_id)
        comment_editing_form = forms.CommentForm(request.POST or None, initial={'approach': approach, 'milestone': milestone_id, 'task': task})
        if comment_editing_form.is_valid():
            comment_editing_form.save()
        for comment in comments:
            if comment.id == edit_comment_id:
                comment_editing_form = forms.CommentForm(instance=comment)
        if request.GET.get('target',False) == 'content':
            comment_editing_form = forms.CommentForm(None, initial={'approach': approach})
        comment_editing_form.fields['txid'].widget.attrs['placeholder'] = 'Optional.'
        return comments, edit_comment_id, comment_editing_form

    comments, edit_comment_id, comment_editing_form = comment_proc(request, approach_id, milestone, milestone_id, task, task_id)

    return render(request, template_name, {'form': form,
                                           'attempt_form': attempt_form,
                                           'attempts': attempts,
                                           'form_action': '/approach/%s/milestone/%s/task/%s' % (approach_id, milestone_id, task_id),
                                           'return_link': '/approach/%s/milestone/%s' % (approach_id, milestone_id),
                                           'comments': comments,
                                           'edit_comment_id': edit_comment_id,
                                           'comment_editing_form': comment_editing_form,
                                           'task': task,
                                           'level_id': task_id,
                                           'info': request.GET.get('info',False)})

def attempt_action(request, approach_id, milestone, milestone_id, task, task_id, attempt, attempt_id, action):

    # Create Comment
    if action == 'comment':
        form = forms.CommentForm(request.POST or None)
        if form.is_valid():
            comment = form.save(commit=False)
            comment.user = request.user
            try:
                recipients, total = get_recipients_and_total(comment.txid)
                __this_txid_is_used__ = Comment.objects.all().filter(txid=comment.txid)
                if not __this_txid_is_used__:
                    comment.satoshis = total
                    comment.save()
                    comment.recipients = recipients
                    return redirect('/approach/%s/milestone/%s/task/%s/attempt/%s' % (approach_id, milestone_id, task_id, attempt_id))
                else:
                    return redirect('/approach/%s/milestone/%s/task/%s/attempt/%s?info=used_txid' % (approach_id, milestone_id, task_id, attempt_id))
            except:
                # Optional Txid.
                comment.satoshis = 0
                comment.save()
                return redirect('/approach/%s/milestone/%s/task/%s/attempt/%s?info=invalid_txid' % (approach_id, milestone_id, task_id, attempt_id))

    return redirect('/approach/%s/milestone/%s/task/%s' % (approach_id, milestone_id, task_id))

def attempt_view(request, approach_id, milestone, milestone_id, task, task_id, attempt, attempt_id, template_name='halfmakery/attempt_tpl.html'):
    attempt = Attempt.objects.get(id=attempt_id)
    form = forms.AttemptFormFull(request.POST or None, instance=attempt)

    if request.POST.get('delete', False) == 'TRUE':
        attempt.delete()
        return redirect('/approach/%s/milestone/%s/task/%s' % (approach_id, milestone_id, task_id))

    elif form.is_valid():
        form.save()

    def comment_proc(request, approach_id, milestone, milestone_id, task, task_id, attempt, attempt_id):
        edit_comment_id = request.GET.get('comment', False)
        if edit_comment_id: edit_comment_id = int(edit_comment_id);
        comments = Comment.objects.all().filter(approach_id=approach_id, milestone_id=milestone_id, task_id=task_id, attempt_id=attempt_id)
        approach = Approach.objects.get(id=approach_id)
        comment_editing_form = forms.CommentForm(request.POST or None, initial={'approach': approach, 'milestone': milestone_id, 'task': task_id, 'attempt': attempt})
        if comment_editing_form.is_valid():
            comment_editing_form.save()
        for comment in comments:
            if comment.id == edit_comment_id:
                comment_editing_form = forms.CommentForm(instance=comment)
        if request.GET.get('target',False) == 'content':
            comment_editing_form = forms.CommentForm(None, initial={'approach': approach})
        comment_editing_form.fields['txid'].widget.attrs['placeholder'] = 'Optional.'
        return comments, edit_comment_id, comment_editing_form

    comments, edit_comment_id, comment_editing_form = comment_proc(request, approach_id, milestone, milestone_id, task, task_id, attempt, attempt_id)

    return render(request, template_name, {'form': form,
                                           'form_action': '/approach/%s/milestone/%s/task/%s/attempt/%s' % (approach_id, milestone_id, task_id, attempt_id),
                                           'return_link': '/approach/%s/milestone/%s/task/%s' % (approach_id, milestone_id, task_id),
                                           'comments': comments,
                                           'edit_comment_id': edit_comment_id,
                                           'attempt': attempt,
                                           'comment_editing_form': comment_editing_form,
                                           'info': request.GET.get('info',False)})

def user(request, user_id, template_name='halfmakery/user_tpl.html'):
    addresses = Address.objects.all().filter(user_id=user_id)
    form = forms.AddressForm(request.POST or None, initial={'user': user_id})
    if form.is_valid():
        address = form.save(commit=False)
        address.user = request.user
        address.save()

    return render(request, template_name, {'addresses': addresses,
                                           'form': form,
                                           'user_id': int(user_id),
                                           'form_action': '/user/%s' % user_id})

def address_action(request, address_id, action):
    """ This view will be used to delete, and execute actions on Users/Addresses. """

    # Delete Approach
    if action == 'delete':
        address = Address.objects.get(id=address_id)
        address.delete()
        return redirect('/user/%s' % request.user.id)

    return redirect('/user/%s' % request.user.id)

def update_priorities(approach_id, priority_map, object='Milestone'):
    """ Updates priorities for objects. """
    if object == 'Milestone':
        for ix, milestone in enumerate(Milestone.objects.all().filter(approach_id=approach_id)):
            milestone.priority = priority_map[milestone.id]
            milestone.save()
        return 'OK'
    else:
        return False

def update_priority_order(request):
    from django.http import Http404
    if request.is_ajax():
        if request.method == 'POST':
            try:
                level = request.POST['level']
                order = request.POST['order']
            except KeyError:
                raise Http404
    order = order.split('&')
    order = [int(o.replace('todo[]=', '')) for o in order]
    priority_map = dict(zip(order,range(len(order))))
    update_priorities(level, priority_map)
    from django.http import HttpResponse
    return HttpResponse('')

def categories_view(request, template_name='halfmakery/categories_tpl.html'):
    categories = Category.objects.all()
    form = forms.CategoryForm(request.POST or None)
    if form.is_valid():
        form.save()
    return render(request, template_name, {'categories': categories,
                                           'form_action': '/categories/',
                                           'form': form})

def category_action(request, category_id, action):
    """ This view will be used to delete, and execute actions on categories. """

    # Delete Category
    if action == 'delete':
        category = Category.objects.get(id=category_id)
        category.delete()
        return redirect('/categories/')

    return redirect('/categories/')

def subcategories_view(request, template_name='halfmakery/subcategories_tpl.html'):
    subcategories = Subcategory.objects.all()
    form = forms.SubcategoryForm(request.POST or None)
    if form.is_valid():
        form.save()
    return render(request, template_name, {'subcategories': subcategories,
                                           'form_action': '/subcategories/',
                                           'form': form})

def subcategory_action(request, subcategory_id, action):
    """ This view will be used to delete, and execute actions on subcategories. """

    # Delete Subcategory
    if action == 'delete':
        subcategory = Subcategory.objects.get(id=subcategory_id)
        subcategory.delete()
        return redirect('/subcategories/')

    return redirect('/subcategories/')

def ideas_view(request, template_name='halfmakery/ideas_tpl.html'):
    ideas = Idea.objects.all().filter(user=request.user).order_by('-id')
    form = forms.IdeaForm(request.POST or None)
    if form.is_valid():
        idea = form.save(commit=False)
        idea.user = request.user

        try:
            recipients, total = get_recipients_and_total(idea.txid)
            # I think, we should let people use the txid which was previously
            # used for commenting, if it was directed to a staff member.
            __this_txid_was_used_for_idea__ = Idea.objects.all().filter(txid=idea.txid)
            if not __this_txid_was_used_for_idea__:
                for user in User.objects.all().filter(is_staff=True):
                    if user.id in recipients:
                        idea.save()
        except:
            pass

    return render(request, template_name, {'ideas': ideas,
                                           'form_action': '/ideas/',
                                           'form': form })

def idea_action(request, idea_id, action):

    # Delete Idea
    if action == 'delete':
        idea = Idea.objects.get(id=idea_id)
        idea.delete()
        return redirect('/ideas/')

    return redirect('/ideas/')

def staff_view(request, template_name='halfmakery/staff_tpl.html'):
    addresses = Address.objects.all()
    return render(request, template_name, {'addresses': addresses})


from bs4 import BeautifulSoup as bS
import requests
import pandas as pd

s = requests.Session()
BASE_URL = "https://www.fifaindex.com"

def get_basic_data(player_html):
    nat = player_html.find("td", {"data-title": "Nationality"}).find("a").get("title")
    ovr_span, pot_span = player_html.find("td", {"data-title": "OVR / POT"}).find_all("span")
    ovr, pot = int(ovr_span.text), int(pot_span.text)
    name = player_html.find("td", {"data-title": "Name"}).find("a").get("title")[:-8]
    positions = '/'.join([position.text for position in player_html.find("td", {"data-title": "Preferred Positions"}).find_all("a")])
    age = int(player_html.find("td", {"data-title": "Age"}).text)
    team = player_html.find("td", {"data-title": "Team"}).find("a").get("title")[:-8]

    return [name, nat, ovr, pot, positions, age, team]

def get_additional_data(profile):
    row = []

    first_card_body = profile.find("div", {"class": "col-lg-8"}).find("div", {"class": "card mb-5"}).find("div", {"class": "card-body"})
    for j, p in enumerate(first_card_body.find_all("p")):
        if j in [3, 4, 5, 10, 11, 13, 14]:
            continue
        if j in [0, 1]:
            row.append(int(p.find("span", {"class": "data-units-metric"}).text[:-3]))
        if j == 2:
            row.append(p.find("span").text)
        if j == 6:
            row.append(p.find("span").text.replace(" ", ""))
        if j in [7, 8]:
            row.append(len(p.find_all("i", {"class": "fas"})))
        if j in [9, 12]:
            row.append(int(p.find("span", {"class": "float-right"}).text[1:].replace(".", "")))

    other_cards = profile.find("div", {"class": "row grid"}).find_all("p")
    for p in other_cards:
        if p.text.startswith("Composure"):
            continue
        num = p.find("span", {"class": "float-right"})
        if num:
            row.append(int(num.text))

    return row

def scrape_data():
    players = []
    clubs = set()
    for fifa in range(21, 22):
        i = 1
        print(fifa)
        while True:
            print(i)

            url = f"{BASE_URL}/players/fifa{fifa}/{i}/?league=19&order=desc"
            page_html = s.get(url).text
            page_parsed = bS(page_html, 'html.parser')
            table = page_parsed.find("table", {"class": "table table-striped table-players"})

            if table is None:
                break

            for player in table.find("tbody").find_all("tr"):
                if not player.has_attr("data-playerid"):
                    continue

                player_data = [fifa] + get_basic_data(player)
                clubs.add(player.find("td", {"data-title": "Team"}).find("a").get("href"))

                profile_url = f'{BASE_URL}{player.find("td", {"data-title": "Name"}).find("a").get("href")}'
                profile_parsed = bS(s.get(profile_url).text, 'html.parser')

                player_data.extend(get_additional_data(profile_parsed))

                players.append(player_data)

            i += 1

    return players, clubs

def scrape_clubs(clubs):
    clubs_rows = []
    for club in clubs:
        club_row = [club.split("/")[-2][-2:]]
        club_url = f'{BASE_URL}{club}'
        club_parsed = bS(s.get(club_url).text, 'html.parser')

        pl = club_parsed.find("div", {"class": "pl-3"})
        club_row.append(pl.find("h1").text[:-8])
        stars = len(pl.find_all("i", {"class": "fas fa-star fa-lg"})) + 0.5 * len(pl.find_all("i", {"class": "fas fa-star-half-alt fa-lg"}))
        club_row.append(stars)

        main_card = club_parsed.find("div", {"class": "col-lg-8"}).find("div", {"class": "card mb-5"})
        for j, li in enumerate(main_card.find_all("li")[0:5]):
            if j == 4:
                club_row.append(li.find("span").text[1:].replace(".", ""))
            else:
                club_row.append(li.find("span").text)
        clubs_rows.append(club_row)

    return clubs_rows

def create_players_df(players):
    df = pd.DataFrame(players, columns=["FIFA", "Name", "Nationality", "OVR", "POT", "Positions", "Age", "Team",
                                        "Height", "Weight", "Foot", "WorkRate", "WeakFoot", "SkillMoves", "Value", "Wage",
                                        'Ball_Control', 'Dribbling', 'Marking', 'Slide_Tackle', 'Stand_Tackle', 'Aggression',
                                        'Reactions', 'Att._Position', 'Interceptions', 'Vision', 'Crossing',
                                        'Short_Pass', 'Long_Pass', 'Acceleration', 'Stamina', 'Strength', 'Balance', 'Sprint_Speed',
                                        'Agility', 'Jumping', 'Heading', 'Shot_Power', 'Finishing', 'Long_Shots', 'Curve', 'FK_Acc.',
                                        'Penalties', 'Volleys', 'GK_Positioning', 'GK_Diving', 'GK_Handlin', 'GK_Kicking', 'GK_Reflexes'])

    df.to_csv("data/players_fifa.csv")

def create_clubs_df(clubs):
    df = pd.DataFrame(clubs, columns=["Fifa", "Name", "Rating", "RivalTeam", "Attack", "Midfield", "Defence", "TransferBudget"])
    df.to_csv("data/clubs_fifa_2021.csv")

if __name__ == "__main__":

    players_data, clubs_list = scrape_data()
    clubs_data = scrape_clubs(clubs_list)

    create_players_df(players_data)
    create_clubs_df(clubs_data)


# -*- coding: utf-8 -*-
from __future__ import unicode_literals

from django.db import models, migrations

class Migration(migrations.Migration):

    dependencies = [
    ]

    operations = [
        migrations.CreateModel(
            name='Filmes',
            fields=[
                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
                ('TituloFilme', models.CharField(max_length=100, null=True, verbose_name=b'Titulo do Filme')),
                ('Genero', models.CharField(max_length=100, null=True, verbose_name=b'Genero do Filme')),
                ('Categoria', models.CharField(max_length=100, null=True, verbose_name=b'Categoria do Filme')),
            ],
            options={
            },
            bases=(models.Model,),
        ),
    ]


import os
import responses
from functools import partial

from smtp2go.core import Smtp2goClient
from smtp2go.settings import API_ROOT, ENDPOINT_SEND

SEND_ENDPOINT = API_ROOT + ENDPOINT_SEND
TEST_API_KEY = 'testapikey'
HEADERS = {
    'X-Ratelimit-Remaining': '250',
    'X-Ratelimit-Limit': '250',
    'X-Ratelimit-Reset': '37'
}

PAYLOAD = {
    'sender': 'dave@example.com',
    'recipients': ['matt@example.com'],
    'subject': 'Trying out smtp2go',
    'text': 'Test message',
    'html': '
Test Message
'
}
SUCCESSFUL_RESPONSE_BODY = {
    "request_id": "aa253464-0bd0-467a-b24b-6159dcd7be60",
    "data": {
        "succeeded": 1,
        "failed": 0,
        "failures": []
    }
}
FAILED_RESPONSE_BODY = {
    "request_id": "aa253464-0bd0-467a-b24b-6159dcd7be60",
    "data": {
        "succeeded": 0,
        "failed": 1,
        "failures": [
            'The API Key passed was not in the correct format, Please '
            'check the key is correct and try again, The full API key can '
            'be found in the API Keys section in the admin console.'
        ]
    }
}

class EnvironmentVariableContextManager():
    """
    Context manager for creating a temporary environment variable.
    """
    def __init__(self, key, value):
        if not value:
            value = ''
        self.key = key
        self.new_value = value

    def __enter__(self):
        # sets the environment variable and saves the old value:
        self.old_value = os.environ.get(self.key)
        os.environ[self.key] = self.new_value

    def __exit__(self, *args):
        # resets environment variable or deletes it:
        if self.old_value:
            os.environ[self.key] = self.old_value
        else:
            del os.environ[self.key]

@responses.activate
def get_response(endpoint, successful=True, status_code=200,
                 headers=None, payload=None):
    with EnvironmentVariableContextManager('SMTP2GO_API_KEY', TEST_API_KEY):
        if not payload:
            payload = PAYLOAD
        # Mock out API Endpoint:
        body = SUCCESSFUL_RESPONSE_BODY if successful else FAILED_RESPONSE_BODY
        responses.add(responses.POST, endpoint, json=body, status=status_code,
                      content_type='application/json',
                      adding_headers=headers)
        client = Smtp2goClient()
        response = client.send(**payload)
        return response

get_successful_response = partial(
    get_response, SEND_ENDPOINT, successful=True,
    status_code=200, headers=HEADERS)
get_failed_response = partial(
    get_response, SEND_ENDPOINT, successful=False,
    status_code=400, headers=HEADERS)


from collections import deque

def sort_tuples(tuples_array, matrix):
    # Creamos una lista de tuplas, donde cada tupla contiene la coordenada y el valor correspondiente en la matriz.
    tuples_list = [(coordinate, matrix[coordinate[0]][coordinate[1]]) for coordinate in tuples_array]
    # Ordenamos la lista de tuplas según el valor correspondiente en la matriz.
    list_ordered_tuples = sorted(tuples_list, key = lambda x: x[1])
    # Devolvemos un arreglo de tuplas ordenado.
    return [tupla[0] for tupla in list_ordered_tuples]

def dfs(graph, weighted_matrix, current, visited):
    if weighted_matrix[current[0]][current[1]] == 1 and graph[current][1] == True:
        return True

    routes = graph[current][0]
    visited.add(current)
    for route in routes:
        if route not in visited and graph[route][1] != False:
            visited.add(route)
            partial_route = dfs(graph, weighted_matrix, route, visited)
            if partial_route is True:
                return True

    return False

def who_is_tapping_me(original_matrix, weighted_matrix, parkings_coordinates):
    dictionary = {}
    for parking in parkings_coordinates:
        dictionary[parking] = ([], True)
        if weighted_matrix[parking[0]][parking[1]] == 1:
            continue

        visiteds = set()
        travel_queue = deque()
        visiteds.add(parking)
        following = adjacents(parking, len(weighted_matrix), len(weighted_matrix[0]))
        for element in following:
            travel_queue.append(element)

        while len(travel_queue) > 0:
            current = travel_queue.pop()
            if original_matrix[current[0]][current[1]] == -1 and weighted_matrix[current[0]][current[1]] > 0:
                following = adjacents(current, len(weighted_matrix), len(weighted_matrix[0]))
                visiteds.add(current)
                for element in following:
                    if element not in visiteds and element not in travel_queue:
                        travel_queue.append(element)

            if original_matrix[current[0]][current[1]] not in (0, -1, 127) and current not in visiteds and weighted_matrix[current[0]][current[1]] != 128:
                aux = dictionary[parking][0]
                aux.append((current[0], current[1]))
                dictionary[parking] = (aux, True)
                visiteds.add(current)

        sorted = sort_tuples(dictionary[parking][0], weighted_matrix)
        dictionary[parking] = (sorted, True)

    return dictionary

# Ayuda a encontrar las casillas adyacentes a una casilla (en coordenadas) dada
# Sus parametros son las coordenadas de una casilla (tupla) y la matriz ponderada
# Retorna un arreglo donde están todos los vecinos válidos (que respeten los límites de la matriz y no sean bloqueos) a una coordenada
def adjacents(coordinates, row_max, column_max):
    mov_columns = [-1, 0, 1, 0]
    mov_rows = [0, 1, 0, -1]
    neighbors = []
    coordinate_x, coordinate_y = coordinates
    for i in range(4):
        adj_x = coordinate_x + mov_columns[i]
        adj_y = coordinate_y + mov_rows[i]
        if 0 <= adj_x < row_max and 0 <= adj_y < column_max:
            neighbors.append((adj_x, adj_y))

    return neighbors

#Se encarga de devolver la matriz ponderada. Recibe la matriz que fue ponderada originalmente y un deque en el que se almacenan las coordenadas de los lugares de aterrizaje y despegue
def weight_matrix(weighted_matrix, landing_spaces):
    available = 0
    parkings = []
    processed_coordinates = deque(landing_spaces)

    while processed_coordinates:
        coordinates = processed_coordinates.popleft()
        neighbors = adjacents(coordinates, len(weighted_matrix), len(weighted_matrix[0]))
        for neighbor in neighbors:
            if weighted_matrix[neighbor[0]][neighbor[1]] == -1 and weighted_matrix[neighbor[0]][neighbor[1]] != 127:
                weighted_matrix[neighbor[0]][neighbor[1]] = weighted_matrix[coordinates[0]][coordinates[1]]
                processed_coordinates.appendleft((neighbor[0], neighbor[1]))
            elif weighted_matrix[neighbor[0]][neighbor[1]] == 128 and weighted_matrix[neighbor[0]][neighbor[1]] != 127:
                parkings.append((neighbor[0], neighbor[1]))
                weighted_matrix[neighbor[0]][neighbor[1]] = weighted_matrix[coordinates[0]][coordinates[1]] + 1
                available += 1
                processed_coordinates.append((neighbor[0], neighbor[1]))

    return (weighted_matrix, parkings)

# En caso de que exista solución la encuentra y retorna un arreglo lleno de parking, cada uno corresponde a un avión (en orden de llegada)
# En caso de que la solución no exista retorna falso
# Como parametros recibe la matriz ponderada, la matriz original, la lista de eventos (entradas y salidas), un arreglo donde va ir almacenando cada parqueadero y un diccionario, donde el Key corresponde a un avión parqueado y en sus Values tiene 1) Las coordenadas de donde se parqueó el avión y 2) El peso que tenía esa coordenada antes de ser ocupada
def backtracking(graph, weighted_matrix, original_matrix, events, parking_info, parkings):
    if len(events) == 0:
        return True

    event = events.popleft()
    if event < 0:
        parked_airplane = abs(event)
        coordinate = parking_info[parked_airplane][0]
        if weighted_matrix[coordinate[0]][coordinate[1]] == 1:
            aux = graph[coordinate][0]
            graph[coordinate] = (aux, True)
            partial_solution = backtracking(graph, weighted_matrix, original_matrix, events, parking_info, parkings)
            if partial_solution:
                return True
            else:
                aux = graph[coordinate][0]
                graph[coordinate] = (aux, False)
                events.appendleft(event)
                return False

        visited = set()
        flag = dfs(graph, weighted_matrix, coordinate, visited)
        if flag:
            aux = graph[coordinate][0]
            graph[coordinate] = (aux, True)
            partial_solution = backtracking(graph, weighted_matrix, original_matrix, events, parking_info, parkings)
            if partial_solution:
                return True

        aux = graph[coordinate][0]
        graph[coordinate] = (aux, False)
        events.appendleft(event)
        return False

    else:
        for parking in parkings:
            if graph[parking][1] is not False:
                visited = set()
                flag = dfs(graph, weighted_matrix, parking, visited)
                if flag:
                    parking_info[event] = (parking, original_matrix[parking[0]][parking[1]])
                    aux = graph[parking][0]
                    graph[parking] = (aux, False)
                    partial_solution = backtracking(graph, weighted_matrix, original_matrix, events, parking_info, parkings)
                    if partial_solution:
                        return True
                    else:
                        aux = graph[parking][0]
                        graph[parking] = (aux, True)
                        del(parking_info[event])

        events.appendleft(event)
        return False

def main():
    n = 1
    while n < 22:
        first_line = input().split()
        if len(first_line) < 3:
            break

        rows = int(first_line[1])
        colums = int(first_line[2])
        original_matrix, weighted_matrix = []*colums, []*colums
        landing_spaces = deque()
        for i in range(rows):
            row_original, row_weighted = []*rows, []*rows
            j = 0
            for element in input().split():
                if element == '..':
                    row_original.append(-1)
                    row_weighted.append(-1)
                elif element == '==':
                    landing_spaces.appendleft((i, j))
                    row_weighted.append(0)
                    row_original.append(0)
                elif element == '##':
                    row_weighted.append(127)
                    row_original.append(127)
                else:
                    row_weighted.append(128)
                    row_original.append(int(element))
                j += 1
            original_matrix.append(row_original)
            weighted_matrix.append(row_weighted)

        events = deque(map(int, input().split()))
        parking_info = {}
        weighted_matrix, parkings = weight_matrix(weighted_matrix, landing_spaces)
        graph = who_is_tapping_me(original_matrix, weighted_matrix, parkings)
        solution = backtracking(graph, weighted_matrix, original_matrix, events, parking_info, parkings)
        if solution == False:
            print(f'Case {n}: No')
            print('\n')
        else:
            print(f'Case {n}: Yes')
            i = 1
            while i <= len(parking_info):
                assigned_parking = str(parking_info[i][1])
                if len(assigned_parking) == 1:
                    assigned_parking = '0' + assigned_parking
                print(assigned_parking, end=' ')
                i += 1
            print('\n')

        n += 1

if __name__ == '__main__':
    main()


import cv2
from skimage.filters import threshold_local
import numpy as np
import imutils
import matplotlib.pyplot as plt
import math

class vision:
    '''
    This class contains the required methods for the vision module, including:
        * detecting the map and warp perspective to rectangular shape.
        * locating thymio orientation and position based off feature points.
        * locating target point.
        * extracting obstacles and extending them with thymio clearance.
    This class relies on a multitude of computer vision techniques and performs
    heavy targeted filtering steps to extract precise information from camera
    pictures.
    Filters used by this class are: thresholding, canny, dilation, erosion,
    opening, closing, color filtering, gaussian blurr, etc.
    '''
    def __init__(self, camera_ip, plot):
        '''
        Initialisation of the vision class.

        Parameters
        ----------
        camera_ip : string for ip or number for local camera
            Ip of camera feed.
        plot : boolean
            Whether or not to plot computed images.

        Returns
        -------
        None.

        '''
        self.camera_ip = camera_ip
        self.plot = plot
        self.camera_img_idx =0

    def get_camera_image(self):
        '''
        This method captures an image from webcam feed, saves it to a file and
        returns it.
        Returns
        -------
        frame : image
            Image capture from camera feed.

        '''
        webcam = cv2.VideoCapture(self.camera_ip)
        check, frame = webcam.read()
        cv2.imwrite(filename='media/saved_img{}.png'.format(self.camera_img_idx), img=frame)
        self.camera_img_idx = self.camera_img_idx + 1
        frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
        webcam.release()
        return frame

    def get_obstacle_vertices(self, image_gray, scaling_px_2_m):
        '''
        This method takes as input a grayscale image with polygon obstacles such that
        bright pixels are obstacles and dark obstacle are free and returns the
        coordinates of obstacle vertices in an array.
        cr https://www.geeksforgeeks.org/find-co-ordinates-of-contours-using-opencv-python/

        Parameters
        ----------
        image_gray : image in grayscale
            Image containing obstacles as bright pixels.
        scaling_px_2_m : float
            Image scaling from pixels to meters.

        Returns
        -------
        vertices_array : array
            Array containing the list of vertices coordinates in meters.

        '''

        image_rgb = cv2.cvtColor(image_gray, cv2.COLOR_GRAY2RGB)

        # Converting image to a binary image
        # ( black and white only image).
        _, threshold = cv2.threshold(image_gray, 110, 255, cv2.THRESH_BINARY)

        # Detecting contours in image.
        contours, _= cv2.findContours(threshold, cv2.RETR_TREE,
                                      cv2.CHAIN_APPROX_SIMPLE)

        vertices_array = [[0]]
        # Going through every contours found in the image.
        k = 0
        for cnt in contours :

            approx = cv2.approxPolyDP(cnt, 0.009 * cv2.arcLength(cnt, True), True)

            # draws boundary of contours.
            cv2.drawContours(image_rgb, [approx], 0, (0, 0, 255), 5)

            # Used to flatted the array containing
            # the co-ordinates of the vertices.
            n = approx.ravel()
            i = 0
            polygon = [[0,0]]
            for j in n :
                if(i % 2 == 0):
                    x = n[i] * scaling_px_2_m
                    y = n[i + 1] * scaling_px_2_m
                    if not(i):
                        polygon[0] = [x,y]
                    else:
                        polygon.append([x,y])
                i = i + 1
            if not(k):
                vertices_array[0] = polygon
            else:
                vertices_array.append(polygon)
            k = k+1

        return vertices_array

    def order_points(self, pts):
        '''
        Initialzie a list of coordinates that will be ordered
        such that the first entry in the list is the top-left,
        the second entry is the top-right, the third is the
        bottom-right, and the fourth is the bottom-left

        Parameters
        ----------
        pts : a (4,2) points array.
            A set of points coordinates to order.

        Returns
        -------
        rect : a (4,2) points array.
            The array containing the same input points but ordered like desired.

        '''
        rect = np.zeros((4, 2), dtype = "float32")
        # the top-left point will have the smallest sum, whereas
        # the bottom-right point will have the largest sum
        s = pts.sum(axis = 1)
        rect[0] = pts[np.argmin(s)]
        rect[2] = pts[np.argmax(s)]
        # now, compute the difference between the points, the
        # top-right point will have the smallest difference,
        # whereas the bottom-left will have the largest difference
        diff = np.diff(pts, axis = 1)
        rect[1] = pts[np.argmin(diff)]
        rect[3] = pts[np.argmax(diff)]
        # return the ordered coordinates
        return rect

    def four_point_transform(self, image, pts):
        '''
        cr: https://medium.com/@evergreenllc2020/building-document-scanner-with-opencv-and-python-2306ee65c3db
        This method takes as input an image and a set of points coordinates. It
        extracts the area enclosed within the points and stretches it to a
        rectangular shape.

        Parameters
        ----------
        image : image
            source image.
        pts : (4,2) array of points.
            Corner points to stretch to.

        Returns
        -------
        warped : image
            Part of the image enclosed inside the corner points and stretch to
            have a rectangular shape.

        '''
        # obtain a consistent order of the points and unpack them
        # individually
        rect = self.order_points(pts)
        (tl, tr, br, bl) = rect
        # compute the width of the new image, which will be the
        # maximum distance between bottom-right and bottom-left
        # x-coordiates or the top-right and top-left x-coordinates
        widthA = np.sqrt(((br[0] - bl[0]) ** 2) + ((br[1] - bl[1]) ** 2))
        widthB = np.sqrt(((tr[0] - tl[0]) ** 2) + ((tr[1] - tl[1]) ** 2))
        maxWidth = max(int(widthA), int(widthB))
        # compute the height of the new image, which will be the
        # maximum distance between the top-right and bottom-right
        # y-coordinates or the top-left and bottom-left y-coordinates
        heightA = np.sqrt(((tr[0] - br[0]) ** 2) + ((tr[1] - br[1]) ** 2))
        heightB = np.sqrt(((tl[0] - bl[0]) ** 2) + ((tl[1] - bl[1]) ** 2))
        maxHeight = max(int(heightA), int(heightB))
        # now that we have the dimensions of the new image, construct
        # the set of destination points to obtain a "birds eye view",
        # (i.e. top-down view) of the image, again specifying points
        # in the top-left, top-right, bottom-right, and bottom-left
        # order
        dst = np.array([
            [0, 0],
            [maxWidth - 1, 0],
            [maxWidth - 1, maxHeight - 1],
            [0, maxHeight - 1]], dtype = "float32")
        # compute the perspective transform matrix and then apply it
        M = cv2.getPerspectiveTransform(rect, dst)
        warped = cv2.warpPerspective(image, M, (maxWidth, maxHeight))
        # return the warped image
        return warped

    def warp_map(self, image):
        '''
        cr: https://medium.com/@evergreenllc2020/building-document-scanner-with-opencv-and-python-2306ee65c3db
        This image takes as input an image of a map taken from a random angle,
        detects the rectangular shape of the map and returns a rectangular image
        containing only the map.

        Parameters
        ----------
        image : image
            Image containing the map taken from a random angle.

        Returns
        -------
        warped : image
            An image of the map with transformed perspective.

        '''
        # get the image and compute the ratio of the old height
        # to the new height, clone it, and resize it
        ratio = image.shape[0] / 500.0
        orig = image.copy()
        image = imutils.resize(image, height = 500)

        # convert the image to grayscale, blur it, and find edges
        # in the image
        gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)

        gray = cv2.GaussianBlur(gray, (5,5), 0)
        edged = cv2.Canny(gray, 75, 200)

        #show the original image and the edge detected image
        if self.plot:
            fx, plots = plt.subplots(1, 2, figsize=(20,10))
            plots[0].set_title("Image")
            plots[0].imshow(image, cmap='gray')
            plots[1].set_title("Edged")
            plots[1].imshow(edged, cmap='gray')

        # find the contours in the edged image, keeping only the
        # largest ones, and initialize the screen contour
        cnts = cv2.findContours(edged.copy(), cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)
        cnts = imutils.grab_contours(cnts)
        cnts = sorted(cnts, key = ( lambda x: cv2.arcLength(x,True)), reverse = True)[:5]

        c = cnts[0]
        screenCnt = self.order_points(c.reshape(-1,2))

        # apply the four point transform to obtain a top-down
        # view of the original image
        warped = self.four_point_transform(orig, screenCnt.reshape(4, 2) * ratio)

        if self.plot:
            # show the original and scanned images
            fx, plots = plt.subplots(1, 2, figsize=(20,10))
            plots[0].set_title("Original")
            plots[0].imshow(imutils.resize(orig, height = warped.shape[0]))
            plots[1].set_title("Warped")
            plots[1].imshow(warped)

        return warped

    def create_obstacle_map(self, warped):
        '''
        This method takes as input an image of the warped map and filters it
        in such a way that it extracts the obstacles and the free parts of the
        map. In the input, obstacles are dark and the map is bright. For the
        output, the obstacles are white pixels.

        Parameters
        ----------
        warped : image
            Warped image of the map.

        Returns
        -------
        obstacle_map : image
            A black and white image with obstacles as white.

        '''

        # look for black pixels only
        color_tolerance = 30
        black_thresh = cv2.inRange(warped, np.array([-color_tolerance, -color_tolerance, -color_tolerance]), np.array([color_tolerance, color_tolerance, color_tolerance]))
        black_mask = 255 -black_thresh
        black_mask_rgb = cv2.cvtColor(black_mask, cv2.COLOR_GRAY2RGB)
        black_masked_rgb = 255 - cv2.max(warped, black_mask_rgb)

        # remove noise: close then open
        black_masked_gray= cv2.cvtColor(black_masked_rgb, cv2.COLOR_RGB2GRAY)
        k1 = 9
        k2 = 25
        se1 = cv2.getStructuringElement(cv2.MORPH_RECT, (k1,k1))
        se2 = cv2.getStructuringElement(cv2.MORPH_RECT, (k2,k2))
        mask_close = cv2.morphologyEx(black_masked_gray, cv2.MORPH_CLOSE, se1)
        mask_close_open = cv2.morphologyEx(mask_close, cv2.MORPH_OPEN, se2)

        mask_open_close = mask_close_open / 255
        obstacle_map = black_masked_gray * mask_open_close
        obstacle_map = obstacle_map.astype(np.uint8)

        if self.plot:
            fx, plots = plt.subplots(1, 2, figsize=(20,10))
            plots[0].set_title("Only black objects")
            plots[0].imshow(black_masked_gray, cmap= 'gray')
            plots[1].set_title("Filter noise")
            plots[1].imshow(obstacle_map, cmap= 'gray')

        return obstacle_map

    def dilate_obstacle_map(self, obstacle_map, thymio_clearance_px):
        '''
        This method performs morphological dilation to obstacle map such that
        it takes nto account the thymio clearance.

        Parameters
        ----------
        obstacle_map : image
            black and white obstacle image.
        thymio_clearance_px : integer
            Amount of pixels for the obstacles to be dilated by.

        Returns
        -------
        dilated_obstacle_map : image
            Dilated obstacle map.

        '''
        # dilate
        kernel = np.ones((thymio_clearance_px,thymio_clearance_px),np.uint8)
        dilated_obstacle_map = cv2.dilate(obstacle_map,kernel,iterations = 1)
        if self.plot:
            plt.figure()
            plt.imshow(dilated_obstacle_map, cmap='gray')
            plt.title('Dilated obstacle map.')

        return dilated_obstacle_map

    def locate_target_px(self, warped, target_color):
        '''
        This method locates the target with a certain color from the warped
        map image.

        Parameters
        ----------
        warped : image
            Image of the warped map.
        target_color : (3,) RGB color array.
            RGB color of the target to locate.

        Returns
        -------
        target_position : (2,) position array.
            Position in pixel of the located target.

        '''

        color_tolerance = 50
        # extract pixels only within a certain tolerance of the target color.
        pixels_in_range = cv2.inRange(warped, np.array([c - color_tolerance for c in target_color]), np.array([c + color_tolerance for c in target_color]))
        color_mask = 255 - pixels_in_range
        # apply color mask to source image and make it black and white
        color_mask_rgb = cv2.cvtColor(color_mask, cv2.COLOR_GRAY2RGB)
        target_pixels_rgb = 255 - cv2.max(warped, color_mask_rgb)
        target_pixels_gray = cv2.cvtColor(target_pixels_rgb, cv2.COLOR_RGB2GRAY)
        # target position = mean position of all extracted white pixels.
        target_pixels_coordinates = cv2.findNonZero(target_pixels_gray)
        target_position = np.average(target_pixels_coordinates,axis=0)[0]

        if self.plot:
            plt.figure()
            plt.imshow(target_pixels_gray, cmap='gray')
            plt.title('Target pixels')

        return target_position

    def apply_color_filter(self, image, color, color_tolerance):
        '''
        Applies a color filter to an image and extract the pixels that are within
        a certain tolerance from the desired color.

        Parameters
        ----------
        image : image
            Image to be filtered.
        color : (3,) RGB color array/list.
            RGB color to filter with.
        color_tolerance : float
            Color tolerance.

        Returns
        -------
        filtered_pixels_gray : Image
            A black and white image with white pixels as the one within the color
            range.

        '''
        # Create color mask
        pixels_in_range = cv2.inRange(image, np.array([c - color_tolerance for c in color]), np.array([c + color_tolerance for c in color]))
        color_mask = 255 - pixels_in_range
        color_mask_rgb = cv2.cvtColor(color_mask, cv2.COLOR_GRAY2RGB)
        # Filter input image.
        filtered_pixels_rgb = 255 - cv2.max(image, color_mask_rgb)
        filtered_pixels_gray = cv2.cvtColor(filtered_pixels_rgb, cv2.COLOR_RGB2GRAY)

        return filtered_pixels_gray

    def locate_feature_in_map_colorpixels(self, feature_color, map_image):
        '''
        apply color filter to map image and get average position of pixels in
        color range of feature.

        Parameters
        ----------
        feature_color : (3,) RGB color array/list.
            RGB color of the feature to find.
        map_image : image
            Map image.

        Returns
        -------
        feature_position : (2,) pixel position array
            Position in pixel of the feature in map_image.

        '''

        color_tolerance = 30

        map_filtered_gray = self.apply_color_filter(map_image, feature_color, color_tolerance)

        feature_pixels_coordinates = cv2.findNonZero(map_filtered_gray)
        feature_position = np.average(feature_pixels_coordinates,axis=0)[0]
        print("From locate feature", feature_position)
        if self.plot:
            plt.figure()
            plt.imshow(map_filtered_gray, cmap='gray')
            plt.title('Feature pixels in map')

        return feature_position

    def plot_key_points(self, image, keypoints):
        '''
        This method plots keypoints in map image.

        Parameters
        ----------
        image : image
            Map image.
        keypoints : list/array.
            List of keypoints coordinates in pixel.

        Returns
        -------
        None.

        '''
        keypoints_without_size = np.copy(image)
        keypoints_with_size = np.copy(image)

        cv2.drawKeypoints(image, keypoints, keypoints_without_size, color = (0, 255, 0))
        cv2.drawKeypoints(image, keypoints, keypoints_with_size, flags = cv2.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS)

        fx, plots = plt.subplots(1, 2, figsize=(20,10))

        plots[0].set_title("Keypoints With Size")
        plots[0].imshow(keypoints_with_size, cmap='gray')

        plots[1].set_title("Keypoints Without Size")
        plots[1].imshow(keypoints_without_size, cmap='gray')

    def locate_feature_in_map_orb(self, map_image, feature_image, feature_color):
        '''
        cr: https://www.geeksforgeeks.org/feature-matching-using-orb-algorithm-in-python-opencv/
        apply color filter to map image and feature image.
        Then apply ORB algorithm to get position of key points in map image.
        Get average position of key points.
        Detection is rotation and scaling independent but is color dependent.

        Parameters
        ----------
        map_image : image
            Image of the map.
        feature_image : image
            Image of the feature.
        feature_color : (3,) RGB color array/list
            RGB color of the feature.

        Returns
        -------
        feature_position : (2,) pixel position array.
            Position of the feature detected using ORB.

        '''

        color_tolerance = 30

        feature_filtered_gray = self.apply_color_filter(feature_image, feature_color, color_tolerance)
        map_filtered_gray = self.apply_color_filter(map_image, feature_color, color_tolerance)

        if self.plot:
            fx, plots = plt.subplots(1, 2, figsize=(20,10))
            plots[0].set_title("Feature image with color filter")
            plots[0].imshow(feature_filtered_gray, cmap= 'gray')
            plots[1].set_title("Map image with color filter")
            plots[1].imshow(map_filtered_gray, cmap= 'gray')

        orb = cv2.ORB_create()

        feature_keypoints, feature_descriptor = orb.detectAndCompute(feature_filtered_gray, None)
        map_keypoints, map_descriptor = orb.detectAndCompute(map_filtered_gray, None)

        if self.plot:
            self.plot_key_points(feature_filtered_gray, feature_keypoints)
            self.plot_key_points(map_filtered_gray, map_keypoints)

            # Print the number of keypoints detected in the feature image
            print("Number of Keypoints Detected In The Feautre image: ", len(feature_keypoints))
            # Print the number of keypoints detected in the map image
            print("Number of Keypoints Detected In The Map Image: ", len(map_keypoints))

        # Create a Brute Force Matcher object.
        bf = cv2.BFMatcher(cv2.NORM_HAMMING, crossCheck = True)

        # Perform the matching between the ORB descriptors of the feature images and the map image
        matches = bf.match(feature_descriptor, map_descriptor)

        # The matches with shorter distance are the ones we want.
        matches = sorted(matches, key = lambda x : x.distance)

        # Use only keypoints with considerable distance
        matches = matches[:len(matches)//2]

        if self.plot:
            results = cv2.drawMatches(feature_filtered_gray, feature_keypoints, map_filtered_gray, map_keypoints, matches, map_image, flags = 2)
            # Display the best matching points
            plt.figure(figsize=(20,10))
            plt.rcParams['figure.figsize'] = [14.0, 7.0]
            plt.title('Best Matching Points')
            plt.imshow(results)
            plt.show()

        # Feature Position is the weighted average of the matched keypoints.
        # Weight is inverse of match distance.
        feature_keypoints_pos = np.reshape([feature_keypoints[mat.queryIdx].pt for mat in matches],(len(matches),2))
        map_keypoints_pos = np.reshape([map_keypoints[mat.trainIdx].pt for mat in matches], (len(matches),2))
        weights = [1/match.distance for match in matches]
        feature_position = np.average(map_keypoints_pos, weights=weights, axis=0)

        return feature_position

    def locate_thymio(self, map_image, feature_image_front, feature_color_front, feature_image_back, feature_color_back, scaling_px_2_m):
        '''
        This method locates the thymio coordinates and orientation in the warped
        map based of two images of features purposely placed on top of the
        thymio. Color filtering is used to improve feature detection algorithms.
        Either ORB or locating with colored pixels in filtered map are available
        to be used.

        Parameters
        ----------
        map_image : image
            Warped image of the map.
        feature_image_front : image
            Image of feature placed on front of thymio.
        feature_color_front : (3,) RGB color array/list
            Color array of the front feature.
        feature_image_back : image
            Image of feature placed on back of thymio.
        feature_color_back : (3,) RGB color array/list
            Color array of the back feature.
        scaling_px_2_m : float
            scaling from pixels to meters.

        Returns
        -------
        thymio_x_m : float
            Thymio x position in meters on the map.
        thymio_y_m : float
            Thymio y position in meters on the map.
        thymio_theta_rad : float
            Thymio orientation on the map.

        '''
        # feature 1 is at the front of thymio, feature 2 is at the back
        # feature_position_front = self.locate_feature_in_map_orb(map_image, feature_image_front, feature_color_front)
        # feature_position_back = self.locate_feature_in_map_orb(map_image, feature_image_back, feature_color_back)
        feature_position_front = self.locate_feature_in_map_colorpixels(feature_color_front, map_image)
        feature_position_back = self.locate_feature_in_map_colorpixels(feature_color_back, map_image)

        # scale coordinates in m.
        thymio_x_m =  feature_position_back[0]  * scaling_px_2_m
        thymio_y_m = feature_position_back[1] * scaling_px_2_m
        # extract orientation with atan2.
        orientation_vector = feature_position_front - feature_position_back
        thymio_theta_rad = math.atan2(orientation_vector[1], orientation_vector[0])

        cv2.line(map_image, (int(feature_position_front[0]), int(feature_position_front[1])), (int(feature_position_back[0]), int(feature_position_back[1])), (255, 0, 0), 30)
        plt.imshow(map_image)
        print(feature_position_front,feature_position_back)
        return thymio_x_m, thymio_y_m, thymio_theta_rad

    def scale_map(self,map_image, map_x, map_y):
        '''
        This method scales a warped map image to match the real dimensions in
        meters of the physical map.
        Parameters
        ----------
        map_image : image
            Image of the warped map.
        map_x : float
            Map x dimension in m.
        map_y : float
            Map y dimension in m.

        Returns
        -------
        scaled_map : image
            Map scaled with physical aspect ratio.
        scaling_px_2_m : float
            map scaling from pixel to m.

        '''
        # height --> up-down , y
        # width --> right-left, x
        (orig_height, orig_width) = map_image.shape[:2]
        new_height = int(orig_width * map_y/map_x)
        orig = map_image.copy()
        scaled_map =  cv2.resize(orig, (orig_width,new_height))
        scaling_px_2_m = map_y/scaled_map.shape[0]
        return scaled_map, scaling_px_2_m

    def draw_line(pointA,pointB,img):
        '''
        This method draws a map between two points on img.

        Parameters
        ----------
        pointA : (2,) position array/list
            position of point A in pixels.
        pointB : (2,) position array/list
            position of point B in pixels..
        img : image
            Map image.

        Returns
        -------
        None.

        '''
        cv2.line(img, (int(pointA[0]), int(pointA[1])), (int(pointB[0]), int(pointB[1])), (255, 0, 0), 30)
        plt.imshow(img)

'''
Example of use for the vision module API.
'''
if False:
    map_x = 0.89
    map_y = 0.86
    thymio_clearance_m = 0.12
    filename = 'media/saved_img.png'
    image = cv2.imread(filename, cv2.IMREAD_COLOR)
    image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)

    vis = vision(camera_ip=0, plot=True)
    #warped = vis.warp_map(vis.get_camera_image())
    warped = vis.warp_map(image)
    scaled, scaling_px_2_m = vis.scale_map(warped, map_x, map_y)
    # Load the feature images
    feature_image_front = cv2.imread('media/thymio_circle_green.png')
    feature_image_back = cv2.imread('media/thymio_circle_blue.png')
    # Convert the features images to RGB
    feature_image_front = cv2.cvtColor(feature_image_front, cv2.COLOR_BGR2RGB)
    feature_image_back = cv2.cvtColor(feature_image_back, cv2.COLOR_BGR2RGB)

    feature_color_front = [8, 72, 47]
    feature_color_back = [39, 52, 104]
    thymio_x_m, thymio_y_m, thymio_theta_rad = vis.locate_thymio(warped, feature_image_front, feature_color_front, feature_image_back, feature_color_back, scaling_px_2_m)

    obstacle_map = vis.create_obstacle_map(warped)
    thymio_clearance_px = int(thymio_clearance_m / scaling_px_2_m)
    dilated_obstacle_map = vis.dilate_obstacle_map(obstacle_map, thymio_clearance_px)

    target_color = [0x86, 0x33, 0x26]
    target_position_px = vis.locate_target_px(warped, target_color)
    target_position_m = target_position_px * scaling_px_2_m

    obstacle_vertices_m = vis.get_obstacle_vertices(dilated_obstacle_map, scaling_px_2_m)


###################
# Made by Eclip5e #
###################

#Check installed Modules
import sys
import subprocess
import pkg_resources

required = {'pyautogui', 'keyboard', 'colorama', 'datetime', 'pywin32', 'opencv-python', 'pydirectinput'}
installed = {pkg.key for pkg in pkg_resources.working_set}
missing = required - installed

if missing:
	print("Installing dependencies...")
	python = sys.executable
	subprocess.check_call([python, '-m', 'pip', 'install', *missing], stdout=subprocess.DEVNULL)

#Import Modules
from pyautogui import *
from datetime import datetime, timedelta
from colorama import init, Fore, Back, Style
from PIL import ImageGrab, Image
import pyautogui
import pydirectinput
import time
import keyboard
import random
import win32api, win32con
import requests
import json
import numpy as np
import cv2 as cv

#Load Settings
with open('settings.json') as f:
	settings = json.load(f)

#Setup
appName = "New World Fishing Bot"
bait = settings["bait"]
reelTime = settings["reelTime"]
repairCycle = settings["repairCycle"]
hotkeys = settings["hotkeys"]

#Vars
version = "0.1"
paused = False

init()

#Start Bot
print(appName + " v" + version + "\n")
print("Made by Eclip5e\n")
print("Initializing...")

#Click Function
def click(x,y):
	win32api.SetCursorPos((x,y))
	pyautogui.click(button='left')

#Image recognition
def search(img, conf=0.8, gray=False):
	simg = pyautogui.locateOnScreen('./images/' + img + '.png', confidence=conf, grayscale=gray)
	if simg != None:
		return simg
	else:
		return False

#CV Image recognition
def search_cv(img, conf=0.8, gray=False):
	reg = ImageGrab.grab(bbox=(0, 0, win32api.GetSystemMetrics(0), win32api.GetSystemMetrics(1)))
	img_cv = cv.cvtColor(np.array(reg), cv.COLOR_RGB2BGR)
	res = cv.matchTemplate(img_cv, cv.imread('./images/' + img + '.png'), cv.TM_CCOEFF_NORMED)
	return (res >= conf).any()

#Type a String
def type(msg):
	for x in msg:
		pyautogui.press(x)
	return True

#Repair Fishing Rod
def repair():
	global stateMsg

	stateMsg = "Repairing fishing rod..."
	pydirectinput.keyUp('altleft')
	time.sleep(0.5)
	pydirectinput.press('tab')
	time.sleep(0.5)
	find = search('rodf3', 0.7, True)
	if find != False:
		pydirectinput.keyDown('r')
		time.sleep(0.5)
		click(find.left - 50, find.top + 5)
		time.sleep(0.1)
		click(find.left - 50, find.top + 5)
		time.sleep(0.5)
		pydirectinput.keyUp('r')
		time.sleep(0.1)
		pydirectinput.press('e')
		time.sleep(0.5)
		pydirectinput.press('tab')
		time.sleep(0.5)
		pydirectinput.press('f3')
		time.sleep(0.5)
		return True
	else:
		return False

#State Output
def pstate(msg):
	sys.stdout.write('\x1b[1A')
	sys.stdout.write('\x1b[2K')
	print(str(msg))

#Macros
def macro():
	global paused
	global hotkeys

	if keyboard.is_pressed(hotkeys[0]) == True: #Pause
		if paused:
			paused = False
		else:
			paused = True
	if paused == True:
		time.sleep(1)
		pstate("Paused")
		macro()
	if keyboard.is_pressed(hotkeys[1]) == True: #Quit
		sys.exit()

time.sleep(2)
print(Fore.GREEN + "Bot running!" + Fore.WHITE)

state = 1
_repairCycle = 0
failsafe_checks = 0
pyautogui.FAILSAFE = False
stateMsg = "Checking for active fishing..."
print("Checking...")

#Main Loop
while True:
	#Check In-game
	find = search('tab', 0.8)
	if find != False:
		#Fishing Loop
		if _repairCycle == repairCycle:
			_repairCycle = 0
			repair()

		macro()

		if state == 1: #Check Fishing
			stateMsg = "Checking for active fishing..."
			pydirectinput.keyUp('altleft')
			failsafe_checks = 0
			find = search('hook', 0.8)
			if find != False:
				state = 2
			else:
				time.sleep(0.25)
		elif state == 2: #Throw Hook
			stateMsg = "Throwing Hook..."
			pyautogui.mouseDown(button='left')
			time.sleep(reelTime)
			pyautogui.mouseUp(button='left')
			state = 3
		elif state == 3: #Wait for Catching
			stateMsg = "Waiting for fish..."
			find = search('catch', 0.7, True)
			if find != False:
				pyautogui.click(button='left')
				pydirectinput.keyDown('altleft')
				failsafe_checks = 0
				state = 4
			else:
				#Check Completed
				failsafe_checks += 1
				if failsafe_checks == 10:
					failsafe_checks = 0
					find = search('hook', 0.7, True)
					if find != False:
						state = 1
		elif state == 4: #Catch
			stateMsg = "Catching fish..."
			find = search('c_state1', 0.75, True)
			find1 = search('c_state4', 0.75, True)
			if find != False or find1 != False:
				#Let loose
				pyautogui.mouseUp(button='left')
				stateMsg = stateMsg + " (Letting loose)"
			else:
				find = search('c_state2', 0.6, True)
				if find != False:
					#Reel in
					pyautogui.mouseDown(button='left')
					stateMsg = stateMsg + " (Reeling in)"

			#Check Completed
			find = search('hook', 0.7, True)
			if find != False:
				state = 1
				_repairCycle += 1

			#Fish Fix
			failsafe_checks += 1
			if failsafe_checks == 10:
				failsafe_checks = 0
				find = search('c_bait', 0.7, True)
				if find != False:
					state = 3
					failsafe_checks = 0

		#State Output
		pstate(stateMsg)
	else:
		#Wait for game
		state = 1
		print("Please open the game", end='\r')
		time.sleep(5)

#Quit Application
print(Fore.LIGHTRED_EX + "Quit" + Fore.WHITE)


import streamlit as st
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from wordcloud import WordCloud
from collections import Counter
import pickle
import networkx as nx
import ast
import numpy as np
import plotly.express as px

# Dashboard vibe
st.set_page_config(layout="wide")

# Load the data
@st.cache_data
def load_data():
    return pd.read_csv('dblp_data.csv'), pd.read_csv('profile_data.csv'), pd.read_csv('sim_data.csv', index_col=0)
final_df, profile_df, sim_df = load_data()

@st.cache_data
def load_graph(filename):
    with open(filename, 'rb') as file:
        return pickle.load(file)

G_full = load_graph('Tomoki_Teng_graph_data.pkl')

def parse_coauthors(coauthors_str):
    # Using ast.literal_eval safely
    return ast.literal_eval(coauthors_str)
# Convert the 'List of Coauthors' to lists
final_df['List of Coauthors'] = final_df['List of Coauthors'].apply(parse_coauthors)

def plot_graph_for_professor(prof_name):
    subset = final_df[final_df['Full Name'] == prof_name]
    G = nx.Graph()

    # Add nodes
    for _, row in subset.iterrows():
        G.add_node(row['Full Name'], color='blue')  # NTU professor (e.g., from SCSE)
        for coauthor in row['List of Coauthors']:
            if coauthor in final_df['Full Name'].values:
                G.add_node(coauthor, color='blue')
            else:  # If coauthor is outside NTU
                G.add_node(coauthor, color='red')
            G.add_edge(row['Full Name'], coauthor)

    # Draw graph
    pos = nx.spring_layout(G)
    colors = [G.nodes[node]['color'] for node in G.nodes]

    fig = plt.figure(figsize=(12, 12))
    nx.draw_networkx_nodes(G, pos, node_color=colors, node_size=500)
    nx.draw_networkx_edges(G, pos)

    # Add labels
    nx.draw_networkx_labels(G, pos, font_size=10)

    return fig

def toggle_page():
    st.session_state.current_page = 'other' if st.session_state.current_page == 'main' else 'main'
    st.experimental_rerun()

def plot_stacked_bar_chart():
    # Convert 'Year' to numeric, if it's not already
    final_df['Year'] = pd.to_numeric(final_df['Year'], errors='coerce')
    # Group by 'Year' and 'Type of Publication', then get the size of each group
    yearly_type_counts = final_df.groupby(['Year', 'Type of Publication']).size().unstack().fillna(0)
    # Define a color mapping for the different types of publications
    color_mapping = {
        'Journal Article': 'blue',
        'Conference Paper': 'orange',
        'Book': 'green',
        'Book Chapter': 'red',
        'Informal Publication': 'purple',
        'Other': 'gray'
    }
    # Create the stacked bar chart
    fig, ax = plt.subplots(figsize=(10, 6))
    # Plot each publication type in a stacked bar chart
    bottom_values = np.zeros(len(yearly_type_counts))
    for column in yearly_type_counts.columns:
        ax.bar(yearly_type_counts.index, yearly_type_counts[column], bottom=bottom_values, label=column, color=color_mapping.get(column, 'gray'))
        bottom_values += yearly_type_counts[column].values
    ax.set_title('Breakdown of Publication Types Over Time', fontsize=16)
    ax.set_xlabel('Year', fontsize=14)
    ax.set_ylabel('Publication Count', fontsize=14)
    ax.legend(title='Type of Publication')
    ax.set_xticks(yearly_type_counts.index)
    ax.set_xticklabels(yearly_type_counts.index, rotation=45)
    plt.tight_layout()
    return fig
def plot_line_chart():
    # Exclude the year 2024 from the dataframe if needed
    final_df['Year'] = pd.to_numeric(final_df['Year'], errors='coerce')
    yearly_type_counts = final_df.groupby(['Year', 'Type of Publication']).size().unstack().fillna(0)
    yearly_type_counts = yearly_type_counts[yearly_type_counts.index != 2024]
    # Create the line chart
    fig, ax = plt.subplots(figsize=(12, 8))
    # Highlight the region from 2019 to 2025 if needed
    ax.axvspan(2019, 2025, color='yellow', alpha=0.3, label='Highlighted Region (2019-2025)')
    # Plot each publication type
    for column in yearly_type_counts.columns:
        ax.plot(yearly_type_counts.index, yearly_type_counts[column], label=column, marker='o')
    ax.set_xlabel('Year', fontsize=14)
    ax.set_ylabel('Publication Count', fontsize=14)
    ax.set_xticks(yearly_type_counts.index)
    ax.set_xticklabels(yearly_type_counts.index, rotation=45)
    ax.legend(title='Type of Publication')
    plt.tight_layout()
    return fig
def plot_influence_bubble_chart():
    # Calculate total publications for each professor
    total_publications = final_df.groupby('Full Name').size().reset_index(name='Total Publications')
    # Merge this with the profile_df to get the citations for each professor
    influence_df = profile_df.merge(total_publications, on='Full Name', how='left')

    # Replace NaN values in 'Total Publications' with 0
    influence_df['Total Publications'] = influence_df['Total Publications'].fillna(0)

    # Normalize data for plotting
    max_citations = influence_df['Citations'].max()
    max_publications = influence_df['Total Publications'].max()
    influence_df['Normalized Citations'] = influence_df['Citations'] / max_citations
    influence_df['Normalized Publications'] = influence_df['Total Publications'] / max_publications

    # Use Plotly Express to create the bubble chart
    fig = px.scatter(
        influence_df,
        x='Normalized Publications',
        y='Normalized Citations',
        size='Total Publications',  # Ensure all values are numbers
        hover_name='Full Name',
        hover_data=['Citations', 'Total Publications'],
        title='Influence Bubble Chart',
        labels={
            'Normalized Publications': 'Normalized Total Publications',
            'Normalized Citations': 'Normalized Citations',
            'Total Publications': 'Total Publications (Size of Bubble)'
        }
    )
    fig.update_layout(
        height=1000,  # Set the height to a larger value to make the plot taller
        width=600,  # Optionally, adjust the width to maintain a certain aspect ratio
    )
    fig.update_traces(marker=dict(opacity=0.7, line=dict(width=0)))

    return fig

# Initialize the session state for current_page if it does not exist
if 'current_page' not in st.session_state:
    st.session_state.current_page = 'main'

if st.session_state.current_page == 'main':
    # Set up the layout for 4 columns (5 columns is too small)
    cols = st.columns(4)

    # First column
    with cols[0].container():
        cols[0].header('Profile Details')
        # Select a professor
        selected_prof = cols[0].selectbox('Select a Professor:', profile_df['Full Name'].unique())

        st.title(f'Profile of {selected_prof}')
        prof_info = profile_df[profile_df['Full Name'] == selected_prof].iloc[0]

        for label, value in [("Email", prof_info["Email"]),
                            ("Website", prof_info["Website URL"]),
                            ("Total Publications", len(final_df[final_df["Full Name"] == selected_prof])),
                            ("Citations", prof_info["Citations"]),
                            ("Education", prof_info["Education"])]:
            cols[0].markdown(
                f'
{label}: {value}
',
                unsafe_allow_html=True)

        # Expander Experience section
        experience_expander = cols[0].expander('Experience')
        experience_expander.write(prof_info["Research_Interests"])

        # Expander Biography section
        biography_expander = cols[0].expander('Biography')
        biography_expander.write(prof_info["Biography"])

        # Conferences and CORE Rank
        cols[0].header('Conferences')
        # Filtering the data properly
        selected_prof_conferences = final_df[(final_df['Full Name'] == selected_prof) & (final_df['CORE Rank'].isin(['A*', 'A']))]
        # Creating a table for Venue and CORE Rank
        conference_table = selected_prof_conferences[['Venue', 'CORE Rank']].drop_duplicates().reset_index(drop=True)
        cols[0].table(conference_table)

    # Second column
    cols[1].header('Common Research Interests')
    interests_text = ' '.join(profile_df['Research_Interests'].dropna())
    wordcloud = WordCloud(width=800, height=400).generate(interests_text)
    fig, ax = plt.subplots(figsize=(10, 5))
    ax.imshow(wordcloud, interpolation='bilinear')
    ax.axis('off')
    cols[1].pyplot(fig)

    # Publications Breakdown
    cols[1].header('Publications Type Breakdown')
    # Filter the data for the selected professor
    selected_prof_data = final_df[final_df['Full Name'] == selected_prof]
    # Create a pivot table for the filtered data
    pivot_df = selected_prof_data.pivot_table(index='Year', columns='Type of Publication', aggfunc='size', fill_value=0)
    fig, ax = plt.subplots(figsize=(10, 6))
    pivot_df.plot(kind='bar', stacked=True, ax=ax)
    ax.set_ylabel('Number of Publications')
    cols[1].pyplot(fig)

    # Conference CORE Rank Breakdown
    selected_prof_conferences = final_df[final_df['Full Name'] == selected_prof]
    # Replace other CORE ranks with 'Others'
    selected_prof_conferences = selected_prof_conferences.replace(
        to_replace=[rank for rank in selected_prof_conferences['CORE Rank'].unique() if rank not in ['A*', 'A', 'B', 'C']],
        value='Others'
    )
    # Pivot table for the filtered data
    pivot_df = selected_prof_conferences.pivot_table(index='CORE Rank', columns='Type of Publication', aggfunc='size', fill_value=0)
    # Reorder the index and columns properly
    core_order = ['Others', 'C', 'A', 'B', 'A*']
    type_order = pivot_df.columns.tolist()  # Get the order of publication types prev plot
    pivot_df = pivot_df.reindex(core_order).reindex(type_order, axis=1).fillna(0)
    # Get the colors prev plot
    colors = plt.cm.tab10.colors
    # CORE Rank breakdown
    fig, ax = plt.subplots(figsize=(8, 5))
    pivot_df.plot(kind='barh', stacked=True, ax=ax, color=colors[:len(type_order)])  # Use the first N colors from the color cycle
    ax.set_xlabel('Number of Publications')
    ax.set_ylabel('CORE Rank')
    cols[1].pyplot(fig)

    # Third column
    cols[2].header('Top Co-authors')
    selected_prof_coauthors = final_df[final_df['Full Name'] == selected_prof]['List of Coauthors']
    coauthors_counter = Counter()
    for coauthors_list in selected_prof_coauthors:
        coauthors_counter.update(coauthors_list)
    # Remove the selected professor's own name from the counter
    coauthors_counter.pop(selected_prof, None)
    # Getting top 10 co-authors
    top_coauthors = dict(coauthors_counter.most_common(10))
    # Determine colors based on whether the co-author is inside or outside NTU
    colors = ['blue' if name in final_df['Full Name'].values else 'red' for name in top_coauthors.keys()]
    fig, ax = plt.subplots(figsize=(10, 5.6))
    sns.barplot(x=list(top_coauthors.values()), y=list(top_coauthors.keys()), ax=ax, palette=colors)
    ax.set_xlabel('Number of Collaborations')
    cols[2].pyplot(fig)

    # Coauthor Relationship Graph
    with cols[2].container():
        cols[2].header('Coauthor Relationship')
        fig = plot_graph_for_professor(selected_prof)
        cols[2].pyplot(fig)

    # Fourth column
    with cols[3].container():
        cols[3].header('Similarity Network Graph')
        # Initialize graph
        G = nx.Graph()
        # Add nodes
        for name in sim_df.index:
            G.add_node(name)
        # Add edges with cosine similarity as weight
        threshold = 0.7
        for i, name1 in enumerate(sim_df.index):
            for j, name2 in enumerate(sim_df.columns):
                similarity = sim_df.iloc[i, j]

                if similarity > threshold and name1 != name2 and similarity < 1:  # Added condition to avoid self-similarity of 1
                    G.add_edge(name1, name2, weight=similarity)
        # Remove nodes with low degree
        degree_threshold = 2
        low_degree_nodes = [node for node, degree in dict(G.degree()).items() if degree <= degree_threshold]
        G.remove_nodes_from(low_degree_nodes)
        # Draw the graph
        fig, ax = plt.subplots(figsize=(10, 6))
        pos = nx.spring_layout(G, k=0.5)
        node_sizes = [G.degree(node) * 200 for node in G.nodes()]
        nx.draw(G, pos, with_labels=True, node_color='skyblue', node_size=node_sizes, font_size=10, alpha=0.7, width=[G[u][v]['weight'] for u,v in G.edges()], ax=ax)
        cols[3].pyplot(fig)

        # Similar Profiles
        nested_cols = cols[3].columns(2)
        # User input for X top similar profiles to display
        top_x = nested_cols[0].number_input('Select top X similar profiles:', min_value=1, max_value=len(sim_df)-1, value=5)
        # Filtering the top X most similar profiles
        selected_prof_similarities = sim_df.loc[selected_prof].sort_values(ascending=False).drop(selected_prof)[0:top_x]
        # Display the top X most similar profiles
        nested_cols[1].write(f'Top {top_x} profiles most similar to {selected_prof}:')
        nested_cols[1].write(selected_prof_similarities)

        # Venue Distribution
        cols[3].header('Venue Distribution')
        # Filter the data
        selected_prof_venue_data = final_df[final_df['Full Name'] == selected_prof]
        # Create a pivot table for the filtered data
        pivot_venue_df = selected_prof_venue_data.pivot_table(
            index='Venue',
            columns='Type of Publication',
            aggfunc='size',
            fill_value=0
        ).head(10)
        fig, ax = plt.subplots(figsize=(10, 6))
        pivot_venue_df.plot(kind='barh', stacked=True, ax=ax)
        ax.set_xlabel('Venue')  # Swap x and y labels
        ax.set_title('Number of Publications')
        cols[3].pyplot(fig)

    # Layout to push the button to the right
    col1, col2 = st.columns([0.95, 0.05])
    with col2:
        if st.button('Go to Other Page', key='btn_go_other'):
            toggle_page()

else:
    st.title("All Professors' Detail")
    # Split the other page into 3 columns
    col1, col_bubble_chart = st.columns([1, 2])  # Adjust the proportions as needed

    # First column with its content
    with col1:
        stacked_bar_chart = plot_stacked_bar_chart()
        st.pyplot(stacked_bar_chart)
        line_chart = plot_line_chart()
        st.pyplot(line_chart)

    # Bubble chart spanning the area of the second and third columns
    with col_bubble_chart:
        influence_bubble_chart = plot_influence_bubble_chart()
        st.plotly_chart(influence_bubble_chart, use_container_width=True)

    # Layout to push the button to the right
    col1, col2 = st.columns([0.8, 0.2])
    with col2:
        if st.button('Back to Main Dashboard', key='btn_go_main'):
            toggle_page()