#!/usr/bin/python3

'''
   Copyright 2019 UNSW CSESoc

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

     http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
'''

import datetime, json, os, re, sqlite3, time, requests
from bs4 import BeautifulSoup
from flask import Flask, request, send_from_directory, g

DATABASE_FILENAME = "courses.db"
LOG_FILENAME = "pathways.log"
TIMETABLE_DIR = "courses"
TIMETABLE_REQUEST_TIMEOUT = 10 # seconds
TIMETABLE_CACHE_TIME = 60*60*6 # 6 hours
DAY_TO_INDEX = {
    "Mon": 0,
    "Tue": 1,
    "Wed": 2,
    "Thu": 3,
    "Fri": 4,
    "Sat": 5,
    "Sun": 6
}

CURRENT_YEAR = "2018"

def connect_db():
    return sqlite3.connect(DATABASE_FILENAME)

# logs the current flask request
def log_request():
    url = request.url
    user_agent = request.user_agent
    remote_addr = request.remote_addr
    now = datetime.datetime.now()
    values = {
        "now": now,
        "remote_addr": remote_addr,
        "user_agent": user_agent,
        "url": url
    }
    values = "\t".join(values)
    with open(LOG_FILENAME, "a") as f:
        f.write(values + "\n")

# fetches the timetable file for the given course
# returns the HTML data if successful, empty string if HTTP 404 (to prevent retries),
# None if any other HTTP error (likely their site is broken, or connection problems)
def fetch_timetable_course(code):
    print("Fetching fresh timetable for " + str(code))
    try:
        resp = requests.get("http://www.timetable.unsw.edu.au/" + CURRENT_YEAR + "/" + str(code) + ".html").text
        return resp
    except Exception as err:
        print("Error encountered " + str(err)) 
    return None

# returns a tuple of (timetable_data, time)
# where timetable_data is the HTML data for the timetable file,
# and time is the timestamp for the data
# loads from cache if data exists and is less than TIMETABLE_CACHE_TIME seconds old
def get_timetable_course_data(code):
    # check if it's been cached
    filename = "%s/%s.html" % (TIMETABLE_DIR, code)
    if os.path.exists(filename):
        mtime = os.path.getmtime(filename)
        # ensure it is not too old
        if time.time() - mtime <= TIMETABLE_CACHE_TIME:
            return (open(filename, "r").read(), mtime)
        else:
            print("Timetable for " + code + " is too old")
    else:
        print("Timetable for " + code + " doesn't exist")

    data = fetch_timetable_course(code)
    if data is not None:
        with open(filename, "w") as f:
            f.write(data)
    return (data, time.time())

# returns a tuple of (timetable_data, time)
# timetable_data is the timetable data for the given course, as a dictionary
# time is the timestamp for the data
def get_timetable_course_schedule(code):
    data, mtime = get_timetable_course_data(code)

    timetable = {}

    # now find some interesting information about the course
    schedule_summary = re.findall(r'<td class="data" colspan="5"><a href="#(.*?)">(.*?)</a></td>.*?</tr>.*?<tr>.*?<td class="data">&nbsp;</td>.*?<td class="data">.*?<table width="100%" cellspacing="0">.*?<tr>.*?<td class="data" width="25%">&nbsp;</td>.*?<td class="data">(.*?)</td>.*?</tr>.*?</table>.*?</td>.*?<td class="data">(.*?)</td>.*?<td class="data">(.*?)</td>.*?<td class="data"><font color="red">(.*?)</font></td>', data, re.DOTALL)
    
    for _, period_name, period_code, contact, census_date, notes in schedule_summary:
        if period_code not in timetable:
            timetable[period_code] = {
                "name": period_name,
                "contact": contact,
                "classes": []
            }

    ROW_RE = re.compile(r'<tr class="row(?:High|Low)light">(.*?)</tr>')
    DATA_RE = re.compile(r'<td class="data">(.*?)</td>')

    soup = BeautifulSoup(data)

    for period in soup.find_all("td", {"class": "sectionSubHeading"}):
        data = period.parent.parent.next_sibling#.next_sibling
        for row in data.find_all("tr", {"class": re.compile("row(High|Low)light")}):
            info = [node.text for node in row.find_all("td", {"class": "data"})]

            name = info[0]
            
            status = info[4] # Open, Full, On Hold
            size, capacity = list(map(int, re.match(r"(\d+)/(\d+)", info[5]).groups()))
            period = info[1]
            code = info[3]
            classCode = info[2]

            if period not in ("T1","T2"):
                continue

            times = info[6].replace("\n", "")
            times = re.split(r"\(.*?\), ", times)
            for time in times:
                match = re.match(r"^\s*(\w+)\s+(\d{2}):(\d{2})\s+-\s+(\d{2}):(\d{2}).*", time)
                if not match:
                    continue

                start = int(match.group(2))
                finish = int(match.group(4))

                start_minute = int(match.group(3))
                finish_minute = int(match.group(5))

                day = match.group(1)

                if start_minute != 0 or finish_minute != 0:
                    print("An unexpected error has occurred: " + str(info))

                # add thing
                details = {
                    "startHour": start,
                    "finishHour": finish,
                    "size": size,
                    "capacity": capacity,
                    "day": DAY_TO_INDEX[day],
                    "status": status,
                    "name": name,
                    "code": code,
                    "classCode": classCode
                }
                timetable[period]["classes"].append(details)

    return (timetable, mtime)

