#!/usr/bin/env python
# -*- coding: utf-8 -*-

import datetime

from backend.Itinerary import Itinerary
from backend.Itinerary_Autolib import Autolib
from backend.Itinerary_Bicycling import Bicycling
from backend.Itinerary_Driving import Driving
from backend.Itinerary_Transit import Transit
from backend.Itinerary_Uber import Uber
from backend.Itinerary_Velib import Velib
from backend.Itinerary_Walking import Walking
from backend.Place import Place
from backend.User import User

def Suggested_Itineraries(user, itinerary):
    # We want a maximum of 3 itineraries shown
    OPTIONS_NB_MAX = 3

    if isinstance(user, User):
        pass
    else:
        raise TypeError("The destination variable should be an object from the class Place.")

    if isinstance(itinerary, Itinerary):
        pass
    else:
        raise TypeError("The origin variable should be an object from the class Place.")

    suggested_options = []

    for option in user.preferences:
        if len(suggested_options) < OPTIONS_NB_MAX:
            if option == 'transit':
                transit_option = Transit(origin=itinerary.origin, destination=itinerary.destination)
                suggested_options.append(['transit', transit_option])
            if option == 'uber':
                uber_option = Uber(origin=itinerary.origin, destination=itinerary.destination, date=None,
                                   uber_type=None, transit_mode_type=None, itinerary_index=0)
                suggested_options.append(['uber', uber_option])
            if option == 'bus':
                bus_option = Transit(origin=itinerary.origin, destination=itinerary.destination,
                                     transit_mode_type='bus')
                suggested_options.append(['bus', bus_option])
            if option == 'walking':
                walking_option = Walking(origin=itinerary.origin, destination=itinerary.destination)
                suggested_options.append(['walking', walking_option])
            if option == 'driving':
                driving_option = Driving(origin=itinerary.origin, destination=itinerary.destination)
                suggested_options.append(['driving', driving_option])
            if option == 'autolib':
                autolib_option = Autolib(origin=itinerary.origin, destination=itinerary.destination)
                suggested_options.append(['autolib', autolib_option])
            if option == 'velib':
                velib_option = Velib(origin=itinerary.origin, destination=itinerary.destination)
                suggested_options.append(['velib', velib_option])
            if option == 'bicycling':
                bicycling_option = Bicycling(origin=itinerary.origin, destination=itinerary.destination)
                suggested_options.append(['bicycling', bicycling_option])

    suggested_itineraries = dict()
    suggested_itineraries['origin'] = {'address': itinerary.origin.address,
                                       'lat': itinerary.origin.lat,
                                       'lng': itinerary.origin.lng}
    suggested_itineraries['destination'] = {'address': itinerary.destination.address,
                                            'lat': itinerary.destination.lat,
                                            'lng': itinerary.destination.lng}
    weather = user.preferences_with_weather(datetime.date.today())
    suggested_itineraries['weather'] = weather[0]

    suggested_itineraries['results'] = []

    for option_nb, transport_option_info in enumerate(suggested_options):
        transport_nom = transport_option_info[0]
        transport_option = transport_option_info[1]
        suggested_itineraries['results'].append({})
        suggested_itineraries['results'][option_nb]['transport_type'] = transport_nom
        suggested_itineraries['results'][option_nb]['duration'] = transport_option.total_duration
        suggested_itineraries['results'][option_nb]['polyline_encoded'] = transport_option.total_polyline
        suggested_itineraries['results'][option_nb]['instructions'] = repr(transport_option).replace("\n", "<br />")

        if transport_option is 'driving':
            suggested_itineraries['results'][option]['distance'] = transport_option.driving_distance
        if transport_option is 'walking':
            suggested_itineraries['results'][option]['distance'] = transport_option.walking_distance
        if transport_option is 'bicycling':
            suggested_itineraries['results'][option]['distance'] = transport_option.bicycling_distance
        if transport_option is 'uber':
            suggested_itineraries['results'][option]['wait_time'] = transport_option.uber_wait_duration

    return suggested_itineraries

if __name__ == "__main__":
    """Script de test"""
    origin = Place("8 avenue Adrien Hebrard")
    destination = Place("nation")
    iti = Itinerary(origin=origin, destination=destination)
    pierre = User('PMR', True, False, False)
    print(Suggested_Itineraries(pierre, iti))


"""
This module implements training and evaluation of a multi-layer perceptron in PyTorch.
You should fill in code into indicated sections.
"""
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function

import argparse
import numpy as np
import os
from mlp_pytorch import MLP
import cifar10_utils
import torch
from torch import nn
from torch import functional as F
from torch.autograd import Variable
import matplotlib.pyplot as plt

# Default constants
DNN_HIDDEN_UNITS_DEFAULT = '100,100'
LEARNING_RATE_DEFAULT = 5e-2
MAX_STEPS_DEFAULT = 20000
BATCH_SIZE_DEFAULT = 100
EVAL_FREQ_DEFAULT = 100

# Directory in which cifar data is saved
DATA_DIR_DEFAULT = './cifar10/cifar-10-batches-py'

FLAGS = None

def accuracy(predictions, targets):
  """
  Computes the prediction accuracy, i.e. the average of correct predictions
  of the network.

  Args:
    predictions: 2D float array of size [batch_size, n_classes]
    labels: 2D int array of size [batch_size, n_classes]
            with one-hot encoding. Ground truth labels for
            each sample in the batch
  Returns:
    accuracy: scalar float, the accuracy of predictions,
              i.e. the average correct predictions over the whole batch

  TODO:
  Implement accuracy computation.
  """

  ########################
  # PUT YOUR CODE HERE  #
  #######################

  # use predictions and targets to calculate accuracy
  pred = np.argmax(predictions, axis=1)

  lab = np.argmax(targets, axis=1)

  # check how many predictions are equal to the labels
  count = 0
  for i in range(len(pred)):
    if pred[i] == lab[i]:
      count += 1

  accuracy = count / len(pred)

  ########################
  # END OF YOUR CODE    #
  #######################

  return accuracy

def train():
  """
  Performs training and evaluation of MLP model.

  TODO:
  Implement training and evaluation of MLP model. Evaluate your model on the whole test set each eval_freq iterations.
  """

  ### DO NOT CHANGE SEEDS!
  # Set the random seeds for reproducibility
  np.random.seed(42)
  torch.manual_seed(42)

  ## Prepare all functions
  # Get number of units in each hidden layer specified in the string such as 100,100
  if FLAGS.dnn_hidden_units:
    dnn_hidden_units = FLAGS.dnn_hidden_units.split(",")
    dnn_hidden_units = [int(dnn_hidden_unit_) for dnn_hidden_unit_ in dnn_hidden_units]
  else:
    dnn_hidden_units = []

  ########################
  # PUT YOUR CODE HERE  #
  #######################

  # get train data
  cifar10 = cifar10_utils.get_cifar10('cifar10/cifar-10-batches-py', validation_size= 2000)
  x, y = cifar10['train'].next_batch(FLAGS.batch_size)
  x = x.reshape(np.size(x, 0), -1)

  # get input shape for network initialization
  n_input = np.size(x, 1)

  # get validation data
  x_val, y_val = cifar10['validation'].next_batch(FLAGS.batch_size)
  x_val = x_val.reshape(np.size(x_val, 0), -1)

  # create model
  net = MLP(n_input, dnn_hidden_units, 10)

  # get loss function and optimizer
  crossEntropy = nn.CrossEntropyLoss()
  optimizer = torch.optim.SGD(net.parameters(), lr=FLAGS.learning_rate, momentum=0.9, weight_decay=0.0005)

  # scheduler init and hyperparams
  step_size = (FLAGS.max_steps / 6)
  gamma = 0.3
  scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=step_size, gamma=gamma)

  # keep track of loss and accuracy
  loss_list = []
  loss_val_list = []
  accuracy_train_list = []
  accuracy_val_list = []

  # loop for set amount of steps
  for i in range(FLAGS.max_steps):

    # apply scheduler
    scheduler.step()

    # convert to torch compatible input
    x = Variable(torch.from_numpy(x), requires_grad=True)

    # get output and convert to numpy
    out = net(x)
    out_numpy = out.data[:].numpy()

    # convert one hot vector to class indices
    label_index = np.argmax(y, axis=1)
    label_index = torch.LongTensor(label_index)

    # apply cross entropy
    loss = crossEntropy(out, label_index)

    # show progress
    if i % FLAGS.eval_freq == 0:
      # calculate accuracy
      accuracy_train = accuracy(out_numpy, y)

      # run torch without keeping track of gradients for validation
      with torch.no_grad():
        # load validation data
        x_val, y_val = cifar10['validation'].next_batch(FLAGS.batch_size)
        x_val = x_val.reshape(np.size(x_val, 0), -1)

        # convert to torch compatible variable
        x_val = Variable(torch.from_numpy(x_val), requires_grad=False)

        # run on validation set
        val_out = net.forward(x_val)
        val_out_numpy = val_out.data[:].numpy()

        # convert one hot vector to class indices
        y_val_index = np.argmax(y_val, axis=1)
        y_val_index = torch.LongTensor(y_val_index)

        # calculate accuracy
        accuracy_val = accuracy(val_out_numpy, y_val)

        # apply cross entropy
        loss_val = crossEntropy.forward(val_out, y_val_index)

      # save variables
      accuracy_train_list.append(accuracy_train)
      accuracy_val_list.append(accuracy_val)
      loss_list.append(loss.data.numpy())
      loss_val_list.append(loss_val.data.numpy())

      # show progress
      print("##############################################################")
      print("Epoch ", i)
      print("---------------------------------------------------------------")
      print("The ACCURACY on the TRAIN set is currently: ", accuracy_train)
      print("---------------------------------------------------------------")
      print("The ACCURACY on the VALIDATION set is currently:", accuracy_val)
      print("---------------------------------------------------------------")
      print("The LOSS on the TRAIN set is currently:", loss.data.numpy())
      print("---------------------------------------------------------------")
      print("The LOSS on the VALIDATION set is currently:", loss_val.data.numpy())
      print("---------------------------------------------------------------")
      print("###############################################################")
      print("\n")

    # Backward and optimize
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()

    # insert data
    x, y = cifar10['train'].next_batch(FLAGS.batch_size)
    x = x.reshape(np.size(x, 0), -1)

  # test without tracking the gradients
  with torch.no_grad():
    # load test data
    x, y = cifar10['test'].images, cifar10['test'].labels
    x = x.reshape(np.size(x, 0), -1)

    # convert to torch compatible input
    x = Variable(torch.from_numpy(x), requires_grad = False)

    # get output and calculate accuracy
    out = net(x)
    out_numpy = out.data[:].numpy()
    print("The accuracy on the test set is:")
    print(accuracy(out_numpy,y))

  # plot
  fig, ax1 = plt.subplots()

  ax1.plot(loss_list, label="Train loss", color='firebrick')
  ax1.plot(loss_val_list, label="Validation loss", color='darksalmon')
  ax1.set_title("Accuracy and Loss curves")
  ax1.set_ylabel("Loss")
  ax1.set_xlabel("Evaluation")
  ax1.tick_params(axis='y', labelcolor='red')
  ax1.legend(loc='upper left')

  ax2 = ax1.twinx()

  ax2.plot(accuracy_train_list, label='Train accuracy', color='royalblue')
  ax2.plot(accuracy_val_list, label='Validation accuracy', color='lightskyblue')
  ax2.tick_params(axis='y', labelcolor='blue')
  ax2.set_ylabel("Accuracy")

  fig.tight_layout()
  ax2.legend(loc='upper right')
  plt.show()

  ########################
  # END OF YOUR CODE    #
  #######################

