'''
Created on 31.03.2018
@author: Sercan Turkmen
         Moamen Ibrahim
         Mina Ghobrial
    REFERENCEs:
    -   [1] Programmable Web Project, Exercise3, resources.py
'''

import json

from urllib.parse import unquote

from flask import Flask, request, Response, g, _request_ctx_stack, redirect, send_from_directory
from flask_restful import Resource, Api, abort
from werkzeug.exceptions import NotFound, UnsupportedMediaType

from app.utils import RegexConverter
from app import database
from app.model.mason import MasonObject

# Constants for hypermedia formats and profiles
MASON = "application/vnd.mason+json"
JSON = "application/json"
CRITIQUE_USER_PROFILE = "/critique/profiles/user-profile/"
CRITIQUE_POST_PROFILE = "/critique/profiles/post_profile/"
CRITIQUE_RATING_PROFILE = "/critique/profiles/rating-profile/"
ERROR_PROFILE = "/critique/profiles/error-profile"

# Fill these in
# Fill with the correct Apiary url"
APIARY_PROJECT = "https://critique.docs.apiary.io"
APIARY_PROFILES_URL = APIARY_PROJECT+"/#reference/profiles/"
APIARY_RELATIONS_URL = APIARY_PROJECT+"/#reference/link-relations/"

CREATE_USER_SCHEMA = json.load(open('app/schema/create_user.json'))
CREATE_RATING_SCHEMA = json.load(open('app/schema/create_rating.json'))
EDIT_USER_SCHEMA = json.load(open('app/schema/edit_user.json'))
CREATE_POSTS_SCHEMA = json.load(open('app/schema/create_posts.json'))
CREATE_REPLY_SCHEMA = json.load(open('app/schema/create_reply.json'))
EDIT_RATING_SCHEMA = json.load(open('app/schema/edit_rating.json'))
EDIT_POST_SCHEMA = json.load(open('app/schema/edit_post.json'))

LINK_RELATIONS_URL = "/critique/link-relations/"

# Borrowed from lab exercises [1]
# Define the application and the api
app = Flask(__name__, static_folder="static", static_url_path="/.")
app.debug = True
# Set the database Engine. In order to modify the database file (e.g. for
# testing) provide the database path   app.config to modify the
# database to be used (for instance for testing)
app.config.update({"Engine": database.Engine()})
# Start the RESTFUL API.
api = Api(app)

class CritiqueObject(MasonObject):  # Borrowed from lab exercises [1]
    '''
    A convenience subclass of MasonObject that defines a bunch of shorthand
    methods for inserting application specific objects into the document. This
    class is particularly useful for adding control objects that are largely
    context independent, and defining them in the resource methods would add a
    lot of noise to our code - not to mention making inconsistencies much more
    likely!

    In the code this object should always be used for root document as
    well as any items in a collection type resource.
    '''

    def __init__(self, **kwargs):  # Borrowed from lab exercises [1]
        '''
        Calls dictionary init method with any received keyword arguments. Adds
        the controls key afterwards because hypermedia without controls is not
        hypermedia.
        '''

        super(CritiqueObject, self).__init__(**kwargs)
        self["@controls"] = {}

    def add_control_up(self, href):
        '''
        This adds the up link to an object. Intended for the document object.
        '''

        self["@controls"]["up"] = {
            "href": href,
            "title": "Parent document"
        }

    def add_control_users_collection(self):
        '''
        This adds the user collection link to an object. Intended for the document object.
        '''

        self["@controls"]["collection"] = {
            "href": api.url_for(Users),
            "title": "List users"
        }

    def add_control_all_posts(self):
        '''
        This adds the posts collection link to an object. Intended for the document object.
        '''

        self["@controls"]["critique:all-posts"] = {
            "href": api.url_for(Posts),
            "title": "List posts"
        }

    def add_control_all_users(self):
        '''
        This adds the users collection link to an object. Intended for the document object.
        '''

        self["@controls"]["critique:all-users"] = {
            "href": api.url_for(Users),
            "title": "List users"
        }

    def add_control_add_user(self):
        '''
        This adds the add-user control to an object. Intended for the
        document object.
        '''

        self["@controls"]["critique:add-user"] = {
            "href": api.url_for(Users),
            "title": "Create a new user",
            "encoding": "json",
            "method": "POST",
            "schema": CREATE_USER_SCHEMA
        }

    def add_control_user_inbox(self, nickname):
        '''
        This adds the user-inbox control to an object. Intended for the
        document object.

        : param str nickname: The nickname of the user
        '''

        self["@controls"]["critique:user-inbox"] = {
            "href": api.url_for(UserInbox, nickname=nickname),
        }

    def add_control_user_river(self, nickname):
        '''
        This adds the user-river control to an object. Intended for the
        document object.

        : param str nickname: The nickname of the user
        '''

        self["@controls"]["critique:user-river"] = {
            "href": api.url_for(UserRiver, nickname=nickname),
        }

    def add_control_user_ratings(self, nickname):
        '''
        This adds the user-ratings control to an object. Intended for the
        document object.

        : param str nickname: The nickname of the user
        '''

        self["@controls"]["critique:user-ratings"] = {
            "href": api.url_for(UserRatings, nickname=nickname),
        }

    def add_control_add_reply(self, postId):
        '''
        This adds the reply to a post control to an object. Intended for the
        document object.

        : param str postId: The postId of the post to be replied
        '''

        self["@controls"]["critique:add-reply"] = {
            "href": api.url_for(Post, postId=postId),
            "title": "Add reply",
            "encoding": "json",
            "method": "POST",
            "schema": CREATE_REPLY_SCHEMA
        }

    def add_control_delete_user(self, nickname):
        '''
        Adds the delete control to an object. This is intended for any
        object that represents a user.

        : param str nickname: The nickname of the user to remove
        '''

        self["@controls"]["critique:delete"] = {
            "href": api.url_for(User, nickname=nickname),
            "title": "Delete a user",
            "method": "DELETE"
        }

    def add_control_delete_post(self, post_id):
        '''
        Adds the delete control to an object. This is intended for any
        object that represents a user's post.

        : param integer post_id : The id of the post to be deleted
        '''

        self["@controls"]["critique:delete"] = {
            "href": api.url_for(Post, postId=post_id),
            "title": "Delete a user's post",
            "method": "DELETE"
        }

    def add_control_edit_user(self, nickname):
        '''
        Adds the edit control to an object. This is intended for any
        object that represents a user.

        : param str nickname: The nickname of the user to edit
        '''

        self["@controls"]["edit"] = {
            "href": api.url_for(User, nickname=nickname),
            "title": "Edit this user",
            "method": "PUT",
            "encoding": "json",
            "schema": EDIT_USER_SCHEMA
        }

    def add_control_add_rating(self, nickname):
        '''
        This adds the add-rating control to an object. Intended for the
        document object.
        '''

        self["@controls"]["critique:add-rating"] = {
            "href": api.url_for(UserRatings, nickname=nickname),
            "title": "Create a new rating",
            "encoding": "json",
            "method": "POST",
            "schema": CREATE_RATING_SCHEMA
        }

    def add_control_sender(self, nickname):
        '''
        This adds the sender control to an object. Intended for the
        document object.
        '''

        self["@controls"]["critique:sender"] = {
            "href": api.url_for(User, nickname=nickname),
            "title": "Sender of the resource",
        }

    def add_control_receiver(self, nickname):
        '''
        This adds the receiver control to an object. Intended for the
        document object.
        '''

        self["@controls"]["critique:receiver"] = {
            "href": api.url_for(User, nickname=nickname),
            "title": "Receiver of the resource",
        }

    def add_control_edit_rating(self, nickname, ratingId):
        '''
        This adds the link to edit a given rating for the
        document object.
        '''
        self["@controls"]["edit"] = {
            "href": api.url_for(Rating, nickname=nickname, ratingId=ratingId),
            "title": "Edit Rating",
            "method": "PUT",
            "encoding": "json",
            "schema": EDIT_RATING_SCHEMA
        }

    def add_control_delete_rating(self, nickname, ratingId):
        '''
        This adds the link to delete a given rating for the
        document object.
        '''
        self["@controls"]["critique:delete"] = {
            "href": api.url_for(Rating, nickname=nickname, ratingId=ratingId),
            "method": "DELETE"
        }

    def add_control_edit_post(self, postId):
        '''
        Adds the edit control to an object. Intended for any object
        that represents a post.

        :param str postId: ID of a specific post.
        '''
        self["@controls"]["edit"] = {
            "href": api.url_for(Post, postId=postId),
            "title": "Edit a post",
            "method": "PUT",
            "encoding": "json",
            "schema": EDIT_POST_SCHEMA
        }

# Borrowed from lab exercises [1]
def create_error_response(status_code, title, message=None):
    '''
    Creates a: py: class:`flask.Response` instance when sending back an
    HTTP error response

    : param integer status_code: The HTTP status code of the response
    : param str title: A short description of the problem
    : param message: A long description of the problem
    : rtype:: py: class:`flask.Response`
    '''

    resource_url = None
    # We need to access the context in order to access the request.path
    ctx = _request_ctx_stack.top
    if ctx is not None:
        resource_url = request.path
    envelope = MasonObject(resource_url=resource_url)
    envelope.add_error(title, message)

    # +";"+ERROR_PROFILE)
    return Response(json.dumps(envelope), status_code, mimetype=MASON)

@app.errorhandler(404)
def resource_not_found(error):  # Borrowed from lab exercises [1]
    return create_error_response(404, "Resource not found",
                                 "This resource url does not exit")

@app.errorhandler(400)
def resource_not_found(error):  # Borrowed from lab exercises [1]
    return create_error_response(400, "Malformed input format",
                                 "The format of the input is incorrect")

@app.errorhandler(500)
def unknown_error(error):  # Borrowed from lab exercises [1]
    return create_error_response(500, "Error",
                                 "The system has failed. Please, contact the administrator")

# Borrowed from lab exercises [1]
# HOOKS
@app.before_request
def connect_db():  # Borrowed from lab exercises [1]
    '''
    Creates a database connection before the request is proccessed.

    The connection is stored in the application context variable flask.g .
    Hence it is accessible from the request object.
    '''

    g.con = app.config["Engine"].connect()

@app.teardown_request
def close_connection(exc):  # Borrowed from lab exercises [1]
    '''
    Closes the database connection
    Check if the connection is created. It might be exception appear before
    the connection is created.
    '''

    if hasattr(g, "con"):
        g.con.close()

