#!/usr/bin/env python3

# Copyright (c) 2016-2019 Lorenzo Mureu
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

import json
import logging
import re
import sys
from dataclasses import dataclass, field, asdict
from os import listdir
from time import sleep
from typing import Any, Callable

from selenium.webdriver import firefox

driver = None
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

CONFIG_FILE = "config.json"

URL_LOGIN = 'https://login.didattica.polito.it/secure/ShibLogin.php'
URL_INTERNAL_LOGIN_PUSH = "https://idp.polito.it/idp/x509mixed-login"
URL_INTERNAL_REDIRECT = "https://idp.polito.it/idp/profile/SAML2/Redirect/SSO"
URL_INTERNAL_LOGIN_REQUIRED = "https://idp.polito.it/idp/Authn/X509Mixed/UserPasswordLogin"
URL_DIDATTICA_LANDING = "https://didattica.polito.it/portal/page/portal/home/Studente"

XPATH_MATERIA = "//a[@class = 'policorpolink' and upper-case(normalize-space(text()))='{0}']"
FIELD_USERNAME = 'j_username'
FIELD_PASSWORD = 'j_password'

XPATH_VIDEOLEZIONI_LINKS = "//li[@class = 'h5']/a"
XPATH_DOWNLOAD_LINK = "//a[normalize-space(text()) = 'Video']"
XPATH_DOWNLOAD_LINK_LQ = "//a[normalize-space(text()) = 'iPhone']"

SLEEP_T = 0.01

PROGRAM = sys.argv[0]

HEADING = """Welcome to {0}.
Please make sure you have read the instructions before continuing.
""".format(PROGRAM)

COPYING = """\
--------------------------------------------------------------------------------
{0} (C) 2016-2019 Lorenzo Mureu <mureulor@gmail.com>
This is free software, you can read the terms of its license by passing the
`--copyright` option.
--------------------------------------------------------------------------------\
""".format(PROGRAM)

LICENSE = """\
Copyright (C) 2016-2019 Lorenzo Mureu <mureulor@gmail.com>

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.\
"""

USAGE = "Syntax: {0} [--copyright]".format(PROGRAM)


@dataclass
class Configuration:
    username: str
    password: str
    url: str
    output_file: str = field(default="links.txt")

    @staticmethod
    def from_file(file_path=CONFIG_FILE):
        logger.info(f"Using configuration file: '{file_path}'.")
        with open(CONFIG_FILE, "r") as fp:
            obj = json.load(fp)

            # Parse required parameters
            if "username" not in obj:
                logger.error("Parsing error: field `username` not found")
                return
            if "password" not in obj:
                logger.error("Parsing error: field `password` not found")
                return
            if "url" not in obj:
                logger.error("Parsing error: field `password` not found")
                return

            conf = Configuration(obj["username"], obj["password"], obj["url"])

            # Parse optional parameters
            if "output_file" in obj:
                conf.output_file = obj["output_file"]

            return conf

    @staticmethod
    def interactive():
        logger.info("Using interactive mode.")
        username = input("Username: ")
        password = input("Password: ")
        url = input("URL: ")
        config = Configuration(username, password, url)
        config.serialize()
        return config

    @classmethod
    def default(cls):
        try:
            config = cls.from_file()
        except (FileNotFoundError, json.JSONDecodeError):
            config = cls.interactive()
        logger.debug(f"{config=}")
        return config

    def serialize(self, file_path=CONFIG_FILE):
        with open(file_path, "w") as fp:
            logger.info(f"Saving configuration file as {file_path}")
            json.dump(asdict(self), fp)


def login(username, password):
    logger.info(f"Logging in with credentials: {username}:{password}")
    driver.get(URL_LOGIN)
    wait()
    driver.find_element_by_id(FIELD_USERNAME).send_keys(username)
    driver.find_element_by_id(FIELD_PASSWORD).send_keys(password)
    wait()
    driver.find_element_by_id('login').submit()

    logger.info("Waiting for after-login redirect...")
    while driver.current_url in [URL_INTERNAL_LOGIN_PUSH,
                                 URL_INTERNAL_REDIRECT]:
        sleep(SLEEP_T)

    if driver.current_url == URL_DIDATTICA_LANDING:
        logger.info("Login successful.")
        return True

    if driver.current_url == URL_INTERNAL_LOGIN_REQUIRED:
        logger.error("Login failed.")
        return False

    logger.warning(f"Unknown response: {driver.current_url}")
    return False


def _remove_extension(string):
    return re.sub(r"\..+$", "", string)


def _extract_last_number(string):
    if len(numbers := re.findall(r"\d+", string)) > 0:
        return int(numbers[-1])
    return 0


# def _input_range(prompt, valid_range: range = None, default=0):
#     return _input(f"{prompt} [{range.start, range.stop}]", default,
#                   lambda el: el in valid_range)


def _input(prompt: str, default: Any, f: Callable):
    while True:
        s = input(f"{prompt}, {default=}: ")
        if s == "":
            return default

        try:
            return f(s)
        except ValueError:
            pass


def guess_last_lesson():
    return max(_extract_last_number(_remove_extension(file))
               for file in listdir())


def get_links(output_file):
    imax = len(driver.find_elements_by_xpath(XPATH_VIDEOLEZIONI_LINKS))
    # default = guess_last_lesson() + 1
    start = guess_last_lesson()
    logger.info(f"Next video lesson is #{start + 1} (index {start})")

    if start > imax:
        print("Already up to date.")
    # start = _input(f"First lesson [1, {imax}]", default,
    #                lambda s: int(s) in range(1, imax + 1))

    with open(output_file, "w") as fp:
        for i in range(start, imax):
            driver.find_elements_by_xpath(XPATH_VIDEOLEZIONI_LINKS)[i].click()
            wait()
            driver.find_element_by_xpath(XPATH_DOWNLOAD_LINK).click()
            wait()
            url = driver.current_url
            print("{0}/{1}: {2}".format(i + 1, imax, shrink(url, 50)))
            fp.write(url + "\n")
            fp.flush()
            driver.back()
            wait()


def wait():
    while driver.execute_script("return document.readyState") != "complete":
        # print(".", end="")
        sleep(SLEEP_T)
    # print()
    # sleep(SLEEP_T)


def shrink(string, max_length=80):
    if len(string) < max_length:
        return string

    idx = (max_length - 5) // 2
    return string[:idx] + "[...]" + string[-idx:]


def copyright_notice():
    print(COPYING)


def usage():
    print(USAGE)


def parse_options():
    if len(sys.argv) > 2:
        usage()
        exit(1)
    if len(sys.argv) == 2:
        if sys.argv[1] == "--copyright":
            print(LICENSE)
            exit(0)
        else:
            usage()
            exit(1)


def main():
    copyright_notice()
    parse_options()
    print(HEADING)

    global driver
    options = firefox.options.Options()
    options.headless = True
    driver = firefox.webdriver.WebDriver(options=options)

    config = Configuration.default()

    if not login(config.username, config.password):
        exit(1)

    driver.get(config.url)
    get_links(config.output_file)

    driver.close()
    print(f"""Thank you for using this program.
Your links were saved in `{config.output_file}`
You can download the lessons by running `wget -nc -ci {config.output_file}`""")


if __name__ == "__main__":
    main()