def print_flags():
  """
  Prints all entries in FLAGS variable.
  """
  for key, value in vars(FLAGS).items():
    print(key + ' : ' + str(value))

def main():
  """
  Main function
  """
  # Print all Flags to confirm parameter settings
  print_flags()

  if not os.path.exists(FLAGS.data_dir):
    os.makedirs(FLAGS.data_dir)

  # Run the training operation
  train()

if __name__ == '__main__':
  # Command line arguments
  parser = argparse.ArgumentParser()
  parser.add_argument('--dnn_hidden_units', type = str, default = DNN_HIDDEN_UNITS_DEFAULT,
                      help='Comma separated list of number of units in each hidden layer')
  parser.add_argument('--learning_rate', type = float, default = LEARNING_RATE_DEFAULT,
                      help='Learning rate')
  parser.add_argument('--max_steps', type = int, default = MAX_STEPS_DEFAULT,
                      help='Number of steps to run trainer.')
  parser.add_argument('--batch_size', type = int, default = BATCH_SIZE_DEFAULT,
                      help='Batch size to run trainer.')
  parser.add_argument('--eval_freq', type=int, default=EVAL_FREQ_DEFAULT,
                        help='Frequency of evaluation on the test set')
  parser.add_argument('--data_dir', type = str, default = DATA_DIR_DEFAULT,
                      help='Directory for storing input data')
  FLAGS, unparsed = parser.parse_known_args()

  main()


from typing import Callable
from PyQt5.QtWidgets import QWidget, QGridLayout, QHBoxLayout
from Views.RollButton import RollButton
from Views.PushButton import PushButton
from Config.ButtonsStyles import ButtonsStyles
from Views.Label import Label
from Views.Dice import Dice
from Config.PlayerColor import PlayerColor

class Panel(QWidget):
    def __init__(self, rollCB: Callable = None, confirmCB: Callable = None, resetCB: Callable = None):
        super().__init__()
        self.setMaximumWidth(800)
        # Settings
        self.setStyleSheet("background: white;")
        self.grid = QGridLayout()
        self.__width = 800

        # Dice
        self.__dice: Dice = None

        # Rows grids
        self.__secondRow: QHBoxLayout = None
        self.__buttonsRow: QHBoxLayout = None

        # Buttons
        self.__confirmButton: PushButton = None
        self.__rollButton: RollButton = None
        self.__resetButton: RollButton = None

        # Default Color
        self.__defaultColor: PlayerColor = PlayerColor.BLACK

        # Messages
        self.__titleMessage: str = "Title"
        self.__notifyMessage: str = "-"
        self.__turnMessage: str = "Turn"

        self.__setTitleRow()
        self.__setDice()
        self.__setSecondMessageRow(self.__defaultColor)
        self.__setButtonsRow(rollCB, confirmCB, resetCB)

        self.setLayout(self.grid)

    def roll(self):
        self.__clearDice()
        self.__setDice()

    def setTitleMessage(self, message: str, color: PlayerColor = None) -> None:
        if color is None:
            color = self.__defaultColor

        self.__titleMessage = message
        self.__setTitleRow(color)

    def setNotifyMessage(self, message: str, color: PlayerColor = None) -> None:
        if color is None:
            color = self.__defaultColor

        self.__notifyMessage = message
        self.__setSecondMessageRow(color)

    def setTurnMessage(self, message: str, color: PlayerColor = None) -> None:
        if color is None:
            color = self.__defaultColor

        self.__turnMessage = message
        self.__setSecondMessageRow(color)

    def __setDice(self):
        # Desenha o dado
        grid_dice = QGridLayout()
        self.__dice = Dice(int(self.__width/5))
        grid_dice.addWidget(self.__dice.widget, 1, 1)

        for l in range(3):
            for c in range(3):
                if l == 1 and c == 1:
                    continue

                widget = QWidget()
                widget.setStyleSheet("background: white;")
                widget.setFixedHeight(int(self.__width/5))
                widget.setFixedWidth(int(self.__width/5))
                grid_dice.addWidget(widget, l, c)

        self.grid.addLayout(grid_dice, 2, 0)

    def __setTitleRow(self, color: PlayerColor = None) -> None:
        if color is None:
            color = self.__defaultColor

        self.grid.addWidget(Label(self.__titleMessage, color).widget, 0, 0)

    def __setSecondMessageRow(self, color: PlayerColor) -> None:
        self.__secondRow = QHBoxLayout()
        self.__secondRow.addWidget(Label(self.__notifyMessage, color, 400, 20).widget, 0)
        self.__secondRow.addWidget(Label(self.__turnMessage, color, 400, 20).widget, 1)
        self.grid.addLayout(self.__secondRow, 1, 0)
        self.show()

    def __setButtonsRow(self, rollCB: Callable, confirmPieceCB: Callable, resetCB: Callable) -> None:
        self.__buttonsRow = QHBoxLayout()

        self.__resetButton = PushButton(
            'RESET MATCH', ButtonsStyles.ResetButton, lambda: resetCB())
        self.__rollButton = PushButton('ROLL DICE', ButtonsStyles.RollButton, lambda: rollCB())
        self.__confirmButton = PushButton(
            'CONFIRM PIECE', ButtonsStyles.RollButton, lambda: confirmPieceCB())

        self.__buttonsRow.addWidget(self.__resetButton)
        self.__buttonsRow.addWidget(self.__rollButton)
        self.__buttonsRow.addWidget(self.__confirmButton)
        self.grid.addLayout(self.__buttonsRow, 3, 0)

    @property
    def diceValue(self) -> int:
        return self.__dice.diceValue

    def __clearDice(self):
        self.__dice.widget.hide()

    @property
    def notifyMessage(self) -> str:
        return self.__notifyMessage

    @property
    def turnMessage(self) -> str:
        return self.__turnMessage

    @property
    def titleMessage(self) -> str:
        return self.__titleMessage


from flask import Flask, render_template, request, redirect, url_for
from flask_sqlalchemy import SQLAlchemy

app = Flask(__name__)
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///books-collection.db'
db = SQLAlchemy(app)

# Code to run SQL commands directly inside python
# import sqlite3
# db = sqlite3.connect("books-collection.db")
# cursor = db.cursor()
# cursor.execute("CREATE TABLE books (id INTEGER PRIMARY KEY, title varchar(250) NOT NULL UNIQUE, author varchar(250) NOT NULL, rating FLOAT NOT NULL)")

class Books(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String(200), unique=True, nullable=False)
    author = db.Column(db.String(200), nullable=False)
    rating = db.Column(db.Float, nullable=False)

    def __repr__(self):
        return f'<Book {self.title} | {self.author} | {self.rating}>'

db.create_all()
all_books = []

@app.route('/')
def home():
    all_books = db.session.query(Books).order_by(Books.rating.desc()).all()
    return render_template('index.html', data=all_books)

@app.route("/add", methods=["GET", "POST"])
def add():
    if request.method == "POST":
        title = request.form['book']
        author = request.form['author']
        rating = request.form['rating']
        new_book = Books(title=title, author=author, rating=rating)
        if new_book.title and new_book.author and new_book.rating:
            db.session.add(new_book)
            db.session.commit()
            return redirect(url_for('home'))
        return render_template('add.html')

    return render_template('add.html')

@app.route('/edit', methods=('GET', 'POST'))
def edit():
    if request.method == "POST":
        rating = request.form['rating']
        id = request.form['id']
        book_to_edit = Books.query.get(id)
        book_to_edit.rating = rating
        db.session.commit()
        return redirect(url_for('home'))

    book_id = request.args.get('id')
    book_selected = Books.query.get(book_id)
    return render_template('edit.html', data=book_selected)

@app.route('/delete')
def delete():
    id = request.args.get('id')
    book_selected = Books.query.get(id)
    db.session.delete(book_selected)
    db.session.commit()
    return redirect(url_for('home'))

if __name__ == "__main__":
    app.run(debug=True)


"""
This file defines the database models
"""

import datetime
import json
from .common import db, Field, auth
from pydal.validators import IS_INT_IN_RANGE
import re
from .util import random_id
from py4web import redirect, URL

from googleapiclient.discovery import build
import google.oauth2.credentials
from .settings import ADMIN_EMAIL

def get_user_email():
    return auth.current_user.get('email') if auth.current_user else None

def get_time():
    return datetime.datetime.utcnow()

def build_drive_service(user=None):
    # Reads the credentials.
    user = user or get_user_email()
    user_info = db(
        db.auth_credentials.email == user).select().first()
    if not user_info:
        print("No user credentials")
        return None
    credentials_dict = json.loads(user_info.credentials)
    creds = google.oauth2.credentials.Credentials(**credentials_dict)
    drive_service = build('drive', 'v3', credentials=creds)
    return drive_service

### Define your table below
#
# db.define_table('thing', Field('name'))
#
## always commit your models to avoid problems later

db.define_table(
    'assignment',
    Field('owner', default=get_user_email), # Email of owner.
    Field('name'), # Assignment name.
    Field('domain_restriction'),
    Field('created_on', 'datetime', default=get_time),
    Field('master_id_gcs'), # Locations of master and student notebooks
    Field('student_id_gcs'), # in gcs and drive.
    Field('master_id_drive'),
    Field('student_id_drive'),
    Field('available_from', 'datetime'),
    Field('available_until', 'datetime'),
    Field('submission_deadline', 'datetime'),
    Field('max_submissions_in_24h_period', 'integer'),
    Field('access_url', default=random_id),
    Field('max_points', 'integer'),
    Field('test_ids', 'text'), # Json string of pairs [(id, name, points), ...] of tests.
)

db.assignment.max_submissions_in_24h_period.requires = IS_INT_IN_RANGE(1, 3)

db.define_table(
    'access',
    Field('user'),
    Field('assignment_id', 'reference assignment'),
)

db.define_table(
    'homework',
    Field('student', default=get_user_email), # Email of student.
    Field('assignment_id', 'reference assignment'),
    Field('created_on', 'datetime', default=get_time),
    Field('grade', 'float'),
    Field('has_invalid_grade', 'boolean', default=False),
    Field('drive_id'),
)