class Users(Resource):

    def get(self):
        '''
        Gets a list of all the users in the database.

        It returns always status code 200.

        RESPONSE ENTITY BODY:

         OUTPUT:
            * Media type: application/vnd.mason+json
                https://github.com/JornWildt/Mason
            * Profile: User
                /profiles/user-profile

        Link relations used in items: self, profile

        Semantic descriptions used in items: nickname, givenName, familyName, bio, avatar

        Link relations used in links: self, profile, add-user, up, all-posts

        Semantic descriptors used in template: items

        NOTE:
         * The attribute givenName is obtained from the column users_profile.firstname
         * The attribute familyName is obtained from the column users_profile.lastname
         * The rest of attributes match one-to-one with column names in the
           database.
        '''
        # PERFORM OPERATIONS
        # create users list
        users_db = g.con.get_users()

        # FILTER AND GENERATE THE RESPONSE
        # Create the envelope
        envelope = CritiqueObject()

        items = envelope["items"] = []

        for user in users_db:
            item = CritiqueObject(
                nickname=user["nickname"],
                givenName=user['firstname'],
                familyName=user['lastname'],
                avatar=user['avatar'],
                bio=user['bio']
            )
            items.append(item)
            item.add_control("self", href=api.url_for(
                User, nickname=user["nickname"]))
            item.add_control("profile", href=CRITIQUE_USER_PROFILE)
            item.add_control_up(api.url_for(Users))

        envelope.add_namespace("critique", LINK_RELATIONS_URL)
        envelope.add_control_all_posts()

        envelope.add_control_add_user()
        envelope.add_control("self", href=api.url_for(Users))

        # RENDER
        # +";" + CRITIQUE_USER_PROFILE)
        return Response(json.dumps(envelope), 200, mimetype=MASON)

    def post(self):
        '''
        Adds a new user in the database.

        REQUEST ENTITY BODY:
         * Media type: JSON
         * Profile: Critique_User

        Semantic descriptors used in template: nickname(mandatory), givenName(mandatory),
        email(mandatory), bio(optional), avatar(optional), familyName(optional),
        birthDate(optional), telephone(optional), gender(optional)

        RESPONSE STATUS CODE:
         * Returns 201 + the url of the new resource in the Location header
         * Return 400 User info is not well formed or entity body is missing.
         * Return 415 if it receives a media type != application/json
         * Return 422 Conflict if there is another user with the same nickname, email, mobile

        NOTE:
         * The attribute givenName is obtained from the column users_profile.firstname
         * The attribute familyName is obtained from the column users_profile.lastname
         * The rest of attributes match one-to-one with column names in the
           database.

        NOTE:
        The: py: method:`Connection.create_user()` receives as a parameter a
        dictionary with the following format.

            {
                'summary': {
                    'nickname': '',
                    'registrationdate': ,
                    'bio': '',
                    'avatar': ''
                },
                'details': {
                    'lastlogindate': ,
                    'firstname': '',
                    'lastname': '',
                    'email': '',
                    'mobile': '',
                    'gender': '',
                    'birthdate': '',
                }
            }

        '''

        if JSON != request.headers.get("Content-Type", ""):
            abort(415)

        # PARSE THE REQUEST:
        request_body = request.get_json(force=True)
        if not request_body:
            return create_error_response(415,
                                         "Unsupported Media Type")

        # pick up nickname, email, and firstName so we can check for conflicts
        try:
            nickname = request_body["nickname"]
            firstName = request_body["givenName"]
            email = request_body["email"]
        except KeyError:
            return create_error_response(400,
                                         "Wrong request format")
        # optional fields

        # Conflict if user already exist
        if g.con.contains_user_extended(nickname, email):
            return create_error_response(422,
                                         "Nickname, email, or mobile already exist in the users list.")

        user = {
            'summary': {
                'nickname': nickname,
                'bio': request_body.get('bio', None),
                'avatar': request_body.get('avatar', None)
            },
            'details': {
                'firstname': firstName,
                'lastname': request_body.get('familyName', None),
                'email': email,
                'mobile': request_body.get('telephone', None),
                'gender': request_body.get('gender', None),
                'birthdate': request_body.get('birthDate', None),
            }
        }

        try:
            nickname = g.con.create_user(nickname, user)
        except ValueError:
            return create_error_response(400,
                                         "Wrong request format")

        # CREATE RESPONSE AND RENDER
        return Response(status=201,
                        headers={"Location": api.url_for(User, nickname=nickname)})

class User(Resource):
    '''
    Resource has the basic information of a specific user.
    Bio is where the user should enter information about himself/herself,
    so it should be text. Email and telephone are unique texts as they
    will not repeat for other users. Gender, avatar and birthdate are texts.
    '''

    def get(self, nickname):
        '''
        Extract information of a user.

        INPUT PARAMETER:

        :param str nickname: Nickname of the required user.

        OUTPUT:
         * Return 200 if the nickname exists.
         * Return 404 if the nickname is not stored in the system.

        RESPONSE ENTITY BODY:

        OUTPUT:
            * Media type: application/vnd.mason+json
                https://github.com/JornWildt/Mason
            * Profile: User
                /critique/profiles/user-profile/

        Link relations used: self, collection, delete, user-inbox, edit,
        user-river, user-ratings, profile, up

        Semantic descriptors used: nickname, givenName, familyName, avatar,
        bio, email, birthday, telephone, gender

        NOTE:
         * The attribute givenName is obtained from the column users_profile.firstname
         * The attribute familyName is obtained from the column users_profile.lastname
         * The attribute telephone is obtained from the column users_profile.mobile
         * The rest of attributes match one-to-one with column names in the
           database.

        NOTE:
        The: py: method:`Connection.get_user()` returns a dictionary with the
        the following format.

            {
                'summary': {
                    'nickname': '',
                    'registrationdate': ,
                    'bio': '',
                    'avatar': ''
                },
                'details': {
                    'lastlogindate': ,
                    'firstname': '',
                    'lastname': '',
                    'email': '',
                    'mobile': '',
                    'gender': '',
                    'birthdate': '',
                }
            }
        '''

        # PERFORM OPERATIONS
        user_db = g.con.get_user(nickname)
        if not user_db:
            return create_error_response(404, "User not found.")

        summary = user_db['summary']
        details = user_db['details']

        # FILTER AND GENERATE RESPONSE
        # Create the envelope:
        envelope = CritiqueObject(
            nickname=nickname,
            givenName=details.get('firstname', None),
            familyName=details.get('lastname', None),
            avatar=summary.get('avatar', None),
            bio=summary.get('bio', None),
            email=details.get('email', None),
            birthdate=details.get('birthdate', None),
            telephone=details.get('mobile', None),
            gender=details.get('gender', None)
        )

        envelope.add_namespace("critique", LINK_RELATIONS_URL)

        envelope.add_control("self", href=api.url_for(User, nickname=nickname))
        envelope.add_control("profile", href=CRITIQUE_USER_PROFILE)
        envelope.add_control("collection", href=api.url_for(Users))
        envelope.add_control_edit_user(nickname)
        envelope.add_control_delete_user(nickname)
        envelope.add_control_user_inbox(nickname)
        envelope.add_control_user_river(nickname)
        envelope.add_control_user_ratings(nickname)
        envelope.add_control_up(api.url_for(Users))

        # +";" + CRITIQUE_USER_PROFILE)
        return Response(json.dumps(envelope), 200, mimetype=MASON)

    def put(self, nickname):
        '''
        Modifies mutable attributes of the specified user.

        REQUEST ENTITY BODY:
         * Media type: JSON
         * Profile: Critique_User

        Semantic descriptors used in template: givenName(optional), familyName(optional),
        avatar(optional), bio(optional), email(optional), birthdate(optional), telephone(optional),
        gender(optional)

        RESPONSE STATUS CODE:
         * Returns 204 + the url of the edited resource in the Location header
         * Return 400 User info is not well formed or entity body is missing.
         * Return 404 If user not found with given nickname.
         * Return 415 if it receives a media type != application/json
         * Return 422 Conflict if there is another user with the same nickname, email, mobile

        NOTE:
         * The attribute givenName is obtained from the column users_profile.firstname
         * The attribute familyName is obtained from the column users_profile.lastname
         * The attribute telephone is obtained from the column users_profile.mobile
         * The rest of attributes match one-to-one with column names in the
           database.

        NOTE:
        The: py: method:`Connection.modify_user()` receives as a parameter a
        dictionary with the following format.

            {
                'summary': {
                    'nickname': '',
                    'registrationdate': ,
                    'bio': '',
                    'avatar': ''
                },
                'details': {
                    'lastlogindate': ,
                    'firstname': '',
                    'lastname': '',
                    'email': '',
                    'mobile': '',
                    'gender': '',
                    'birthdate': '',
                }
            }
        '''

        if JSON != request.headers.get("Content-Type", ""):
            return create_error_response(415, "UnsupportedMediaType",
                                         "Use a JSON compatible format")

        user_db = g.con.get_user(nickname)
        if not user_db:
            return create_error_response(404, "User not found.")

        summary = user_db['summary']
        details = user_db['details']

        request_body = request.get_json()
        if not request_body:
            return create_error_response(415, "Format of the input is not json.")

        email = request_body.get('email', None)
        if email is not None and details['email'] != email and g.con.contains_user_email(email):
            return create_error_response(
                422, "Nickname, email, or mobile already exist in the users list.")

        summary['avatar'] = request_body.get(
            'avatar', summary['avatar'])
        summary['bio'] = request_body.get(
            'bio', summary['bio'])

        details['firstname'] = request_body.get(
            'givenName', details['firstname'])
        details['lastname'] = request_body.get(
            'familyName', details['lastname'])
        details['mobile'] = request_body.get(
            'telephone', details['mobile'])
        details['email'] = request_body.get(
            'email', details['email'])
        details['birthdate'] = request_body.get(
            'birthdate', details['birthdate'])
        details['gender'] = request_body.get(
            'gender', details['gender'])

        if not g.con.modify_user(nickname, summary, details):
            return create_error_response(500, "The system has failed. Please, contact the administrator.")

        return Response(status=204,
                        headers={"Location": api.url_for(User, nickname=nickname)})

    def delete(self, nickname):
        '''
        Deletes the specified user.

        :param str nickname: The nickname of the user. Example: Scott.

        RESPONSE STATUS CODE:

            * If the user is deleted returns 204.
            * If the nickname does not exist return 404
            * If there was a db error return 500

        '''
        # PEROFRM OPERATIONS

        userExist = g.con.contains_user(nickname)
        if not userExist:
            return create_error_response(404, "User not found.")

        # Try to delete the user. If it could not be deleted, the database
        # returns None.
        try:
            if g.con.delete_user(nickname):
                # RENDER RESPONSE
                return Response('', 204)
            else:
                # GENERATE ERROR RESPONSE
                return create_error_response(404, "User not found.")
        except:
            return create_error_response(500,
                                         "The system has failed. Please, contact the administrator.")

