import argparse
import datetime
import subprocess
import threading
import time
import os
from enum import Enum

import telebot

import libremu_backend as engine
import text_data as text
import keyboards

import config

base_error_message = 'There are some problem with request. Forward messages to @nikolay_klimov if you think this is a bag.'

bot = telebot.TeleBot(config.tg_token, num_threads=4)
fsm = {} # Finite State Machine
current_shown_dates={} # TODO: get rid off

class BotState(Enum):
    WAIT                = 0
    REP_DELETE_CHOOSE   = 1
    AT_CALENDAR         = 2
    AT_TIME             = 10
    AT_TIME_TEXT        = 3
    AFTER_INPUT         = 4
    GROUPE_CHOOSE       = 5
    GROUP_ADD_ITEM      = 6
    GROUP_DEL_ITEM      = 7
    GROUP_ADD           = 8
    GROUP_DEL           = 9

class FSMData:
    state = BotState.WAIT
    data = {}

    def reset(self):
        self.state = BotState.WAIT
        self.data = {}

@bot.message_handler(content_types=["text"])
def handle_text(message):
    input_text = message.text
    id = message.chat.id

    # Special case
    if input_text.find('/start') == 0:
        on_start_command(message)
        fsm[id] = FSMData()
        return

    engine.log_debug("Processing input text message: %s. Bot state = %s"%(input_text, str(fsm[id].state)))

    if fsm[id].state == BotState.WAIT:
        on_wait_status(message)

    elif fsm[id].state == BotState.REP_DELETE_CHOOSE:
        on_rep_delete_choose_status(message)

    elif fsm[id].state == BotState.AT_CALENDAR:
        on_at_calendar_status(message)

    elif fsm[id].state == BotState.AT_TIME_TEXT:
        on_at_time_text_status(message)

    elif fsm[id].state == BotState.AFTER_INPUT:
        on_after_input_status(message)

    elif fsm[id].state == BotState.GROUP_ADD_ITEM:
        on_group_add_item_status(message)

    elif fsm[id].state == BotState.GROUP_DEL_ITEM:
        on_group_del_item_status(message)

    elif fsm[id].state == BotState.GROUP_ADD:
        on_group_add_status(message)

    elif fsm[id].state == BotState.AT_TIME:
        on_at_time_status(message)

    else:
        engine.log_error("Unknown bot state: uid = " + str(id) + " state = " + str(fsm[id].state))
        fsm[id].reset()
        handle_text(message)

def on_wait_status(message):
    input_text = message.text
    id = message.chat.id

    if input_text.find('/help') == 0:
        on_help_command(message)

    elif input_text.find('/delete_rep') == 0:
        on_delete_rep_command(message)

    elif input_text.find('/at') == 0:
        on_at_command(message)

    elif input_text.find('/group') == 0:
        # on_group_command(message)
        pass

    elif input_text.find('/add_group') == 0:
        # on_add_group_command(message)
        pass

    elif input_text.find('/del_group') == 0:
        # on_del_group_command(message)
        pass

    elif input_text.find('/del_group_item') == 0:
        # on_del_group_item_command(message)
        pass

    elif input_text.find('/list') == 0:
        on_list_command(message)

    else:
        (text, error) = engine.handle_text_message(message.chat.id, input_text)
        if error == 0:
            bot.send_message(message.chat.id, text)
        else:
            keyboard = keyboards.action()
            bot.send_message(id, input_text, reply_markup=keyboard)

def on_rep_delete_choose_status(message):
    delete_rep_event(message)

def on_at_calendar_status(message):
    id = message.chat.id
    message_id = fsm[id].data['message_id']
    bot.delete_message(chat_id=id, message_id=message_id)
    fsm[id].reset()
    handle_text(message)

def on_at_time_status(message):
    # id = message.chat.id
    # keyboard = keyboards.time()
    # bot.send_message(id, )
    pass

def on_at_time_text_status(message):
    id = message.chat.id
    input_text = message.text
    if fsm[id].data['text']:
        input_text += ' ' + fsm[id].data['text']
    command = fsm[id].data['date_spec'] + ' at ' + input_text
    bot.send_message(id, 'Resulting command:\n' + command)
    (text, err) = engine.handle_text_message(id, command)
    if err:
        bot.send_message(id, 'Wrong resulting command. Try again')
    else:
        bot.send_message(id, text)
    fsm[id].reset()

def on_after_input_status(message):
    id = message.chat.id
    command = message.text + ' ' + fsm[id].data['text']
    bot.send_message(id, 'Resulting command:\n' + command)
    (text, _) = engine.handle_text_message(id, command)
    bot.send_message(id, text)
    fsm[id].reset()

def on_group_add_item_status(message):
    # should be cathced in keyboard handle
    pass

def on_group_del_item_status(message):
    id = message.chat.id
    event_id_str = message.text
    if not event_id_str.isdigit():
        msg = bot.reply_to(message, 'You should write number')
        return

    event_id = int(event_id_str)-1
    id_list = fsm[id].data['id_list']
    if id_list and event_id >= 0 and event_id < len(id_list):
        del_id = id_list[event_id]
        if engine.delete_group_item(del_id):
            bot.send_message(id, "Done.")
        else:
            bot.send_message(id, base_error_message)
        fsm[id].reset()
    else:
        fsm[id].reset()
        bot.send_message(id, "Number is out of limit. Operation abort.")
    pass

def on_group_add_status(message):
    id = message.chat.id
    group_name = message.text
    if engine.add_user_group(id, group_name):
        bot.send_message(id, 'Done.')
    else:
        bot.send_message(id, base_error_message)
    fsm[id].reset()

# ------------------- command handlers

def on_start_command(message):
    username = ''
    first_name = ''
    last_name = ''
    if message.from_user.username: username = message.from_user.username
    if message.from_user.first_name: first_name = message.from_user.first_name
    if message.from_user.last_name: last_name = message.from_user.last_name
    engine.add_user(message.from_user.id,
                    username,
                    message.chat.id,
                    first_name,
                    last_name,
                    -3)
    bot.send_message(message.chat.id, 'Hello! ^_^\nType /help')

def on_help_command(message):
    if message.text == '/help more':
        bot.send_message(message.chat.id, text.detailed_help_message_ru, parse_mode='Markdown')
    else:
        bot.send_message(message.chat.id, text.main_help_message_ru, parse_mode='Markdown')

def on_delete_rep_command(message):
    global fsm
    uid = message.chat.id
    fsm[uid].state = BotState.WAIT
    rep_event_list = engine.get_rep_events(uid)

    if not rep_event_list:
        bot.send_message(message.chat.id, 'No current rep event')
        return

    [text_list, rep_id_list] = list(zip(*rep_event_list))
    fsm[uid].state = BotState.REP_DELETE_CHOOSE
    fsm[uid].data['rep_id_list'] = rep_id_list

    header = "Here is yout rep events list. Choose witch to delete:\n"
    list_str = '\n'.join([ str(i+1) + ") " + key for i, key in enumerate(text_list)])
    bot.send_message(uid, header + list_str)

def on_at_command(message):
    handle_calendar_call(message.chat.id)

def on_group_command(message):
    choose_group_message(message.from_user.id)

def on_add_group_command(message):
    uid = message.chat.id
    offset = len('/add_group ')
    if offset >= len(message.text):
        bot.send_message(uid, 'You should write group name')
    group_name = message.text[offset:]
    if group_name == '':
        fsm[uid].state = BotState.GROUP_ADD
        bot.send_message(uid, 'Ok, write new group name')
    else:
        if engine.add_user_group(uid, group_name):
            bot.send_message(uid, 'Done.')
        else:
            bot.send_message(uid, base_error_message)

def on_del_group_command(message):
    choose_group_message(message.chat.id, next_state=BotState.GROUP_DEL)

def on_list_command(message):
    text_list = engine.get_active_events(message.from_user.id)
    if not text_list:
        bot.send_message(message.chat.id, 'No current active event')
    else:
        list_str = '\n'.join([ str(i+1) + ") " + key for i, key in enumerate(text_list)])
        bot.send_message(message.chat.id, list_str, parse_mode='Markdown')

def on_del_group_item_command(message):
    choose_group_message(message.chat.id, next_state=BotState.GROUP_DEL_ITEM)

# --------------- Keyboard callback handlers