db.define_table(
    'grade',
    Field('student', default=get_user_email),
    Field('assignment_id', 'reference assignment'),
    Field('grade_date', 'datetime', default=get_time),
    Field('homework_id', 'reference homework'),
    Field('grade', 'float'),
    Field('drive_id'), # Location in Drive of feedback
    Field('feedback_id_gcs'), # Location in GCS of feedback #ADD
    Field('submission_id_gcs'), # Location in GCS of submission #ADD
    Field('is_valid', 'boolean', default=False),
    Field('cell_id_to_points', 'text'), # Json dictionary of grade breakdown.
)

db.define_table(
    'grading_request',
    Field('homework_id', 'reference homework', ondelete="SET NULL"),
    Field('student', default=get_user_email),
    Field('input_id_gcs'), # Location in GCS of what was graded.
    Field('created_on', 'datetime', default=get_time),
    Field('request_nonce', default=random_id),
    Field('completed', 'boolean', default=False),
    Field('grade', 'float'),
    Field('delay', 'float'),
)

db.commit()

def get_assignment_teachers(assignment_id, exclude=None):
    """Returns the assignment teachers."""
    q = (db.access.assignment_id == assignment_id)
    if exclude is not None:
        q &= (db.access.user != exclude)
    teachers = [r.user for r in db(q).select()]
    return sorted(teachers)

def set_assignment_teachers(assignment_id, teacher_list):
    current_rows = db(db.access.assignment_id == assignment_id).select()
    current_users = [r.user for r in current_rows]
    users_to_delete = [u for u in current_users if u not in teacher_list]
    users_to_add = [u for u in teacher_list if u not in current_users]
    db((db.access.assignment_id == assignment_id) &
       (db.access.user.belongs(users_to_delete))).delete()
    for u in users_to_add:
        db.access.insert(user=u, assignment_id=assignment_id)

def can_access_assignment(assignment_id):
    return get_user_email() == ADMIN_EMAIL or (
        not db((db.access.assignment_id == assignment_id) &
               (db.access.user == get_user_email())).isempty())

def is_admin():
    return get_user_email() == ADMIN_EMAIL


# Based on https://gist.github.com/ageitgey/1c1cb1c60ace321868f7410d48c228e1

import dlib
import cv2
import logging
import os

from .util import get_basename, mkdir_if_not_exists
from skimage import io

logger = logging.getLogger(__name__)

class Detect:
    def __init__(self):
        self.detector = dlib.get_frontal_face_detector()

    def _get_faces(self, image):
        logging.debug(f"Getting faces for {image}")
        img = io.imread(image)
        faces = self.detector(img)

        if len(faces) == 0:
            logging.debug(f"No faces in {image}")

        return faces

    def count(self, image):
        faces = self._get_faces(image)
        return len(faces)

    def crop(self, image, outpath):
        logging.debug(f"Cropping {image} to {outpath}")
        mkdir_if_not_exists(outpath)
        rects = self.locate(image)
        img = cv2.imread(image, cv2.IMREAD_COLOR)
        basename = get_basename(image)

        for index, rect in enumerate(rects):
            outfile = f"{outpath}/{basename}-{index}.jpg"
            x, y, x2, y2 = rect
            crop = img[y:y2, x:x2]
            cv2.imwrite(outfile, crop)
            logging.debug(f"Cropped to {outfile}")

    def locate(self, image, output = None, to_directory = None):
        faces = self._get_faces(image)
        rects = []

        if to_directory:
            mkdir_if_not_exists(output)

        logging.debug(f"Getting rects")

        for index, rect in enumerate(faces):
            rects.append([
                rect.left(),
                rect.top(),
                rect.right(),
                rect.bottom()
            ])

        if output:
            logging.debug(f"Writing bounding boxes to {output}")

            out = cv2.imread(str(image), cv2.IMREAD_COLOR)

            for rect in rects:
                logging.debug(f"Plotting rect: {rect}")
                x, y, x2, y2 = rect
                p1 = (x, y)
                p2 = (x2, y2)
                c = (255, 0, 0)
                logging.debug(f"{p1}, {p2}")
                cv2.rectangle(out, p1, p2, c, 5)

            # If output is a directory, generate a name based on the
            # input filename
            if to_directory:
                outpath = f"{output}/{get_basename(image)}-crop.jpg"
            else:
                outpath = output

            logging.debug(f"Writing to {outpath}")

            cv2.imwrite(outpath, out)

        return rects


# Copy/Pasted from https://github.com/Textualize/textual/pull/2931

from __future__ import annotations

import re
from collections import defaultdict
from dataclasses import dataclass
from pathlib import Path
from typing import ClassVar, NamedTuple, Optional

from rich.cells import get_character_cell_size
from rich.style import Style
from rich.text import Text
from textual import events, log
from textual._cells import cell_len
from textual._types import Protocol
from textual.binding import Binding
from textual.geometry import Offset, Region, Size, Spacing, clamp
from textual.message import Message
from textual.reactive import Reactive, reactive
from textual.scroll_view import ScrollView
from textual.strip import Strip
from tree_sitter import Language, Parser, Tree
from tree_sitter.binding import Query

TREE_SITTER_PATH = Path(__file__) / "../../../../tree-sitter/"
LANGUAGES_PATH = TREE_SITTER_PATH / "textual-languages.so"

# TODO - remove hardcoded python.scm highlight query file
HIGHLIGHTS_PATH = TREE_SITTER_PATH / "highlights/python.scm"

# TODO - temporary proof of concept approach
HIGHLIGHT_STYLES = {
    "string": Style(color="#E6DB74"),
    "string.documentation": Style(color="yellow"),
    "comment": Style(color="#75715E"),
    "keyword": Style(color="#F92672"),
    "include": Style(color="#F92672"),
    "keyword.function": Style(color="#F92672"),
    "keyword.return": Style(color="#F92672"),
    "conditional": Style(color="#F92672"),
    "number": Style(color="#AE81FF"),
    "class": Style(color="#A6E22E"),
    "function": Style(color="#A6E22E"),
    "function.call": Style(color="#A6E22E"),
    "method": Style(color="#A6E22E"),
    "method.call": Style(color="#A6E22E"),
    # "constant": Style(color="#AE81FF"),
    "variable": Style(color="white"),
    "parameter": Style(color="cyan"),
    "type": Style(color="cyan"),
    "escape": Style(bgcolor="magenta"),
}

class Highlight(NamedTuple):
    """A range to highlight within a single line"""

    start_column: int | None
    end_column: int | None
    highlight_name: str | None

class Selection(NamedTuple):
    """A range of characters within a document from a start point to the end point.
    The position of the cursor is always considered to be the `end` point of the selection.
    """

    start: tuple[int, int] = (0, 0)
    end: tuple[int, int] = (0, 0)

    @classmethod
    def cursor(cls, position: tuple[int, int]) -> Selection:
        """Create a Selection with the same start and end point."""
        return cls(position, position)

    @property
    def is_cursor(self) -> bool:
        """Return True if the selection has 0 width, i.e. it's just a cursor."""
        start, end = self
        return start == end

class Edit(Protocol):
    """Protocol for actions performed in the text editor that can be done and undone."""

    def do(self, editor: TextEditor) -> object | None:
        """Do the action."""

    def undo(self, editor: TextEditor) -> object | None:
        """Undo the action."""

class Insert(NamedTuple):
    """Implements the Edit protocol for inserting text at some position."""

    text: str
    from_position: tuple[int, int]
    to_position: tuple[int, int]
    move_cursor: bool = True

    def do(self, editor: TextEditor) -> None:
        if self.text:
            editor._insert_text_range(self.text, self.from_position, self.to_position, self.move_cursor)

    def undo(self, editor: TextEditor) -> None:
        """Undo the action."""

@dataclass
class Delete:
    from_position: tuple[int, int]
    to_position: tuple[int, int]
    cursor_destination: tuple[int, int] | None = None

    def do(self, editor: TextEditor) -> None:
        """Do the action."""
        self.deleted_text = editor._delete_range(self.from_position, self.to_position, self.cursor_destination)
        return self.deleted_text

    def undo(self, editor: TextEditor) -> None:
        """Undo the action."""

    def __rich_repr__(self):
        yield "from_position", self.from_position
        yield "to_position", self.to_position
        if hasattr(self, "deleted_text"):
            yield "deleted_text", self.deleted_text