class UserRatings(Resource):
    '''
    Contains the ratings list with ratings from all other users to this specific user.
    It should have a track of the ratings made by users to each other including the person
    who made the rating and that who received it. Also, it contains the rating id to keep
    track of the ratings.
    '''

    def post(self, nickname):
        '''
        Adds a new rating in the database.

        REQUEST ENTITY BODY:
         * Media type: JSON
         * Profile: rating-profile

        Semantic descriptors used in template: ratingId (mandatory), sender (mandatory), receiver (mandatory),
                            bestRating(optional), ratingValue(mandatory)

        RESPONSE STATUS CODE:
         * Returns 201 + the url of the new resource in the Location header if the rating was successfully created
         * Return 400 if Rating info is not well formed or entity body is missing.
         * Return 404 if User not found.
         * Return 415 if it receives a media type != application/json
         * Return 422 if Sender or reciever not found
         * Return 500 in case of system failure.

        NOTE:
         * The attribute givenName is obtained from the column users_profile.firstname
         * The attribute familyName is obtained from the column users_profile.lastname
         * The rest of attributes match one-to-one with column names in the
           database.

        NOTE:
        The: py: method:`Connection.create_rating()` receives as a parameter a
        dictionary with the following format.

            {
                'rating_id': '',
                'timestamp': '',
                'sender': '',
                'receiver': '',
                'rating': ''
            }
        '''

        if JSON != request.headers.get("Content-Type", ""):
            abort(415)

        # PARSE THE REQUEST:
        request_body = request.get_json(force=True)
        if not request_body:
            return create_error_response(415,
                                         "Unsupported Media Type")

        userExist = g.con.contains_user(nickname)
        if not userExist:
            return create_error_response(404, "User not found.")

        # pick up rating_id so we can check for conflicts
        try:
            sender = request_body["sender"]
            rating = request_body["ratingValue"]
        except KeyError:
            return create_error_response(400,
                                         "Wrong request format")
        # optional fields

        userExist = g.con.contains_user(sender)
        if not userExist:
            return create_error_response(404, "Sender not found.")

        try:
            rating_id = g.con.create_rating(sender, nickname, rating)
        except ValueError:
            return create_error_response(400,
                                         "Wrong request format")

        # CREATE RESPONSE AND RENDER
        return Response(status=201,
                        headers={"Location": api.url_for(Rating,
                                                         nickname=nickname, ratingId=rating_id)})

    def get(self, nickname):
        '''
        Extracts the ratings given to the user.

        INPUT PARAMETER:

        :param str nickname: The nickname of the user. Example: `Scott`.

        OUTPUT:
            * Return 200 if the nickname exists.
            * Return 404 if the nickname not found.

        RESPONSE ENTITY BODY:

        OUTPUT:
            * Media type: application/vnd.mason+json
                https://github.com/JornWildt/Mason
            * Profile: Rating
                /profiles/rating-profile

        Link relations used in items: self, profile, sender, receiver, up

        Semantic descriptions used in items: ratingId, bestRating, ratingValue, sender, receiver

        Link relations used in links: self, add-rating, up

        Semantic descriptors used in template: items

        NOTE:
            * The attribute ratingId is obtained from the column ratings.rating_id
            * The attribute ratingValue is obtained from the column ratings.rating

        NOTE:
            * Database returns in this format
                {
                    rating_id: "string",
                    timestamp: "number",
                    sender: "string",
                    receiver: "string",
                    rating: "string",
                }
        '''

        userExist = g.con.contains_user(nickname)
        if not userExist:
            return create_error_response(404, "User not found.")

        # PERFORM OPERATIONS
        # create ratings list
        ratings_db = g.con.get_ratings(receiver=nickname)

        # FILTER AND GENERATE THE RESPONSE
        # Create the envelope
        envelope = CritiqueObject()

        items = envelope["items"] = []

        for rating in ratings_db:
            item = CritiqueObject(
                ratingId=rating["rating_id"],
                bestRating=10,
                ratingValue=rating['rating'],
                sender=rating['sender'],
                receiver=rating['receiver']
            )

            items.append(item)
            item.add_control_sender(rating['sender'])
            item.add_control_receiver(rating['receiver'])
            item.add_control("self",
                             href=api.url_for(Rating, nickname=nickname, ratingId=rating["rating_id"]))
            item.add_control("profile", href=CRITIQUE_RATING_PROFILE)
            item.add_control_up(api.url_for(UserRatings, nickname=nickname))

        envelope.add_namespace("critique", LINK_RELATIONS_URL)

        envelope.add_control("self", href=api.url_for(
            UserRatings, nickname=nickname))
        envelope.add_control_add_rating(nickname)
        envelope.add_control_up(api.url_for(User, nickname=nickname))

        # RENDER
        # +";" + CRITIQUE_RATING_PROFILE)
        return Response(json.dumps(envelope), 200, mimetype=MASON)

class UserInbox(Resource):

    '''
    Contains private posts sent to the user, it should include text as the actual post,
    however, rating is optional in the message.
    '''

    def get(self, nickname):
        '''
        Get posts sent to the user which are currently not public

        INPUT PARAMETER:

       :param user_id: default is None, takes the user id of the user
            that you want the posts of. if the parameter is None, it
            will raise a ValueError exception.
        :type nickname: nickname of the user

        OUTPUT:
            * Return 200 if the nickname exists.
            * Return 404 if the nickname not found.

        RESPONSE ENTITY BODY:

        OUTPUT:
            * Media type: application/vnd.mason+json
                https://github.com/JornWildt/Mason
            * Profile: Inbox
                /profiles/post_profile

        Link relations used in items: self, profile, sender, receiver, up, delete, edit

        Semantic descriptions used in items: sender, receiver, timestamp, postId, replyTo, body, anonymous, public

        Link relations used in links: self, add-post, add-reply, up

        Semantic descriptors used in template: items

        '''

        userExist = g.con.contains_user(nickname)
        if not userExist:
            return create_error_response(404, "User not found.")

        # FILTER AND GENERATE THE RESPONSE
        # Create the envelope
        envelope = CritiqueObject()

        items = envelope["items"] = []

        # PEFORM OPERATIONS INITIAL CHECKS
        # Get the post from db
        post_db = g.con.get_posts_by_user(nickname, False)
        if not post_db:
            return create_error_response(404, "Posts not found",
                                         "There is no posts for %s" % nickname)

        for post in post_db:
            item = CritiqueObject(
                postId=post["post_id"],
                sender=post['sender'],
                timestamp=post['timestamp'],
                bestRating=10,
                ratingValue=post['rating'],
                receiver=post['receiver'],
                replyTo=post['reply_to'],
                body=post['post_text'],
                anonymous=post['anonymous'],
                public=post['public']
            )
            if (post['public'] == 0):
                # check if the post is not public and then append
                items.append(item)
                item.add_control("self",
                                    href=api.url_for(Post, postId=post['post_id']))
                item.add_control("profile", href=CRITIQUE_POST_PROFILE)
                item.add_control_delete_post(post['post_id'])
                item.add_control_edit_post(post['post_id'])
                item.add_control_sender(post['sender'])
                if post["receiver"] is not None:
                    item.add_control_receiver(post["receiver"])
                item.add_control_add_reply(post['post_id'])
                item.add_control_up(api.url_for(UserInbox, nickname=nickname))

        envelope.add_namespace("critique", LINK_RELATIONS_URL)
        envelope.add_control("profile", href=CRITIQUE_POST_PROFILE)
        envelope.add_control("self", href=api.url_for(
            UserInbox, nickname=nickname))
        envelope.add_control_up(api.url_for(User, nickname=nickname))

        # RENDER
        # +";" + CRITIQUE_POST_PROFILE)
        return Response(json.dumps(envelope), 200, mimetype=MASON)

    def post(self, nickname):
        '''
        Creates a new post to the list of posts. Returns the post URI.

        INPUT PARAMETERS:
        :param nickname: nickname of the user who is sending.
        :type nickname: string
        :param receiver_nickname: nickname of the user that we are sending to.
        :type receiver_nickname: string

        REQUEST ENTITY BODY:
        * Media type: application/vnd.mason+json
                https://github.com/JornWildt/Mason
        * Profile: Post Profile
                /critique/profiles/post_profile

        Semantic descriptors used in template: post_text(mandatory),
        anonymous(mandatory)

        RESPONSE STATUS CODE:
         * Returns 201 + the url of the new resource in the Location header
         * Return 400 Post info is not well formed or entity body is missing.
         * Return 415 if it receives a media type != application/json
         * Return 422 Conflict if the sender or receiver are not found

        NOTE:
        The: py: method:`Connection.create_post()` receives as a parameter
        a dictionary with the following format:
            {
                'post_id': '',
                'receiver': '',
                'timestamp': '',
                'reply_to': '',
                'post_text': '',
                'rating': '',
                'anonymous': '',
                'public': ''
            }
         '''
        # CONTENT TYPE CHECK
        if JSON != request.headers.get("Content-Type", ""):
            abort(415)

        # PARSE REQUEST
        request_body = request.get_json(force=True)
        if not request_body:
            return create_error_response(415,
                                         "Unsupported Media Type")

        # CHECK IF USER EXISTS
        userExist = g.con.contains_user(nickname)
        if not userExist:
            return create_error_response(404, "User not found")

        # check mandatory fields
        try:
            post_text = request_body["body"]
            sender_nickname = request_body["sender"]
            anonymous = request_body["anonymous"]
        except KeyError:
            return create_error_response(400, "Wrong request format")

        userExist = g.con.contains_user(sender_nickname)
        if not userExist:
            return create_error_response(404, "Sender user not found")

        new_post_id = g.con.create_post(sender_nickname=sender_nickname,
                                        receiver_nickname=nickname,
                                        reply_to=None,
                                        post_text=post_text,
                                        anonymous=anonymous,
                                        public=0,
                                        rating=None)
        if not new_post_id:
            return create_error_response(500, "Problem with database",
                                         "can not access database.")

        url = api.url_for(UserInbox, nickname=nickname, postId=new_post_id)

        return Response(status=201,  headers={"Location": url})

class UserRiver(Resource):
    '''
    Contains public posts sent to the user, it should include text as the actual post,
    however, rating is optional in the posts.
    '''

    def get(self, nickname):
        '''
        Get posts sent to the user which are currently public

        INPUT PARAMETER:

       :param user_id: default is None, takes the user id of the user
            that you want the posts of. if the parameter is None, it
            will raise a ValueError exception.
        :type nickname: nickname of the user

        OUTPUT:
            * Return 200 if the nickname exists.
            * Return 404 if the nickname not found.

        RESPONSE ENTITY BODY:

        OUTPUT:
            * Media type: application/vnd.mason+json
                https://github.com/JornWildt/Mason
            * Profile: River
                /profiles/post_profile

        Link relations used in items: self, profile

        Semantic descriptions used in items: sender, receiver, timestamp, postId, replyTo, body, anonymous, public

        Link relations used in links: self, add-post, add-reply

        Semantic descriptors used in template: items

        '''

        userExist = g.con.contains_user(nickname)
        if not userExist:
            return create_error_response(404, "User not found.",
                                         "can't find a user to retrieve their river")

        # FILTER AND GENERATE THE RESPONSE
        # Create the envelope
        envelope = CritiqueObject()

        items = envelope["items"] = []

        # PEFORM OPERATIONS INITIAL CHECKS
        # Get the post from db and create posts list
        post_db = g.con.get_posts_by_user(nickname, False)
        if not post_db:
            return create_error_response(404, "Message not found",
                                         "There is no a message with id %s" % nickname)

        for post in post_db:
            item = CritiqueObject(
                postId=post["post_id"],
                ratingValue=post['rating'],
                receiver=post['receiver'],
                replyTo=post['reply_to'],
                body=post['post_text'],
                timestamp=post['timestamp'],
                bestRating=10,
                anonymous=post['anonymous'],
                public=post['public'],
                sender=post["sender"]
            )

            if (post['public'] == 1):
                # check if the post is public and then append
                items.append(item)
                item.add_control("self",
                                 href=api.url_for(UserRiver, nickname=post['receiver']))
                item.add_control("profile", href=CRITIQUE_POST_PROFILE)
                item.add_control_delete_post(post['post_id'])
                item.add_control_edit_post(post['post_id'])
                item.add_control_sender(post['sender'])
                item.add_control_receiver(post['receiver'])
                item.add_control_up(api.url_for(UserRiver, nickname=post['receiver']))

        envelope.add_namespace("critique", LINK_RELATIONS_URL)
        envelope.add_control_user_river(nickname)
        envelope.add_control_add_reply(nickname)
        envelope.add_control_up(api.url_for(User, nickname=nickname))
        envelope.add_control("profile", href=CRITIQUE_POST_PROFILE)
        envelope.add_control("self", href=api.url_for(
            UserRiver, nickname=nickname))

        # if post['reply_to']:
        #     envelope.add_control("atom-thread:in-reply-to",
        #                          href=api.url_for(Post, postId=post['post_id']))
        # else:
        #     envelope.add_control("atom-thread:in-reply-to", href=None)

        # RENDER
        # +";" + CRITIQUE_POST_PROFILE)
        return Response(json.dumps(envelope), 200, mimetype=MASON)