@bot.callback_query_handler(func=lambda call: call.data == 'next-month' or call.data == 'previous-month')
def change_month(call):
    engine.log_debug("Processing button callback: %s"%(call.data))
    next_month = call.data == 'next-month'
    chat_id = call.message.chat.id
    saved_date = current_shown_dates.get(chat_id)
    if saved_date is None:
        engine.log_error("Called calendar change_month handler, but there no saved_date by " + str(chat_id) + " chat_id")
        return

    year, month = saved_date
    if next_month:
        month += 1
        if month > 12:
            month, year = (1, year+1)
    else:
        month -= 1
        if month < 1:
            month, year = (12, year-1)

    now = datetime.datetime.now()
    cur_day = now.day if now.month == month and now.year == year else None
    current_shown_dates[chat_id] = (year, month)
    markup = keyboards.calendar(year, month, cur_day)
    bot.edit_message_text("Please, choose a date", call.from_user.id, call.message.message_id, reply_markup=markup)
    bot.answer_callback_query(call.id, text="")

@bot.callback_query_handler(func=lambda call: call.data[0:13] == 'calendar-day-')
def get_day(call):
    engine.log_debug("Processing button callback: %s"%(call.data))
    chat_id = call.message.chat.id
    saved_date = current_shown_dates.get(chat_id)
    if(saved_date is None):
        engine.log_error("Called calendar get_day handler, but there no saved_date by " + str(chat_id) + " chat_id")
        return

    day = call.data[13:]
    date = datetime.datetime(int(saved_date[0]), int(saved_date[1]), int(day), 0, 0, 0)
    fsm[chat_id].state = BotState.AT_TIME_TEXT
    fsm[chat_id].data['date_spec'] = day + '-' + str(saved_date[1]) + '-' + str(saved_date[0])
    # delete keyboard
    bot.edit_message_text(chat_id=call.message.chat.id, message_id=call.message.message_id, text=call.message.text)
    if fsm[chat_id].data['text']:
        bot.send_message(chat_id, 'Ok, ' + date.strftime(r'%b %d') + '. Now write the time of event.')
    else:
        bot.send_message(chat_id, 'Ok, ' + date.strftime(r'%b %d') + '. Now write the time and text of event.')
    bot.answer_callback_query(call.id, text="")
    handle_hour_keyboard(chat_id)

@bot.callback_query_handler(func=lambda call: call.data == 'today' or call.data == 'tomorrow')
def on_calendar_today(call):
    engine.log_debug("Processing button callback: %s"%(call.data))
    chat_id = call.message.chat.id

    date = datetime.datetime.now()
    if call.data == 'tomorrow':
        date += datetime.timedelta(days=1)
    # date = datetime.datetime(int(saved_date[0]), int(saved_date[1]), int(day), 0, 0, 0)
    fsm[chat_id].state = BotState.AT_TIME_TEXT
    fsm[chat_id].data['date_spec'] = str(date.day) + '-' + str(date.month) + '-' + str(date.year)
    # delete keyboard
    bot.edit_message_text(chat_id=call.message.chat.id, message_id=call.message.message_id, text=call.message.text)
    if fsm[chat_id].data['text']:
        bot.send_message(chat_id, 'Ok, ' + date.strftime(r'%b %d') + '. Now write the time of event.')
    else:
        bot.send_message(chat_id, 'Ok, ' + date.strftime(r'%b %d') + '. Now write the time and text of event.')
    bot.answer_callback_query(call.id, text="")
    handle_hour_keyboard(chat_id)

@bot.callback_query_handler(func=lambda call: call.data[:10] == 'time_hour:')
def on_time_hour(call):
    engine.log_debug("Processing button callback: %s"%(call.data))
    chat_id = call.message.chat.id
    hour = int(call.data[10:])
    fsm[chat_id].data['hour'] = hour
    handle_minutes_keyboard(chat_id)

@bot.callback_query_handler(func=lambda call: call.data[:12] == 'time_minute:')
def on_time_minutes(call):
    engine.log_debug("Processing button callback: %s"%(call.data))
    chat_id = call.message.chat.id
    minute = int(call.data[12:])
    message_id = fsm[chat_id].data['message_id']
    bot.delete_message(chat_id, message_id)

    hour = fsm[chat_id].data['hour']
    text = fsm[chat_id].data['text']
    date_spec = fsm[chat_id].data['date_spec']
    if hour is not None and minute is not None:
        result_command = date_spec + ' at ' + str(hour) + '.' + str(minute) + ' ' + text
        (reply, err) = engine.handle_text_message(chat_id, result_command)
        if err:
            bot.send_message(chat_id, base_error_message)
        else:
            bot.send_message(chat_id, reply)
    fsm[chat_id].reset()

@bot.callback_query_handler(func=lambda call: call.data[0:3] == 'grp')
def on_select_group(call):
    engine.log_debug("Processing button callback: %s"%(call.data))
    uid = call.message.chat.id
    gid = int(call.data[3:])
    engine.log_debug("Processing keyboard callback. Call.data = %s. Bot state = %s"%(call.data, str(fsm[uid].state)))
    if fsm[uid].state == BotState.GROUP_ADD_ITEM:
        text = fsm[uid].data['text']
        if engine.add_group_item(gid, text):
            bot.send_message(uid, 'Done')
        else:
            bot.send_message(uid, base_error_message)
    elif fsm[uid].state == BotState.GROUP_DEL:
        if engine.delete_user_group(gid):
            bot.send_message(uid, 'Done')
        else:
            bot.send_message(uid, base_error_message)
    else:
        items = engine.get_group_items(gid)
        if items:
            [text_list, id_list] = list(zip(*items)) # TODO: add group name
            text = '\n'.join([ str(i+1) + ') ' + item for i, item in enumerate(text_list) ])
            fsm[uid].data['id_list'] = id_list
            bot.send_message(uid, text)
        else:
            bot.send_message(uid, 'No items in group')

    bot.delete_message(uid, fsm[uid].data['message_id'])
    if fsm[uid].state == BotState.GROUP_DEL_ITEM:
        bot.send_message(uid, 'Choose element to delete')
    else:
        fsm[uid].reset()

@bot.callback_query_handler(func=lambda call: True)
def callback_inline(call):
    engine.log_debug("Processing button callback: %s"%(call.data))
    id = call.message.chat.id
    engine.log_debug("Processing keyboard callback. Call.data = %s. Bot state = %s"%(call.data, str(fsm[id].state)))
    if call.message:
        if call.data == 'at':
            handle_calendar_call(id, call.message.text)
        elif call.data == 'after':
            fsm[id].state = BotState.AFTER_INPUT
            fsm[id].data['text'] = call.message.text
            bot.send_message(id, 'Ok, now write time duration.')
        elif call.data == 'group':
            fsm[id].state = BotState.GROUP_ADD_ITEM
            fsm[id].data['text'] = call.message.text
            choose_group_message(id, next_state=BotState.GROUP_ADD_ITEM, add_if_not_exist=False)
        elif call.data == 'Ok':
            bot.edit_message_reply_markup(chat_id=call.message.chat.id, message_id=call.message.message_id)
            return
        elif call.data == 'ignore':
            fsm[id].reset()
        elif call.data != "Ok":
            call.message.text = call.data + " " + call.message.text
            handle_text(call.message)
        bot.delete_message(chat_id=call.message.chat.id, message_id=call.message.message_id)

# ------------- helper function

def delete_rep_event(message):
    event_id_str = message.text
    if not event_id_str.isdigit():
        msg = bot.reply_to(message, 'You should write number. Operation aborted.')
        fsm[message.chat.id].reset()
        return

    event_id = int(event_id_str)-1
    id_list = fsm[message.chat.id].data['rep_id_list']
    if id_list and event_id >= 0 and event_id < len(id_list):
        del_id = id_list[event_id]
        if engine.del_rep_event(del_id):
            bot.send_message(message.chat.id, "Done.")
        else:
            bot.send_message(uid, base_error_message)
        fsm[message.chat.id].reset()
    else:
        bot.send_message(message.chat.id, "Number is out of limit. Operation aborted.")
        fsm[message.chat.id].reset()

def handle_calendar_call(chat_id, text=None):
    now = datetime.datetime.now()
    date = (now.year,now.month)
    current_shown_dates[chat_id] = date

    markup = keyboards.calendar(now.year, now.month, now.day)
    fsm[chat_id].state = BotState.AT_CALENDAR
    fsm[chat_id].data['text'] = text
    keyboard_message = bot.send_message(chat_id, "Please, choose a date", reply_markup=markup)
    fsm[chat_id].data['message_id'] = keyboard_message.message_id