class TextEditor(ScrollView, can_focus=True):
    DEFAULT_CSS = """\
$editor-active-line-bg: white 8%;
TextEditor {
    background: $panel;
}
TextEditor > .text-editor--active-line {
    background: $editor-active-line-bg;
}
TextEditor > .text-editor--active-line-gutter {
    color: $text;
    background: $editor-active-line-bg;
}
TextEditor > .text-editor--gutter {
    color: $text-muted 40%;
}
TextEditor > .text-editor--cursor {
    color: $text;
    background: white 80%;
}

TextEditor > .text-editor--selection {
    background: $primary;
}
"""

    COMPONENT_CLASSES: ClassVar[set[str]] = {
        "text-editor--active-line",
        "text-editor--active-line-gutter",
        "text-editor--gutter",
        "text-editor--cursor",
        "text-editor--selection",
    }

    BINDINGS = [
        # Cursor movement
        Binding("up", "cursor_up", "cursor up", show=False),
        Binding("shift+up", "cursor_up_select", "cursor up select", show=False),
        Binding("down", "cursor_down", "cursor down", show=False),
        Binding("shift+down", "cursor_down_select", "cursor down select", show=False),
        Binding("left", "cursor_left", "cursor left", show=False),
        Binding("shift+left", "cursor_left_select", "cursor left select", show=False),
        Binding("ctrl+left", "cursor_left_word", "cursor left word", show=False),
        Binding("right", "cursor_right", "cursor right", show=False),
        Binding("shift+right", "cursor_right_select", "cursor right select", show=False),
        Binding("ctrl+right", "cursor_right_word", "cursor right word", show=False),
        Binding("home,ctrl+a", "cursor_line_start", "cursor line start", show=False),
        Binding("end,ctrl+e", "cursor_line_end", "cursor line end", show=False),
        Binding("backspace", "delete_left", "delete left", show=False),
        Binding("ctrl+w", "delete_word_left", "delete left to start of word", show=False),
        Binding("ctrl+d", "delete_right", "delete right", show=False),
        Binding("ctrl+f", "delete_word_right", "delete right to start of word", show=False),
        Binding("ctrl+x", "delete_line", "delete line", show=False),
        Binding("ctrl+u", "delete_to_start_of_line", "delete to line start", show=False),
        Binding("ctrl+k", "delete_to_end_of_line", "delete to line end", show=False),
    ]

    language: Reactive[str | None] = reactive(None)
    """The language to use for syntax highlighting (via tree-sitter)."""
    selection: Reactive[Selection] = reactive(Selection(), always_update=True)
    """The cursor position (zero-based line_index, offset)."""
    show_line_numbers: Reactive[bool] = reactive(True)
    """True to show line number gutter, otherwise False."""
    _document_size: Reactive[Size] = reactive(Size(), init=False)
    """Tracks the width of the document. Used to update virtual size. Do not
    update virtual size directly."""

    @dataclass
    class Changed(Message):
        """Posted when the value changes.

        Can be handled using `on_input_changed` in a subclass of `Input` or in a parent
        widget in the DOM.
        """

        text_editor: TextEditor
        """The `Input` widget that was changed."""

        value: str
        """The value that the input was changed to."""

        @property
        def control(self) -> TextEditor:
            """Alias for self.input."""
            return self.text_editor

    def __init__(
        self,
        name: str | None = None,
        id: str | None = None,
        classes: str | None = None,
        disabled: bool = False,
    ) -> None:
        super().__init__(name=name, id=id, classes=classes, disabled=disabled)

        # --- Core editor data
        self.document_lines: list[str] = []
        """Each string in this list represents a line in the document. Includes new line characters."""

        self._highlights: dict[int, list[Highlight]] = defaultdict(list)
        """Mapping line numbers to the set of cached highlights for that line."""

        self._highlights_query: str | None = None
        """The string containing the tree-sitter AST query used for syntax highlighting."""

        self._last_intentional_cell_width: int = 0
        """Tracks the last column (measured in terms of cell length, since we care here about where
         the cursor visually moves more than the logical characters) the user explicitly navigated to so that we can reset
        to it whenever possible."""

        self._word_pattern = re.compile(r"(?<=\W)(?=\w)|(?<=\w)(?=\W)")
        """Compiled regular expression for what we consider to be a 'word'."""

        self._undo_stack: list[Edit] = []
        """A stack (the end of the list is the top of the stack) for tracking edits."""

        self._selecting = False
        """True if we're currently selecting text, otherwise False."""

        # --- Abstract syntax tree and related parsing machinery
        self._language: Language | None = None
        self._parser: Parser | None = None
        """The tree-sitter parser which extracts the syntax tree from the document."""
        self._syntax_tree: Tree | None = None
        """The tree-sitter Tree (AST) built from the document."""

    def watch_language(self, new_language: str | None) -> None:
        """Update the language used in AST parsing.

        When the language reactive string is updated, fetch the Language definition
        from our tree-sitter library file. If the language reactive is set to None,
        then the no parser is used."""
        log.debug(f"updating editor language to {new_language!r}")
        if new_language:
            self._language = Language(LANGUAGES_PATH.resolve(), new_language)
            parser = Parser()
            self._parser = parser
            self._parser.set_language(self._language)
            self._syntax_tree = self._build_ast(parser)
            self._highlights_query = Path(HIGHLIGHTS_PATH.resolve()).read_text()

        log.debug(f"parser set to {self._parser}")

    def watch__document_size(self, size: Size) -> None:
        log.debug(f"document size set to {size!r} ")
        document_width, document_height = size
        self.virtual_size = Size(document_width + self.gutter_width, document_height)

    def _build_ast(
        self,
        parser: Parser,
    ) -> Tree | None:
        """Fully parse the document and build the abstract syntax tree for it.

        Returns None if there's no parser available (e.g. when no language is selected).
        """
        if parser:
            return parser.parse(self._read_callable)
        else:
            return None

    def _read_callable(self, byte_offset: int, point: tuple[int, int]) -> str:
        row, column = point
        lines = self.document_lines
        row_out_of_bounds = row >= len(lines)
        column_out_of_bounds = not row_out_of_bounds and column > len(lines[row])
        if row_out_of_bounds or column_out_of_bounds:
            return_value = None
        elif column == len(lines[row]) and row < len(lines):
            return_value = b"\n"
        else:
            return_value = lines[row][column].encode("utf8")
        # print(f"(point={point!r}) (offset={byte_offset!r}) {return_value!r}")
        return return_value

    def load_text(self, text: str) -> None:
        """Load text from a string into the editor."""
        lines = text.splitlines(keepends=False)
        if not text:
            text = "\n"
        if text[-1] == "\n":
            lines.append("")
        self.load_lines(lines)

    def load_lines(self, lines: list[str]) -> None:
        """Load text from a list of lines into the editor.

        This will replace any previously loaded lines."""
        self.document_lines = lines
        self._document_size = self._get_document_size(lines)
        if self._parser is not None:
            self._syntax_tree = self._build_ast(self._parser)
            self._prepare_highlights()

        log.debug(f"loaded text. parser = {self._parser} ast = {self._syntax_tree}")

    def clear(self) -> None:
        self.load_text("")

    # --- Methods for measuring things (e.g. virtual sizes)
    def _get_document_size(self, document_lines: list[str]) -> Size:
        """Return the virtual size of the document - the document only
        refers to the area in which the cursor can move. It does not, for
        example, include the width of the gutter."""
        text_width = max(cell_len(line) for line in document_lines)
        height = len(document_lines)
        # We add one to the text width to leave a space for the cursor, since it
        # can rest at the end of a line where there isn't yet any character.
        # Similarly, the cursor can rest below the bottom line of text, where
        # a line doesn't currently exist.
        return Size(text_width + 1, height)

    def _refresh_size(self) -> None:
        self._document_size = self._get_document_size(self.document_lines)

    def render_line(self, widget_y: int) -> Strip:
        document_lines = self.document_lines

        document_y = round(self.scroll_y + widget_y)
        out_of_bounds = document_y >= len(document_lines)
        if out_of_bounds:
            return Strip.blank(self.size.width)

        line_string = document_lines[document_y].replace("\n", "").replace("\r", "")
        line_text = Text(f"{line_string} ", end="", tab_size=4)
        line_text.set_length(self.virtual_size.width)

        # Apply highlighting
        null_style = Style.null()
        if self._highlights:
            highlights = self._highlights[document_y]
            for start, end, highlight_name in highlights:
                node_style = HIGHLIGHT_STYLES.get(highlight_name, null_style)
                line_text.stylize(node_style, start, end)

        start, end = self.selection
        end_row, end_column = end

        selection_style = self.get_component_rich_style("text-editor--selection")

        # Start and end can be before or after each other, depending on the direction
        # you move the cursor during selecting text, but the "top" of the selection
        # is always before the "bottom" of the selection.
        selection_top = min(start, end)
        selection_bottom = max(start, end)
        selection_top_row, selection_top_column = selection_top
        selection_bottom_row, selection_bottom_column = selection_bottom

        if start != end and selection_top_row <= document_y <= selection_bottom_row:
            # If this row is part of the selection
            if document_y == selection_top_row == selection_bottom_row:
                # Selection within a single line
                line_text.stylize_before(
                    selection_style,
                    start=selection_top_column,
                    end=selection_bottom_column,
                )
            else:
                # Selection spanning multiple lines
                if document_y == selection_top_row:
                    line_text.stylize_before(
                        selection_style,
                        start=selection_top_column,
                        end=len(line_string),
                    )
                elif document_y == selection_bottom_row:
                    line_text.stylize_before(selection_style, end=selection_bottom_column)
                else:
                    line_text.stylize_before(selection_style, end=len(line_string))

        # Show the cursor and the selection
        if end_row == document_y:
            cursor_style = self.get_component_rich_style("text-editor--cursor")
            line_text.stylize(cursor_style, end_column, end_column + 1)
            active_line_style = self.get_component_rich_style("text-editor--active-line")
            line_text.stylize_before(active_line_style)

        # Show the gutter
        if self.show_line_numbers:
            if end_row == document_y:
                gutter_style = self.get_component_rich_style("text-editor--active-line-gutter")
            else:
                gutter_style = self.get_component_rich_style("text-editor--gutter")

            gutter_width_no_margin = self.gutter_width - 2
            gutter = Text(
                f"{document_y + 1:>{gutter_width_no_margin}}  ",
                style=gutter_style,
                end="",
            )
        else:
            gutter = Text("", end="")

        gutter_segments = self.app.console.render(gutter)
        text_segments = self.app.console.render(
            line_text, self.app.console.options.update_width(self.virtual_size.width)
        )

        virtual_width, virtual_height = self.virtual_size
        text_crop_start = int(self.scroll_x)
        text_crop_end = text_crop_start + virtual_width

        gutter_strip = Strip(gutter_segments)
        text_strip = Strip(text_segments).crop(text_crop_start, text_crop_end)

        strip = Strip.join([gutter_strip, text_strip]).simplify()

        return strip

    @property
    def gutter_width(self) -> int:
        # The longest number in the gutter plus two extra characters: `│ `.
        gutter_margin = 2
        gutter_longest_number = len(str(len(self.document_lines) + 1)) + gutter_margin if self.show_line_numbers else 0
        return gutter_longest_number

    # --- Syntax highlighting
    def _prepare_highlights(
        self,
        start_point: tuple[int, int] | None = None,
        end_point: tuple[int, int] | None = None,
    ) -> None:
        # TODO - we're ignoring get changed ranges for now. Either I'm misunderstanding
        #  it or I've made a mistake somewhere with AST editing.

        highlights = self._highlights
        query: Query = self._language.query(self._highlights_query)

        log.debug(f"capturing nodes in range {start_point!r} -> {end_point!r}")

        captures_kwargs = {}
        if start_point is not None:
            captures_kwargs["start_point"] = start_point
        if end_point is not None:
            captures_kwargs["end_point"] = end_point

        captures = query.captures(self._syntax_tree.root_node, **captures_kwargs)

        highlight_updates: dict[int, list[Highlight]] = defaultdict(list)
        for capture in captures:
            node, highlight_name = capture
            node_start_row, node_start_column = node.start_point
            node_end_row, node_end_column = node.end_point

            if node_start_row == node_end_row:
                highlight = Highlight(node_start_column, node_end_column, highlight_name)
                highlight_updates[node_start_row].append(highlight)
            else:
                # Add the first line
                highlight_updates[node_start_row].append(Highlight(node_start_column, None, highlight_name))
                # Add the middle lines - entire row of this node is highlighted
                for node_row in range(node_start_row + 1, node_end_row):
                    highlight_updates[node_row].append(Highlight(0, None, highlight_name))

                # Add the last line
                highlight_updates[node_end_row].append(Highlight(0, node_end_column, highlight_name))

        for line_index, updated_highlights in highlight_updates.items():
            highlights[line_index] = updated_highlights

    def edit(self, edit: Edit) -> object | None:
        log.debug(f"performing edit {edit!r}")
        result = edit.do(self)
        self._undo_stack.append(edit)

        # TODO: Think about this...
        self._undo_stack = self._undo_stack[-20:]

        return result

    def undo(self) -> None:
        if self._undo_stack:
            action = self._undo_stack.pop()
            action.undo(self)

    # --- Lower level event/key handling
    def _on_key(self, event: events.Key) -> None:
        log.debug(f"{event!r}")
        key = event.key
        if event.is_printable or key == "tab" or key == "enter":
            if key == "tab":
                insert = "    "
            elif key == "enter":
                insert = "\n"
            else:
                insert = event.character
            event.stop()
            assert event.character is not None
            start, end = self.selection
            self.insert_text_range(insert, start, end)
            event.prevent_default()
        elif key == "shift+tab":
            self.dedent_line()
            event.stop()

    def get_target_document_location(self, offset: Offset) -> tuple[int, int]:
        if offset is None:
            return

        target_x = max(offset.x - self.gutter_width + int(self.scroll_x), 0)
        target_row = clamp(offset.y + int(self.scroll_y), 0, len(self.document_lines) - 1)
        target_column = self.cell_width_to_column_index(target_x, target_row)

        return target_row, target_column

    def _fix_direction(self, start: tuple[int, int], end: tuple[int, int]) -> tuple[tuple[int, int], tuple[int, int]]:
        """Given a range, return a new range (x, y) such that x <= y which covers the same characters."""
        if start > end:
            return end, start
        return start, end

    def _on_mouse_down(self, event: events.MouseDown) -> None:
        event.stop()
        offset = event.get_content_offset(self)
        target_row, target_column = self.get_target_document_location(offset)
        self.selection = Selection.cursor((target_row, target_column))
        log.debug(f"started selection {self.selection!r}")
        self._selecting = True

    def _on_mouse_move(self, event: events.MouseMove) -> None:
        event.stop()
        if self._selecting:
            offset = event.get_content_offset(self)
            target = self.get_target_document_location(offset)
            selection_start, _ = self.selection
            self.selection = Selection(selection_start, target)
            log.debug(f"selection updated {self.selection!r}")

    def _on_mouse_up(self, event: events.MouseUp) -> None:
        event.stop()
        self._record_last_intentional_cell_width()
        self._selecting = False

    def _on_paste(self, event: events.Paste) -> None:
        text = event.text
        if text:
            self.insert_text(text, self.selection)
        event.stop()

    def cell_width_to_column_index(self, cell_width: int, row_index: int) -> int:
        """Return the column that the cell width corresponds to on the given row."""
        total_cell_offset = 0
        line = self.document_lines[row_index]
        for column_index, character in enumerate(line):
            total_cell_offset += cell_len(character)
            if total_cell_offset >= cell_width + 1:
                log(f"cell width {cell_width} -> column_index {column_index}")
                return column_index
        return len(line)

    def watch_selection(self) -> None:
        self.scroll_cursor_visible()

    # --- Cursor utilities
    def scroll_cursor_visible(self):
        # The end of the selection is always considered to be position of the cursor
        # ... this is a constraint we need to enforce in code.
        row, column = self.selection.end
        text = self.active_line_text[:column]
        column_offset = cell_len(text)
        self.scroll_to_region(
            Region(x=column_offset, y=row, width=3, height=1),
            spacing=Spacing(right=self.gutter_width),
            animate=False,
            force=True,
        )

    @property
    def cursor_at_first_row(self) -> bool:
        return self.selection.end[0] == 0

    @property
    def cursor_at_last_row(self) -> bool:
        return self.selection.end[0] == len(self.document_lines) - 1

    @property
    def cursor_at_start_of_row(self) -> bool:
        return self.selection.end[1] == 0

    @property
    def cursor_at_end_of_row(self) -> bool:
        cursor_row, cursor_column = self.selection.end
        row_length = len(self.document_lines[cursor_row])
        cursor_at_end = cursor_column == row_length
        return cursor_at_end

    @property
    def cursor_at_start_of_document(self) -> bool:
        return self.cursor_at_first_row and self.cursor_at_start_of_row

    @property
    def cursor_at_end_of_document(self) -> bool:
        """True if the cursor is at the very end of the document."""
        return self.cursor_at_last_row and self.cursor_at_end_of_row

    def cursor_to_line_end(self, select: bool = False) -> None:
        """Move the cursor to the end of the line.

        Args:
            select: Select the text between the old and new cursor locations.
        """

        start, end = self.selection
        cursor_row, cursor_column = end
        target_column = len(self.document_lines[cursor_row])

        if select:
            self.selection = Selection(start, target_column)
        else:
            self.selection = Selection.cursor((cursor_row, target_column))

        self._record_last_intentional_cell_width()

    def cursor_to_line_start(self, select: bool = False) -> None:
        """Move the cursor to the start of the line.

        Args:
            select: Select the text between the old and new cursor locations.
        """
        start, end = self.selection
        cursor_row, cursor_column = end
        if select:
            self.selection = Selection(start, (cursor_row, 0))
        else:
            self.selection = Selection.cursor((cursor_row, 0))
            print(f"new selection = {self.selection}")

    # ------ Cursor movement actions
    def action_cursor_left(self) -> None:
        """Move the cursor one position to the left.

        If the cursor is at the left edge of the document, try to move it to
        the end of the previous line.
        """
        target_row, target_column = self.get_cursor_left_position()
        self.selection = Selection.cursor((target_row, target_column))
        self._record_last_intentional_cell_width()

    def action_cursor_left_select(self):
        """Move the end of the selection one position to the left.

        This will expand or contract the selection.
        """
        new_cursor_position = self.get_cursor_left_position()
        selection_start, selection_end = self.selection
        self.selection = Selection(selection_start, new_cursor_position)
        self._record_last_intentional_cell_width()

    def get_cursor_left_position(self) -> tuple[int, int]:
        """Get the position the cursor will move to if it moves left."""
        if self.cursor_at_start_of_document:
            return 0, 0
        cursor_row, cursor_column = self.selection.end
        length_of_row_above = len(self.document_lines[cursor_row - 1])
        target_row = cursor_row if cursor_column != 0 else cursor_row - 1
        target_column = cursor_column - 1 if cursor_column != 0 else length_of_row_above
        return target_row, target_column

    def action_cursor_right(self) -> None:
        """Move the cursor one position to the right.

        If the cursor is at the end of a line, attempt to go to the start of the next line.
        """
        target = self.get_cursor_right_position()
        self.selection = Selection.cursor(target)
        self._record_last_intentional_cell_width()

    def action_cursor_right_select(self):
        """Move the end of the selection one position to the right.

        This will expand or contract the selection.
        """
        new_cursor_position = self.get_cursor_right_position()
        selection_start, selection_end = self.selection
        self.selection = Selection(selection_start, new_cursor_position)
        self._record_last_intentional_cell_width()

    def get_cursor_right_position(self) -> tuple[int, int]:
        """Get the position the cursor will move to if it moves right."""
        if self.cursor_at_end_of_document:
            return self.selection.end
        cursor_row, cursor_column = self.selection.end
        target_row = cursor_row + 1 if self.cursor_at_end_of_row else cursor_row
        target_column = 0 if self.cursor_at_end_of_row else cursor_column + 1
        return target_row, target_column

    def action_cursor_down(self) -> None:
        """Move the cursor down one cell."""
        target = self.get_cursor_down_position()
        self.selection = Selection.cursor(target)

    def action_cursor_down_select(self) -> None:
        """Move the cursor down one cell, selecting the range between the old and new positions."""
        target = self.get_cursor_down_position()
        start, end = self.selection
        self.selection = Selection(start, target)

    def get_cursor_down_position(self):
        """Get the position the cursor will move to if it moves down."""
        cursor_row, cursor_column = self.selection.end
        if self.cursor_at_last_row:
            return cursor_row, len(self.document_lines[cursor_row])

        target_row = min(len(self.document_lines) - 1, cursor_row + 1)
        # Attempt to snap last intentional cell length
        target_column = self.cell_width_to_column_index(self._last_intentional_cell_width, target_row)
        target_column = clamp(target_column, 0, len(self.document_lines[target_row]))
        return target_row, target_column

    def action_cursor_up(self) -> None:
        """Move the cursor up one cell."""
        target = self.get_cursor_up_position()
        self.selection = Selection.cursor(target)

    def action_cursor_up_select(self) -> None:
        """Move the cursor up one cell, selecting the range between the old and new positions."""
        target = self.get_cursor_up_position()
        start, end = self.selection
        self.selection = Selection(start, target)

    def get_cursor_up_position(self) -> tuple[int, int]:
        if self.cursor_at_first_row:
            return 0, 0
        cursor_row, cursor_column = self.selection.end
        target_row = max(0, cursor_row - 1)
        # Attempt to snap last intentional cell length
        target_column = self.cell_width_to_column_index(self._last_intentional_cell_width, target_row)
        target_column = clamp(target_column, 0, len(self.document_lines[target_row]))
        return target_row, target_column

    def action_cursor_line_end(self) -> None:
        self.cursor_to_line_end()

    def action_cursor_line_start(self) -> None:
        self.cursor_to_line_start()

    def action_cursor_left_word(self) -> None:
        """Move the cursor left by a single word, skipping spaces."""

        if self.cursor_at_start_of_document:
            return

        cursor_row, cursor_column = self.selection.end

        # Check the current line for a word boundary
        line = self.document_lines[cursor_row][:cursor_column]
        matches = list(re.finditer(self._word_pattern, line))

        if matches:
            # If a word boundary is found, move the cursor there
            cursor_column = matches[-1].start()
        elif cursor_row > 0:
            # If no word boundary is found and we're not on the first line, move to the end of the previous line
            cursor_row -= 1
            cursor_column = len(self.document_lines[cursor_row])
        else:
            # If we're already on the first line and no word boundary is found, move to the start of the line
            cursor_column = 0

        self.selection = Selection.cursor((cursor_row, cursor_column))
        self._record_last_intentional_cell_width()

    def action_cursor_right_word(self) -> None:
        """Move the cursor right by a single word, skipping spaces."""

        if self.cursor_at_end_of_document:
            return

        cursor_row, cursor_column = self.selection.end

        # Check the current line for a word boundary
        line = self.document_lines[cursor_row][cursor_column:]
        matches = list(re.finditer(self._word_pattern, line))

        if matches:
            # If a word boundary is found, move the cursor there
            cursor_column += matches[0].end()
        elif cursor_row < len(self.document_lines) - 1:
            # If no word boundary is found and we're not on the last line, move to the start of the next line
            cursor_row += 1
            cursor_column = 0
        else:
            # If we're already on the last line and no word boundary is found, move to the end of the line
            cursor_column = len(self.document_lines[cursor_row])

        self.selection = Selection.cursor((cursor_row, cursor_column))
        self._record_last_intentional_cell_width()

    @property
    def active_line_text(self) -> str:
        # TODO - consider empty documents
        return self.document_lines[self.selection.end[0]]

    def get_column_cell_width(self, row: int, column: int) -> int:
        """Given a row and column index within the editor, return the cell offset
        of the column from the start of the row (the left edge of the editor content area).
        """
        line = self.document_lines[row]
        return cell_len(line[:column])

    def _record_last_intentional_cell_width(self) -> None:
        row, column = self.selection.end
        column_cell_length = self.get_column_cell_width(row, column)
        log(f"last intentional cell width = {column_cell_length}")
        self._last_intentional_cell_width = column_cell_length

    # --- Editor operations
    def insert_text(self, text: str, position: tuple[int, int], move_cursor: bool = True) -> None:
        self.edit(Insert(text, position, position, move_cursor))

    def insert_text_range(
        self,
        text: str,
        from_position: tuple[int, int],
        to_position: tuple[int, int],
        move_cursor: bool = True,
    ):
        self.edit(Insert(text, from_position, to_position, move_cursor))

    def _insert_text_range(
        self,
        text: str,
        from_position: tuple[int, int],
        to_position: tuple[int, int],
        move_cursor: bool = True,
    ) -> None:
        """Insert text at a given range and move the cursor to the end of the inserted text."""

        inserted_text = text
        lines = self.document_lines

        from_row, from_column = from_position
        to_row, to_column = to_position

        if from_position > to_position:
            from_row, from_column, to_row, to_column = (
                to_row,
                to_column,
                from_row,
                from_column,
            )

        insert_lines = inserted_text.splitlines()
        if inserted_text.endswith("\n"):
            # Special case where a single newline character is inserted.
            insert_lines.append("")

        before_selection = lines[from_row][:from_column]
        after_selection = lines[to_row][to_column:]

        insert_lines[0] = before_selection + insert_lines[0]
        destination_column = len(insert_lines[-1])
        insert_lines[-1] = insert_lines[-1] + after_selection
        lines[from_row : to_row + 1] = insert_lines
        destination_row = from_row + len(insert_lines) - 1

        cursor_destination = (destination_row, destination_column)

        start_byte = self._position_to_byte_offset(from_position)
        if self._syntax_tree is not None:
            self._syntax_tree.edit(
                start_byte=start_byte,
                old_end_byte=self._position_to_byte_offset(to_position),
                new_end_byte=start_byte + len(inserted_text),
                start_point=from_position,
                old_end_point=to_position,
                new_end_point=cursor_destination,
            )
            self._syntax_tree = self._parser.parse(self._read_callable, self._syntax_tree)
            self._prepare_highlights()
        self._refresh_size()
        if move_cursor:
            self.selection = Selection.cursor(cursor_destination)
        self.post_message(self.Changed(self, ""))

    def _position_to_byte_offset(self, position: tuple[int, int]) -> int:
        """Given a document coordinate, return the byte offset of that coordinate."""

        # TODO - this assumes all line endings are a single byte `\n`
        lines = self.document_lines
        row, column = position
        lines_above = lines[:row]
        bytes_lines_above = sum(len(line) + 1 for line in lines_above)
        bytes_this_line_left_of_cursor = len(lines[row][:column])
        return bytes_lines_above + bytes_this_line_left_of_cursor

    def dedent_line(self) -> None:
        """Reduces the indentation of the current line by one level.

        A dedent is simply a Delete operation on some amount of whitespace
        which may exist at the start of a line.
        """
        cursor_row, cursor_column = self.selection.end

        # Define one level of indentation as four spaces
        indent_level = " " * 4

        current_line = self.document_lines[cursor_row]

        # If the line is indented, reduce the indentation
        # TODO - if the line is less than the indent level we should just dedent as far as possible.
        if current_line.startswith(indent_level):
            self.document_lines[cursor_row] = current_line[len(indent_level) :]

        if cursor_column > len(current_line):
            self.selection = Selection.cursor((cursor_row, len(current_line)))

        self._refresh_size()
        self.refresh()

    def delete_range(
        self,
        from_position: tuple[int, int],
        to_position: tuple[int, int],
        cursor_destination: tuple[int, int] | None = None,
    ) -> str:
        return self.edit(Delete(from_position, to_position, cursor_destination))

    def _delete_range(
        self,
        from_position: tuple[int, int],
        to_position: tuple[int, int],
        cursor_destination: tuple[int, int] | None,
    ) -> str:
        """Delete text between `from_position` and `to_position`.

        Returns:
            A string containing the deleted text.
        """

        from_row, from_column = from_position
        to_row, to_column = to_position

        lines = self.document_lines

        # Ensure that from_position is before to_position
        if from_position > to_position:
            from_row, from_column, to_row, to_column = (
                to_row,
                to_column,
                from_row,
                from_column,
            )

        # If the range is within a single line
        if from_row == to_row:
            line = lines[from_row]
            deleted_text = line[from_column:to_column]
            lines[from_row] = line[:from_column] + line[to_column:]
        else:
            # The range spans multiple lines
            start_line = lines[from_row]
            end_line = lines[to_row]

            deleted_text = start_line[from_column:] + "\n"
            for row in range(from_row + 1, to_row):
                deleted_text += lines[row] + "\n"

            deleted_text += end_line[:to_column]
            if to_column == len(end_line):
                deleted_text += "\n"

            # Update the lines at the start and end of the range
            lines[from_row] = start_line[:from_column] + end_line[to_column:]

            # Delete the lines in between
            del lines[from_row + 1 : to_row + 1]

        if self._syntax_tree is not None:
            start_byte = self._position_to_byte_offset(from_position)
            self._syntax_tree.edit(
                start_byte=start_byte,
                old_end_byte=self._position_to_byte_offset(to_position),
                new_end_byte=start_byte,
                start_point=from_position,
                old_end_point=to_position,
                new_end_point=from_position,
            )
            self._syntax_tree = self._parser.parse(self._read_callable, self._syntax_tree)
            self._prepare_highlights()

        self._refresh_size()
        if cursor_destination is not None:
            self.selection = Selection.cursor(cursor_destination)
        else:
            # Move the cursor to the start of the deleted range
            self.selection = Selection.cursor((from_row, from_column))

        self.post_message(self.Changed(self, ""))
        return deleted_text

    def action_delete_left(self) -> None:
        """Deletes the character to the left of the cursor and updates the cursor position."""
        if self.cursor_at_start_of_document:
            return

        lines = self.document_lines

        start, end = self.selection
        end_row, end_column = end

        if self.cursor_at_start_of_row:
            to_position = (end_row - 1, len(lines[end_row - 1]))
        else:
            to_position = (end_row, end_column - 1)

        self.edit(Delete(start, to_position))

    def action_delete_right(self) -> None:
        """Deletes the character to the right of the cursor and keeps the cursor at the same position."""
        if self.cursor_at_end_of_document:
            return

        start, end = self.selection
        end_row, end_column = end

        if self.cursor_at_end_of_row:
            to_position = (end_row + 1, 0)
        else:
            to_position = (end_row, end_column + 1)

        self.edit(Delete(start, to_position))

    def action_delete_line(self) -> None:
        """Deletes the lines which intersect with the selection."""
        start, end = self.selection
        start_row, start_column = start
        end_row, end_column = end

        from_position = (start_row, 0)
        to_position = (end_row + 1, 0)

        self.edit(Delete(from_position, to_position))

    def action_delete_to_start_of_line(self) -> None:
        """Deletes from the cursor position to the start of the line."""
        from_position = self.selection.end
        cursor_row, cursor_column = from_position
        to_position = (cursor_row, 0)
        self.edit(Delete(from_position, to_position))

    def action_delete_to_end_of_line(self) -> None:
        """Deletes from the cursor position to the end of the line."""
        from_position = self.selection.end
        cursor_row, cursor_column = from_position
        to_position = (cursor_row, len(self.document_lines[cursor_row]))
        self.edit(Delete(from_position, to_position))

    def action_delete_word_left(self) -> None:
        """Deletes the word to the left of the cursor and updates the cursor position."""
        if self.cursor_at_start_of_document:
            return

        # If there's a non-zero selection, then "delete word left" typically only
        # deletes the characters within the selection range, ignoring word boundaries.
        start, end = self.selection
        if start != end:
            self.edit(Delete(start, end))

        cursor_row, cursor_column = end

        # Check the current line for a word boundary
        line = self.document_lines[cursor_row][:cursor_column]
        matches = list(re.finditer(self._word_pattern, line))

        if matches:
            # If a word boundary is found, delete the word
            from_position = (cursor_row, matches[-1].start())
        elif cursor_row > 0:
            # If no word boundary is found, and we're not on the first line, delete to the end of the previous line
            from_position = (cursor_row - 1, len(self.document_lines[cursor_row - 1]))
        else:
            # If we're already on the first line and no word boundary is found, delete to the start of the line
            from_position = (cursor_row, 0)

        self.edit(Delete(from_position, self.selection.end))

    def action_delete_word_right(self) -> None:
        """Deletes the word to the right of the cursor and keeps the cursor at the same position."""
        if self.cursor_at_end_of_document:
            return

        start, end = self.selection
        if start != end:
            self.edit(Delete(start, end))

        cursor_row, cursor_column = end

        # Check the current line for a word boundary
        line = self.document_lines[cursor_row][cursor_column:]
        matches = list(re.finditer(self._word_pattern, line))

        if matches:
            # If a word boundary is found, delete the word
            to_position = (cursor_row, cursor_column + matches[0].end())
        elif cursor_row < len(self.document_lines) - 1:
            # If no word boundary is found, and we're not on the last line, delete to the start of the next line
            to_position = (cursor_row + 1, 0)
        else:
            # If we're already on the last line and no word boundary is found, delete to the end of the line
            to_position = (cursor_row, len(self.document_lines[cursor_row]))

        self.edit(Delete(end, to_position))

    # --- Debugging
    @dataclass
    class EditorDebug:
        cursor: tuple[int, int]
        language: str
        document_size: Size
        virtual_size: Size
        scroll: Offset
        undo_stack: list[Edit]
        tree_sexp: str
        active_line_text: str
        active_line_cell_len: int
        highlight_cache_key_count: int
        highlight_cache_total_size: int
        highlight_cache_current_row_size: int
        highlight_cache_current_row: list[Highlight]

    def debug_state(self) -> EditorDebug:
        return self.EditorDebug(
            cursor=self.selection,
            language=self.language,
            document_size=self._document_size,
            virtual_size=self.virtual_size,
            scroll=self.scroll_offset,
            undo_stack=list(reversed(self._undo_stack)),
            # tree_sexp=self._syntax_tree.root_node.sexp(),
            tree_sexp="",
            active_line_text=repr(self.active_line_text),
            active_line_cell_len=cell_len(self.active_line_text),
            highlight_cache_key_count=len(self._highlights),
            highlight_cache_total_size=sum(len(highlights) for key, highlights in self._highlights.items()),
            highlight_cache_current_row_size=len(self._highlights[self.selection.end[0]]),
            highlight_cache_current_row=self._highlights[self.selection.end[0]],
        )