class Ratings(Resource):
    '''
    Contains the ratings list with ratings from all other users to this specific user.
    It should have a track of the ratings made by users to each other including the person
    who made the rating and that who received it. Also, it contains the rating id to keep
    track of the ratings.
    '''

    def get(self):
        return Response('NOT IMPLEMENTED', 200)

class Post(Resource):
    '''
    Returns the information needed from a specific post where it contains the post text,
    timestamp, sender id and receiver id which should not be null value.
    Post_text is the post content that holds the data needed while rating is an integer of
    the ratings to the post from others.
    Public is an integer flag indicating if the post is public or private.
    While anonymous is the same but indicates whether the user wants to post it without being known.
    '''

    def get(self, postId):
        '''
        Extracts a post and all it's information.

        :param str postId: ID of the requested post

        OUTPUT:
         * Return 200 if the post exists.
         * Return 404 if the post is not found.
         * Return 500 in case of system failure.

        RESPONSE ENTITY BODY:

        OUTPUT:
         * Media type: application/vnd.mason+json
                https://github.com/JornWildt/Mason
         * Profile: Post
                /critique/profiles/post-profile/

        Link relations used: self, profile, add-reply, delete, edit,
        collection, up, sender, receiver
        '''
        post_db = g.con.get_post(postId)
        if not post_db:
            return create_error_response(404, "Post not found.")

        envelope = CritiqueObject(
            sender=post_db["sender"],
            receiver=post_db["receiver"],
            timestamp=post_db["timestamp"],
            postId=post_db["post_id"],
            body=post_db["post_text"],
            replyTo=post_db["reply_to"],
            anonymous=post_db["anonymous"],
            public=post_db["public"],
            bestRating=10,
            ratingValue=post_db["rating"]
        )
        envelope.add_namespace("critique", LINK_RELATIONS_URL)

        envelope.add_control("self", href=api.url_for(Post, postId=postId))
        envelope.add_control("profile", href=CRITIQUE_POST_PROFILE)
        envelope.add_control("collection", href=api.url_for(Posts))
        envelope.add_control_edit_post(postId=postId)
        envelope.add_control_add_reply(postId=postId)
        envelope.add_control_delete_post(post_id=postId)
        envelope.add_control_sender(nickname=post_db["sender"])
        up_url = api.url_for(Posts)
        if post_db["public"] == 1 and post_db["reply_to"] is None:
            up_url = api.url_for(UserRiver, nickname=post_db["receiver"])
        elif post_db["public"] == 0:
            up_url = api.url_for(UserInbox, nickname=post_db["receiver"])
        envelope.add_control_up(up_url)
        if post_db["receiver"] is not None:
            envelope.add_control_receiver(nickname=post_db["receiver"])

        # +";"+ CRITIQUE_POST_PROFILE)
        return Response(json.dumps(envelope), 200, mimetype=MASON)

    def post(self, postId):
        '''
        posts a post as reply.

        REQUEST ENTITY BODY:
         * Media type: JSON
         * Profile: Critique_Post

        RESPONSE STATUS CODE:
         * Returns 201 + the url of the new resource in the Location header
         * Return 400 Post info is not well formed or entity body is missing.
         * Return 404 Post not found.
         * Return 415 if it receives a media type != application/json
         * Return 422 Sender not found.

        NOTE:
        The: py: method:`Connection.create_post()` receives as a parameter
        a dictionary with the following format:
            {
                'post_id': '',
                'sender': '',
                'timestamp': '',
                'reply_to': '',
                'post_text': '',
                'rating': '',
                'anonymous': '',
                'public': ''
            }
        '''
        # check format
        if JSON != request.headers.get("Content-Type", ""):
            abort(415)

        # check if postId is available
        postExists = g.con.contains_post(post_id=postId)
        if not postExists:
            return create_error_response(404, "Post not found",
                                         "There is no post with id %s" % postId)

        # parse request
        request_body = request.get_json(force=True)
        if not request_body:
            return create_error_response(415, "Unsupported Media Type")

        try:
            sender = request_body['sender']
            post_text = request_body['body']
        except KeyError:
            return create_error_response(400, "Wrong request format",
                                         "Post body missing or sender nickname invalid")

        userExist = g.con.contains_user(sender)
        if not userExist:
            return create_error_response(404, "Sending user not found")

        # getting the receiving user
        # parent_post_db = g.con.get_post(postId)
        # reply_to = parent_post_db['post_id']

        new_post_id = g.con.create_post(sender_nickname=sender,
                                        receiver_nickname=None,
                                        reply_to=postId,
                                        post_text=post_text,
                                        anonymous=0,
                                        public=1,
                                        rating=None)
        if not new_post_id:
            return create_error_response(500, "Problem with database",
                                         "can not access database.")

        url = api.url_for(Post, postId=new_post_id)

        return Response(status=201,  headers={"Location": url})

    def delete(self, postId):
        '''
        Deletes a specific post.

        :param str postId: ID of the post to delete.

        RESPONSE STATUS CODE:
         * Return 204 Post deleted successfully.
         * Return 404 Post not found.
         * Return 500 The system has failed. Please, contact the administrator.
        '''
        postExist = g.con.contains_post(postId)
        if not postExist:
            return create_error_response(404, "Post not found.")

        try:
            g.con.delete_post(postId)
        except:
            return create_error_response(500,
                                         "The system has failed. Please, contact the administrator.")
        return Response('', 204)

    def put(self, postId):
        '''
        Modifies the contents of a specified post.

        REQUEST ENTITY BODY:
         * Media type: JSON
         * Profile: Critique_Post

        Semantic descriptors used in template: post_text (optional)

        RESPONSE STATUS CODE:
         * Returns 204 + the url of the edited resource in the Location header
         * Return 404 If post not found with given id.
         * Return 415 if it receives a media type != application/json

        NOTE:
         * The attribute post_text is obtained from the column posts_profile.post_text
         * The rest of attributes match one-to-one with column names in the
           database.
        '''

        post_db = g.con.get_post(postId)
        if not post_db:
            return create_error_response(404, "Post not found.")

        if JSON != request.headers.get("Content-Type", ""):
            return create_error_response(415, "UnsupportedMediaType",
                                         "Use a JSON compatible format")

        request_body = request.get_json(force=True)
        if not request_body:
            return create_error_response(415, "Format of the input is not json.")
        try:
            post_db['post_text'] = request_body.get(
                'body', post_db['post_text'])
            post_db['rating'] = request_body.get(
                'ratingValue', post_db['rating'])
            post_db['public'] = request_body.get(
                'public', post_db['public'])
        except KeyError:
            return create_error_response(400, "Wrong request format",
                                         "Can't acquire, either post body, rating or public value")
        if not g.con.modify_post(postId, post_db['post_text'], post_db['rating'], post_db['public']):
            return create_error_response(500, "The system has failed. Please, contact the administrator.")

        return Response(status=204,
                        headers={"Location": api.url_for(Post, postId=postId)})

class Rating(Resource):
    '''
    Contains the ratings list with ratings from all other users to this specific user.
    It should have a track of the ratings made by users to each other including the person
    who made the rating and that who received it. Also, it contains the rating id to keep
    track of the ratings.
    '''

    def get(self, nickname, ratingId):
        '''
        Extract a rating from the database.

        Returns status code 404 if the ratingId does not exist in the database.

        INPUT PARAMETER
        :param str ratingId: ID of the rating to be retrieved from the system.

        OUTPUT:
         * Return 200 if the rating ID exists.
         * Return 404 if the rating ID not found.
         * Return 500 in case of system failure.

        RESPONSE ENTITY BODY:

        OUTPUT:
            * Media type: application/vnd.mason+json
                https://github.com/JornWildt/Mason
            * Profile: Rating Profile
                /critique/profiles/rating-profile

        Link relations used: add-rating, edit, delete, self, profile, collection.

        '''

        ratingExist = g.con.contains_rating(ratingId)
        if not ratingExist:
            return create_error_response(404, "Rating not found",
                                         "There is no rating with the given ID.")

        userExist = g.con.contains_user(nickname)
        if not userExist:
            return create_error_response(404, "Resquested user not found",
                                         "There is no user with the given nickname")
        # Filter and generate the response
        # Create the envelope
        # get the rating from DB
        rating_db = g.con.get_rating(ratingId)
        item = CritiqueObject(
            bestRating=10,
            ratingValue=rating_db["rating"],
            ratingId=rating_db["rating_id"],
            sender=rating_db["sender"],
            receiver=rating_db["receiver"]
        )
        item.add_namespace("critique", LINK_RELATIONS_URL)

        item.add_control("self", href=api.url_for(
            Rating, nickname=rating_db["receiver"], ratingId=rating_db["rating_id"]))
        item.add_control("profile", href=CRITIQUE_RATING_PROFILE)
        item.add_control("collection", href=api.url_for(
            UserRatings, nickname=rating_db["receiver"]))
        item.add_control_edit_rating(
            ratingId=rating_db["rating_id"], nickname=rating_db["receiver"])
        item.add_control_delete_rating(
            ratingId=rating_db["rating_id"], nickname=rating_db["receiver"])
        item.add_control_sender(nickname=rating_db["sender"])
        item.add_control_receiver(nickname=rating_db["receiver"])
        item.add_control_up(api.url_for(UserRatings, nickname=rating_db["receiver"]))

        # +";" + CRITIQUE_RATING_PROFILE)
        return Response(json.dumps(item), 200, mimetype=MASON)

    def put(self, nickname, ratingId):
        '''
        Modifies a rating.

        REQUEST ENTITY BODY:
         * Media type: JSON
         * Profile: Critique_User

        Semantic descriptors used in template: ratingId (required), new_rating (optional)

        RESPONSE STATUS CODE:
         * Returns 204 + the url of the edited resource in the Location header
         * Return 404 If rating not found with given id.
         * Return 415 if it receives a media type != application/json
         * Return 500 if there was a db error

        NOTE:
         * The attribute ratingId is obtained from the column ratings_profile.ratingId
         * The attribute new_rating is obtained from the column ratings_profile.new_rating
         * The rest of attributes match one-to-one with column names in the
           database.
        '''

        rating_db = g.con.get_rating(ratingId)
        if not rating_db:
            return create_error_response(404, "User not found.")

        if JSON != request.headers.get("Content-Type", ""):
            return create_error_response(415, "UnsupportedMediaType",
                                         "Use a JSON compatible format")

        request_body = request.get_json(force=True)
        if not request_body:
            return create_error_response(415, "Format of the input is not json.")
        try:
            rating_db['rating'] = request_body.get(
                'ratingValue', rating_db['rating'])
        except KeyError:
            return create_error_response(400, "Wrong request format",
                                         "rating missing")

        if not g.con.modify_rating(ratingId, rating_db['rating']):
            return create_error_response(500, "The system has failed. Please, contact the administrator.")

        return Response(status=204,
                        headers={"Location": api.url_for(Rating, nickname=nickname, ratingId=ratingId)})

    def delete(self, nickname, ratingId):
        '''
        Deletes the specified rating.

        :param integer ratingId: The id of the a rating. Example: rtg-015.

        RESPONSE STATUS CODE:

            * If the rating is deleted returns 204.
            * If the rating does not exist return 404
            * If there was a db error return 500
        '''
        # PEROFRM OPERATIONS

        ratingExist = g.con.contains_rating(ratingId)
        if not ratingExist:
            return create_error_response(404, "Rating not found.")

        # Try to delete the Rating. If it could not be deleted, the database
        # returns None.

        try:
            g.con.delete_rating(ratingId)
        except:
            return create_error_response(500,
                                         "The system has failed. Please, contact the administrator.")
        return Response('', 204)