def handle_hour_keyboard(chat_id):
    if fsm[chat_id].data['message_id']:
        message_id = fsm[chat_id].data['message_id']
        bot.delete_message(chat_id, message_id)

    markup = keyboards.hour()
    fsm[chat_id].state = BotState.AT_TIME
    keyboard_message = bot.send_message(chat_id, "Please, choose hour", reply_markup=markup)
    fsm[chat_id].data['message_id'] = keyboard_message.message_id

def handle_minutes_keyboard(chat_id):
    if fsm[chat_id].data['message_id']:
        message_id = fsm[chat_id].data['message_id']
        bot.delete_message(chat_id, message_id)

    markup = keyboards.minutes()
    fsm[chat_id].state = BotState.AT_TIME
    hour = fsm[chat_id].data['hour']
    text = 'Ok, %d. Now choose minute.'%hour
    keyboard_message = bot.send_message(chat_id, text, reply_markup=markup)
    fsm[chat_id].data['message_id'] = keyboard_message.message_id

def choose_group_message(id, next_state=None, add_if_not_exist=False):
    groups = engine.get_user_groups(id)
    if not groups:
        if add_if_not_exist:
            fsm[id].state = BotState.GROUP_ADD
            bot.send_message(id, 'There is not group yet. Write name for your new group')
        else:
            bot.send_message(id, 'No groups.')
        return
    [text_list, id_list] = list(zip(*groups))
    if next_state:
        fsm[id].state = next_state
    keyboard = keyboards.groups(text_list, id_list)
    keyboard_message = bot.send_message(id, 'Choose group.', reply_markup=keyboard)
    fsm[id].data['message_id'] = keyboard_message.message_id

@bot.message_handler(content_types=['voice'])
def voice_processing(message):
    engine.log_debug('Start to processing voice. File id = ' + message.voice.file_id)
    file_info = bot.get_file(message.voice.file_id)
    file = bot.download_file(file_info.file_path)
    if not os.path.exists('voice'):
        os.makedirs('voice')
    filename_ogg = message.voice.file_id + '.ogg'
    filename_wav = message.voice.file_id + '.wav'
    with open('voice/' + filename_ogg, 'wb') as f:
        f.write(file)

    command = [
        'opusdec',
        '--rate', '16000',
        '--force-wav',
        '--quiet',
        'voice/' + filename_ogg,
        'voice/' + filename_wav
    ]
    proc = subprocess.Popen(command,
                           stdout=subprocess.PIPE,
                           stderr=subprocess.PIPE)
    err = proc.stderr.read()
    if err: engine.log_error(err)

    command = [
        'asrclient-cli.py',
        '--key=' + config.yandex_api_token,
        '--silent',
        'voice/' + filename_wav
    ]
    proc = subprocess.Popen(' '.join(command), shell=True,
                           stdout=subprocess.PIPE,
                           stderr=subprocess.PIPE)
    result = []
    for line in proc.stdout:
        result.append(line)

    err = proc.stderr.read()
    if err: engine.log_error(err)

    engine.log_info('Speech rec result: ' + str(result))

    if len(result) <= 1:
        bot.send_message(message.chat.id, "Can't recognize =(")
    else:
        keyboard = keyboards.action()
        bot.send_message(message.chat.id, result[:-1], reply_markup=keyboard)

def on_engine_event(text, chat_id):
    keyboard = keyboards.action()
    bot.send_message(chat_id, text, reply_markup=keyboard)

if __name__ == '__main__':
    parser = argparse.ArgumentParser()
    parser.add_argument("-v",
                        "--verbose",
                        help="show log lines in stdout",
                        action="store_true")
    parser.add_argument("--one-poll",
                        help="do not try to polling againg after error",
                        action="store_true")
    args = parser.parse_args()
    verbose = True if args.verbose else False
    one_poll = True if args.one_poll else False

    engine.initialize(verbose)
    engine.register_callback(on_engine_event)
    user_chat_id_list = engine.get_user_chat_id_all()
    for chat_id in user_chat_id_list:
        fsm[chat_id] = FSMData()

    engine.run()

    while True:
        try:
            bot.polling()
        except:
            engine.log_error("I am down =(")
            for key in fsm:
                fsm[key].reset()

        if one_poll:
            break
        time.sleep(10)

    engine.stop()


from django.shortcuts import render, redirect, get_object_or_404, reverse
from django.views.generic import TemplateView, FormView
from app_user.models import User, Project, ProgramLang, ProjectPicture
from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin
from django.core.paginator import Paginator
import random
from .forms import (
    ProjectCreationForm,
    ProjectEditForm,
    AddProjectPictureForm,
    UserLangSelectFilterForm,
    ProjectLangSelectFilterForm,
)

class HomeView(TemplateView):
    """
    View for the home page.
    """

    model = User
    template_name = "app_home/index.html"

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        return context

class AboutView(TemplateView):
    """
    View for the about page.
    """

    model = User
    template_name = "app_home/about.html"

class DeveloperOverview(TemplateView):
    """
    View for the general overview displaying all users except the
    logged in user.
    """

    model = User
    template_name = "app_home/developer_overview.html"

    def get(self, request, *args, **kwargs):
        if request.GET.get("p_language", None):
            p_langs = ProgramLang.objects.filter(
                pk__in=request.GET.getlist("p_language")
            )
        else:
            p_langs = []
        form = UserLangSelectFilterForm(initial={"p_language": p_langs})

        all_users = User.objects.all().exclude(pk=request.user.pk)
        href_filter = ""
        for lang in p_langs:
            all_users = all_users.filter(p_language=lang).order_by("id")
            href_filter += f"&p_language={lang.pk}"

        # set up pagination for users (8 per page)
        # passing in users filtered by p_lang that are distinct
        paginator = Paginator(all_users, 8)
        page_number = request.GET.get("page")
        users = paginator.get_page(page_number)
        p_nums = "p" * users.paginator.num_pages
        context = {
            "form": form,
            "href_filter": href_filter,
            "users": users,
            "p_nums": p_nums,
        }
        return render(request, "app_home/developer_overview.html", context)

class ProfileDetailView(LoginRequiredMixin, TemplateView):
    """
    View for rendering detailed profile view for chosen user.
    """

    model = User
    template_name = "app_home/user_detail_view.html"

    def get(self, request, *args, **kwargs):
        profile_pk = kwargs["pk"]
        user_to_get = get_object_or_404(User, pk=profile_pk)
        try:
            user_to_get_projects = Project.objects.filter(user=user_to_get)
        except Exception:
            user_to_get_projects = None

        try:
            p_langs = user_to_get.p_language.values()
        except Exception:
            p_langs = None

        context = {
            "user": request.user,
            "user_for_profile": user_to_get,
            "p_langs": p_langs,
            "user_projects": user_to_get_projects,
        }
        return render(request, "app_home/user_detail_view.html", context)

class ProjectOverview(TemplateView):
    """
    Project overview view.
    """

    model = Project
    template_name = "app_home/project_overview.html"

    def get(self, request, *args, **kwargs):
        if request.GET.get("p_language", None):
            p_langs = ProgramLang.objects.filter(
                pk__in=request.GET.getlist("p_language")
            )
        else:
            p_langs = []
        form = ProjectLangSelectFilterForm(initial={"p_language": p_langs})

        all_projects = Project.objects.all()
        href_filter = ""
        for lang in p_langs:
            all_projects = all_projects.filter(p_language=lang).order_by("id")
            href_filter += f"&p_language={lang.pk}"

        # set up pagination
        paginator = Paginator(all_projects, 4)
        page_number = request.GET.get("page")
        projects = paginator.get_page(page_number)
        p_nums = "p" * projects.paginator.num_pages
        context = {
            "form": form,
            "href_filter": href_filter,
            "projects": projects,
            "p_nums": p_nums,
        }
        return render(request, "app_home/project_overview.html", context)

class ProjectDetailView(LoginRequiredMixin, TemplateView):
    """
    View for rendering the detail page for a project.
    """

    model = Project
    template_name = "app_home/project_detail_view.html"

    def get(self, request, *args, **kwargs):
        project_pk = kwargs["pk"]
        project_to_get = get_object_or_404(Project, pk=project_pk)

        context = {"user": request.user, "project": project_to_get}

        return render(request, "app_home/project_detail_view.html", context)