def traverse_tree(cursor):
    reached_root = False
    while reached_root is False:
        yield cursor.node

        if cursor.goto_first_child():
            continue

        if cursor.goto_next_sibling():
            continue

        retracing = True
        while retracing:
            if not cursor.goto_parent():
                retracing = False
                reached_root = True

            if cursor.goto_next_sibling():
                retracing = False

# if __name__ == "__main__":
#     language = Language(LANGUAGES_PATH.resolve(), "python")
#     parser = Parser()
#     parser.set_language(language)
#
#     CODE = """\
#     from textual.app import App
#
#
#     class ScreenApp(App):
#         def on_mount(self) -> None:
#             self.screen.styles.background = "darkblue"
#             self.screen.styles.border = ("heavy", "white")
#
#
#     if __name__ == "__main__":
#         app = ScreenApp()
#         app.run()
#     """
#
#     document_lines = CODE.splitlines(keepends=False)
#
#     def read_callable(byte_offset, point):
#         row, column = point
#         if row >= len(document_lines) or column >= len(document_lines[row]):
#             return None
#         return document_lines[row][column:].encode("utf8")
#
#     tree = parser.parse(bytes(CODE, "utf-8"))
#
#     print(list(traverse_tree(tree.walk())))


# A part of NonVisual Desktop Access (NVDA)
# Copyright (C) 2016-2021 NV Access Limited
# This file is covered by the GNU General Public License.
# See the file COPYING for more details.
from typing import (
	Optional,
	Dict,
)