class Posts(Resource):
    def get(self):
        return Response('NOT IMPLEMENTED', 200)

# Add the Regex Converter so we can use regex expressions when we define the
# routes
# Borrowed from lab exercises [1]
app.url_map.converters["regex"] = RegexConverter

# Define the routes
api.add_resource(Users, "/critique/api/users/",
                 endpoint="users")
api.add_resource(User, "/critique/api/users/<nickname>/",
                 endpoint="user")
api.add_resource(UserInbox, "/critique/api/users/<nickname>/inbox/",
                 endpoint="inbox")
api.add_resource(UserRiver, "/critique/api/users/<nickname>/river/",
                 endpoint="river")
api.add_resource(UserRatings, "/critique/api/users/<nickname>/ratings/",
                 endpoint="user-ratings")
api.add_resource(Rating, "/critique/api/users/<nickname>/ratings/<regex('rtg-\d+'):ratingId>/",
                 endpoint="rating")
api.add_resource(Posts, "/critique/api/posts/",
                 endpoint="posts")
api.add_resource(Post, "/critique/api/posts/<postId>/",
                 endpoint="post")

# Redirect profile

# Borrowed from lab exercises [1]
@app.route("/critique/profiles/<profile_name>/")
def redirect_to_profile(profile_name):
    return redirect(APIARY_PROFILES_URL + profile_name)

# Borrowed from lab exercises [1]
@app.route("/critique/link-relations/<rel_name>/")
def redirect_to_relations(rel_name):
    return redirect(APIARY_RELATIONS_URL + rel_name)

# Send our schema file(s)

# @app.route("/critique/schema/<schema_name>/")
# def send_json_schema(schema_name):
#     #return send_from_directory("static/schema", "{}.json".format(schema_name))
#     return send_from_directory(app.static_folder, "schema/{}.json".format(schema_name))

# Start the application
# DATABASE SHOULD HAVE BEEN POPULATED PREVIOUSLY
if __name__ == '__main__':  # Borrowed from lab exercises [1]
    # Debug true activates automatic code reloading and improved error messages
    app.run(debug=True)


# my_break.py    27Dec2021  crs - exercise break,continue

print("my_break")
i = 0
max = 7
while i < max:
    i = i + 1   # Always insure bumping
    print("i(", i, ")")
    if i % 5 == 0:
        print("Restart loop at i:", i, "via continue")
        continue

    if i % 6 == 0:
        print("Force loop stoping at 6")
        break
    if i % 2 == 0:
        print("i(", i, ") is even")
    if i % 3 == 0:
        print("i(", i, ") is divisible by 3")
    if i % 5 == 0:
        print("i(", i, ") is divisible by 5")


import unittest

from c2pa.cai_tool import encode_hashlink

class TestEncodeHashlink(unittest.TestCase):
    def test_hashlink(self):
        testing_data = b'Hello World!'
        mh = encode_hashlink(testing_data)
        self.assertEqual(mh, b'zQmWvQxTqbG2Z9HPJgG57jjwR154cKhbtJenbyYTWkjgF3e')

if __name__ == '__main__':
    unittest.main()


import queue

#How to Create a queue
my_q= queue.Queue()
#A queue takes a number and then uses it goes back and uses another number which I
# I haven't used and uses and pops the number out of the list everytime it uses itONCE
my_numbers=[2,56,837,4700,63527]
#Each thread will take one of the numbers and process the numbers for many diff
# We want multiple threads to obtain data concurrently from a source
'''
reasons: e.g:
    * port check
    * factorial calc
'''
# Next it will go to the next element which hasn't been processed yet
for nums in my_numbers:
    my_q.put(nums)

print(my_q.get())#prints 2
#.get method gets the first element and works out to FIFO

#To Create a LIFO queue
q= queue.LifoQueue()
numbers= [1,2,3,4,5,6,7]
for my_nums in numbers:
    q.put(my_nums)
print(q.get())#outputs 7


# imports
import streamlit as st
import pdfminer
import requests
#from pdfminer import high_level
from pdfminer.layout import LAParams
import pandas as pd
import arxiv
import textwrap
from bs4 import BeautifulSoup
import openai
import streamlit as st
from streamlit_tags import st_tags
from io import BytesIO

# converts pdf to text
@st.experimental_memo()
def pdf_2_text(file: BytesIO) -> str:
    with open(file, "rb") as fp:
        text = pdfminer.high_level.extract_text(
                    fp,
                    codec="utf-8",
                    laparams=LAParams(
                        line_margin=0.5,
                        word_margin=0.1,
                        boxes_flow=0.5,
                        detect_vertical=True,
                        all_texts=True,),
                    maxpages=0,)
    return text

# chunks large texts to 4000 tokens
def chunk_text(text):
    return textwrap.wrap(text, 4000)

# texts from the web
def url_2_text(url):
    response = requests.get(url)
    soup = BeautifulSoup(response.content, 'html.parser')
    for tag in soup.find_all():
        tag.attrs = {}
    text = soup.get_text()
    return textwrap.fill(text, width=150)

# arxiv pdfs
def search_arxiv(query):
    search = arxiv.Search(
        query = query,
        max_results = 300,
        sort_by = arxiv.SortCriterion.SubmittedDate,
        sort_order = arxiv.SortOrder.Descending
        )

    all_data = []
    for result in search.results():
        temp = ["","","","",""]
        temp[0] = result.title
        temp[1] = result.published
        temp[2] = result.entry_id
        temp[3] = result.summary
        temp[4] = result.pdf_url
        all_data.append(temp)

    column_names = ['Title','Date','Id','Summary','URL']
    df = pd.DataFrame(all_data, columns=column_names)

    print("Number of papers extracted : ",df.shape[0])
    return df
@st.experimental_memo()
def arxiv_2_file(url, filename):
    url = url
    filename = filename+'.pdf'
    response = requests.get(url)

    if response.status_code == 200:
        with open(filename, "wb") as f:
            f.write(response.content)
        res = "PDF file downloaded successfully."
    else:
        res = "Failed to download the PDF file."
    return res

# summerizer
def get_completion(prompt, model="gpt-3.5-turbo"): # Andrew mentioned that the prompt/ completion paradigm is preferable for this class
    messages = [{"role": "user", "content": prompt}]
    response = openai.ChatCompletion.create(
        model=model,
        messages=messages,
        temperature=0, # this is the degree of randomness of the model's output
    )
    return response.choices[0].message["content"]

def summerizer(text):
    prompt = f"""
        Your task is to extract and summerize relevant information from a text extracted from an article/paper by first
        cleaning the text of irrelevant or unclear information, that is, ignore information that is irrelevant to the
        purpose of the paper and any unclear part of the text.
        From the text below, delimited by triple quotes extract the relevant information. Limit to 2000 words.
        Text: ```{text}```
        """
    return get_completion(prompt)

# WEB PAGE
st.set_page_config(layout="centered", page_title="GPT-Powered Paper and Article Summerizer", page_icon="❄" )

c1, c2 = st.columns([0.32, 2])
# The snowflake logo will be displayed in the first column, on the left.
with c1:
    st.image("images/logo.jpg", width=85,)

with c2:
    st.caption("")
    st.title("GPT-Powered Paper and Article Summerizer")
# We need to set up session state via st.session_state so that app interactions don't reset the app.
if not "valid_inputs_received" in st.session_state:
    st.session_state["valid_inputs_received"] = False

############ SIDEBAR CONTENT ############
st.sidebar.write("")

# For elements to be displayed in the sidebar, we need to add the sidebar element in the widget.
# We create a text input field for users to enter their API key.
API_KEY = st.sidebar.text_input(
    "Enter your OPENAI API key",
    help="Once you created you OPENAI account, you can get your free API token in your settings page: https://platform.openai.com/account/api-keys",
    type="password",
)

openai.api_key = API_KEY

st.sidebar.markdown("---")
# Let's add some info about the app to the sidebar.
st.sidebar.write(
    """
App created by [Kabir Hamzah](https://twitter.com/kabir_hamzah) using [Streamlit](https://streamlit.io/)🎈 and [ChatGPT](https://chat.openai.com/) model.
""")

ArtTab, LocTab, ArcTab= st.tabs(["Article URL", "Local Paper", "Archive Link"])

with ArtTab:
    url = st.text_input("Provide url to desired article: ")
    if url:
        text = url_2_text(url)
        text = summerizer(text)
        st.text(textwrap.fill(text, 150))

with LocTab:
    file = st.file_uploader("Select PDF file")
    if file:
        doc = pdf_2_text(file.name)
        texts = chunk_text(doc)
        # print(len(texts[0]))
        # print(texts[0])
        all_sum_texts = []
        for text in texts:
            sum_text = summerizer(text)
            all_sum_texts.append(sum_text)
        st.text(textwrap.fill(' '.join(all_sum_texts), 150))

with ArcTab:
    topic = st.text_input("Enter the topic you need to search for: ")
    if topic:
        df = search_arxiv(topic)
        st.dataframe(df, use_container_width=True)
        url_and_name = st.text_input('Enter url and name of desired paper to download',
        help="Copy url from the table above and entire a desired file name, separate the url and name using a comma.")

        if url_and_name:
            url = url_and_name.split(',')[0]
            filename = url_and_name.split(',')[1]
            text = arxiv_2_file(url, filename)
            #text = summerizer(text)
            st.text(text)


import numpy as np
import cv2 as cv
import glob
import json
import math
import os

from quartonian_handler import QuartonianHandler

def get_tranformation_matrix_from_transform(trans: list, rotation_matrix) -> list:

    transformation_matrix = np.identity(4)

    for i in range(3):
        for j in range(3):
            transformation_matrix[i][j] = rotation_matrix[i][j]

        transformation_matrix[i][3] = trans[i]

    return transformation_matrix

rows = 8
columns = 8

square_length_m = 0.024375

file_path = "logs/real_life_checkboard_short/train/"
log_path = "logs"
show_imgs = False

# termination criteria
criteria = (cv.TERM_CRITERIA_EPS + cv.TERM_CRITERIA_MAX_ITER, 30, 0.001)

# prepare object points, like (0,0,0), (1,0,0), (2,0,0) ....,(6,5,0)
objp = np.zeros(((rows-1)*(rows-1), 3), np.float32)