class CreateProjectView(LoginRequiredMixin, FormView):
    """
    View to create a project.
    """

    template_name = "app_home/create_project.html"
    form_class = ProjectCreationForm

    def get(self, request):
        project_form = ProjectCreationForm()
        context = {"form": project_form}

        return render(request, "app_home/create_project.html", context)

    def post(self, request):
        user = request.user
        form = self.form_class(request.POST)
        p_langs = form.data.getlist("p_language")

        if form.is_valid():
            new_project = form.save()
            new_project.user.add(user)
            for lang in p_langs:
                new_project.p_language.add(lang)
                new_project.save()
            messages.success(request, "Project successfully created!")
            return redirect(
                reverse(
                    "app_home:project-detail-view",
                    kwargs={"pk": new_project.pk},
                )
            )
        elif "profanity" in form.errors.as_text():
            messages.error(
                request, "Please do not use profanities in your project name!"
            )
        elif "duplicate_name" in form.errors.as_text():
            messages.error(
                request, "Project name already taken! Please choose another."
            )
        else:
            messages.error(
                request, "Please select at least one programming language"
            )
        return redirect(reverse("app_home:create-project"))

class EditProjectView(LoginRequiredMixin, TemplateView):
    """
    View to edit a project
    """

    model = Project
    template_name = "app_home/edit_project.html"

    def get(self, request, *args, **kwargs):
        project = get_object_or_404(Project, pk=kwargs["pk"])
        if project.user.get() != request.user:
            return redirect("app_home:project-overview")
        else:
            context = {
                "project": project,
                "form": ProjectEditForm(instance=project),
            }

        return render(request, "app_home/edit_project.html", context)

    def post(self, request, *args, **kwargs):
        user = request.user
        project = get_object_or_404(Project, pk=kwargs["pk"])
        form = ProjectEditForm(request.POST, instance=project)
        p_langs = form.data.getlist("p_language")
        if form.is_valid():
            project = form.save(commit=False)
            project.p_language.set(p_langs)
            project.save()
            messages.success(request, "Project successfully edited!")
            return redirect(
                reverse(
                    "app_home:project-detail-view", kwargs={"pk": project.pk}
                )
            )
        elif "profanity" in form.errors.as_text():
            messages.error(
                request, "Please do not use profanities in your project name!"
            )
            return render(
                request, "app_home/edit_project.html", {"form": form}
            )
        elif "duplicate_name" in form.errors.as_text():
            messages.error(
                request, "Project name already in use! Please choose another"
            )
            return render(
                request, "app_home/edit_project.html", {"form": form}
            )
        elif not form["p_language"].value():
            messages.error(
                request, "Please select at least one programming language"
            )
        else:
            messages.error(
                request, "Form could not be submitted. Please try again"
            )
        return render(request, "app_home/edit_project.html", {"form": form})

class DeleteProjectView(LoginRequiredMixin, TemplateView):
    """
    View to delete a project
    """

    model = Project
    template_name = "app_home/about.html"

    def get(self, request, *args, **kwargs):
        project = get_object_or_404(Project, pk=kwargs["pk"])
        if project:
            if project.user.get() != request.user:
                return redirect("app_home:project-overview")
            else:
                project.delete()
                messages.success(request, "Project successfully deleted!")
        else:
            messages.error(
                request, "Project could not be deleted. Please try again."
            )
        return redirect(reverse("app_home:project-overview"))

class AddProjectPicture(LoginRequiredMixin, TemplateView):
    """
    View to add a picture to a project
    """

    model = ProjectPicture
    template_name = "app_home/project_picture.html"

    def get(self, request, *args, **kwargs):
        project = get_object_or_404(Project, pk=kwargs["pk"])
        if project.project_pic:
            pic_query_set = project.project_pic.values()
        else:
            pic_query_set = [""]
        form = AddProjectPictureForm()
        context = {
            "project": project,
            "pictures": pic_query_set,
            "form": form,
        }
        return render(request, "app_home/project_picture.html", context)

    def post(self, request, *args, **kwargs):
        project = get_object_or_404(Project, pk=kwargs["pk"])
        project_pk = project.pk

        new_picture = AddProjectPictureForm(
            request.POST, request.FILES, initial={"project": project}
        )
        if new_picture.is_valid():
            new_picture = new_picture.save(commit=False)
            new_picture.project = project
            new_picture.save()
            messages.success(request, "Picture successfully added!")
        else:
            messages.error(
                request, "Your picture could not be added at this time."
            )

        return redirect(
            reverse("app_home:add-project-pic", kwargs={"pk": project_pk})
        )

class DeleteProjectPicture(LoginRequiredMixin, TemplateView):
    """
    View to delete pictures from projects
    """

    model = ProjectPicture
    template_name = "app_home/project_picture.html"

    def get(self, request, *args, **kwargs):
        pic_to_delete = get_object_or_404(ProjectPicture, pk=kwargs["pk"])
        project = pic_to_delete.project
        project_owner_pk = project.user.values()[0]["id"]
        if request.user.pk == project_owner_pk:
            pic_to_delete.delete()
            messages.success(request, "Picture successfully deleted!")
            return redirect(
                reverse("app_home:add-project-pic", kwargs={"pk": project.pk})
            )
        messages.error(request, "Picture could not be deleted!")
        return redirect(reverse("app_home:project-overview"))


from functools import cmp_to_key
import json
import networkx as nx
import os
import platform
import sys
import torch
import warnings
from collections import defaultdict
from pathlib import Path
from PIL import Image, ImageDraw, ImageFont
from termcolor import colored
from utils.logger import logger, shell_cmd
from utils.envs import enver, setup_envs_of_dit
from utils.calculator import (
    rect_area,
    rect_overlap,
    rect_contain,
    union_rects,
    expand_rect_bound,
)

warnings.filterwarnings("ignore")

try:
    from detectron2.config import CfgNode as CN
    from detectron2.config import get_cfg
    from detectron2.utils.visualizer import ColorMode, Visualizer
    from detectron2.data import MetadataCatalog
    from detectron2.data.detection_utils import read_image
    from detectron2.engine import DefaultPredictor

    from unilm.dit.object_detection.ditod import add_vit_config
except Exception as e:
    logger.err(e)
    raise e

# GPT-Paper repo path
repo_path = Path(__file__).parents[1]
# GPT-Paper repo parent path
repo_parent_path = repo_path.parent

detectron2_path = repo_parent_path / "detectron2"
unilm_path = repo_parent_path / "unilm"