from comtypes import COMError
from comtypes.hresult import S_OK
import appModuleHandler
import speech
import textUtils
from speech import sayAll
import api
from scriptHandler import willSayAllResume, isScriptWaiting
import controlTypes
from controlTypes import OutputReason
import treeInterceptorHandler
from cursorManager import ReviewCursorManager
import browseMode
from browseMode import BrowseModeDocumentTreeInterceptor
import textInfos
from speech.types import SpeechSequence
from textInfos import DocumentWithPageTurns
from NVDAObjects.IAccessible import IAccessible
from globalCommands import SCRCAT_SYSTEMCARET
from NVDAObjects.IAccessible.ia2TextMozilla import MozillaCompoundTextInfo
from comInterfaces import IAccessible2Lib as IA2
import winUser
import mouseHandler
from logHandler import log
import ui
import config

class ElementsListDialog(browseMode.ElementsListDialog):
	ELEMENT_TYPES = (
		browseMode.ElementsListDialog.ELEMENT_TYPES[0], # Links
	)

class BookPageViewTreeInterceptor(DocumentWithPageTurns,ReviewCursorManager,BrowseModeDocumentTreeInterceptor):
	ElementsListDialog = ElementsListDialog
	TextInfo=treeInterceptorHandler.RootProxyTextInfo
	pageChangeAlreadyHandled = False

	def turnPage(self,previous=False):
		self.rootNVDAObject.turnPage(previous=previous)
		# turnPage waits for a pageChange event before returning,
		# but the pageChange event will still get fired.
		# We need to know that we've already handled it.
		self.pageChangeAlreadyHandled=True

	def event_pageChange(self, obj, nextHandler):
		if self.pageChangeAlreadyHandled:
			# This page change has already been handled.
			self.pageChangeAlreadyHandled = False
			return
		info = self.makeTextInfo(textInfos.POSITION_FIRST)
		self.selection = info
		if not self.rootNVDAObject.hasFocus:
			# Don't report anything if the book area isn't focused.
			return
		info.expand(textInfos.UNIT_LINE)
		speech.speakTextInfo(info, unit=textInfos.UNIT_LINE, reason=OutputReason.CARET)

	def isAlive(self):
		return winUser.isWindow(self.rootNVDAObject.windowHandle)

	def __contains__(self,obj):
		return obj==self.rootNVDAObject

	def _getTableCellAt(self,tableID,startPos,destRow,destCol):
		""" Override of documentBase.DocumentWithTableNavigation._getTableCellAt."""
		# Locate the table in the object ancestry of the given document position.
		obj=startPos.NVDAObjectAtStart
		while not obj.table and obj!=startPos.obj.rootNVDAObject:
			obj=obj.parent
		if not obj.table:
			# No table could be found
			raise LookupError
		table = obj.table
		try:
			cell = table.IAccessibleTable2Object.cellAt(
				destRow - 1, destCol - 1
			).QueryInterface(IA2.IAccessible2)
			cell = IAccessible(IAccessibleObject=cell, IAccessibleChildID=0)
			# If the cell we fetched is marked as hidden, raise LookupError which will instruct calling code to try an adjacent cell instead.
			if cell.IA2Attributes.get('hidden'):
				raise LookupError("Found hidden cell")
			# Return the position of the found cell
			return self.makeTextInfo(cell)
		except (COMError, RuntimeError):
			# Any of the above calls could throw a COMError, and sometimes a RuntimeError.
			# Treet this as the cell not existing.
			raise LookupError

	def _changePageScriptHelper(self,gesture,previous=False):
		if isScriptWaiting():
			return
		try:
			self.turnPage(previous=previous)
		except RuntimeError:
			return
		info=self.makeTextInfo(textInfos.POSITION_FIRST)
		self.selection=info
		info.expand(textInfos.UNIT_LINE)
		if not willSayAllResume(gesture):
			speech.speakTextInfo(info, unit=textInfos.UNIT_LINE, reason=OutputReason.CARET)

	def script_moveByPage_forward(self,gesture):
		self._changePageScriptHelper(gesture)
	script_moveByPage_forward.resumeSayAllMode = sayAll.CURSOR.CARET

	def script_moveByPage_back(self,gesture):
		self._changePageScriptHelper(gesture,previous=True)
	script_moveByPage_back.resumeSayAllMode = sayAll.CURSOR.CARET

	def _tabOverride(self,direction):
		return False

	def script_showSelectionOptions(self, gesture):
		fakeSel = self.selection
		if fakeSel.isCollapsed:
			# Double click to access the toolbar; e.g. for annotations.
			try:
				p = fakeSel.pointAtStart
			except NotImplementedError:
				log.debugWarning("Couldn't get point to click")
				return
			log.debug("Double clicking")
			winUser.setCursorPos(p.x, p.y)
			mouseHandler.doPrimaryClick()
			mouseHandler.doPrimaryClick()
			return

		# The user makes a selection using browse mode virtual selection.
		# Update the selection in Kindle.
		# This will cause the options to appear.
		fakeSel.innerTextInfo.updateSelection()
		# The selection might have been adjusted to meet word boundaries,
		# so retrieve and report the selection from Kindle.
		# we can't just use self.makeTextInfo, as that will use our fake selection.
		realSel = self.rootNVDAObject.makeTextInfo(textInfos.POSITION_SELECTION)
		speech.speakTextSelected(realSel.text)
		# Remove our virtual selection and move the caret to the active end.
		fakeSel.innerTextInfo = realSel
		fakeSel.collapse(end=not self._lastSelectionMovedStart)
		self.selection = fakeSel
	# Translators: Describes a command.
	script_showSelectionOptions.__doc__ = _("Shows options related to selected text or text at the cursor")
	script_showSelectionOptions.category = SCRCAT_SYSTEMCARET

	__gestures = {
		"kb:control+c": "showSelectionOptions",
		"kb:applications": "showSelectionOptions",
		"kb:shift+f10": "showSelectionOptions",
	}

	def _iterEmbeddedObjs(self, hypertext, startIndex, direction):
		"""Recursively iterate through all embedded objects in a given direction starting at a given hyperlink index.
		"""
		log.debug("Starting at hyperlink index %d" % startIndex)
		for index in range(startIndex, hypertext.nHyperlinks if direction == "next" else -1, 1 if direction == "next" else -1):
			hl = hypertext.hyperlink(index)
			obj = IAccessible(IAccessibleObject=hl.QueryInterface(IA2.IAccessible2), IAccessibleChildID=0)
			log.debug("Yielding object at index %d" % index)
			yield obj
			try:
				objHt = obj.iaHypertext
			except:
				# This is a graphic, etc. which doesn't support text.
				continue
			log.debug("Object has hypertext. Recursing")
			for subObj in self._iterEmbeddedObjs(objHt, 0 if direction == "next" else objHt.nHyperlinks - 1, direction):
				yield subObj

	NODE_TYPES_TO_ROLES = {
		"link": {controlTypes.Role.LINK, controlTypes.Role.FOOTNOTE},
		"graphic": {controlTypes.Role.GRAPHIC},
		"table": {controlTypes.Role.TABLE},
	}

	def _iterNodesByType(self, nodeType, direction="next", pos=None):
		if not pos:
			pos = self.makeTextInfo(textInfos.POSITION_FIRST if direction == "next" else textInfos.POSITION_LAST)
		obj = pos.innerTextInfo._startObj
		if nodeType=="container":
			while obj!=self.rootNVDAObject:
				if obj.role==controlTypes.Role.TABLE:
					ti=self.makeTextInfo(obj)
					yield browseMode.TextInfoQuickNavItem(nodeType, self, ti)
					return
				obj=obj.parent
			return
		roles = self.NODE_TYPES_TO_ROLES.get(nodeType)
		if not roles:
			raise NotImplementedError
		# Find the first embedded object in the requested direction.
		# Use the text, as enumerating IAccessibleHypertext means more cross-process calls.
		offset = pos.innerTextInfo._start._startOffset
		if direction == "next":
			text = obj.IAccessibleTextObject.text(offset + 1, obj.IAccessibleTextObject.nCharacters)
			embed = text.find(textUtils.OBJ_REPLACEMENT_CHAR)
			if embed != -1:
				embed += offset + 1
		else:
			if offset > 0:
				text = obj.IAccessibleTextObject.text(0, offset)
				embed = text.rfind(textUtils.OBJ_REPLACEMENT_CHAR)
			else:
				# We're at the start; we can't go back any further.
				embed = -1
		log.debug("%s embedded object from offset %d: %d" % (direction, offset, embed))
		hli = -1 if embed == -1 else obj.iaHypertext.hyperlinkIndex(embed)
		while True:
			if hli != -1:
				for embObj in self._iterEmbeddedObjs(obj.iaHypertext, hli, direction):
					if embObj.role in roles:
						ti = self.makeTextInfo(embObj)
						yield browseMode.TextInfoQuickNavItem(nodeType, self, ti)
			# No more embedded objects here.
			# We started in an embedded object, so continue in the parent.
			if obj == self.rootNVDAObject:
				log.debug("At root, stopping")
				break # Can't go any further.
			log.debug("Continuing in parent")
			# Get the index of the embedded object we just came from.
			parent = obj.parent
			if not getattr(parent,'IAccessibleTextObject',None):
				obj=parent
				continue
			hl = obj.IAccessibleObject.QueryInterface(IA2.IAccessibleHyperlink)
			offset = hl.startIndex
			obj=parent
			hli = obj.iaHypertext.hyperlinkIndex(offset)
			# Continue the walk from the next embedded object.
			hli += 1 if direction == "next" else -1

	def script_find(self, gesture):
		# Translators: Reported when a user tries to use a find command when it isn't supported.
		ui.message(_("Not supported in this document"))

	def script_findNext(self, gesture):
		# Translators: Reported when a user tries to use a find command when it isn't supported.
		ui.message(_("Not supported in this document"))

	def script_findPrevious(self, gesture):
		# Translators: Reported when a user tries to use a find command when it isn't supported.
		ui.message(_("Not supported in this document"))