objp[:,:2] = np.mgrid[0:(rows-1),0:(rows-1)].T.reshape(-1,2)

objp = objp[:,:] * square_length_m

objpoints = []
imgpoints = []
images = glob.glob(file_path + '*.png')

images.sort()

print("Starting calibration")

if len(images) == 0:
    raise Exception("No calibration images found, please ensure that the file path is correct")

if len(images) < 10:
    raise Exception("Please include at least 10 images or more for proper calibration")

invalid_imgs = []
file_names = []

for i, path_name in enumerate(images):
    file_name = path_name[len(file_path):]

    print("Loading image: ", file_name)

    img = cv.imread(path_name)
    gray = cv.cvtColor(img, cv.COLOR_RGB2GRAY)

    if show_imgs:
        cv.imshow(file_name, gray)
        cv.waitKey(0)

    ret, corners = cv.findChessboardCorners(gray, (rows-1, columns-1), None)

    print("Board found" if ret else "Board not found!")

    if ret:
        objpoints.append(objp)
        corners2 = cv.cornerSubPix(gray, corners, (11,11), (-1,-1), criteria)
        imgpoints.append(corners2)

        cv.drawChessboardCorners(img, (rows-1, columns-1), corners2, ret)

        if show_imgs:
            cv.imshow(file_name + " with corners", img)
            cv.waitKey(0)

        file_names.append(file_name)

    else:
        invalid_imgs.append(file_name)

    print()

cv.destroyAllWindows()

print(len(invalid_imgs), "/", len(images), " images were invalid", sep="")

ret, mtx, dist, rvecs, tvecs = cv.calibrateCamera(objpoints, imgpoints, gray.shape[::-1], None, None)

q = QuartonianHandler()

print("Camera successfully calibrated" if ret else "Failed to calibrate camera")

if ret:
    calibration_data = {
        "mtx": np.around(mtx, decimals=14).tolist(),
        "dist": np.around(dist, decimals=14).tolist(),
    }

    with open(os.path.join(log_path, "camera_cal.json"), "w") as calibration_file:
        json.dump(calibration_data, calibration_file, indent=4)

    transforms = {"frames":[]}

    for i, euler_rotation in enumerate(rvecs):
        rotation_matrix = q.convert_euler_to_rotation_matrix(euler_rotation)

        # TODO: CHANGE
        transform = get_tranformation_matrix_from_transform(tvecs[i], rotation_matrix)

        view_data = {"file_path": file_names[i],
                     "transform_matrix": np.around(transform, decimals=14).tolist()}

        transforms["frames"].append(view_data)

    with open(os.path.join(log_path, "camera_transforms.json"), "w") as transform_file:
        json.dump(transforms, transform_file, indent=4)

    """with open(os.path.join(log_path, "camera_rvecs.json"), "w") as rvecs_file:
        rvecs_dict = {}
        for i, vec in enumerate(rvecs):
            rvecs_dict[file_names[i]] = vec.tolist()
        json.dump(rvecs_dict, rvecs_file, indent=4)"""

    print("Finished camera calibration")


import collections

class Node:
    """"Definition for a Node."""
    def __init__(self, val=0, neighbors=None):
        self.val = val
        self.neighbors = neighbors if neighbors is not None else []

def cloneGraph(self, node: 'Node') -> 'Node':
    if not node:
        return node

    is_visited = {node: Node(node.val)}
    to_visit = collections.deque([node])

    while to_visit:
        curr_node = to_visit.pop()

        for n in curr_node.neighbors:
            if n not in is_visited:
                neighbor_clone = Node(n.val)
                to_visit.append(n)
                is_visited.update({n: neighbor_clone})
                is_visited[curr_node].neighbors.append(neighbor_clone)
            else:
                is_visited[curr_node].neighbors.append(is_visited[n])

    return is_visited[node]


# -*- coding: utf-8 -*-
"""
Created on Wed Dec 16 10:14:18 2020

@author: Babou
"""

import numpy as np
import math
import matplotlib.pyplot as plt

def sigmoid(Z):
    A = 1./(1+np.exp(-Z))
    return A

#def sigmoid_backward(dA, Z):
#    dZ = dA * sigmoid(Z) * (1-sigmoid(Z))
#    return dZ

def sigmoid_backward(Z):
    dZ = sigmoid(Z) * (1-sigmoid(Z))
    return dZ

def relu(Z):
    A = np.maximum(0,Z)
    return A

def relu_backward(dA, Z):
    dZ = np.array(dA, copy=True) # just converting dz to a correct object.
    # When z <= 0, you should set dz to 0 as well.
    dZ[Z <= 0] = 0
    return dZ

def tanh_backward(dA, A):
    dZ = np.multiply(dA, 1 - np.power(A, 2))
    return dZ

def softmax(Z):
    e_Z = np.exp(Z - np.max(Z))
    A = e_Z / e_Z.sum(axis=0)
    return A

def softmax_backward(A_softmax, Y):
    return A_softmax - Y

def batchnorm_forward(X, gamma, beta):
    mu = np.mean(X, axis=0)
    var = np.var(X, axis=0)

    X_norm = (X - mu) / np.sqrt(var + 1e-8)
    out = gamma * X_norm + beta

    cache = (X, X_norm, mu, var, gamma, beta)

    return out, cache, mu, var

def dict_to_vector(dictionary):

    vector = []
    dim_dict = {}

    i = 0
    for key, value in dictionary.items():
        dim_dict[key] = {
            "rows":dictionary[key].shape[0],
            "cols":dictionary[key].shape[1]
        }
        vector_i = np.reshape(dictionary[key], (-1,1))
        if i == 0:
            vector = vector_i
        else:
            vector = np.concatenate((vector, vector_i), axis=0)
        i = i + 1

    return vector, dim_dict

def vector_to_dict(vector, dim_dict):

    dictionary = {}

    i_dict = 0
    for key, value in dim_dict.items():
        rows = dim_dict[key]['rows']
        cols = dim_dict[key]['cols']
        dict_i = np.zeros((rows, cols))
        for r in range(rows):
            for c in range(cols):
                dict_i[r][c] = vector[i_dict]
                i_dict = i_dict + 1
        dictionary[key] = dict_i

    return dictionary

def check_gradients(layers_defs, keep_prob, parameters, grads, X, Y, epsilon):

    #This method must not include: L2 reg, Dropout, or any optimization method
    m = X.shape[1]
    L = len(layers_defs)

    vector_params, dim_dict = dict_to_vector(parameters)
    J_plus = np.zeros((vector_params.shape[0], 1))
    J_minus = np.zeros((vector_params.shape[0], 1))
    gradapprox = np.zeros((vector_params.shape[0], 1))
    for i in range(vector_params.shape[0]):
        vector_thetaplus = np.copy(vector_params)
        vector_thetaplus[i] = vector_thetaplus[i] + epsilon
        dict_thetaplus = vector_to_dict(vector_thetaplus, dim_dict)
        thetaplus_propagate = forward_propagate(layers_defs, keep_prob, dict_thetaplus, X)
        J_plus[i] = (1./m) * (-np.dot(Y,np.log(thetaplus_propagate['A' + str(L-1)]).T) - np.dot(1-Y, np.log(1-thetaplus_propagate['A' + str(L-1)]).T))

        vector_thetaminus = np.copy(vector_params)
        vector_thetaminus[i] = vector_thetaminus[i] - epsilon
        dict_thetaminus = vector_to_dict(vector_thetaminus, dim_dict)
        thetaminus_propagate = forward_propagate(layers_defs, keep_prob, dict_thetaminus, X)
        J_minus[i] = (1./m) * (-np.dot(Y,np.log(thetaminus_propagate['A' + str(L-1)]).T) - np.dot(1-Y, np.log(1-thetaminus_propagate['A' + str(L-1)]).T))

        gradapprox[i] = (J_plus[i] - J_minus[i])/(2*epsilon)

    grads_sorted = {}
    inverted_grads_keys = list(grads.keys())[::-1]
    for key in inverted_grads_keys:
        if((key[:2]!="dA") and (key[:2]!="dZ")):
            grads_sorted[key] = grads[key]
    vector_grads, dim_dict = dict_to_vector(grads_sorted)

    numerator = np.linalg.norm(vector_grads - gradapprox)
    denominator = np.linalg.norm(vector_grads) + np.linalg.norm(gradapprox)
    difference = numerator / denominator

    return difference

def initialize_parameters(layers_defs, type_init):

    L = len(layers_defs)
    coeff = 0.01

    parameters = {}

    for l in range(1, L):
        if (type_init=="He"):
            coeff = np.sqrt(2./layers_defs[l-1])
        parameters['W' + str(l)] = np.random.randn(layers_defs[l], layers_defs[l-1]) * coeff
        parameters['b' + str(l)] = np.zeros((layers_defs[l], 1))

    return parameters

def initialize_velocity(layers_defs, parameters):

    L = len(layers_defs)
    v = {}

    for l in range(1, L):
        v["dW" + str(l)] = np.zeros((parameters['W' + str(l)].shape))
        v["db" + str(l)] = np.zeros((parameters['b' + str(l)].shape))

    return v

def initialize_adam(layers_defs, parameters) :

    L = len(layers_defs)
    v = {}
    s = {}

    for l in range(1, L):
        v["dW" + str(l)] = np.zeros((parameters['W' + str(l)].shape))
        v["db" + str(l)] = np.zeros((parameters['b' + str(l)].shape))
        s["dW" + str(l)] = np.zeros((parameters['W' + str(l)].shape))
        s["db" + str(l)] = np.zeros((parameters['b' + str(l)].shape))

    return v, s

def random_mini_batches(mini_batch_size, train_X, train_Y):

    m = train_X.shape[1]
    mini_batches = []

    permutation = list(np.random.permutation(m))
    shuffled_X = train_X[:, permutation]
    shuffled_Y = train_Y[:, permutation].reshape((train_Y.shape[0],m))
    num_complete_minibatches = math.floor(m/mini_batch_size)
    for k in range(0, num_complete_minibatches):
        mini_batch_X = shuffled_X[:, k*mini_batch_size: (k+1)*mini_batch_size]
        mini_batch_Y = shuffled_Y[:, k*mini_batch_size: (k+1)*mini_batch_size]
        mini_batch = (mini_batch_X, mini_batch_Y)
        mini_batches.append(mini_batch)

    if m % mini_batch_size != 0:
        mini_batch_X = shuffled_X[:, (k+1)*mini_batch_size:m]
        mini_batch_Y = shuffled_Y[:, (k+1)*mini_batch_size:m]
        mini_batch = (mini_batch_X, mini_batch_Y)
        mini_batches.append(mini_batch)

    return mini_batches

def forward_propagate(layers_defs, keep_prob, parameters, X):

    L = len(layers_defs)
    FW_propagate = {};
    FW_propagate['A0'] = X

    for l in range(1, L-1):
        Zl = np.dot(parameters['W' + str(l)], FW_propagate['A' + str(l-1)]) + parameters['b' + str(l)]
        FW_propagate['Z' + str(l)] = Zl
        FW_propagate['A' + str(l)] = relu(Zl)

        if keep_prob:
            FW_propagate['D' + str(l)] = np.random.randn(FW_propagate['A' + str(l)].shape[0], FW_propagate['A' + str(l)].shape[1])
            FW_propagate['D' + str(l)] = FW_propagate['D' + str(l)] < keep_prob
            FW_propagate['A' + str(l)] = FW_propagate['A' + str(l)] * FW_propagate['D' + str(l)]
            FW_propagate['A' + str(l)] = FW_propagate['A' + str(l)] / keep_prob

    ZL = np.dot(parameters['W' + str(L-1)], FW_propagate['A' + str(L-2)]) + parameters['b' + str(L-1)]
    FW_propagate['Z' + str(L-1)] = ZL
    FW_propagate['A' + str(L-1)] = sigmoid(ZL)

    return FW_propagate