class DITLayoutAnalyzer:
    """
    * app.py · nielsr/dit-document-layout-analysis
        * https://huggingface.co/spaces/nielsr/dit-document-layout-analysis/blob/main/app.py
    * unilm/dit/object_detection/inference.py
        * https://github.com/microsoft/unilm/blob/master/dit/object_detection/inference.py
    """

    def __init__(self, size="base"):
        self.model_size = size
        self.is_setup_model = False

    def setup_model(self, quiet=True):
        logger.note("> Setting up model of DITLayoutAnalyzer ...")
        logger.enter_quiet(quiet)
        enver.set_envs(set_proxy=True, cuda_device=True, huggingface=True)
        os.environ = enver.envs
        self.load_configs()
        self.load_weights()
        self.set_device()
        self.create_predictor()
        self.set_metadata()
        self.is_setup_model = True
        enver.restore_envs()
        os.environ = enver.envs
        logger.exit_quiet(quiet)

    def test_run(self):
        self.setup_model(quiet=False)
        # self.annotate_image(
        #     input_image_path=repo_path / "examples" / "example_pdf_4.png",
        #     output_image_path=repo_path / "examples" / "example_pdf_4_output.png",
        # )

    # Step 1: Instantiate config
    def load_configs(self):
        self.cfg = get_cfg()
        add_vit_config(self.cfg)

        if self.model_size == "large":
            size_str = "large"
        else:
            size_str = "base"

        yaml_name = f"cascade_dit_{size_str}.yaml"
        cascade_dit_yml = (
            unilm_path
            / "dit"
            / "object_detection"
            / "publaynet_configs"
            / "cascade"
            / yaml_name
        )

        logger.note(f"> Loading configs from `{yaml_name}`:")
        logger.file(f"  - {cascade_dit_yml}")

        self.cfg.merge_from_file(cascade_dit_yml)

    # Step 2: Load model weights to config
    def load_weights(self):
        if self.model_size == "large":
            size_str = "l"
        else:
            size_str = "b"

        pth_name = f"publaynet_dit-{size_str}_cascade.pth"
        publaynet_dit_cascade_pth = repo_path / "data" / pth_name
        if not publaynet_dit_cascade_pth.exists():
            raise FileNotFoundError(f"`{pth_name}` not found.")

        logger.note(f"> Loading weights from `{pth_name}`:")
        logger.file(f"  - {publaynet_dit_cascade_pth}")

        self.cfg.MODEL.WEIGHTS = str(publaynet_dit_cascade_pth)

    # Step 3: Set CUDA device
    def set_device(self):
        if torch.cuda.is_available():
            cuda_device = os.environ.get("CUDA_VISIBLE_DEVICES", 0)
            logger.note(f"> Using GPU:{cuda_device} ...")
            self.cfg.MODEL.DEVICE = "cuda"
        else:
            logger.note(f"> Using CPU ...")
            self.cfg.MODEL.DEVICE = "cpu"

    # Step 4: Create Predictor
    def create_predictor(self):
        logger.note("> Creating predictor ...")
        self.predictor = DefaultPredictor(self.cfg)

    # Step 5: Set meta data
    def set_metadata(self):
        logger.note("> Setting metadata ...")
        # logger.mesg(self.cfg.DATASETS)
        dataset_name = "publaynet_val"
        self.metadata = MetadataCatalog.get(dataset_name)
        # logger.mesg(self.metadata)
        self.thing_classes = ["text", "title", "list", "table", "figure"]
        self.metadata.set(thing_classes=self.thing_classes)

    def annotate_image(
        self,
        annotate_info_json_path,
        input_image_path=None,
        output_image_path=None,
        quiet=False,
    ):
        logger.enter_quiet(quiet)

        if input_image_path is None:
            raise ValueError("`input_image_path` is None!")

        logger.note(f"> Analyzing the layout of input image:")
        logger.file(f"  - {input_image_path}")

        image = read_image(str(input_image_path))
        pred_output = self.predictor(image)["instances"]
        pred_things = [self.thing_classes[c] for c in pred_output.pred_classes]

        logger.note("> Results:")
        image_height, image_width = pred_output.image_size
        logger.back(f"  - image_size: {image_width}(w) * {image_height}(h)")
        logger.mesg(f"  - num_instances: {len(pred_output)}")
        logger.back(f"  - pred_classes: {pred_output.pred_classes.tolist()}")
        logger.mesg(f"  - pred_things: {pred_things}")
        logger.back(f"  - pred_boxes: {pred_output.pred_boxes.tensor.tolist()}")
        logger.back(f"  - scores: {pred_output.scores.tolist()}")

        self.dump_annotate_info(
            annotate_info_json_path, input_image_path, output_image_path, pred_output
        )

        logger.exit_quiet(quiet)

        return pred_output

    def dump_annotate_info(
        self, annotate_info_json_path, input_image_path, output_image_path, output
    ):
        image_height, image_width = output.image_size
        self.annotate_infos = {
            "page": {
                "original_image_path": str(input_image_path),
                "current_image_path": str(output_image_path),
                "image_width": image_width,
                "image_height": image_height,
                "regions_num": len(output),
            },
            "regions": [],
        }
        pred_classes = output.pred_classes.tolist()
        pred_things = [self.thing_classes[c] for c in pred_classes]
        pred_boxes = [
            [round(x, 1) for x in box] for box in output.pred_boxes.tensor.tolist()
        ]
        pred_scores = [round(x * 100, 2) for x in output.scores.tolist()]
        for i in range(len(output)):
            region_info = {
                "idx": i + 1,
                "thing": pred_things[i],
                "score": pred_scores[i],
                "box": pred_boxes[i],
            }
            self.annotate_infos["regions"].append(region_info)

        logger.success(f"> Saving annotated info:")
        logger.file(f"  - {annotate_info_json_path}")
        with open(annotate_info_json_path, "w") as wf:
            wf.write(json.dumps(self.annotate_infos, indent=4))

        return annotate_info_json_path

class LayoutLMv3Analyzer:
    """
    * unilm/layoutlmv3 at master · microsoft/unilm
        * https://github.com/microsoft/unilm/tree/master/layoutlmv3#document-layout-analysis-on-publaynet
    """

    def __init__(self) -> None:
        pass

def draw_regions_on_page(
    regions_info_json_path,
    output_parent_path,
    spacing=2,
    show_region_idx=True,
    score_use_percent=True,
    overwrite=False,
    quiet=False,
):
    logger.enter_quiet(quiet)

    with open(regions_info_json_path, "r") as rf:
        regions_infos = json.load(rf)
    original_page_image_path = Path(regions_infos["page"]["original_image_path"])
    drawn_page_image_path = output_parent_path / original_page_image_path.name
    if not overwrite and drawn_page_image_path.exists():
        logger.exit_quiet(quiet)
        return

    region_colors = {
        "text": (0, 128, 0),
        "title": (128, 0, 0),
        "list": (0, 0, 128),
        "table": (128, 128, 0),
        "figure": (0, 0, 128),
    }
    page_num = int(original_page_image_path.stem.split("_")[-1])
    page_image = Image.open(original_page_image_path)
    page_image_width, page_image_height = page_image.size
    image_draw = ImageDraw.Draw(page_image, "RGBA")

    regions = regions_infos["regions"]
    logger.mesg(f"- Draw on Page {page_num} with {len(regions)} regions")
    logger.back(f"  - {drawn_page_image_path}")

    for region in regions:
        region_idx = region["idx"]
        region_box = region["box"]
        region_thing = region["thing"]
        region_score = region["score"]

        region_box = [
            region_box[0] - spacing,
            region_box[1] - spacing,
            region_box[2] + spacing,
            region_box[3] + spacing,
        ]

        text_font = ImageFont.truetype("times.ttf", 40)
        if show_region_idx:
            region_idx_str = f"{region_idx}."
        else:
            region_idx_str = ""

        if score_use_percent:
            region_score_str = f"{round(region_score)}%"
        else:
            region_score_str = f"{region_score}"

        text_str = f"{region_idx_str}{region_thing}({region_score_str})"
        text_bbox = image_draw.textbbox(
            region_box[:2], text_str, font=text_font, anchor="rt"
        )
        image_draw.text(
            region_box[:2], text_str, fill="black", font=text_font, anchor="rt"
        )
        region_rect_color = region_colors[region_thing]
        image_draw.rectangle(
            region_box,
            outline=region_rect_color,
            fill=(*region_rect_color, 64),
            width=2,
        )

        image_draw.rectangle(text_bbox, fill=(*region_rect_color, 80))
    page_image.save(drawn_page_image_path)
    logger.exit_quiet(quiet)

def calc_regions_overlaps(regions):
    """
    Return: List of list of tuples. List length is num of regions.
    In each level-1 list, there is level-2 list of tuples.

    For each tuple in i-th list:
    The 1st element is the idx of overlapped region with region i, and
    the 2nd element is the overlapped area ratio of overlapped region in region i.
    """
    regions_overlaps = []
    for i in range(len(regions)):
        region_i_overlaps = []
        for j in range(len(regions)):
            region_i = regions[i]
            region_j = regions[j]
            region_i_overlaps_j, region_i_j_overlapped_area_ratio = rect_overlap(
                region_i["box"], region_j["box"], t=-5
            )
            if i != j and region_i_overlaps_j:
                region_i_overlaps.append(
                    (regions[j]["idx"], region_i_j_overlapped_area_ratio)
                )
        regions_overlaps.append(region_i_overlaps)

    for i in range(len(regions)):
        region_i_overlaps = regions_overlaps[i]
        region_i = regions[i]

        if region_i_overlaps:
            region_i_overlaps_str = ", ".join(
                [f"{x[0]}({round(x[1],2)})" for x in region_i_overlaps]
            )
            region_i_overlaps_str = f"- {region_i_overlaps_str}"
            line_color = "light_red"
            overlap_region_num_str = f", overlaps with {len(region_i_overlaps)} regions"
        else:
            region_i_overlaps_str = ""
            line_color = "light_cyan"
            overlap_region_num_str = ""

        logger.store_indent()
        logger.indent(2)
        logger.back(
            colored(
                f"- {region_i['idx']}: {region_i['thing']} region ({region_i['score']})"
                + overlap_region_num_str,
                line_color,
            )
        )
        if region_i_overlaps_str:
            logger.indent(2)
            logger.back(region_i_overlaps_str)
        logger.restore_indent()

    return regions_overlaps

