import json
import os
import mimetypes
import flask
import ruamel.yaml as yaml
import sqlalchemy.dialects.postgresql
from matplotlib.image import thumbnail

import config
import markdown

from datetime import datetime
from os import path
from flask_sqlalchemy import SQLAlchemy
from flask_bcrypt import Bcrypt
from flask_migrate import Migrate, current
from urllib.parse import urlencode
from PIL import Image, ImageOps

app = flask.Flask(__name__)
bcrypt = Bcrypt(app)

app.config["SQLALCHEMY_DATABASE_URI"] = config.DB_URI
app.config["SECRET_KEY"] = config.DB_PASSWORD
app.config["NAX_CONTENT_LENGTH"] = config.MAX_CONTENT_LENGTH

db = SQLAlchemy(app)
migrate = Migrate(app, db)


def make_thumbnail(pil_image):
    pil_image = ImageOps.exif_transpose(pil_image)
    pil_image.thumbnail(config.THUMBNAIL_SIZE)
    pil_image = pil_image.convert("RGB")
    return pil_image


@app.template_filter("split")
def split(value, separator=None, maxsplit=-1):
    return value.split(separator, maxsplit)


@app.template_filter("median")
def median(value):
    value = list(value)  # prevent generators
    return sorted(value)[len(value) // 2]


@app.template_filter("set")
def set_filter(value):
    return set(value)


@app.template_global()
def modify_query(**new_values):
    args = flask.request.args.copy()
    for key, value in new_values.items():
        args[key] = value

    return f"{flask.request.path}?{urlencode(args)}"


@app.context_processor
def default_variables():
    return {
        "current_user": db.session.get(User, flask.session.get("username")),
        "site_name": config.SITE_NAME,
        "config": config,
    }


with app.app_context():
    class User(db.Model):
        username = db.Column(db.String(32), unique=True, nullable=False, primary_key=True)
        password_hashed = db.Column(db.String(60), nullable=False)
        admin = db.Column(db.Boolean, nullable=False, default=False, server_default="false")
        pictures = db.relationship("PictureResource", back_populates="author")
        joined_timestamp = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
        galleries = db.relationship("Gallery", back_populates="owner")
        galleries_joined = db.relationship("UserInGallery", back_populates="user")
        ratings = db.relationship("PictureRating", back_populates="user")

        def __init__(self, username, password):
            self.username = username
            self.password_hashed = bcrypt.generate_password_hash(password).decode("utf-8")

        @property
        def formatted_name(self):
            if self.admin:
                return self.username + "*"
            return self.username


    class Licence(db.Model):
        id = db.Column(db.String(64), primary_key=True)  # SPDX identifier
        title = db.Column(db.UnicodeText, nullable=False)  # the official name of the licence
        description = db.Column(db.UnicodeText,
                                nullable=False)  # brief description of its permissions and restrictions
        info_url = db.Column(db.String(1024),
                             nullable=False)  # the URL to a page with general information about the licence
        url = db.Column(db.String(1024),
                        nullable=True)  # the URL to a page with the full text of the licence and more information
        pictures = db.relationship("PictureLicence", back_populates="licence")
        free = db.Column(db.Boolean, nullable=False,
                         default=False)  # whether the licence is free or not
        logo_url = db.Column(db.String(1024), nullable=True)  # URL to the logo of the licence
        pinned = db.Column(db.Boolean, nullable=False,
                           default=False)  # whether the licence should be shown at the top of the list

        def __init__(self, id, title, description, info_url, url, free, logo_url=None,
                     pinned=False):
            self.id = id
            self.title = title
            self.description = description
            self.info_url = info_url
            self.url = url
            self.free = free
            self.logo_url = logo_url
            self.pinned = pinned


    class PictureLicence(db.Model):
        id = db.Column(db.Integer, primary_key=True, autoincrement=True)

        resource_id = db.Column(db.Integer, db.ForeignKey("picture_resource.id"))
        licence_id = db.Column(db.String(64), db.ForeignKey("licence.id"))

        resource = db.relationship("PictureResource", back_populates="licences")
        licence = db.relationship("Licence", back_populates="pictures")

        def __init__(self, resource, licence):
            self.resource = resource
            self.licence = licence


    class Resource(db.Model):
        __abstract__ = True

        id = db.Column(db.Integer, primary_key=True, autoincrement=True)
        title = db.Column(db.UnicodeText, nullable=False)
        description = db.Column(db.UnicodeText, nullable=False)
        timestamp = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
        origin_url = db.Column(db.String(2048),
                               nullable=True)  # should be left empty if it's original or the source is unknown but public domain


    class PictureNature(db.Model):
        # Examples:
        # "photo", "paper-scan", "2d-art-photo", "sculpture-photo", "computer-3d", "computer-painting",
        # "computer-line-art", "diagram", "infographic", "text", "map", "chart-graph", "screen-capture",
        # "screen-photo", "pattern", "collage", "ai", and so on
        id = db.Column(db.String(64), primary_key=True)
        description = db.Column(db.UnicodeText, nullable=False)
        resources = db.relationship("PictureResource", back_populates="nature")

        def __init__(self, id, description):
            self.id = id
            self.description = description


    class PictureObjectInheritance(db.Model):
        parent_id = db.Column(db.String(64), db.ForeignKey("picture_object.id"),
                              primary_key=True)
        child_id = db.Column(db.String(64), db.ForeignKey("picture_object.id"),
                             primary_key=True)

        parent = db.relationship("PictureObject", foreign_keys=[parent_id],
                                 back_populates="child_links")
        child = db.relationship("PictureObject", foreign_keys=[child_id],
                                back_populates="parent_links")

        def __init__(self, parent, child):
            self.parent = parent
            self.child = child


    class PictureObject(db.Model):
        id = db.Column(db.String(64), primary_key=True)
        description = db.Column(db.UnicodeText, nullable=False)

        child_links = db.relationship("PictureObjectInheritance",
                                      foreign_keys=[PictureObjectInheritance.parent_id],
                                      back_populates="parent")
        parent_links = db.relationship("PictureObjectInheritance",
                                       foreign_keys=[PictureObjectInheritance.child_id],
                                       back_populates="child")

        def __init__(self, id, description, parents):
            self.id = id
            self.description = description
            if parents:
                for parent in parents:
                    db.session.add(PictureObjectInheritance(parent, self))


    class PictureRegion(db.Model):
        # This is for picture region annotations
        id = db.Column(db.Integer, primary_key=True, autoincrement=True)
        json = db.Column(sqlalchemy.dialects.postgresql.JSONB, nullable=False)

        resource_id = db.Column(db.Integer, db.ForeignKey("picture_resource.id"),
                                nullable=False)
        object_id = db.Column(db.String(64), db.ForeignKey("picture_object.id"), nullable=True)

        resource = db.relationship("PictureResource", backref="regions")
        object = db.relationship("PictureObject", backref="regions")

        def __init__(self, json, resource, object):
            self.json = json
            self.resource = resource
            self.object = object


    class PictureResource(Resource):
        # This is only for bitmap pictures. Vectors will be stored under a different model
        # File name is the ID in the picture directory under data, without an extension
        file_format = db.Column(db.String(64), nullable=False)  # MIME type
        width = db.Column(db.Integer, nullable=False)
        height = db.Column(db.Integer, nullable=False)
        nature_id = db.Column(db.String(32), db.ForeignKey("picture_nature.id"), nullable=True)
        author_name = db.Column(db.String(32), db.ForeignKey("user.username"), nullable=False)
        author = db.relationship("User", back_populates="pictures")

        nature = db.relationship("PictureNature", back_populates="resources")

        replaces_id = db.Column(db.Integer, db.ForeignKey("picture_resource.id"), nullable=True)
        replaced_by_id = db.Column(db.Integer, db.ForeignKey("picture_resource.id"),
                                   nullable=True)

        replaces = db.relationship("PictureResource", remote_side="PictureResource.id",
                                   foreign_keys=[replaces_id], back_populates="replaced_by",
                                   post_update=True)
        replaced_by = db.relationship("PictureResource", remote_side="PictureResource.id",
                                      foreign_keys=[replaced_by_id], post_update=True)

        copied_from_id = db.Column(db.Integer, db.ForeignKey("picture_resource.id"),
                                   nullable=True)
        copied_from = db.relationship("PictureResource", remote_side="PictureResource.id",
                                      backref="copies", foreign_keys=[copied_from_id])

        licences = db.relationship("PictureLicence", back_populates="resource")
        galleries = db.relationship("PictureInGallery", back_populates="resource")
        ratings = db.relationship("PictureRating", back_populates="resource")

        def __init__(self, title, author, description, origin_url, licence_ids, mime,
                     nature=None):
            self.title = title
            self.author = author
            self.description = description
            self.origin_url = origin_url
            self.file_format = mime
            self.width = self.height = 0
            self.nature = nature
            db.session.add(self)
            db.session.commit()
            for licence_id in licence_ids:
                joiner = PictureLicence(self, db.session.get(Licence, licence_id))
                db.session.add(joiner)

        def put_annotations(self, json):
            # Delete all previous annotations
            db.session.query(PictureRegion).filter_by(resource_id=self.id).delete()

            for region in json:
                object_id = region["object"]
                picture_object = db.session.get(PictureObject, object_id)

                region_data = {
                    "type": region["type"],
                    "shape": region["shape"],
                }

                region_row = PictureRegion(region_data, self, picture_object)
                db.session.add(region_row)

        @property
        def average_rating(self):
            if not self.ratings:
                return None
            return db.session.query(db.func.avg(PictureRating.rating)).filter_by(resource=self).scalar()

        @property
        def rating_totals(self):
            all_ratings = db.session.query(PictureRating.rating).filter_by(resource=self)
            return {rating: all_ratings.filter_by(rating=rating).count() for rating in range(1, 6)}

        @property
        def stars(self):
            if not self.ratings:
                return 0
            average = self.average_rating
            whole_stars = int(average)
            partial_star = average - whole_stars

            return [100] * whole_stars + [int(partial_star * 100)] + [0] * (4 - whole_stars)


    class PictureInGallery(db.Model):
        id = db.Column(db.Integer, primary_key=True, autoincrement=True)
        resource_id = db.Column(db.Integer, db.ForeignKey("picture_resource.id"),
                                nullable=False)
        gallery_id = db.Column(db.Integer, db.ForeignKey("gallery.id"), nullable=False)

        resource = db.relationship("PictureResource")
        gallery = db.relationship("Gallery")

        def __init__(self, resource, gallery):
            self.resource = resource
            self.gallery = gallery


    class UserInGallery(db.Model):
        id = db.Column(db.Integer, primary_key=True, autoincrement=True)
        username = db.Column(db.String(32), db.ForeignKey("user.username"), nullable=False)
        gallery_id = db.Column(db.Integer, db.ForeignKey("gallery.id"), nullable=False)

        user = db.relationship("User")
        gallery = db.relationship("Gallery")

        def __init__(self, user, gallery):
            self.user = user
            self.gallery = gallery


    class Gallery(db.Model):
        id = db.Column(db.Integer, primary_key=True, autoincrement=True)
        title = db.Column(db.UnicodeText, nullable=False)
        description = db.Column(db.UnicodeText, nullable=False)
        pictures = db.relationship("PictureInGallery", back_populates="gallery")
        owner_name = db.Column(db.String(32), db.ForeignKey("user.username"), nullable=False)
        owner = db.relationship("User", back_populates="galleries")
        users = db.relationship("UserInGallery", back_populates="gallery")

        def __init__(self, title, description, owner):
            self.title = title
            self.description = description
            self.owner = owner


    class PictureRating(db.Model):
        id = db.Column(db.Integer, primary_key=True, autoincrement=True)
        resource_id = db.Column(db.Integer, db.ForeignKey("picture_resource.id"), nullable=False)
        username = db.Column(db.String(32), db.ForeignKey("user.username"), nullable=False)
        rating = db.Column(db.Integer, db.CheckConstraint("rating >= 1 AND rating <= 5"),
                           nullable=False)

        resource = db.relationship("PictureResource", back_populates="ratings")
        user = db.relationship("User", back_populates="ratings")

        def __init__(self, resource, user, rating):
            self.resource = resource
            self.user = user
            self.rating = rating


@app.route("/")
def index():
    return flask.render_template(
            "home.html",
            random_resources=PictureResource.query.filter_by(replaced_by=None).order_by(db.func.random()).limit(10).all(),
            recent_resources=PictureResource.query.filter_by(replaced_by=None).order_by(PictureResource.timestamp.desc()).limit(10).all(),
            recent_unannotated_resources=PictureResource.query.filter_by(replaced_by=None).filter(~PictureResource.regions.any()).order_by(PictureResource.timestamp.desc()).limit(10).all(),
    )


@app.route("/info/")
def usage_guide():
    with open("help/usage.md") as f:
        return flask.render_template("help.html", content=markdown.markdown2html(f.read()))


@app.route("/accounts/")
def accounts():
    return flask.render_template("login.html")


@app.route("/login", methods=["POST"])
def login():
    username = flask.request.form["username"]
    password = flask.request.form["password"]

    user = db.session.get(User, username)

    if user is None:
        flask.flash("This username is not registered.")
        return flask.redirect("/accounts")

    if not bcrypt.check_password_hash(user.password_hashed, password):
        flask.flash("Incorrect password.")
        return flask.redirect("/accounts")

    flask.flash("You have been logged in.")

    flask.session["username"] = username
    return flask.redirect("/")


@app.route("/logout")
def logout():
    flask.session.pop("username", None)
    flask.flash("You have been logged out.")
    return flask.redirect("/")


@app.route("/signup", methods=["POST"])
def signup():
    username = flask.request.form["username"]
    password = flask.request.form["password"]

    if db.session.get(User, username) is not None:
        flask.flash("This username is already taken.")
        return flask.redirect("/accounts")

    if set(username) > set("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_"):
        flask.flash(
            "Usernames can only contain the Latin alphabet, digits, hyphens, and underscores.")
        return flask.redirect("/accounts")

    if len(username) < 3 or len(username) > 32:
        flask.flash("Usernames must be between 3 and 32 characters long.")
        return flask.redirect("/accounts")

    if len(password) < 6:
        flask.flash("Passwords must be at least 6 characters long.")
        return flask.redirect("/accounts")

    user = User(username, password)
    db.session.add(user)
    db.session.commit()

    flask.session["username"] = username

    flask.flash("You have been registered and logged in.")

    return flask.redirect("/")


@app.route("/profile", defaults={"username": None})
@app.route("/profile/<username>")
def profile(username):
    if username is None:
        if "username" in flask.session:
            return flask.redirect("/profile/" + flask.session["username"])
        else:
            flask.flash("Please log in to perform this action.")
            return flask.redirect("/accounts")

    user = db.session.get(User, username)
    if user is None:
        flask.abort(404)

    return flask.render_template("profile.html", user=user)


@app.route("/object/<id>")
def has_object(id):
    object_ = db.session.get(PictureObject, id)
    if object_ is None:
        flask.abort(404)

    descendants_cte = (
        db.select(PictureObject.id)
        .where(PictureObject.id == id)
        .cte(name="descendants_cte", recursive=True)
    )

    descendants_cte = descendants_cte.union_all(
        db.select(PictureObjectInheritance.child_id)
        .where(PictureObjectInheritance.parent_id == descendants_cte.c.id)
    )

    query = db.session.query(PictureResource).filter(
        PictureResource.regions.any(
            PictureRegion.object_id.in_(
                db.select(descendants_cte.c.id)
            )
        )
    )

    page = int(flask.request.args.get("page", 1))
    per_page = int(flask.request.args.get("per_page", 16))

    resources = query.paginate(page=page, per_page=per_page)

    return flask.render_template("object.html", object=object_, resources=resources,
                                 page_number=page,
                                 page_length=per_page, num_pages=resources.pages,
                                 prev_page=resources.prev_num,
                                 next_page=resources.next_num, PictureRegion=PictureRegion)


@app.route("/upload")
def upload():
    if "username" not in flask.session:
        flask.flash("Log in to upload pictures.")
        return flask.redirect("/accounts")

    licences = Licence.query.order_by(Licence.free.desc(), Licence.pinned.desc(),
                                      Licence.title).all()

    types = PictureNature.query.all()

    return flask.render_template("upload.html", licences=licences, types=types)


@app.route("/upload", methods=["POST"])
def upload_post():
    title = flask.request.form["title"]
    description = flask.request.form["description"]
    origin_url = flask.request.form["origin_url"]
    author = db.session.get(User, flask.session.get("username"))
    licence_ids = flask.request.form.getlist("licence")
    nature_id = flask.request.form["nature"]

    if author is None:
        flask.abort(401)

    file = flask.request.files["file"]

    if not file or not file.filename:
        flask.flash("Select a file")
        return flask.redirect(flask.request.url)

    if not file.mimetype.startswith("image/") or file.mimetype == "image/svg+xml":
        flask.flash("Only images are supported")
        return flask.redirect(flask.request.url)

    if not title:
        flask.flash("Enter a title")
        return flask.redirect(flask.request.url)

    if not description:
        description = ""

    if not nature_id:
        flask.flash("Select a picture type")
        return flask.redirect(flask.request.url)

    if not licence_ids:
        flask.flash("Select licences")
        return flask.redirect(flask.request.url)

    licences = [db.session.get(Licence, licence_id) for licence_id in licence_ids]
    if not any(licence.free for licence in licences):
        flask.flash("Select at least one free licence")
        return flask.redirect(flask.request.url)

    resource = PictureResource(title, author, description, origin_url, licence_ids,
                               file.mimetype,
                               db.session.get(PictureNature, nature_id))
    db.session.add(resource)
    db.session.commit()
    file.save(path.join(config.DATA_PATH, "pictures", str(resource.id)))
    pil_image = Image.open(path.join(config.DATA_PATH, "pictures", str(resource.id)))
    resource.width, resource.height = pil_image.size
    pil_image = make_thumbnail(pil_image)
    pil_image.save(path.join(config.DATA_PATH, "picture-thumbnails", str(resource.id)), **config.THUMBNAIL_SAVE_OPTIONS)
    db.session.commit()

    if flask.request.form.get("annotations"):
        try:
            resource.put_annotations(json.loads(flask.request.form.get("annotations")))
            db.session.commit()
        except json.JSONDecodeError:
            flask.flash("Invalid annotations")

    flask.flash("Picture uploaded successfully")

    return flask.redirect("/picture/" + str(resource.id))


@app.route("/picture/<int:id>/")
def picture(id):
    resource = db.session.get(PictureResource, id)
    if resource is None:
        flask.abort(404)

    image = Image.open(path.join(config.DATA_PATH, "pictures", str(resource.id)))

    current_user = db.session.get(User, flask.session.get("username"))
    have_permission = current_user and (current_user == resource.author or current_user.admin)

    own_rating = None
    if current_user:
        own_rating = PictureRating.query.filter_by(resource=resource, user=current_user).first()

    return flask.render_template("picture.html", resource=resource,
                                 file_extension=mimetypes.guess_extension(resource.file_format),
                                 size=image.size, copies=resource.copies,
                                 have_permission=have_permission, own_rating=own_rating)


@app.route("/picture/<int:id>/annotate")
def annotate_picture(id):
    resource = db.session.get(PictureResource, id)
    if resource is None:
        flask.abort(404)

    current_user = db.session.get(User, flask.session.get("username"))
    if current_user is None:
        flask.abort(401)

    if resource.author != current_user and not current_user.admin:
        flask.abort(403)

    return flask.render_template("picture-annotation.html", resource=resource,
                                 file_extension=mimetypes.guess_extension(resource.file_format))


@app.route("/picture/<int:id>/put-annotations-form")
def put_annotations_form(id):
    resource = db.session.get(PictureResource, id)
    if resource is None:
        flask.abort(404)

    current_user = db.session.get(User, flask.session.get("username"))
    if current_user is None:
        flask.abort(401)

    if resource.author != current_user and not current_user.admin:
        flask.abort(403)

    return flask.render_template("put-annotations-form.html", resource=resource)


@app.route("/picture/<int:id>/put-annotations-form", methods=["POST"])
def put_annotations_form_post(id):
    resource = db.session.get(PictureResource, id)
    if resource is None:
        flask.abort(404)

    current_user = db.session.get(User, flask.session.get("username"))
    if current_user is None:
        flask.abort(401)

    if resource.author != current_user and not current_user.admin:
        flask.abort(403)

    resource.put_annotations(json.loads(flask.request.form["annotations"]))

    db.session.commit()

    return flask.redirect("/picture/" + str(resource.id))


@app.route("/picture/<int:id>/save-annotations", methods=["POST"])
@app.route("/api/picture/<int:id>/put-annotations", methods=["POST"])
def save_annotations(id):
    resource = db.session.get(PictureResource, id)
    if resource is None:
        flask.abort(404)

    current_user = db.session.get(User, flask.session.get("username"))
    if resource.author != current_user and not current_user.admin:
        flask.abort(403)

    resource.put_annotations(flask.request.json)

    db.session.commit()

    response = flask.make_response()
    response.status_code = 204
    return response


@app.route("/picture/<int:id>/get-annotations")
@app.route("/api/picture/<int:id>/api/get-annotations")
def get_annotations(id):
    resource = db.session.get(PictureResource, id)
    if resource is None:
        flask.abort(404)

    regions = db.session.query(PictureRegion).filter_by(resource_id=id).all()

    regions_json = []

    for region in regions:
        regions_json.append({
            "object": region.object_id,
            "type": region.json["type"],
            "shape": region.json["shape"],
        })

    return flask.jsonify(regions_json)


@app.route("/picture/<int:id>/delete")
def delete_picture(id):
    resource = db.session.get(PictureResource, id)
    if resource is None:
        flask.abort(404)

    current_user = db.session.get(User, flask.session.get("username"))
    if current_user is None:
        flask.abort(401)

    if resource.author != current_user and not current_user.admin:
        flask.abort(403)

    PictureLicence.query.filter_by(resource=resource).delete()
    PictureRegion.query.filter_by(resource=resource).delete()
    PictureInGallery.query.filter_by(resource=resource).delete()
    PictureRating.query.filter_by(resource=resource).delete()
    if resource.replaces:
        resource.replaces.replaced_by = None
    if resource.replaced_by:
        resource.replaced_by.replaces = None
    resource.copied_from = None
    for copy in resource.copies:
        copy.copied_from = None
    db.session.delete(resource)
    db.session.commit()

    # Delete the hard links for the picture and its thumbnail
    os.unlink(path.join(config.DATA_PATH, "pictures", str(resource.id)))
    os.unlink(path.join(config.DATA_PATH, "picture-thumbnails", str(resource.id)))

    return flask.redirect("/")


@app.route("/picture/<int:id>/mark-replacement", methods=["POST"])
def mark_picture_replacement(id):
    resource = db.session.get(PictureResource, id)
    if resource is None:
        flask.abort(404)

    current_user = db.session.get(User, flask.session.get("username"))
    if current_user is None:
        flask.abort(401)

    if resource.copied_from.author != current_user and not current_user.admin:
        flask.abort(403)

    resource.copied_from.replaced_by = resource
    resource.replaces = resource.copied_from

    db.session.commit()

    return flask.redirect("/picture/" + str(resource.copied_from.id))


@app.route("/picture/<int:id>/remove-replacement", methods=["POST"])
def remove_picture_replacement(id):
    resource = db.session.get(PictureResource, id)
    if resource is None:
        flask.abort(404)

    current_user = db.session.get(User, flask.session.get("username"))
    if current_user is None:
        flask.abort(401)

    if resource.author != current_user and not current_user.admin:
        flask.abort(403)

    resource.replaced_by.replaces = None
    resource.replaced_by = None

    db.session.commit()

    return flask.redirect("/picture/" + str(resource.id))


@app.route("/picture/<int:id>/edit-metadata")
def edit_picture(id):
    resource = db.session.get(PictureResource, id)
    if resource is None:
        flask.abort(404)

    current_user = db.session.get(User, flask.session.get("username"))
    if current_user is None:
        flask.abort(401)

    if resource.author != current_user and not current_user.admin:
        flask.abort(403)

    licences = Licence.query.order_by(Licence.free.desc(), Licence.pinned.desc(),
                                      Licence.title).all()

    types = PictureNature.query.all()

    return flask.render_template("edit-picture.html", resource=resource, licences=licences,
                                 types=types,
                                 PictureLicence=PictureLicence)


@app.route("/picture/<int:id>/rate", methods=["POST"])
def rate_picture(id):
    resource = db.session.get(PictureResource, id)
    if resource is None:
        flask.abort(404)

    current_user = db.session.get(User, flask.session.get("username"))
    if current_user is None:
        flask.abort(401)

    rating = int(flask.request.form.get("rating"))

    if not rating:
        # Delete the existing rating
        if PictureRating.query.filter_by(resource=resource, user=current_user).first():
            db.session.delete(PictureRating.query.filter_by(resource=resource,
                                                             user=current_user).first())
            db.session.commit()

        return flask.redirect("/picture/" + str(resource.id))

    if not 1 <= rating <= 5:
        flask.flash("Invalid rating")
        return flask.redirect("/picture/" + str(resource.id))

    if PictureRating.query.filter_by(resource=resource, user=current_user).first():
        PictureRating.query.filter_by(resource=resource, user=current_user).first().rating = rating
    else:
        # Create a new rating
        db.session.add(PictureRating(resource, current_user, rating))

    db.session.commit()

    return flask.redirect("/picture/" + str(resource.id))


@app.route("/picture/<int:id>/edit-metadata", methods=["POST"])
def edit_picture_post(id):
    resource = db.session.get(PictureResource, id)
    if resource is None:
        flask.abort(404)

    current_user = db.session.get(User, flask.session.get("username"))
    if current_user is None:
        flask.abort(401)

    if resource.author != current_user and not current_user.admin:
        flask.abort(403)

    title = flask.request.form["title"]
    description = flask.request.form["description"]
    origin_url = flask.request.form["origin_url"]
    licence_ids = flask.request.form.getlist("licence")
    nature_id = flask.request.form["nature"]

    if not title:
        flask.flash("Enter a title")
        return flask.redirect(flask.request.url)

    if not description:
        description = ""

    if not nature_id:
        flask.flash("Select a picture type")
        return flask.redirect(flask.request.url)

    if not licence_ids:
        flask.flash("Select licences")
        return flask.redirect(flask.request.url)

    licences = [db.session.get(Licence, licence_id) for licence_id in licence_ids]
    if not any(licence.free for licence in licences):
        flask.flash("Select at least one free licence")
        return flask.redirect(flask.request.url)

    resource.title = title
    resource.description = description
    resource.origin_url = origin_url
    for licence_id in licence_ids:
        joiner = PictureLicence(resource, db.session.get(Licence, licence_id))
        db.session.add(joiner)
    resource.nature = db.session.get(PictureNature, nature_id)

    db.session.commit()

    return flask.redirect("/picture/" + str(resource.id))


@app.route("/picture/<int:id>/copy")
def copy_picture(id):
    resource = db.session.get(PictureResource, id)
    if resource is None:
        flask.abort(404)

    current_user = db.session.get(User, flask.session.get("username"))
    if current_user is None:
        flask.abort(401)

    new_resource = PictureResource(resource.title, current_user, resource.description,
                                   resource.origin_url,
                                   [licence.licence_id for licence in resource.licences],
                                   resource.file_format,
                                   resource.nature)

    for region in resource.regions:
        db.session.add(PictureRegion(region.json, new_resource, region.object))

    db.session.commit()

    # Create a hard link for the new picture
    old_path = path.join(config.DATA_PATH, "pictures", str(resource.id))
    new_path = path.join(config.DATA_PATH, "pictures", str(new_resource.id))
    os.link(old_path, new_path)

    new_resource.width = resource.width
    new_resource.height = resource.height
    new_resource.copied_from = resource

    db.session.commit()

    return flask.redirect("/picture/" + str(new_resource.id))


@app.route("/picture/<int:id>/put-file")
def put_file(id):
    resource = db.session.get(PictureResource, id)
    if resource is None:
        flask.abort(404)

    current_user = db.session.get(User, flask.session.get("username"))
    if current_user is None:
        flask.abort(401)

    if resource.author != current_user and not current_user.admin:
        flask.abort(403)

    return flask.render_template("put-file.html", resource=resource)


@app.route("/picture/<int:id>/put-file", methods=["POST"])
def put_file_post(id):
    resource = db.session.get(PictureResource, id)
    if resource is None:
        flask.abort(404)

    current_user = db.session.get(User, flask.session.get("username"))
    if current_user is None:
        flask.abort(401)

    if resource.author != current_user and not current_user.admin:
        flask.abort(403)

    file = flask.request.files["file"]

    if not file or not file.filename:
        flask.flash("Select a file")
        return flask.redirect(flask.request.url)

    if not file.mimetype.startswith("image/") or file.mimetype == "image/svg+xml":
        flask.flash("Only images are supported")
        return flask.redirect(flask.request.url)

    file_path = path.join(config.DATA_PATH, "pictures", str(resource.id))
    # Since it's a hard link, we need to remove it, so it won't affect copies
    os.unlink(file_path)
    file.save(file_path)
    pil_image = Image.open(file_path)
    resource.width, resource.height = pil_image.size
    pil_image = make_thumbnail(pil_image)
    pil_image.save(path.join(config.DATA_PATH, "picture-thumbnails", str(resource.id)), **config.THUMBNAIL_SAVE_OPTIONS)

    return flask.redirect("/picture/" + str(resource.id))


@app.route("/gallery/<int:id>/")
def gallery(id):
    gallery = db.session.get(Gallery, id)
    if gallery is None:
        flask.abort(404)

    current_user = db.session.get(User, flask.session.get("username"))

    have_permission = current_user and (current_user == gallery.owner or current_user.admin or UserInGallery.query.filter_by(user=current_user, gallery=gallery).first())
    have_extended_permission = current_user and (current_user == gallery.owner or current_user.admin)

    return flask.render_template("gallery.html", gallery=gallery,
                                 have_permission=have_permission,
                                 have_extended_permission=have_extended_permission)


@app.route("/create-gallery")
def create_gallery():
    if "username" not in flask.session:
        flask.flash("Log in to create galleries.")
        return flask.redirect("/accounts")

    return flask.render_template("create-gallery.html")


@app.route("/create-gallery", methods=["POST"])
def create_gallery_post():
    if not flask.session.get("username"):
        flask.abort(401)

    if not flask.request.form.get("title"):
        flask.flash("Enter a title")
        return flask.redirect(flask.request.url)

    description = flask.request.form.get("description", "")

    gallery = Gallery(flask.request.form["title"], description,
                      db.session.get(User, flask.session["username"]))
    db.session.add(gallery)
    db.session.commit()

    return flask.redirect("/gallery/" + str(gallery.id))


@app.route("/gallery/<int:id>/add-picture", methods=["POST"])
def gallery_add_picture(id):
    gallery = db.session.get(Gallery, id)
    if gallery is None:
        flask.abort(404)

    if "username" not in flask.session:
        flask.abort(401)

    if flask.session["username"] != gallery.owner_name and not current_user.admin and not UserInGallery.query.filter_by(user=current_user, gallery=gallery).first():
        flask.abort(403)

    picture_id = flask.request.form.get("picture_id")
    if "/" in picture_id:  # also allow full URLs
        picture_id = picture_id.rstrip("/").rpartition("/")[1]
    if not picture_id:
        flask.flash("Select a picture")
        return flask.redirect("/gallery/" + str(gallery.id))
    picture_id = int(picture_id)

    picture = db.session.get(PictureResource, picture_id)
    if picture is None:
        flask.flash("Invalid picture")
        return flask.redirect("/gallery/" + str(gallery.id))

    if PictureInGallery.query.filter_by(resource=picture, gallery=gallery).first():
        flask.flash("This picture is already in the gallery")
        return flask.redirect("/gallery/" + str(gallery.id))

    db.session.add(PictureInGallery(picture, gallery))

    db.session.commit()

    return flask.redirect("/gallery/" + str(gallery.id))


@app.route("/gallery/<int:id>/remove-picture", methods=["POST"])
def gallery_remove_picture(id):
    gallery = db.session.get(Gallery, id)
    if gallery is None:
        flask.abort(404)

    if "username" not in flask.session:
        flask.abort(401)

    current_user = db.session.get(User, flask.session.get("username"))

    if flask.session["username"] != gallery.owner_name and not current_user.admin and not UserInGallery.query.filter_by(user=current_user, gallery=gallery).first():
        flask.abort(403)

    picture_id = int(flask.request.form.get("picture_id"))

    picture = db.session.get(PictureResource, picture_id)
    if picture is None:
        flask.flash("Invalid picture")
        return flask.redirect("/gallery/" + str(gallery.id))

    picture_in_gallery = PictureInGallery.query.filter_by(resource=picture,
                                                          gallery=gallery).first()
    if picture_in_gallery is None:
        flask.flash("This picture isn't in the gallery")
        return flask.redirect("/gallery/" + str(gallery.id))

    db.session.delete(picture_in_gallery)

    db.session.commit()

    return flask.redirect("/gallery/" + str(gallery.id))


@app.route("/gallery/<int:id>/add-pictures-from-query", methods=["POST"])
def gallery_add_from_query(id):
    gallery = db.session.get(Gallery, id)
    if gallery is None:
        flask.abort(404)

    if "username" not in flask.session:
        flask.abort(401)

    if flask.session["username"] != gallery.owner_name and not current_user.admin and not UserInGallery.query.filter_by(user=current_user, gallery=gallery).first():
        flask.abort(403)

    query_yaml = flask.request.form.get("query", "")

    yaml_parser = yaml.YAML()
    query_data = yaml_parser.load(query_yaml) or {}
    query = get_picture_query(query_data)

    pictures = query.all()

    count = 0

    for picture in pictures:
        if not PictureInGallery.query.filter_by(resource=picture, gallery=gallery).first():
            db.session.add(PictureInGallery(picture, gallery))
            count += 1

    db.session.commit()

    flask.flash(f"Added {count} pictures to the gallery")

    return flask.redirect("/gallery/" + str(gallery.id))


@app.route("/gallery/<int:id>/users")
def gallery_users(id):
    gallery = db.session.get(Gallery, id)
    if gallery is None:
        flask.abort(404)

    current_user = db.session.get(User, flask.session.get("username"))
    have_permission = current_user and (current_user == gallery.owner or current_user.admin)

    return flask.render_template("gallery-users.html", gallery=gallery,
                                 have_permission=have_permission)


@app.route("/gallery/<int:id>/edit")
def edit_gallery(id):
    gallery = db.session.get(Gallery, id)
    if gallery is None:
        flask.abort(404)

    current_user = db.session.get(User, flask.session.get("username"))
    if current_user is None:
        flask.abort(401)

    if current_user != gallery.owner and not current_user.admin:
        flask.abort(403)

    return flask.render_template("edit-gallery.html", gallery=gallery)


@app.route("/gallery/<int:id>/edit", methods=["POST"])
def edit_gallery_post(id):
    gallery = db.session.get(Gallery, id)
    if gallery is None:
        flask.abort(404)

    current_user = db.session.get(User, flask.session.get("username"))
    if current_user is None:
        flask.abort(401)

    if current_user != gallery.owner and not current_user.admin:
        flask.abort(403)

    title = flask.request.form["title"]
    description = flask.request.form.get("description")

    if not title:
        flask.flash("Enter a title")
        return flask.redirect(flask.request.url)

    if not description:
        description = ""

    gallery.title = title
    gallery.description = description

    db.session.commit()

    return flask.redirect("/gallery/" + str(gallery.id))


@app.route("/gallery/<int:id>/delete")
def delete_gallery(id):
    gallery = db.session.get(Gallery, id)
    if gallery is None:
        flask.abort(404)

    current_user = db.session.get(User, flask.session.get("username"))
    if current_user is None:
        flask.abort(401)

    if current_user != gallery.owner and not current_user.admin:
        flask.abort(403)

    PictureInGallery.query.filter_by(gallery=gallery).delete()
    UserInGallery.query.filter_by(gallery=gallery).delete()
    db.session.delete(gallery)
    db.session.commit()

    return flask.redirect("/")


@app.route("/gallery/<int:id>/users/add", methods=["POST"])
def gallery_add_user(id):
    gallery = db.session.get(Gallery, id)
    if gallery is None:
        flask.abort(404)

    current_user = db.session.get(User, flask.session.get("username"))
    if current_user is None:
        flask.abort(401)

    if current_user != gallery.owner and not current_user.admin:
        flask.abort(403)

    username = flask.request.form.get("username")
    if username == gallery.owner_name:
        flask.flash("The owner is already in the gallery")
        return flask.redirect("/gallery/" + str(gallery.id) + "/users")

    user = db.session.get(User, username)
    if user is None:
        flask.flash("User not found")
        return flask.redirect("/gallery/" + str(gallery.id) + "/users")

    if UserInGallery.query.filter_by(user=user, gallery=gallery).first():
        flask.flash("User is already in the gallery")
        return flask.redirect("/gallery/" + str(gallery.id) + "/users")

    db.session.add(UserInGallery(user, gallery))

    db.session.commit()

    return flask.redirect("/gallery/" + str(gallery.id) + "/users")


@app.route("/gallery/<int:id>/users/remove", methods=["POST"])
def gallery_remove_user(id):
    gallery = db.session.get(Gallery, id)
    if gallery is None:
        flask.abort(404)

    current_user = db.session.get(User, flask.session.get("username"))
    if current_user is None:
        flask.abort(401)

    if current_user != gallery.owner and not current_user.admin:
        flask.abort(403)

    username = flask.request.form.get("username")
    user = db.session.get(User, username)
    if user is None:
        flask.flash("User not found")
        return flask.redirect("/gallery/" + str(gallery.id) + "/users")

    user_in_gallery = UserInGallery.query.filter_by(user=user, gallery=gallery).first()
    if user_in_gallery is None:
        flask.flash("User is not in the gallery")
        return flask.redirect("/gallery/" + str(gallery.id) + "/users")

    db.session.delete(user_in_gallery)

    db.session.commit()

    return flask.redirect("/gallery/" + str(gallery.id) + "/users")


class APIError(Exception):
    def __init__(self, status_code, message):
        self.status_code = status_code
        self.message = message


def get_picture_query(query_data):
    query = db.session.query(PictureResource)

    def has_condition(id):
        descendants_cte = (
            db.select(PictureObject.id)
            .where(PictureObject.id == id)
            .cte(name=f"descendants_cte_{id}", recursive=True)
        )

        descendants_cte = descendants_cte.union_all(
            db.select(PictureObjectInheritance.child_id)
            .where(PictureObjectInheritance.parent_id == descendants_cte.c.id)
        )

        return PictureResource.regions.any(
            PictureRegion.object_id.in_(
                db.select(descendants_cte.c.id)
            )
        )

    requirement_conditions = {
        # Has an object with the ID in the given list
        "has_object": lambda value: PictureResource.regions.any(
                PictureRegion.object_id.in_(value)),
        # Has an object with the ID in the given list, or a subtype of it
        "has": lambda value: db.or_(*[has_condition(id) for id in value]),
        "nature": lambda value: PictureResource.nature_id.in_(value),
        "licence": lambda value: PictureResource.licences.any(
                PictureLicence.licence_id.in_(value)),
        "author": lambda value: PictureResource.author_name.in_(value),
        "title": lambda value: PictureResource.title.ilike(value),
        "description": lambda value: PictureResource.description.ilike(value),
        "origin_url": lambda value: db.func.lower(db.func.substr(
                PictureResource.origin_url,
                db.func.length(db.func.split_part(PictureResource.origin_url, "://", 1)) + 4
        )).in_(value),
        "above_width": lambda value: PictureResource.width >= value,
        "below_width": lambda value: PictureResource.width <= value,
        "above_height": lambda value: PictureResource.height >= value,
        "below_height": lambda value: PictureResource.height <= value,
        "before_date": lambda value: PictureResource.timestamp <= datetime.utcfromtimestamp(
                value),
        "after_date": lambda value: PictureResource.timestamp >= datetime.utcfromtimestamp(
                value),
        "in_gallery": lambda value: PictureResource.galleries.any(PictureInGallery.gallery_id.in_(value)),
        "above_rating": lambda value: db.select(db.func.coalesce(db.func.avg(PictureRating.rating), 5)).where(PictureRating.resource_id == PictureResource.id).scalar_subquery() >= value,
        "below_rating": lambda value: db.select(db.func.coalesce(db.func.avg(PictureRating.rating), 0)).where(PictureRating.resource_id == PictureResource.id).scalar_subquery() <= value,
        "above_rating_count": lambda value: db.select(db.func.count(PictureRating.id)).where(PictureRating.resource_id == PictureResource.id).correlate(PictureResource).scalar_subquery() >= value,
        "below_rating_count": lambda value: db.select(db.func.count(PictureRating.id)).where(PictureRating.resource_id == PictureResource.id).correlate(PictureResource).scalar_subquery() <= value,
        "above_region_count": lambda value: db.select(db.func.count(PictureRegion.id)).where(PictureRegion.resource_id == PictureResource.id).correlate(PictureResource).scalar_subquery() >= value,
        "below_region_count": lambda value: db.select(db.func.count(PictureRegion.id)).where(PictureRegion.resource_id == PictureResource.id).correlate(PictureResource).scalar_subquery() <= value,
        "copied_from": lambda value: PictureResource.copied_from_id.in_(value),
    }

    if "want" in query_data:
        for i in query_data["want"]:
            if len(i) != 1:
                raise APIError(400, "Each requirement must have exactly one key")
            requirement, value = list(i.items())[0]
            if requirement not in requirement_conditions:
                raise APIError(400, f"Unknown requirement type: {requirement}")

            condition = requirement_conditions[requirement]
            query = query.filter(condition(value))
    if "exclude" in query_data:
        for i in query_data["exclude"]:
            if len(i) != 1:
                raise APIError(400, "Each exclusion must have exactly one key")
            requirement, value = list(i.items())[0]
            if requirement not in requirement_conditions:
                raise APIError(400, f"Unknown requirement type: {requirement}")

            condition = requirement_conditions[requirement]
            query = query.filter(~condition(value))
    if not query_data.get("include_obsolete", False):
        query = query.filter(PictureResource.replaced_by_id.is_(None))

    return query


@app.route("/query-pictures")
def graphical_query_pictures():
    return flask.render_template("graphical-query-pictures.html")


@app.route("/query-pictures-results")
def graphical_query_pictures_results():
    query_yaml = flask.request.args.get("query", "")
    yaml_parser = yaml.YAML()
    query_data = yaml_parser.load(query_yaml) or {}
    try:
        query = get_picture_query(query_data)
    except APIError as e:
        flask.abort(e.status_code)

    page = int(flask.request.args.get("page", 1))
    per_page = int(flask.request.args.get("per_page", 16))

    resources = query.paginate(page=page, per_page=per_page)

    return flask.render_template("graphical-query-pictures-results.html", resources=resources,
                                 query=query_yaml,
                                 page_number=page, page_length=per_page,
                                 num_pages=resources.pages,
                                 prev_page=resources.prev_num, next_page=resources.next_num)


@app.route("/raw/picture/<int:id>")
def raw_picture(id):
    resource = db.session.get(PictureResource, id)
    if resource is None:
        flask.abort(404)

    response = flask.send_from_directory(path.join(config.DATA_PATH, "pictures"),
                                         str(resource.id))
    response.mimetype = resource.file_format

    return response


@app.route("/raw/picture-thumbnail/<int:id>")
def raw_picture_thumbnail(id):
    resource = db.session.get(PictureResource, id)
    if resource is None:
        flask.abort(404)

    if not path.exists(path.join(config.DATA_PATH, "picture-thumbnails", str(resource.id))):
        pil_image = Image.open(path.join(config.DATA_PATH, "pictures", str(resource.id)))
        pil_image = make_thumbnail(pil_image)
        pil_image.save(path.join(config.DATA_PATH, "picture-thumbnails", str(resource.id)), **config.THUMBNAIL_SAVE_OPTIONS)

    response = flask.send_from_directory(path.join(config.DATA_PATH, "picture-thumbnails"),
                                         str(resource.id))
    response.mimetype = resource.file_format

    return response


@app.route("/object/")
def graphical_object_types():
    return flask.render_template("object-types.html", objects=PictureObject.query.all())


@app.route("/api/object-types")
def object_types():
    objects = db.session.query(PictureObject).all()
    return flask.jsonify({object.id: object.description for object in objects})


@app.route("/api/query-pictures", methods=["POST"])  # sadly GET can't have a body
def query_pictures():
    offset = int(flask.request.args.get("offset", 0))
    limit = int(flask.request.args.get("limit", 16))
    ordering = flask.request.args.get("ordering", "date-desc")

    yaml_parser = yaml.YAML()
    query_data = yaml_parser.load(flask.request.data) or {}
    try:
        query = get_picture_query(query_data)
    except APIError as e:
        return flask.jsonify({"error": e.message}), e.status_code

    rating_count_subquery = db.select(db.func.count(PictureRating.id)).where(
        PictureRating.resource_id == PictureResource.id).scalar_subquery()
    region_count_subquery = db.select(db.func.count(PictureRegion.id)).where(
        PictureRegion.resource_id == PictureResource.id).scalar_subquery()
    rating_subquery = db.select(db.func.coalesce(db.func.avg(PictureRating.rating), 0)).where(
        PictureRating.resource_id == PictureResource.id).scalar_subquery()

    match ordering:
        case "date-desc":
            query = query.order_by(PictureResource.timestamp.desc())
        case "date-asc":
            query = query.order_by(PictureResource.timestamp.asc())
        case "title-asc":
            query = query.order_by(PictureResource.title.asc())
        case "title-desc":
            query = query.order_by(PictureResource.title.desc())
        case "random":
            query = query.order_by(db.func.random())
        case "number-regions-desc":
            query = query.order_by(region_count_subquery.desc())
        case "number-regions-asc":
            query = query.order_by(region_count_subquery.asc())
        case "rating-desc":
            query = query.order_by(rating_subquery.desc())
        case "rating-asc":
            query = query.order_by(rating_subquery.asc())
        case "number-ratings-desc":
            query = query.order_by(rating_count_subquery.desc())
        case "number-ratings-asc":
            query = query.order_by(rating_count_subquery.asc())

    query = query.offset(offset).limit(limit)
    resources = query.all()

    json_response = {
        "date_generated": datetime.utcnow().timestamp(),
        "resources": [],
        "offset": offset,
        "limit": limit,
    }

    json_resources = json_response["resources"]

    for resource in resources:
        json_resource = {
            "id": resource.id,
            "title": resource.title,
            "description": resource.description,
            "timestamp": resource.timestamp.timestamp(),
            "origin_url": resource.origin_url,
            "author": resource.author_name,
            "file_format": resource.file_format,
            "width": resource.width,
            "height": resource.height,
            "nature": resource.nature_id,
            "licences": [licence.licence_id for licence in resource.licences],
            "replaces": resource.replaces_id,
            "replaced_by": resource.replaced_by_id,
            "regions": [],
            "download": config.ROOT_URL + flask.url_for("raw_picture", id=resource.id),
        }
        for region in resource.regions:
            json_resource["regions"].append({
                "object": region.object_id,
                "type": region.json["type"],
                "shape": region.json["shape"],
            })

        json_resources.append(json_resource)

    return flask.jsonify(json_response)


@app.route("/api/picture/<int:id>/")
def api_picture(id):
    resource = db.session.get(PictureResource, id)
    if resource is None:
        flask.abort(404)

    json_resource = {
        "id": resource.id,
        "title": resource.title,
        "description": resource.description,
        "timestamp": resource.timestamp.timestamp(),
        "origin_url": resource.origin_url,
        "author": resource.author_name,
        "file_format": resource.file_format,
        "width": resource.width,
        "height": resource.height,
        "nature": resource.nature_id,
        "licences": [licence.licence_id for licence in resource.licences],
        "replaces": resource.replaces_id,
        "replaced_by": resource.replaced_by_id,
        "regions": [],
        "download": config.ROOT_URL + flask.url_for("raw_picture", id=resource.id),
        "rating_average": resource.average_rating,
        "rating_count": resource.rating_totals,
    }
    for region in resource.regions:
        json_resource["regions"].append({
            "object": region.object_id,
            "type": region.json["type"],
            "shape": region.json["shape"],
        })

    return flask.jsonify(json_resource)


@app.route("/api/licence/")
def api_licences():
    licences = db.session.query(Licence).all()
    json_licences = {
        licence.id: {
            "title": licence.title,
            "free": licence.free,
            "pinned": licence.pinned,
        } for licence in licences
    }

    return flask.jsonify(json_licences)


@app.route("/api/licence/<id>/")
def api_licence(id):
    licence = db.session.get(Licence, id)
    if licence is None:
        flask.abort(404)

    json_licence = {
        "id": licence.id,
        "title": licence.title,
        "description": licence.description,
        "info_url": licence.info_url,
        "legalese_url": licence.url,
        "free": licence.free,
        "logo_url": licence.logo_url,
        "pinned": licence.pinned,
    }

    return flask.jsonify(json_licence)


@app.route("/api/nature/")
def api_natures():
    natures = db.session.query(PictureNature).all()
    json_natures = {
        nature.id: nature.description for nature in natures
    }

    return flask.jsonify(json_natures)


@app.route("/api/user/")
def api_users():
    offset = int(flask.request.args.get("offset", 0))
    limit = int(flask.request.args.get("limit", 16))

    users = db.session.query(User).offset(offset).limit(limit).all()

    json_users = {
        user.username: {
            "admin": user.admin,
        } for user in users
    }

    return flask.jsonify(json_users)


@app.route("/api/user/<username>/")
def api_user(username):
    user = db.session.get(User, username)
    if user is None:
        flask.abort(404)

    json_user = {
        "username": user.username,
        "admin": user.admin,
        "joined": user.joined_timestamp.timestamp(),
    }

    return flask.jsonify(json_user)


@app.route("/api/login", methods=["POST"])
def api_login():
    username = flask.request.json["username"]
    password = flask.request.json["password"]

    user = db.session.get(User, username)

    if user is None:
        return flask.jsonify({"error": "This username is not registered. To prevent spam, you must use the HTML interface to register."}), 401

    if not bcrypt.check_password_hash(user.password_hashed, password):
        return flask.jsonify({"error": "Incorrect password"}), 401

    flask.session["username"] = username

    return flask.jsonify({"message": "You have been logged in. Your HTTP client must support cookies to use features of this API that require authentication."})


@app.route("/api/logout", methods=["POST"])
def api_logout():
    flask.session.pop("username", None)
    return flask.jsonify({"message": "You have been logged out."})


@app.route("/api/upload", methods=["POST"])
def api_upload():
    if "username" not in flask.session:
        return flask.jsonify({"error": "You must be logged in to upload pictures"}), 401

    json_ = json.loads(flask.request.form["json"])
    title = json_["title"]
    description = json_.get("description", "")
    origin_url = json_.get("origin_url", "")
    author = db.session.get(User, flask.session["username"])
    licence_ids = json_["licence"]
    nature_id = json_["nature"]
    file = flask.request.files["file"]

    if not file or not file.filename:
        return flask.jsonify({"error": "An image file must be uploaded"}), 400

    if not file.mimetype.startswith("image/") or file.mimetype == "image/svg+xml":
        return flask.jsonify({"error": "Only bitmap images are supported"}), 400

    if not title:
        return flask.jsonify({"error": "Give a title"}), 400

    if not description:
        description = ""

    if not nature_id:
        return flask.jsonify({"error": "Give a picture type"}), 400

    if not licence_ids:
        return flask.jsonify({"error": "Give licences"}), 400

    licences = [db.session.get(Licence, licence_id) for licence_id in licence_ids]
    if not any(licence.free for licence in licences):
        return flask.jsonify({"error": "Use at least one free licence"}), 400

    resource = PictureResource(title, author, description, origin_url, licence_ids,
                               file.mimetype,
                               db.session.get(PictureNature, nature_id))
    db.session.add(resource)
    db.session.commit()
    file.save(path.join(config.DATA_PATH, "pictures", str(resource.id)))
    pil_image = Image.open(path.join(config.DATA_PATH, "pictures", str(resource.id)))
    resource.width, resource.height = pil_image.size
    pil_image.thumbnail(config.THUMBNAIL_SIZE)
    pil_image = pil_image.convert("RGB")
    pil_image.save(path.join(config.DATA_PATH, "picture-thumbnails", str(resource.id)), **config.THUMBNAIL_SAVE_OPTIONS)
    db.session.commit()

    if json_.get("annotations"):
        try:
            resource.put_annotations(json_["annotations"])
            db.session.commit()
        except json.JSONDecodeError:
            return flask.jsonify({"error": "Invalid annotations"}), 400

    return flask.jsonify({"message": "Picture uploaded successfully", "id": resource.id})


@app.route("/api/picture/<int:id>/update", methods=["POST"])
def api_update_picture(id):
    resource = db.session.get(PictureResource, id)
    if resource is None:
        return flask.jsonify({"error": "Picture not found"}), 404
    current_user = db.session.get(User, flask.session.get("username"))
    if current_user is None:
        return flask.jsonify({"error": "You must be logged in to edit pictures"}), 401
    if resource.author != current_user and not current_user.admin:
        return flask.jsonify({"error": "You are not the author of this picture"}), 403

    title = flask.request.json.get("title", resource.title)
    description = flask.request.json.get("description", resource.description)
    origin_url = flask.request.json.get("origin_url", resource.origin_url)
    licence_ids = flask.request.json.get("licence", [licence.licence_id for licence in resource.licences])
    nature_id = flask.request.json.get("nature", resource.nature_id)

    if not title:
        return flask.jsonify({"error": "Give a title"}), 400

    if not description:
        description = ""

    if not nature_id:
        return flask.jsonify({"error": "Give a picture type"}), 400

    if not licence_ids:
        return flask.jsonify({"error": "Give licences"}), 400

    licences = [db.session.get(Licence, licence_id) for licence_id in licence_ids]

    if not any(licence.free for licence in licences):
        return flask.jsonify({"error": "Use at least one free licence"}), 400

    resource.title = title
    resource.description = description
    resource.origin_url = origin_url
    resource.licences = licences
    resource.nature = db.session.get(PictureNature, nature_id)

    db.session.commit()

    return flask.jsonify({"message": "Picture updated successfully"})


@app.route("/api/picture/<int:id>/delete", methods=["POST"])
def api_delete_picture(id):
    resource = db.session.get(PictureResource, id)
    if resource is None:
        return flask.jsonify({"error": "Picture not found"}), 404
    current_user = db.session.get(User, flask.session.get("username"))
    if current_user is None:
        return flask.jsonify({"error": "You must be logged in to delete pictures"}), 401
    if resource.author != current_user and not current_user.admin:
        return flask.jsonify({"error": "You are not the author of this picture"}), 403

    PictureInGallery.query.filter_by(resource=resource).delete()
    db.session.delete(resource)
    db.session.commit()

    return flask.jsonify({"message": "Picture deleted successfully"})


@app.route("/api/picture/<int:id>/put-file", methods=["POST"])
def api_put_file(id):
    resource = db.session.get(PictureResource, id)
    if resource is None:
        return flask.jsonify({"error": "Picture not found"}), 404
    current_user = db.session.get(User, flask.session.get("username"))
    if current_user is None:
        return flask.jsonify({"error": "You must be logged in to edit pictures"}), 401
    if resource.author != current_user and not current_user.admin:
        return flask.jsonify({"error": "You are not the author of this picture"}), 403

    file = flask.request.files["file"]

    if not file or not file.filename:
        return flask.jsonify({"error": "Provide a file"}), 400

    if not file.mimetype.startswith("image/") or file.mimetype == "image/svg+xml":
        return flask.jsonify({"error": "Only images are supported"}), 415

    file_path = path.join(config.DATA_PATH, "pictures", str(resource.id))
    # Since it's a hard link, we need to remove it, so it won't affect copies
    os.unlink(file_path)
    file.save(file_path)
    pil_image = Image.open(file_path)
    resource.width, resource.height = pil_image.size
    pil_image = make_thumbnail(pil_image)
    pil_image.save(path.join(config.DATA_PATH, "picture-thumbnails", str(resource.id)), **config.THUMBNAIL_SAVE_OPTIONS)

    return flask.jsonify({"message": "File uploaded successfully"})


@app.route("/api/picture/<int:id>/rate", methods=["POST"])
def api_rate_picture(id):
    resource = db.session.get(PictureResource, id)
    if resource is None:
        flask.abort(404)

    current_user = db.session.get(User, flask.session.get("username"))
    if current_user is None:
        flask.abort(401)

    rating = int(flask.request.json.get("rating", 0))

    if not rating:
        # Delete the existing rating
        if PictureRating.query.filter_by(resource=resource, user=current_user).first():
            db.session.delete(PictureRating.query.filter_by(resource=resource,
                                                             user=current_user).first())
            db.session.commit()

        return flask.jsonify({"message": "Existing rating removed"})

    if not 1 <= rating <= 5:
        flask.flash("Invalid rating")
        return flask.jsonify({"error": "Invalid rating"}), 400

    if PictureRating.query.filter_by(resource=resource, user=current_user).first():
        PictureRating.query.filter_by(resource=resource, user=current_user).first().rating = rating
    else:
        # Create a new rating
        db.session.add(PictureRating(resource, current_user, rating))

    db.session.commit()

    return flask.jsonify({"message": "Rating saved"})


@app.route("/api/gallery/<int:id>/")
def api_gallery(id):
    gallery = db.session.get(Gallery, id)
    if gallery is None:
        flask.abort(404)

    json_gallery = {
        "id": gallery.id,
        "title": gallery.title,
        "description": gallery.description,
        "owner": gallery.owner_name,
        "users": [user.username for user in gallery.users],
    }

    return flask.jsonify(json_gallery)


@app.route("/api/gallery/<int:id>/edit", methods=["POST"])
def api_edit_gallery(id):
    gallery = db.session.get(Gallery, id)
    if gallery is None:
        flask.abort(404)

    current_user = db.session.get(User, flask.session.get("username"))
    if current_user is None:
        flask.abort(401)

    if current_user != gallery.owner and not current_user.admin:
        flask.abort(403)

    title = flask.request.json.get("title", gallery.title)
    description = flask.request.json.get("description", gallery.description)

    if not title:
        return flask.jsonify({"error": "Give a title"}), 400

    if not description:
        description = ""

    gallery.title = title
    gallery.description = description

    db.session.commit()

    return flask.jsonify({"message": "Gallery updated successfully"})


@app.route("/api/new-gallery", methods=["POST"])
def api_new_gallery():
    if "username" not in flask.session:
        return flask.jsonify({"error": "You must be logged in to create galleries"}), 401

    title = flask.request.json.get("title")
    description = flask.request.json.get("description", "")

    if not title:
        return flask.jsonify({"error": "Give a title"}), 400

    gallery = Gallery(title, description, db.session.get(User, flask.session["username"]))
    db.session.add(gallery)
    db.session.commit()

    return flask.jsonify({"message": "Gallery created successfully", "id": gallery.id})


@app.route("/api/gallery/<int:id>/add-picture", methods=["POST"])
def api_gallery_add_picture(id):
    gallery = db.session.get(Gallery, id)
    if gallery is None:
        flask.abort(404)

    if "username" not in flask.session:
        return flask.jsonify({"error": "You must be logged in to add pictures to galleries"}), 401

    current_user = db.session.get(User, flask.session.get("username"))

    if flask.session["username"] != gallery.owner_name and not current_user.admin and not UserInGallery.query.filter_by(user=current_user, gallery=gallery).first():
        return flask.jsonify({"error": "You do not have permission to add pictures to this gallery"}), 403

    picture_id = flask.request.json.get("picture_id")

    try:
        picture_id = int(picture_id)
    except ValueError:
        return flask.jsonify({"error": "Invalid picture ID"}), 400

    picture = db.session.get(PictureResource, picture_id)
    if picture is None:
        return flask.jsonify({"error": "The picture doesn't exist"}), 404

    if PictureInGallery.query.filter_by(resource=picture, gallery=gallery).first():
        return flask.jsonify({"error": "This picture is already in the gallery"}), 400

    db.session.add(PictureInGallery(picture, gallery))

    db.session.commit()

    return flask.jsonify({"message": "Picture added to gallery"})


@app.route("/api/gallery/<int:id>/remove-picture", methods=["POST"])
def api_gallery_remove_picture(id):
    gallery = db.session.get(Gallery, id)
    if gallery is None:
        flask.abort(404)

    if "username" not in flask.session:
        return flask.jsonify({"error": "You must be logged in to remove pictures from galleries"}), 401

    current_user = db.session.get(User, flask.session.get("username"))

    if flask.session["username"] != gallery.owner_name and not current_user.admin and not UserInGallery.query.filter_by(user=current_user, gallery=gallery).first():
        return flask.jsonify({"error": "You do not have permission to remove pictures from this gallery"}), 403

    picture_id = flask.request.json.get("picture_id")

    try:
        picture_id = int(picture_id)
    except ValueError:
        return flask.jsonify({"error": "Invalid picture ID"}), 400

    picture = db.session.get(PictureResource, picture_id)
    if picture is None:
        return flask.jsonify({"error": "The picture doesn't exist"}), 404

    picture_in_gallery = PictureInGallery.query.filter_by(resource=picture, gallery=gallery).first()
    if picture_in_gallery is None:
        return flask.jsonify({"error": "This picture isn't in the gallery"}), 400

    db.session.delete(picture_in_gallery)

    db.session.commit()

    return flask.jsonify({"message": "Picture removed from gallery"})


@app.route("/api/gallery/<int:id>/users/add", methods=["POST"])
def api_gallery_add_user(id):
    gallery = db.session.get(Gallery, id)
    if gallery is None:
        flask.abort(404)

    current_user = db.session.get(User, flask.session.get("username"))
    if current_user is None:
        flask.abort(401)

    if current_user != gallery.owner and not current_user.admin:
        flask.abort(403)

    username = flask.request.json.get("username")
    if username == gallery.owner_name:
        return flask.jsonify({"error": "The owner cannot be added to trusted users"}), 400

    user = db.session.get(User, username)
    if user is None:
        return flask.jsonify({"error": "User not found"}), 404

    if UserInGallery.query.filter_by(user=user, gallery=gallery).first():
        return flask.jsonify({"error": "User is already in the gallery"}), 400

    db.session.add(UserInGallery(user, gallery))

    db.session.commit()

    return flask.jsonify({"message": "User added to gallery"})


@app.route("/api/gallery/<int:id>/users/remove", methods=["POST"])
def api_gallery_remove_user(id):
    gallery = db.session.get(Gallery, id)
    if gallery is None:
        flask.abort(404)

    current_user = db.session.get(User, flask.session.get("username"))
    if current_user is None:
        flask.abort(401)

    if current_user != gallery.owner and not current_user.admin:
        flask.abort(403)

    username = flask.request.json.get("username")
    user = db.session.get(User, username)
    if user is None:
        return flask.jsonify({"error": "User not found"}), 404

    user_in_gallery = UserInGallery.query.filter_by(user=user, gallery=gallery).first()
    if user_in_gallery is None:
        return flask.jsonify({"error": "User is not in the gallery"}), 400

    db.session.delete(user_in_gallery)

    db.session.commit()

    return flask.jsonify({"message": "User removed from gallery"})


@app.route("/api/gallery/<int:id>/delete", methods=["POST"])
def api_delete_gallery(id):
    gallery = db.session.get(Gallery, id)
    if gallery is None:
        flask.abort(404)

    current_user = db.session.get(User, flask.session.get("username"))
    if current_user is None:
        flask.abort(401)

    if current_user != gallery.owner and not current_user.admin:
        flask.abort(403)

    PictureInGallery.query.filter_by(gallery=gallery).delete()
    UserInGallery.query.filter_by(gallery=gallery).delete()
    db.session.delete(gallery)

    db.session.commit()

    return flask.jsonify({"message": "Gallery deleted"})


def random_picture():
    return db.session.query(PictureResource).order_by(db.func.random()).first()


for code in range(400, 600):
    if os.path.exists(f"templates/errors/{code}.html"):
        app.register_error_handler(code, lambda e: error_page(e.code))


@app.route("/error/<int:code>")
def error_page(code):
    user_agent = flask.request.headers.get("User-Agent", "")
    ip_address = flask.request.remote_addr
    return flask.render_template(f"errors/{code}.html", random_picture=random_picture(),
                                 user_agent=user_agent, ip_address=ip_address), code