def backward_propagate(layers_defs, keep_prob, lambd, parameters, X, Y, FW_propagate):

    L = len(layers_defs)
    BW_propagate = {}
    m = X.shape[1]

    BW_propagate['dA' + str(L-1)] =  - ( np.divide(Y, FW_propagate['A' + str(L-1)]) - np.divide(1 - Y, 1 - FW_propagate['A' + str(L-1)]) )
    BW_propagate['dZ' + str(L-1)] = BW_propagate['dA' + str(L-1)]*sigmoid_backward(FW_propagate['Z' + str(L-1)])

    for l in reversed(range(L-1)):
        BW_propagate['db' + str(l+1)] = (1./m)*np.sum(BW_propagate['dZ' + str(l+1)], axis = 1, keepdims = True)
        BW_propagate['dW' + str(l+1)] = (1./m)*np.dot(BW_propagate['dZ' + str(l+1)], FW_propagate['A' + str(l)].T) + (lambd/m)*parameters['W' + str(l+1)] ;
        BW_propagate['dA' + str(l)] = np.dot(parameters['W' + str(l+1)].T, BW_propagate['dZ' + str(l+1)]);

        if l != 0:
            if keep_prob:
                BW_propagate['dA' + str(l)] = BW_propagate['dA' + str(l)] * FW_propagate['D' + str(l)]
                BW_propagate['dA' + str(l)] = BW_propagate['dA' + str(l)] / keep_prob
            BW_propagate['dZ' + str(l)] = relu_backward(BW_propagate['dA' + str(l)], FW_propagate['Z' + str(l)])

    return BW_propagate

def update_parameters_with_gd(layers_defs, learning_rate, parameters, BW_propagate):

    L = len(layers_defs)

    for l in range(1, L):
        parameters["W" + str(l)] = parameters["W" + str(l)] - BW_propagate["dW" + str(l)]*learning_rate
        parameters["b" + str(l)] = parameters["b" + str(l)] - BW_propagate["db" + str(l)]*learning_rate

    return parameters

def update_parameters_with_momentum(layers_defs, learning_rate, beta, parameters, v, BW_propagate):

    L = len(layers_defs)

    for l in range(1, L):
        v["dW" + str(l)] = beta*v["dW" + str(l)] + (1 - beta)*BW_propagate['dW' + str(l)]
        v["db" + str(l)] = beta*v["db" + str(l)] + (1 - beta)*BW_propagate['db' + str(l)]
        parameters["W" + str(l)] = parameters["W" + str(l)] - learning_rate*v["dW" + str(l)]
        parameters["b" + str(l)] = parameters["b" + str(l)] - learning_rate*v["db" + str(l)]

    return parameters, v

def update_parameters_with_adam(layers_defs, learning_rate, beta1, beta2, epsilon, parameters, v, s, t, BW_propagate):

    L = len(layers_defs)
    v_corrected = {}
    s_corrected = {}

    for l in range(1, L):
        v["dW" + str(l)] = beta1*v["dW" + str(l)] + (1 - beta1)*BW_propagate['dW' + str(l)]
        v["db" + str(l)] = beta1*v["db" + str(l)] + (1 - beta1)*BW_propagate['db' + str(l)]
        v_corrected["dW" + str(l)] = v["dW" + str(l)] / (1 - np.power(beta1, t))
        v_corrected["db" + str(l)] = v["db" + str(l)] / (1 - np.power(beta1, t))
        s["dW" + str(l)] = beta2*s["dW" + str(l)] + (1 - beta2)*np.power(BW_propagate['dW' + str(l)], 2)
        s["db" + str(l)] = beta2*s["db" + str(l)] + (1 - beta2)*np.power(BW_propagate['db' + str(l)], 2)
        s_corrected["dW" + str(l)] = s["dW" + str(l)] / (1 - np.power(beta2, t))
        s_corrected["db" + str(l)] = s["db" + str(l)] / (1 - np.power(beta2, t))
        parameters["W" + str(l)] = parameters["W" + str(l)] - learning_rate * v_corrected["dW" + str(l)]/(np.square(s_corrected["dW" + str(l)]) + epsilon)
        parameters["b" + str(l)] = parameters["b" + str(l)] - learning_rate * v_corrected["db" + str(l)]/(np.square(s_corrected["db" + str(l)]) + epsilon)

    return parameters, v, s

def train_model(layers_defs, type_init, nb_iter, mini_batch_size, learning_rate, keep_prob, lambd, opt_method, beta, beta1, beta2, epsilon, train_X, train_Y, visualize=False):

    L = len(layers_defs);
    costs = np.zeros((nb_iter))
    m = train_X.shape[1]

    parameters = initialize_parameters(layers_defs, type_init)

    if opt_method == "gd":
        pass
    elif opt_method == "momentum":
        v = initialize_velocity(layers_defs, parameters)
    elif opt_method == "adam":
        t = 0
        v, s = initialize_adam(layers_defs, parameters)

    weights = {}

    for i in range(0, nb_iter):
        minibatches = random_mini_batches(mini_batch_size, train_X, train_Y)

        for minibatch in minibatches:

            (minibatch_X, minibatch_Y) = minibatch

            FW_propagate = forward_propagate(layers_defs, keep_prob, parameters, minibatch_X)
            BW_propagate = backward_propagate(layers_defs, keep_prob, lambd, parameters, minibatch_X, minibatch_Y, FW_propagate)

            L2_regularization_cost = 0
            for l in range(1, L-1):
                L2_regularization_cost = L2_regularization_cost + np.square(parameters['W' + str(l)]).sum()
            L2_regularization_cost = (lambd/(2*m))* L2_regularization_cost
            logprobs = np.multiply(np.log(FW_propagate['A' + str(L-1)]),minibatch_Y) + np.multiply(np.log(1 - FW_propagate['A' + str(L-1)]), 1 - minibatch_Y)
            cost = -1./m * np.sum(logprobs) + L2_regularization_cost
            #cost = (1./m) * (-np.dot(minibatch_Y,np.log(FW_propagate['A' + str(L-1)]).T) - np.dot(1-minibatch_Y, np.log(1-FW_propagate['A' + str(L-1)]).T)) + L2_regularization_cost
            costs[i] = cost

            if opt_method == "gd":
                parameters = update_parameters_with_gd(layers_defs, learning_rate, parameters, BW_propagate)
            elif opt_method == "momentum":
                parameters, v = update_parameters_with_momentum(layers_defs, learning_rate, beta, parameters, v, BW_propagate)
            elif opt_method == "adam":
                t = t + 1
                parameters, v, s = update_parameters_with_adam(layers_defs, learning_rate, beta1, beta2, epsilon, parameters, v, s, t, BW_propagate)

        if visualize:
            weights[i] = parameters["W1"]

        if ((i==0) or (i==nb_iter-1)):
            print("Cost at iteration: " + str(cost))

    return parameters, costs, BW_propagate, weights

def predict(layers_defs, keep_prob, parameters, X, nb_classes, display_probas=0):

    L = len(layers_defs)

    probas_predict = forward_propagate(layers_defs, keep_prob, parameters, X)['A' + str(L-1)];

    if(display_probas):
        print(probas_predict)

    m = X.shape[1]
    Y_predict = np.zeros((1,m))
    # convert probas to 0/1 predictions
    for i in range(0, probas_predict.shape[1]):
        if nb_classes <= 2:
            if probas_predict[0,i] > 0.5:
                Y_predict[0,i] = 1
            else:
                Y_predict[0,i] = 0
        else:
            Y_predict[0,i] = probas_predict[:,i].argmax()

    return Y_predict, probas_predict


import numpy as np
import tvm
from tvm import tir
from tvn.script import tir as T

@tvm.script.ir_module
class MatmulModule:
    @T.prim_func
    def main(
        X: T.Buffer[(1024, 1024), "float16"],
        Y: T.Buffer[(1024, 1024), "float16"],
        Z: T.Buffer[(1024, 1024), "float32"],
    ) -> None:
        T.func_attr({"global_symbol": "main", "tir.noalias": True})
        for i, j, k in T.grid(1024, 1024, 1024):
            with T.block("matmul"):
                vi, vj, vk = T.axis.remap("SSR", [i, j, k])
                with T.init():
                    Z[vi, vj] = T.float32(0)
                # We cast data from fp16 to fp32 because tensore core load fp16 but calculate fp32
                Z[vi, vj] += T.cast(X[vi, vk], "float32") * T.cast(Y[vj, vk], "float32")