def remove_regions_overlaps(
    regions,
    regions_overlaps,
    overlap_area_ratio_threshold=0.9,
    ignore_score_threshold=0.15,
):
    """
    Rules of thumb:

    1. Keep the separated regions.
    2. For overlapped regions:
    (a) Ignore the regions with scores lower than `ignore_score_threshold`.
        (Reason: This would remove mis-categorized,
                 such as figures which contain texts that should be separated.)
    (b) Ignore the regions that are totally contained by other higher-score regions.
        (Reason: There are always other regions with higher score that overlaps with it.)
    (c) For the remained overlapped regions (after removing the cases in (a) and (b)),
        group the regions with connected components, and union them with bound rects.
    """
    filtered_regions = []
    for i in range(len(regions)):
        region_i = regions[i]
        region_i_overlaps = regions_overlaps[i]
        keep_region_i = True
        if region_i_overlaps:
            # Ignore the regions with scores lower than `ignore_score_threshold`.
            if region_i["score"] / 100 <= ignore_score_threshold:
                keep_region_i = False
                regions_overlaps[i] = []
                logger.back(
                    f"Ignore region {i+1} its score {(region_i['score'])} "
                    f"is lower than {round(ignore_score_threshold*100)}"
                )
            # Ignore the regions that are totally contained by other higher-score regions.
            else:
                for j in range(len(region_i_overlaps)):
                    region_j_idx, region_i_j_overlapped_area_ratio = region_i_overlaps[
                        j
                    ]
                    region_j = regions[region_j_idx - 1]
                    logger.back(
                        f"region {i+1} ({region_i['score']}) and region {region_j_idx} ({(region_j['score'])}) "
                        f"[{round(region_i_j_overlapped_area_ratio*100)}%]"
                    )
                    if (
                        region_i_j_overlapped_area_ratio >= overlap_area_ratio_threshold
                        and region_j["score"] >= region_i["score"]
                    ):
                        # Means this region is a totally child region, ignore it.
                        logger.back(
                            f"Ignore region {i+1} as region {region_j_idx} has score {(region_j['score'])} "
                            f"[{round(region_i_j_overlapped_area_ratio*100)}%]"
                        )
                        keep_region_i = False
                        regions_overlaps[i] = []
                        break
        # Keep separated regions
        if keep_region_i:
            filtered_regions.append(region_i)

    # Re-create the overlapped regions after removing the totally child regions
    # (totally child means: 100% contained by other higher-score regions)
    new_overlap_regions = []
    for i in range(len(regions_overlaps)):
        region_i_overlaps = regions_overlaps[i]
        region_i = regions[i]
        if region_i_overlaps:
            new_region_i_overlaps = []
            for j in range(len(region_i_overlaps)):
                region_j_idx, region_i_j_overlapped_area_ratio = region_i_overlaps[j]
                if regions_overlaps[region_j_idx - 1]:  # region_j is not removed
                    new_region_i_overlaps.append(
                        (region_j_idx, region_i_j_overlapped_area_ratio)
                    )

            if new_region_i_overlaps:
                new_overlap_regions.append(region_i)

    # Group the remained overlapped regions with connected components
    if new_overlap_regions:
        new_overlap_regions_idxs = [x["idx"] for x in new_overlap_regions]
        new_overlap_regions_str = ", ".join(map(str, new_overlap_regions_idxs))
        logger.warn(
            f"{len(new_overlap_regions)} regions remains overlapped: [{new_overlap_regions_str}]"
        )

        # Re-calculate the regions overlaps
        new_regions_overlaps = calc_regions_overlaps(new_overlap_regions)
        new_overlap_regions_edges = []
        for i, new_regions_overlaps in enumerate(new_regions_overlaps):
            region_idx = new_overlap_regions_idxs[i]
            for new_region_idx, new_region_overlap_area_ratio in new_regions_overlaps:
                new_overlap_regions_edges.append((region_idx, new_region_idx))

        # Union (combine) connected regions with Group Theory
        G = nx.Graph()
        G.add_nodes_from(new_overlap_regions_idxs)
        G.add_edges_from(new_overlap_regions_edges)
        connected_regions_components = list(nx.connected_components(G))
        logger.success(
            f"> Combine {connected_regions_components} to {len(connected_regions_components)} regions"
        )

        # Remove the remained overlapped regions from filtered regions
        # as they would be grouped (combined) to new large (and less) regions
        filtered_regions = [
            region
            for region in filtered_regions
            if region["idx"] not in new_overlap_regions_idxs
        ]

        # Create new combined regions from connected components
        for c_idx, connected_regions_component in enumerate(
            connected_regions_components
        ):
            connected_regions_idxs = list(connected_regions_component)
            connected_regions_boxes = [
                regions[region_idx - 1]["box"] for region_idx in connected_regions_idxs
            ]
            union_region_box = union_rects(connected_regions_boxes)
            union_region_score = 0
            union_region_thing = ""
            union_region_idx = len(regions) + c_idx + 1

            # Determine the thing and score of new combined region
            thing_scores = defaultdict(float)
            for i in connected_regions_idxs:
                region_i = regions[i - 1]
                region_i_thing = region_i["thing"]
                region_i_score = region_i["score"]
                region_i_area = rect_area(*region_i["box"])
                thing_scores[region_i_thing] += region_i_score * region_i_area

            connected_region_areas_sum = sum(
                rect_area(*x) for x in connected_regions_boxes
            )
            connected_region_scores_sum = sum(thing_scores.values())
            connected_region_score = (
                connected_region_scores_sum / connected_region_areas_sum
            )

            union_region_thing = max(thing_scores, key=thing_scores.get)
            union_region_score = round(connected_region_score, 2)

            union_region = {
                "idx": union_region_idx,
                "thing": union_region_thing,
                "score": union_region_score,
                "box": union_region_box,
            }

            logger.indent(2)
            logger.success(
                f"- New {union_region_thing} region {union_region_idx} ({union_region_score})"
            )
            logger.indent(-2)
            filtered_regions.append(union_region)

    return filtered_regions

class RegionsOrderer:
    def sort_two_regions_by_reading_order(self, region1, region2):
        """
        Return:
            -1: rect1 is before rect2
             1: rect1 is after rect2

        In this certain case, there is no need to consider the conditions
        that rects are intersected,
        as we have already split them to no-overlapped ones.

        1. Check Left and Right:
            r1 <= l2: rect1 is before rect2
            l1 >= r2: rect1 is after rect2
        2. Then check Top and Bottom:
            b1 <= t2: rect1 is before rect2
            t1 >= b2: rect1 is after rect2
        3. Others:
            raise Error

        Since rects have no intersections, the checks above is sufficient.

        """
        rect1 = region1["box"]
        rect2 = region2["box"]

        l1, t1, r1, b1 = expand_rect_bound(rect1, t=-5)
        l2, t2, r2, b2 = expand_rect_bound(rect2, t=-5)
        if r1 <= l2:
            return -1
        elif l1 >= r2:
            return 1
        else:
            if b1 <= t2:
                return -1
            elif t1 >= b2:
                return 1
            else:
                raise ValueError("Rects have intersections!")

    def sort_regions_by_reading_order(self, regions):
        for region in regions:
            region_box = region["box"]
            region_thing = region["thing"]
            region_score = region["score"]
            logger.back(
                f"- {region['idx']}: {region_thing} region ({region_score}) {region_box}"
            )
        sorted_regions = sorted(
            regions, key=cmp_to_key(self.sort_two_regions_by_reading_order)
        )
        for idx, region in enumerate(sorted_regions):
            sorted_regions[idx]["idx"] = idx + 1
            region_box = region["box"]
            region_thing = region["thing"]
            region_score = region["score"]
            logger.back(
                f"- {region['idx']}: {region_thing} region ({region_score}) {region_box}"
            )

        return sorted_regions

if __name__ == "__main__":
    dit_layout_analyzer = DITLayoutAnalyzer(size="large")
    dit_layout_analyzer.test_run()


from django import forms
from django.forms import ModelForm
from .models import ToDoModel