class BookPageViewTextInfo(MozillaCompoundTextInfo):

	def _get_locationText(self):
		curLocation=self.obj.IA2Attributes.get('kindle-first-visible-location-number')
		maxLocation=self.obj.IA2Attributes.get('kindle-max-location-number')
		pageNumber=self.obj.pageNumber
		# Translators: A position in a Kindle book
		# xgettext:no-python-format
		text=_("{bookPercentage}%, location {curLocation} of {maxLocation}").format(bookPercentage=int((float(curLocation)/float(maxLocation))*100),curLocation=curLocation,maxLocation=maxLocation)
		if pageNumber:
			# Translators: a page in a Kindle book
			text+=", "+_("Page {pageNumber}").format(pageNumber=pageNumber)
		return text

	def getTextWithFields(self, formatConfig: Optional[Dict] = None) -> textInfos.TextInfo.TextWithFieldsT:
		if not formatConfig:
			formatConfig = config.conf["documentFormatting"]
		items = super(BookPageViewTextInfo, self).getTextWithFields(formatConfig=formatConfig)
		for item in items:
			if isinstance(item, textInfos.FieldCommand) and item.command == "formatChange":
				if formatConfig['reportPage']:
					item.field['page-number'] = self.obj.pageNumber
			elif (isinstance(item, textInfos.FieldCommand) and item.command == "controlStart"
					and item.field.get("mathMl")):
				# We have MathML, so don't report alt text (if any) as content.
				item.field.pop("content", None)
		return items

	def getFormatFieldSpeech(
			self,
			attrs: textInfos.Field,
			attrsCache: Optional[textInfos.Field] = None,
			formatConfig: Optional[Dict[str, bool]] = None,
			reason: Optional[OutputReason] = None,
			unit: Optional[str] = None,
			extraDetail: bool = False,
			initialFormat: bool = False
	) -> SpeechSequence:
		out: SpeechSequence = []
		comment = attrs.get("kindle-user-note")
		if comment:
			# For now, we report this the same way we do comments.
			attrs["comment"] = comment
		highlight = attrs.get("kindle-highlight")
		oldHighlight = attrsCache.get("kindle-highlight") if attrsCache is not None else None
		if oldHighlight != highlight:
			translation = (
				# Translators: Reported when text is highlighted.
				_("highlight") if highlight else
				# Translators: Reported when text is not highlighted.
				_("no highlight")
			)
			out.append(translation)
		popular = attrs.get("kindle-popular-highlight-count")
		oldPopular = attrsCache.get("kindle-popular-highlight-count") if attrsCache is not None else None
		if oldPopular != popular:
			translation = (
				# Translators: Reported in Kindle when text has been identified as a popular highlight;
				# i.e. it has been highlighted by several people.
				# %s is replaced with the number of people who have highlighted this text.
				_("%s highlighted") % popular if popular else
				# Translators: Reported when moving out of a popular highlight.
				_("out of popular highlight")
			)
			out.append(translation)

		superSpeech = super(BookPageViewTextInfo, self).getFormatFieldSpeech(
			attrs,
			attrsCache=attrsCache,
			formatConfig=formatConfig,
			reason=reason,
			unit=unit,
			extraDetail=extraDetail,
			initialFormat=initialFormat
		)
		out.extend(superSpeech)
		textInfos._logBadSequenceTypes(out)
		return out

	def updateSelection(self):
		# hack: For now, we need to do all selection on the root.
		# This means only entire embedded objects can be selected.
		if self._startObj == self.obj:
			sel = self._start.copy()
		else:
			log.debug("Start object isn't root, getting embedding")
			sel = self._getEmbedding(self._startObj)
			assert sel.obj == self.obj
		if self._endObj == self.obj:
			end = self._end
		else:
			log.debug("End object isn't root, getting embedding")
			end = self._getEmbedding(self._endObj)
			assert end.obj == self.obj
		sel.setEndPoint(end, "endToEnd")
		log.debug("Setting selection to (%d, %d)" % (sel._startOffset, sel._endOffset))
		sel.updateSelection()

	def _getControlFieldForObject(self, obj, ignoreEditableText=True):
		field = super(BookPageViewTextInfo, self)._getControlFieldForObject(obj, ignoreEditableText=ignoreEditableText)
		if field and field["role"] == controlTypes.Role.MATH:
			try:
				field["mathMl"] = obj.mathMl
			except LookupError:
				pass
		return field

	def getMathMl(self, field):
		mathMl = field.get("mathMl")
		if not mathMl:
			raise LookupError("No mathml attribute")
		return mathMl