def register_intrinsic():
    @T.prim_func
    def wmma_load_a_desc(a: T.handle, c: T.handle) -> None:
        A = T.match_buffer(
            a, (16, 16), "float16", align=128, offset_factor=16, scope="shared"
        )
        C = T.match_buffer(
            c, (16, 16), "float16", align=128, offset_factor=16, scope="wmma.matrix_a"
        )

        with T.block("root"):
            T.reads(A[0:16, 0:16])
            T.writes(C[0:16, 0:16])
            for i, j in T.grid(16, 16):
                with T.block("load"):
                    vii, vjj = T.axis.remap("SS", [i, j])
                    C[vii, vjj] = A[vii, vjj]

    @T.prim_func
    def wmma_load_a_impl(a: T.handle, c: T.handle) -> None:
        s1 = T.var("int32")
        s0 = T.var("int32")
        A = T.match_buffer(
            a,
            (16, 16),
            "float16",
            align=128,
            offset_factor=16,
            scope="shared",
            strides=[s1, s0],
        )
        C = T.match_buffer(
            c, (16, 16), "float16", align=128, offset_factor=16, scope="wmma.matrix_a"
        )

        with T.block("root"):
            T.reads(A[0:16, 0:16])
            T.writes(C[0:16, 0:16])
            T.evaluate(
                T.tvm_load_matrix_sync(
                    C.data,
                    16,
                    16,
                    16,
                    C.elem_offset // 256
                    + T.floordiv(T.floormod(C.elem_offset, 256), 16),
                    A.access_ptr("r"),
                    s1,
                    "row_major",
                    dtype="handle",
                )
            )

    @T.prim_func
    def wmma_load_b_desc(a: T.handle, c: T.handle) -> None:
        A = T.match_buffer(
            a, (16, 16), "float16", align=128, offset_factor=16, scope="shared"
        )
        C = T.match_buffer(
            c, (16, 16), "float16", align=128, offset_factor=16, scope="wmma.matrix_b"
        )

        with T.block("root"):
            T.reads(A[0:16, 0:16])
            T.writes(C[0:16, 0:16])
            for i, j in T.grid(16, 16):
                with T.block("load"):
                    vii, vjj = T.axis.remap("SS", [i, j])
                    C[vii, vjj] = A[vii, vjj]

    @T.prim_func
    def wmma_load_b_impl(a: T.handle, c: T.handle) -> None:
        s1 = T.var("int32")
        s0 = T.var("int32")
        A = T.match_buffer(
            a,
            (16, 16),
            "float16",
            align=128,
            offset_factor=16,
            scope="shared",
            strides=[s1, s0],
        )
        C = T.match_buffer(
            c, (16, 16), "float16", align=128, offset_factor=16, scope="wmma.matrix_b"
        )

        with T.block("root"):
            T.reads(A[0:16, 0:16])
            T.writes(C[0:16, 0:16])
            T.evaluate(
                T.tvm_load_matrix_sync(
                    C.data,
                    16,
                    16,
                    16,
                    C.elem_offset // 256
                    + T.floordiv(T.floormod(C.elem_offset, 256), 16),
                    A.access_ptr("r"),
                    s1,
                    "col_major",
                    dtype="handle",
                )
            )

    @T.prim_func
    def wmma_sync_desc(a: T.handle, b: T.handle, c: T.handle) -> None:
        A = T.match_buffer(
            a, (16, 16), "float16", align=128, offset_factor=16, scope="wmma.matrix_a"
        )
        B = T.match_buffer(
            b, (16, 16), "float16", align=128, offset_factor=16, scope="wmma.matrix_b"
        )
        C = T.match_buffer(
            c,
            (16, 16),
            "float32",
            align=128,
            offset_factor=16,
            scope="wmma.accumulator",
        )

        with T.block("root"):
            T.reads(C[0:16, 0:16], A[0:16, 0:16], B[0:16, 0:16])
            T.writes(C[0:16, 0:16])
            for i, j, k in T.grid(16, 16, 16):
                with T.block(""):
                    vii, vjj, vkk = T.axis.remap("SSR", [i, j, k])
                    C[vii, vjj] += T.cast(A[vii, vkk], "float32") * T.cast(
                        B[vjj, vkk], "float32"
                    )

    @T.prim_func
    def wmma_sync_impl(a: T.handle, b: T.handle, c: T.handle) -> None:
        A = T.match_buffer(
            a, (16, 16), "float16", align=128, offset_factor=16, scope="wmma.matrix_a"
        )
        B = T.match_buffer(
            b, (16, 16), "float16", align=128, offset_factor=16, scope="wmma.matrix_b"
        )
        C = T.match_buffer(
            c,
            (16, 16),
            "float32",
            align=128,
            offset_factor=16,
            scope="wmma.accumulator",
        )

        with T.block("root"):
            T.reads(C[0:16, 0:16], A[0:16, 0:16], B[0:16, 0:16])
            T.writes(C[0:16, 0:16])
            T.evaluate(
                T.tvm_mma_sync(
                    C.data,
                    C.elem_offset // 256
                    + T.floordiv(T.floormod(C.elem_offset, 256), 16),
                    A.data,
                    A.elem_offset // 256
                    + T.floordiv(T.floormod(A.elem_offset, 256), 16),
                    B.data,
                    B.elem_offset // 256
                    + T.floordiv(T.floormod(B.elem_offset, 256), 16),
                    C.data,
                    C.elem_offset // 256
                    + T.floordiv(T.floormod(C.elem_offset, 256), 16),
                    dtype="handle",
                )
            )

    @T.prim_func
    def wmma_fill_desc(c: T.handle) -> None:
        C = T.match_buffer(
            c,
            (16, 16),
            "float32",
            align=128,
            offset_factor=16,
            scope="wmma.accumulator",
        )

        with T.block("root"):
            T.reads()
            T.writes(C[0:16, 0:16])
            for i, j in T.grid(16, 16):
                with T.block("init"):
                    vii, vjj = T.axis.remap("SS", [i, j])
                    C[vii, vjj] = T.float32(0)

    @T.prim_func
    def wmma_fill_impl(c: T.handle) -> None:
        C = T.match_buffer(
            c,
            (16, 16),
            "float32",
            align=128,
            offset_factor=16,
            scope="wmma.accumulator",
        )
        with T.block("root"):
            T.reads()
            T.writes(C[0:16, 0:16])
            T.evaluate(
                T.tvm_fill_fragment(
                    C.data,
                    16,
                    16,
                    16,
                    C.elem_offset // 256
                    + T.floordiv(T.floormod(C.elem_offset, 256), 16),
                    T.float32(0),
                    dtype="handle",
                )
            )

    @T.prim_func
    def wmma_store_desc(a: T.handle, c: T.handle) -> None:
        A = T.match_buffer(
            a,
            (16, 16),
            "float32",
            align=128,
            offset_factor=16,
            scope="wmma.accumulator",
        )
        C = T.match_buffer(
            c, (16, 16), "float32", align=128, offset_factor=16, scope="global"
        )
        with T.block("root"):
            T.reads(A[0:16, 0:16])
            T.writes(C[0:16, 0:16])
            for i, j in T.grid(16, 16):
                with T.block("store"):
                    vii, vjj = T.axis.remap("SS", [i, j])
                    C[vii, vjj] = A[vii, vjj]

    @T.prim_func
    def wmma_store_impl(a: T.handle, c: T.handle) -> None:
        s1 = T.var("int32")
        s0 = T.var("int32")
        A = T.match_buffer(
            a,
            (16, 16),
            "float32",
            align=128,
            offset_factor=16,
            scope="wmma.accumulator",
        )
        C = T.match_buffer(
            c,
            (16, 16),
            "float32",
            align=128,
            offset_factor=16,
            scope="global",
            strides=[s1, s0],
        )
        with T.block("root"):
            T.reads(A[0:16, 0:16])
            T.writes(C[0:16, 0:16])
            T.evaluate(
                T.tvm_store_matrix_sync(
                    A.data,
                    16,
                    16,
                    16,
                    A.elem_offset // 256
                    + T.floordiv(T.floormod(A.elem_offset, 256), 16),
                    C.access_ptr("w"),
                    s1,
                    "row_major",
                    dtype="handle",
                )
            )

    try:
        # handle exception if we register multi times
        tir.TensorIntrin.register("wmma_load_a", wmma_load_a_desc, wmma_load_a_impl)
        tir.TensorIntrin.register("wmma_load_b", wmma_load_b_desc, wmma_load_b_impl)
        tir.TensorIntrin.register("wmma_sync", wmma_sync_desc, wmma_sync_impl)
        tir.TensorIntrin.register("wmma_fill", wmma_fill_desc, wmma_fill_impl)
        tir.TensorIntrin.register("wmma_store", wmma_store_desc, wmma_store_impl)
    except ValueError:
        pass

# Split and bind loops to threads
def test_tensor_core():
    sch = tir.Schedule(MatmulModule)
    block = sch.get_block("matmul")
    i, j, k = sch.get_loops(block)

    # We split factors to 16 because wmma is suitable for 16*16*16
    i, ii = sch.split(i, factors=[None, 16])
    j, ji = sch.split(j, factors=[None, 16])
    k, ki = sch.split(k, factors=[None, 16])
    sch.reorder(i, j, k, ii, ji, ki)
    wmma_sync = sch.blockize(loop=ii)
    sch.mod.show()

    i0, i1, i2 = sch.split(i, factors=[8, 4, 2])
    j0, j1, j2 = sch.split(j, factors=[8, 4, 2])
    k0, k1, k2 = sch.split(k, factors=[16, 2, 2])

    sch.reorder(i0, j0, i1, j1, k0, k1, i2, j2, k2)
    bx = sch.fuse(i0, j0)
    sch.bind(bx, "blockIdx.x")
    ty = sch.fuse(i1, j1)
    sch.bind(ty, "threadIdx.y")
    # We can't bind to `threadIdx.x` since we have warp-level operators under the loop
    sch.mod.show()

    # We cache A and B to shared memory
    X_shared = sch.cache_read(wmma_sync, read_buffer_index=0, storage_scope="shared")
    Y_shared = sch.cache_read(wmma_sync, read_buffer_index=1, storage_scope="shared")

    def schedule_shared(block):
        sch.compute_at(block, k0)
        x, y = sch.get_loops(block)[-2:]
        fused = sch.fuse(x, y)
        x0, x1, x2, x3 = sch.split(fused, factors=[None, 16, 32, 8])
        sch.bind(x1, "threadIdx.y")
        # here we must bind threadIdx.x == 32 to satisfy the requirements of warp-level operation.
        sch.bind(x2, "threadIdx.x")
        sch.vectorize(x3)

    schedule_shared(X_shared)
    schedule_shared(Y_shared)
    sch.mod.show()

    # cache input data
    X_local = sch.cache_read(wmma_sync, 0, storage_scope="wmma.matrix_a")
    Y_local = sch.cache_read(wmma_sync, 1, storage_scope="wmma.matrix_b")
    sch.compute_at(X_local, k1)
    sch.compute_at(Y_local, k1)

    # cache output data
    write_back_block = sch.cache_write(wmma_sync, 0, storage_scope="wmma.accumulator")
    sch.reverse_compute_at(write_back_block, ty)
    sch.mod.show()

    def schedule_copy(block):
        x, y = sch.get_loops(block)[-2:]
        x0, x1 = sch.split(x, factors=[None, 16])
        y0, y1 = sch.split(y, factors=[None, 16])
        sch.reorder(x0, y0, x1, y1)

    schedule_copy(X_local)
    schedule_copy(Y_local)
    schedule_copy(write_back_block)
    sch.mod.show()

    # Tensorize
    init = sch.decompose_reduction(wmma_sync, k0)
    sch.tensorize(sch.get_loops(X_local)[-2], "wmma_load_a")
    sch.tensorize(sch.get_loops(Y_local)[-2], "wmma_load_b")
    sch.tensorize(init, "wmma_fill")
    sch.tensorize(wmma_sync, "wmma_sync")
    sch.tensorize(sch.get_loops(write_back_block)[-2], "wmma_store")
    sch.mod.show()

    # Evaluate
    rt_mod = tvm.build(sch.mod, target="cuda")

    dev = tvm.cuda()
    num_flop = 1024**3 * 2
    A_np = np.random.randn(1024, 1024).astype("float16")
    B_np = np.random.randn(1024, 1024).astype("float16")
    C_np = A_np.astype("float32") @ (B_np.astype("float32").T)

    A_nd = tvm.nd.array(A_np, dev)
    B_nd = tvm.nd.array(B_np, dev)
    C_nd = tvm.nd.array(np.empty((1024, 1024), dtype="float32"), dev)

    rt_mod(A_nd, B_nd, C_nd)
    np.testing.assert_allclose(C_np, C_nd.numpy(), rtol=1e-3, atol=1e-3)

    evaluator = rt_mod.time_evaluator("main", dev, number=10)
    print(
        "Performance: %f GFLOPS" % (num_flop / evaluator(A_nd, B_nd, C_nd).mean / 1e9)
    )

if __name__ == "__main__":
    register_intrinsic()
    test_tensor_core()


from flask import Flask
from werkzeug.wsgi import SharedDataMiddleware
from os.path import join as joinpath, dirname
from functools import partial

Z = partial(joinpath, dirname(__file__))

app = Flask(__name__)

app.wsgi_app = SharedDataMiddleware(app.wsgi_app,
            {
                '/dojolib': Z('./lib/dojotoolkit'),
                '/olesk': Z('./lib/olesk'),
                '/d3': Z('./lib/d3'),
                '/': Z('.')
            },
            cache=False,
            cache_timeout=0
      )

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