class ToDoForm(ModelForm):
    class Meta:
        model = ToDoModel
        fields = ['title', 'description', 'high_priority']

        widgets = {
            'title': forms.TextInput(attrs={'class': 'form-control'}),
            'description': forms.Textarea(attrs={'class': 'form-control'}),
            # 'high_priority': forms.CheckboxInput(attrs={'class': 'form-control'}),
        }

        def clean_title(self):
            new_title = self.cleaned_data['title'].lower()

            # if new_title == 'create':
            #     from django.core.exceptions import ValidationError
            #     raise ValidationError('title may not be "Create"')
            return new_title


"""goals

Revision ID: 2db51ca0949b
Revises: 2ba80b8acdf9
Create Date: 2018-08-16 22:16:43.684402

"""
from alembic import op
import sqlalchemy as sa

# revision identifiers, used by Alembic.
revision = '2db51ca0949b'
down_revision = '2ba80b8acdf9'
branch_labels = None
depends_on = None

def upgrade():
    # ### commands auto generated by Alembic - please adjust! ###
    op.create_table('goal',
    sa.Column('id', sa.INTEGER(), nullable=False),
    sa.Column('user_id', sa.INTEGER(), nullable=True),
    sa.Column('distance', sa.TEXT(), nullable=True),
    sa.Column('date', sa.DATE(), nullable=True),
    sa.ForeignKeyConstraint(['user_id'], ['user.id'], ),
    sa.PrimaryKeyConstraint('id')
    )
    op.create_index(op.f('ix_goal_id'), 'goal', ['id'], unique=False)
    op.create_index(op.f('ix_goal_user_id'), 'goal', ['user_id'], unique=False)
    # ### end Alembic commands ###

def downgrade():
    # ### commands auto generated by Alembic - please adjust! ###
    op.drop_index(op.f('ix_goal_user_id'), table_name='goal')
    op.drop_index(op.f('ix_goal_id'), table_name='goal')
    op.drop_table('goal')
    # ### end Alembic commands ###


from setuptools import setup
import os

with open(
        os.path.join(os.path.dirname(__file__), 'eagleSqlTools', 'VERSION')
) as version_file:
    version = version_file.read().strip()

setup(
    name='eagleSqlTools',
    version=version,
    description='Utilities for accessing EAGLE public database.',
    url='',
    author='Kyle Oman',
    author_email='koman@astro.rug.nl',
    license='GNU GPL v3',
    packages=['eagleSqlTools'],
    install_requires=['numpy'],
    include_package_data=True,
    zip_safe=False
)


import os
import pybullet as p
import numpy as np
from scipy.spatial.transform import Rotation as R
import xml.etree.ElementTree as ET

urdf = 'models/hook/Hook_60/base.urdf'
assert os.path.exists(urdf), f'{urdf} not exists'

physics_client_id = p.connect(p.GUI)
p.resetDebugVisualizerCamera(
    cameraDistance=0.3,
    cameraYaw=120,
    cameraPitch=0,
    cameraTargetPosition=[0.0, 0.0, 0.0]
)

tree = ET.parse(urdf)
root = tree.getroot()
center = np.array(
    [
        float(i) for i in root[0].find(
            "inertial"
        ).find(
            "origin"
        ).attrib['xyz'].split(' ')
    ]
)

rot = [0, np.pi / 2, 0]
matrix = R.from_rotvec(rot).as_matrix()
quat = R.from_rotvec(rot).as_quat()
quat = list(quat)

offset = matrix @ center

obj_id = p.loadURDF(urdf, [0, 0, 0], quat)
pos, rot = p.getBasePositionAndOrientation(obj_id)
pose = list(pos) + list(rot)
print(pose)

obj_id = p.loadURDF(urdf, [0, 0, 0], quat)
p.resetBasePositionAndOrientation(obj_id, [0, 0, 0], quat)
pos, rot = p.getBasePositionAndOrientation(obj_id)
pose = list(pos) + list(rot)
print(pose)

pose = np.asarray(pose)
pose[:3] = pose[:3] + offset
print(list(pose))

while True:
    # key callback
    keys = p.getKeyboardEvents()
    if ord('q') in keys and keys[ord('q')] & (p.KEY_WAS_TRIGGERED | p.KEY_IS_DOWN):
        break


from IPython.display import display
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

def show_prices(cereals, per_100g=False):
    """Creates and displays a bar chart over cereal prices

    Parameters:
    cereals: List of Cereal objects
    per_100g: Boolean. True, if the bar chart should be per 100 grams for easier comparison. Default False
    """
    # names = [cereal.get_brand_name() for cereal in cereals]
    # stores = __get_stores(cereals)
    # prices_in_stores = __get_prices_with_cereal_name(stores, cereals)
    # title = 'Cereal Prices'
    # __plot_prices(names, stores, prices_in_stores, title)
    stores = list(__get_stores(cereals))
    for store in stores:
        title = __create_title(store, per_100g)
        cereals_prices = {
            cereal.get_brand_name(): __get_price(value, cereal.grams, per_100g)
            for cereal in cereals
            for key, value in cereal.price.items()
            if key == store
        }
        __plot_prices_in_store(cereals_prices, title)

def show_nutrition(cereals):
    """Creates and displays a dataframe over the nutritional content
    (protein, carbohydrates, fiber, fat, salt)
    per 100 grams in cereals

    Parameters:
    cereals: List of Cereal objects
    """
    data = __get_nutrition_data(cereals)
    df = pd.DataFrame(data)
    print("Nutritional Content per 100 grams in Cereals")
    # df = df.style.set_caption('Nutritional Content per 100 grams in Cereals')
    display(df)

def show_recommended_nutrition():
    """Creates and displays a dataframe over the daily recommended maximum nutritional intake
    (protein, carbohydrates, fiber, fat, salt)
    """
    df = __get_recommended_nutrition()
    print("Daily Recommended Maximum Nutritional Intake")
    # df = df.style.set_caption('Daily Recommended Nutritional Intake')
    display(df)

def show_pct_of_recommended_nutrition(cereal, sex):
    """Creates and displays a bar chart over the percentage of a cereal's
    nutritional content compared to the recommended daily intake
    (protein, carbohydrates, fiber, fat, salt)

    Parameters:
    cereal: Cereal object
    sex: String - either 'Male' or 'Female'. Sex needed for recommended daily nutritional intake
    """
    data = __get_recommended_nutrition()
    df = data[data[:]["Sex"] == sex.capitalize()]
    recommended = __get_recommended_nutrition_from_dataframe(df)
    actual = cereal.nutrition.get_nutrition_as_list()
    pcts = [(ac / rec) * 100 for ac, rec in zip(actual, recommended)]
    labels = "Calories", "Protein", "  Carbohydrates", "Fiber", "Fat", "Salt"
    title = (
        "Percentage of %s %s's Nutrional Content compared to Daily Recommended Intake"
        % (cereal.brand.capitalize(), cereal.name.capitalize())
    )
    __plot_percentage(pcts, labels, title)

def __plot_prices_in_store(cereals_prices, title):
    names = cereals_prices.keys()
    prices = cereals_prices.values()
    plt.bar(names, prices, width=0.5, align="center")
    plt.xticks(rotation=15, horizontalalignment="right", fontweight="light")
    plt.ylabel("Price in DKK", fontsize=10)
    plt.grid(axis="y", color="grey")
    plt.title(title)
    plt.show()

def __plot_percentage(pcts, labels, title):
    plt.bar(labels, pcts, width=0.5, align="center")
    plt.ylabel("Percentage (%)", fontsize=10)
    plt.grid(axis="y", color="grey")
    plt.title(title)
    plt.show()

def __get_stores(cereals):
    return list(set([key for cereal in cereals for key in cereal.price.keys()]))

def __get_price(price, grams, per_100g):
    if per_100g:
        return price / 10
    return (price / 1000) * grams

def __get_nutrition_data(cereals):
    names = [cereal.get_brand_name() for cereal in cereals]
    calories = [str(cereal.nutrition.calories) + " kcal" for cereal in cereals]
    proteins = [str(cereal.nutrition.protein) + " g" for cereal in cereals]
    carbs = [str(cereal.nutrition.carbohydrates) + " g" for cereal in cereals]
    fibers = [str(cereal.nutrition.fiber) + " g" for cereal in cereals]
    fats = [str(cereal.nutrition.fat) + " g" for cereal in cereals]
    salts = [str(cereal.nutrition.salt) + " g" for cereal in cereals]
    return {
        "Cereal Name": names,
        "Calories": calories,
        "Protein": proteins,
        "Carbohydrates": carbs,
        "Fiber": fibers,
        "Fat": fats,
        "Salt": salts,
    }