# loads details about a given course from the database
# set detailed to True or False depending on how much detail you want...
def get_course_details(code, detailed=False):
    cur = g.db.cursor()

    if detailed:
        cols = ["code", "name", "description", "prerequisites", "corequisites", "exclusions", "gened", "outline", "uoc"]
    else:
        cols = ["code", "name"]

    result = cur.execute("SELECT %s FROM courses WHERE code = ?" % ", ".join(cols), (code,)).fetchone()
    if not result:
        # create placeholder result
        d = {}
        d["code"] = code
        d["exists"] = False
    else:
        # create tuples
        d = {}
        for i in range(len(cols)):
            d[cols[i]] = result[i]
        d["exists"] = True

        if detailed:
            # add timetable stuff
            schedule, mtime = get_timetable_course_schedule(code)

            if "T1" in schedule:
                d["s1"] = schedule["T1"]
            if "T2" in schedule:
                d["s2"] = schedule["T2"]
            d["timetableLastUpdated"] = mtime

    return d

# ~~~~~~~~~~~~
# FLASK STUFF!
# ~~~~~~~~~~~~

app = Flask(__name__)

@app.before_request
def before_request():
    g.db = connect_db()

@app.teardown_request
def teardown_request(exception):
    if hasattr(g, "db"):
        g.db.close()

@app.route("/tree/<code>")
def tree(code):
    cur = g.db.cursor()

    log_request()

    above = list()
    below = list()
    
    try:
        centre = get_course_details(code, detailed=True)
    except Exception as e:
        print(e)
        return json.dumps({"error": True})

    for above_code, type in cur.execute("SELECT source, type FROM relationships WHERE destination = ?", (code,)).fetchall():
    	if type != "exclusion":
            details = get_course_details(above_code)
            details["type"] = type
            above.append(details)

    for below_code, type in cur.execute("SELECT destination, type FROM relationships WHERE source = ?", (code,)).fetchall():
    	if type != "exclusion":
            details = get_course_details(below_code)
            details["type"] = type
            below.append(details)

    # sort things nicely
    above.sort(key=lambda node: node["code"])
    below.sort(key=lambda node: node["code"])

    return json.dumps({"centre": centre, "above": above, "below": below, "error": False})

@app.route("/all-courses")
def all_courses():
    cur = g.db.cursor()
    courses = []
    result = cur.execute("SELECT code, name FROM courses ORDER BY code ASC")
    for code, name in result:
        courses.append({"label": name + " - " + code, "value": code})

    return json.dumps(courses)

@app.route("/all-courses-names")
def all_courses_names():
    cur = g.db.cursor()
    courses = []
    result = cur.execute("SELECT code, name FROM courses ORDER BY code ASC")
    for code, name in result:
        courses.append(name + " - " + code)
    return json.dumps(courses)

@app.route("/all-courses-reverse")
def all_courses_reverse():
    cur = g.db.cursor()
    courses = {}
    result = cur.execute("SELECT code, name FROM courses ORDER BY code ASC")
    for code, name in result:
        key = name + " - " + code
        courses[key] = code
    return json.dumps(courses)

@app.route("/all-gened")
def all_gened():
    cur = g.db.cursor()
    courses = []
    result = cur.execute("SELECT code, name FROM courses WHERE gened = 1 ORDER BY code ASC")
    for code, name in result:
        courses.append({"label": name + " - " + code, "value": code})
    return json.dumps(courses)

@app.route("/")
def static_from_root():
    return send_from_directory(app.static_folder, "index.html")

if __name__ == "__main__":
    os.system("python scraper/unsw_scraper.py")
    app.run(debug=True, port=7001)