class BookPageView(DocumentWithPageTurns,IAccessible):
	"""Allows navigating page text content with the arrow keys."""

	treeInterceptorClass=BookPageViewTreeInterceptor
	TextInfo=BookPageViewTextInfo

	def _get_pageNumber(self):
		try:
			first=self.IA2Attributes['kindle-first-visible-physical-page-label']
			last=self.IA2Attributes['kindle-last-visible-physical-page-label']
		except KeyError:
			try:
				first=self.IA2Attributes['kindle-first-visible-physical-page-number']
				last=self.IA2Attributes['kindle-last-visible-physical-page-number']
			except KeyError:
				return None
		if first!=last:
			return "%s to %s"%(first,last)
		else:
			return first

	def turnPage(self,previous=False):
		if self.IAccessibleActionObject.doAction(1 if previous else 0) != S_OK:
			raise RuntimeError("no more pages")
		self.invalidateCache()

class PageTurnFocusIgnorer(IAccessible):

	def _get_shouldAllowIAccessibleFocusEvent(self):
		# hack: When turning pages to a new section, Kindle  fires focus on the new section in the table of contents treeview.
		# We must ignore this focus event as it is a hinderance to a screen reader user while reading the book.
		focus = api.getFocusObject()
		if isinstance(focus, BookPageView) and focus.hasFocus:
			# The book area reports that it still has the focus, so this event is bogus.
			return False
		return super(PageTurnFocusIgnorer,self).shouldAllowIAccessibleFocusEvent

class Math(IAccessible):

	def _get_mathMl(self):
		mathMl = self.IA2Attributes.get("mathml")
		if not mathMl:
			raise LookupError
		return mathMl

class AppModule(appModuleHandler.AppModule):

	def chooseNVDAObjectOverlayClasses(self,obj,clsList):
		if isinstance(obj,IAccessible):
			clsList.insert(0,PageTurnFocusIgnorer)
			if (
				(
					isinstance(obj.IAccessibleObject, IA2.IAccessible2)
					and obj.IA2Attributes.get("class") == "KindleBookPageView"
				)
				# We must rely on .name in Kindle <= 1.19.
				or (hasattr(obj,'IAccessibleTextObject') and obj.name=="Book Page View")
			):
				clsList.insert(0,BookPageView)
			elif obj.role == controlTypes.Role.MATH:
				clsList.insert(0, Math)
		return clsList

	def event_NVDAObject_init(self, obj):
		if (
			isinstance(obj, IAccessible)
			and isinstance(obj.IAccessibleObject, IA2.IAccessible2)
			and obj.role == controlTypes.Role.LINK
		):
			xRoles = obj.IA2Attributes.get("xml-roles", "").split(" ")
			if "kindle-footnoteref" in xRoles:
				obj.role = controlTypes.Role.FOOTNOTE


import torch
import torch.nn.functional as F
from torch.nn import ModuleList
from torch_geometric.nn import PNAConv, BatchNorm, global_mean_pool
from .Base import Base

class PNAStack(Base):
    def __init__(
        self,
        deg: list,
        edge_dim: int,
        *args,
        **kwargs,
    ):

        self.aggregators = ["mean", "min", "max", "std"]
        self.scalers = [
            "identity",
            "amplification",
            "attenuation",
            "linear",
        ]
        self.deg = torch.Tensor(deg)
        self.edge_dim = edge_dim

        super().__init__(*args, **kwargs)

    def get_conv(self, input_dim, output_dim):
        return PNAConv(
            in_channels=input_dim,
            out_channels=output_dim,
            aggregators=self.aggregators,
            scalers=self.scalers,
            deg=self.deg,
            edge_dim=self.edge_dim,
            pre_layers=1,
            post_layers=1,
            divide_input=False,
        )

    def __str__(self):
        return "PNAStack"


from django.shortcuts import render
from django.urls import reverse
from django.http import HttpResponseRedirect
from django.db.models import Q
from .models import Voter, Scope, Person
import random
from .forms import VoterForm
from django.core.mail import EmailMessage
from django.core.exceptions import ObjectDoesNotExist
from django.conf import settings
from django.views import View
from django.contrib import messages

class Home(View):

    def get(self, request, *args, **kwargs):
        persons = list(Person.objects.filter(voted=False).values_list("name", flat=True))
        return render(request, "home.html", {"persons":sorted(persons)})

    def post(self, request):
        form = VoterForm(request.POST)

        if form.is_valid():
            userIP = request.META.get('HTTP_X_FORWARDED_FOR')
            ip = userIP.split(',')[0] if userIP else request.META.get('REMOTE_ADDR')

            email = request.POST["email"]
            name = request.POST["name"]

            scope = list(Scope.objects.filter(Q(isChosen=False) & ~Q(name__icontains=name)).values_list("name", flat=True).order_by("?"))

            #handle case if scope is empty
            try:
                chosen = random.choice(scope)
            except IndexError:
                alreadyVoted = "Brak osób do wylosowania!"
                persons = list(Person.objects.filter(voted=False).values_list("name", flat=True))
                return render(request, "home.html", {"persons":sorted(persons), "alreadyVoted":alreadyVoted})

            chosen_obj = Scope.objects.get(name=chosen)

            #get Person object and handle exception if doesnt exists
            try:
                person_obj = Person.objects.get(name=name, voted=False)
            except ObjectDoesNotExist:
                alreadyVoted = "Ta osoba już wylosowała!"
                persons = list(Person.objects.filter(voted=False).values_list("name", flat=True))
                return render(request, "home.html", {"persons":sorted(persons), "alreadyVoted":alreadyVoted})

            #create and save voter with proper data
            voter_obj = Voter(name=name,email=email, ip_address=ip, chosen_person=chosen_obj)
            voter_obj.save()

            #mark scope person as chosen
            chosen_obj.isChosen = True
            chosen_obj.save()

            # mark voting person as voted
            person_obj.voted = True
            person_obj.save()

            #send email confirmation
            email = EmailMessage(
                f"Świąteczne losowanie",
                f"Cześć {name},
 Wylosowana przez Ciebie osoba to: {chosen}.
"
                "Wesołych Świąt!",
                settings.DEFAULT_FROM_EMAIL,
                [email]
            )
            email.content_subtype="html"
            email.send()

            #request.session["chosen"] = chosen
            messages.info(request, chosen)
            return HttpResponseRedirect(request.path)


from flask import Flask, request, jsonify
import psycopg2
import config

app = Flask(__name__)

connection = psycopg2.connect(config.DATABASE_URL)
cursor = connection.cursor()

@app.route('/discordid')
async def get_smmoid():
    try:
        discid = int(request.args.get('id'))
        stmt = "select smmoid from plebs where discid=%s and verified=true"
        cursor.execute(stmt, (str(discid),))
        data = cursor.fetchone()
        print(data)
        # data = await db.get_smmoid(discid)
        return jsonify({'smmoid': data[0] if data is not None else -1})
    except ValueError:
        return jsonify({'Error': "The 'id' field must be an integer"})

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=8375, debug=True)