def __get_recommended_nutrition():
    return pd.read_csv("files/daily_nutrition.csv")

def __get_recommended_nutrition_from_dataframe(df):
    calories = int(df.loc[:, "Calories"].str.replace(" kcal", ""))
    protein = float(df.loc[:, "Protein"].str.replace(" g", ""))
    carbs = float(df.loc[:, "Carbohydrates"].str.replace(" g", ""))
    fiber = float(df.loc[:, "Fiber"].str.replace(" g", ""))
    fat = float(df.loc[:, "Fat"].str.replace(" g", ""))
    salt = float(df.loc[:, "Salt"].str.replace(" g", ""))
    return [calories, protein, carbs, fiber, fat, salt]

def __create_title(store, per_100g):
    if per_100g:
        return "Cereal Prices per 100 grams at " + store.capitalize()
    return "Cereal Prices per at " + store.capitalize()


from flask import Flask, request, jsonify
from bson import json_util
from pymongo import MongoClient
from mongo import read_data, delete_data, update_general
from bson import ObjectId
from endpoints import delete, insert, complete_list, update, complete_auth

app = Flask("quoteapi")

@app.route('/') #Homepage
def home():
    return "Welcome! This site is a prototype API for famous quotes :)"

@app.route("/Data") #to obtain all the information about authors, categories and phrases from our entire database
def data():
    read = complete_list()
    return jsonify(read)

@app.route("/AuthorId") #to obtain all the authors of the API
def data_aut():
    read = complete_auth()
    return json_util.dumps(read)

@app.route("/Authors/<Collection>") #to get all the authors contained in a given collection
def all_names(Collection):
    aut = read_data({}, {"Author":1, "_id": 0}, f"{Collection}")
    return json_util.dumps(aut)

@app.route("/Quotes/<Collection>") #to get all the phrases contained in a given collection
def all_quotes(Collection):
    aut = read_data({}, {"Quote":1, "_id": 0}, f"{Collection}")
    return json_util.dumps(aut)

##DELETE objeos en una colleccion
@app.route("/<collection>/delete") #Delete a certain author and his / her quote from the biology collection
def delete_prueba(collection):
    args = dict(request.args)
    x = json_util.dumps(delete(args, collection))
    update_general()
    return x

@app.route("/<collection>/new") # Insert a new author and his citation from the biology collection
def insert_new(collection):
    args = dict(request.args)
    x = json_util.dumps(insert(args, collection))
    update_general()
    return x

@app.route("/<collection>/update")
def update_lit(collection):
   args = dict(request.args)
   x = json_util.dumps(update(args, collection))
   update_general()
   return x

app.run(debug=True)


import unittest
from collections import defaultdict, deque
import sys

def minimumBribes(q):
    moves = 0
    for pos, val in enumerate(q):

        d = (val - 1) - pos
        if d > 2:
            return "Too chaotic"
        start = max(0, val - 2)
        end = pos + 1
        for j in range(start, end):
            if q[j] > val:
                moves += 1
    return moves

def minimumBribesa(final):
    n = len(final)
    queue = [i for i in range(1, n + 1)]

    state = defaultdict(int)
    n = len(final)
    for index in range(n):
        state[final[index]] = index

    movements = 0
    ite = deque(queue[:])
    while ite:
        target = ite.popleft()
        pos = state[target]
        index = queue.index(target)
        dist = abs(pos - index)
        if dist > 2:
            return "Too chaotic"
        while queue[pos] != target:
            movements += 1
            temp = queue[index + 1]
            queue[index + 1] = target
            queue[index] = temp
            index = index + 1

    return movements

class Test(unittest.TestCase):

    def test_case(self):
        peaple = 5
        target = [2, 1, 5, 3, 4]
        result = minimumBribes(target)
        self.assertEqual(3, result)

    def test_case_caotic(self):
        peaple = 5
        target = [2, 5, 1, 3, 4]
        result = minimumBribes(target)
        self.assertEqual("Too chaotic", result)

    def test_case_caotic_b(self):
        target = [5, 1, 2, 3, 7, 8, 6, 4]
        result = minimumBribes(target)
        self.assertEqual("Too chaotic", result)

    def test_case_caotic(self):
        #arget = [1, 2, 3, 4, 5, 6, 7, 8]
        target = [1, 2, 5, 3, 7, 8, 6, 4]
        result = minimumBribes(target)
        self.assertEqual(7, result)


# -*- coding: utf-8 -*-

import pickle
import numpy as np
import torch
import os
from torch.utils.data import Dataset

class NHPDataset(Dataset):
    '''
    Create Dataset for Neural Hawkey Process
    using financial data from original paper
    '''

    def __init__(self, file_path):
        self.event_type = []
        self.event_time = []

        with open(file_path, 'rb') as f:

            if 'dev' in file_path:
                seqs = pickle.load(f, encoding='latin1')['dev']
            elif 'train' in file_path:
                seqs = pickle.load(f, encoding='latin1')['train']
            elif 'test' in file_path:
                seqs = pickle.load(f, encoding='latin1')['test']

            for idx, seq in enumerate(seqs):
                self.event_type.append(torch.Tensor([int(event['type_event']) for event in seq]))
                self.event_time.append(torch.Tensor([float(event['time_since_start']) for event in seq]))

    def __len__(self):
        return len(self.event_type)

    def __getitem__(self, index):

        event_type = torch.LongTensor(self.event_type[index].long())[1:]
        event_time = torch.Tensor(self.event_time[index])
        #delta_time = torch.zeros_like(event_time)
        delta_time = event_time[1:] - event_time[:-1]

        return delta_time, event_type

def prepare_datasets(data_directory, train_size=0.6, val_size=0.2):
    """
    Prepare train, val and test datasets from several files
    with given proportions.
    Input:
        data_directory (str) - path to directory with data in .npy format
        train_size (float) - proportion of the dataset to include in the train split
        val_size (float) - proportion of the dataset to include in the val split
    Output:
        train_dsets (dict) - dictionary with train datasets and correspondig file names
        val_dsets (dict) - dictionary with val datasets and correspondig file names
        test_dsets (dict) - dictionary with test datasets and correspondig file names
    """

    datasets = {}
    for file in os.listdir(data_directory):
      if file.endswith(".npy"):
        datasets[file[:3]] = np.load(data_directory + file)

    train_dsets, val_dsets, test_dsets = {}, {}, {}
    for (name, dset) in datasets.items():

        train_part, val_part = int(train_size * len(dset)), int(val_size * len(dset))
        train_dsets[name] = LOBDataset(dset[:train_part])
        val_dsets[name]   = LOBDataset(dset[train_part:train_part + val_part])
        test_dsets[name]  = LOBDataset(dset[train_part + val_part:])

    return train_dsets, val_dsets, test_dsets

class LOBDataset(Dataset):
    """
    Wrapper for LOB dataset.
    """

    def __init__(self, data):
        """
        Input:
            data (B, S, 3) - batch of sequences
        """

        self.event_type = []
        self.event_time = []

        for seq in data:
            self.event_type.append(torch.Tensor(seq[:,2]))
            self.event_time.append(torch.Tensor(seq[:,0])/1000)

    def __len__(self):
        return len(self.event_type)

    def __getitem__(self, index):

        event_type = torch.LongTensor(self.event_type[index].long())
        event_time = torch.Tensor(self.event_time[index])

        return event_time, event_type

def collate_fn(batch, n_events=2):

      """
      While initializing LSTM we have it read a special beginning-of-stream (BOS) event (k0, t0),
      where k0 is a special event type and t0 is set to be 0
      (expanding the LSTM’s input dimensionality by one) see Appendix A.2
      Input:
          batch tuple((batch_size, seq_len)x2) - batch with event types sequence and corresponding inter-arrival times
      Output:
          pad_event_seq (batch_size, seq_len) - padded event types sequence
          pad_time_seq (batch_size, seq_len) - padded event times sequence
      """
      seq_time = torch.stack([sample[0] for sample in batch])
      seq_events = torch.stack([sample[1] for sample in batch])

      pad_event = torch.zeros_like(seq_events[:,0]) + n_events
      pad_time = torch.zeros_like(seq_time[:,0])
      pad_event_seq = torch.cat((pad_event.reshape(-1,1), seq_events), dim=1)
      pad_time_seq = torch.cat((pad_time.reshape(-1,1), seq_time), dim=1)

      return pad_event_seq.long(), pad_time_seq