import argparse
import os
import re
import time
import pymupdf
import requests


def get_doi(pdf_file: str) -> str:
    """
    Get the DOI (if present) from metadata or text of a pdf file. If found, DOI is return as a string. If no doi are
    found for a file, the value associated to the key is left empty. get_doi() use a regex for matching DOI and exclude
    last character that shouldn't be there (like ".", "\n", ")").
    Output of the function is the DOI in a string.

    - First, the function will look in the "subject" field of the pdf metadata. I don't know if that's a standard, but
    when DOI is the in metadata, it seems to be in that field. If the DOI is found, it's stored in a string.
    - Secondly, if no DOI is found in pdf metadata, the function look for DOI in the text of the five first pages of the
    pdf.
    - Lastly, if no DOI is found at all, an error message is displayed.
    """

    regex = "10\\.\\d+/[\\w./-]+[^\\.\\n\\\\ )]"
    doi = ""
    pdf = pymupdf.open(pdf_file)

    # Get doi from metadata
    doi_metadata = re.search(regex, pdf.metadata.get("subject"))
    if doi_metadata is not None:
        doi = doi_metadata.group()

    # Get doi from text
    if doi == "":
        text = ""
        if pdf.page_count <= 5:
            for page in range(0, pdf.page_count):
                text += pdf.get_page_text(page)
        else:
            for page in range(0, 4):
                text += pdf.get_page_text(page)
        doi_text = re.search(regex, text)
        if doi_text is not None:
            doi = doi_text.group()

    # If no doi is found
    if doi == "":
        print(f"No DOI found for \"{pdf_file}\".")

    return doi


def doi_dict(directory: str) -> dict:
    """
    For a specified directory, create a dict associating pdf files with corresponding DOI. Names of the files are
    used as keys and DOI as values.
    Output of the function is the completed dict.

    doi_dict() use get_doi() to retrive DOI from pdf.
    """

    doi_dict = {}

    for item in os.listdir(directory):
        # Including only pdf files
        if item.endswith(".pdf"):
            doi_dict[item] = ""

    for key in doi_dict.keys():
        doi_dict[key] = get_doi(directory + "/" + key)

    return doi_dict


def doi_to_bib(doi: str, filename: str = "") -> str:
    """
    Generate one bibliography entry for one DOI using the crossref API. The function can also take a optional
    filename argument to associate a file with the DOI for reference.
    Output of the function is a string in biblatex format.

    doi_to_bib() generating a usable url for the crossref API using the input DOI and getting a json in response. The
    json contain informations about the associated DOI, doi_to_bib() then retrieve these informations, transform them in
    the biblatex format and put them in a string.
    """

    bib_str = ""

    # Crossref API expect "%2F" instead of "/"
    metadata = requests.get("https://api.crossref.org/works/" + doi.replace("/", "%2F"))
    # Check if API response give usable response
    if metadata.ok is True:
        json = metadata.json()
        # Determination of the document type and creation of the citation key for LaTeX
        if json["message"]["type"] == "journal-article":
            bib_str += (f"@article{{{json["message"]["author"][0]["family"].lower()}"
                        f"{json["message"]["created"]["date-parts"][0][0]},\n")
        elif json["message"]["type"] == "reference-book":
            bib_str += (f"@book{{{json["message"]["author"][0]["family"].lower()}"
                        f"{json["message"]["created"]["date-parts"][0][0]},\n")
        elif json["message"]["type"] == "book-chapter":
            bib_str += (f"@inbook{{{json["message"]["author"][0]["family"].lower()}"
                        f"{json["message"]["created"]["date-parts"][0][0]},\n")
        elif json["message"]["type"] == "proceedings-article":
            bib_str += (f"@inproceedings{{{json["message"]["author"][0]["family"].lower()}"
                        f"{json["message"]["created"]["date-parts"][0][0]},\n")
        # Title
        bib_str += f"title = {{{json["message"]["title"][0]}}},\n"
        # Agglomerated list of authors with order preserved
        authors_list = ""
        for num_author in range(len(json["message"]["author"])):
            authors_list += (f"{json["message"]["author"][num_author]["family"]}, "
                             f"{json["message"]["author"][num_author]["given"]} and ")
        # Removing the last " and "
        bib_str += f"author = {{{authors_list[:-5]}}}, \n"
        # Date
        bib_str += f"date = {{{json["message"]["created"]["date-time"][:10]}}},\n"
        # DOI
        bib_str += f"doi = {{{json["message"]["DOI"]}}},\n"
        # URL
        bib_str += f"url = {{{json["message"]["URL"]}}},\n"
        # Last date of url consultation by crossref
        bib_str += f"urldate = {{{json["message"]["indexed"]["date-time"][:10]}}},\n"

        # Check for entries not always present in crossref
        # TODO for later, see if a more compact way to check entries can be done
        # Title of journal
        if json["message"].get("container-title") != []:
            bib_str += f"journaltitle = {{{json["message"]["container-title"][0].replace("&amp;", "&")}}},\n"
        # Abreviation of journal
        if json["message"].get("short-container-title") != []:
            bib_str += f"shortjournal = {{{json["message"]["short-container-title"][0].replace("&amp;", "&")}}},\n"
        # Volume of journal
        if json["message"].get("volume") is not None:
            bib_str += f"volume = {{{json["message"]["volume"]}}},\n"
        # Issue of journal
        if json["message"].get("issue") is not None:
            bib_str += f"number = {{{json["message"]["issue"]}}},\n"
        # Pages where the article is located
        if json["message"].get("page") is not None:
            bib_str += f"pages = {{{json["message"]["page"].replace("-", "--")}}},\n"
        # ISSN of the journal
        # Either print or online version, sometimes both
        # [0] is selected because [1] is not always present
        # Would be better to only select the online one, but no idea how
        if json["message"].get("ISSN") is not None:
            bib_str += f"issn = {{{json["message"]["ISSN"][0]}}}\n"
        # Publisher
        if json["message"].get("publisher") is not None:
            bib_str += f"publisher = {{{json["message"]["publisher"]}}},\n"
        # Language of the article
        # Need to be expanded with more common languages
        if json["message"].get("language") is not None:
            if json["message"]["language"] == "en":
                bib_str += "langid = {english},\n"
            elif json["message"]["language"] == "fr":
                bib_str += "langid = {french},\n"
            elif json["message"]["language"] == "de":
                bib_str += "langid = {german},\n"
            elif json["message"]["language"] == "es":
                bib_str += "langid = {spanish},\n"
        # If no lang is specified, assuming english as default
        else:
            bib_str += "langid = {english},\n"

        # Filename of the article in directory for reference
        bib_str += f"file = {{{filename}}}\n"
        # Closing of entry for doi in loop
        bib_str += "}\n\n"

        return bib_str

    else:
        print(f"\"{doi}\" is not a valid DOI or Crossref API can't use it. "
              f"File associated with this DOI is \"{filename}\".")
        return


def dict_to_bib(doi_dict: dict) -> str:
    """
    Generate bibliography using the crossref API with the DOI dict generated from doi_dict().
    Output of the function is a string in biblatex format.

    dict_to_bib() relies on doi_to_bib() to convert a DOI to a bibliography string.
    """

    bib_str = ""

    for key in doi_dict.keys():
        if doi_dict[key] != "":
            response = doi_to_bib(doi_dict[key], key)
            if response is not None:
                bib_str += response
            # The public pool of Crossref API have a rate limit of 50 requests per second
            # so we wait a bit before sending another one (wait 0.025 sec is 50 requests per 1.25sec)
            time.sleep(0.025)
    return bib_str


def main():
    """
    Command line parser
    """

    parser = argparse.ArgumentParser(prog="pdf2biblatex",
                                     description="Generate BibLaTeX file from pdf and DOI.")
    parser.add_argument("--directory", "-d", type=str, nargs="?", const=os.getcwd(),
                        help="Where %(prog)s will look for pdf, then extract their DOI and generate a BibLaTeX file by"
                        " using the crossref API. Also indicate when a DOI is not found or invalid."
                        " Default is current directory.")
    parser.add_argument("--add-doi", "-a", type=str, nargs="+",
                        help="Generate a BibLaTeX entry for each given DOI. If a BibLaTeX file have already been"
                        " created, each entry are merged into it, otherwise a new file is created.")
    parser.add_argument("--bib-file", "-b", type=str,
                        help="What name to use for the BibLaTeX file. If not set, the file is named after the directory"
                        " as \"directory.bib\".")

    args = parser.parse_args()

    if args.directory:
        bib_dict = doi_dict(args.directory)
        bib_txt = dict_to_bib(bib_dict)

        os.chdir(args.directory)
        if args.bib_file:
            with open(f"{args.bib_file}", "w", encoding="utf-8") as bib_file:
                bib_file.write(bib_txt)
        else:
            with open(f"{os.path.basename(os.getcwd())}.bib", "w", encoding="utf-8") as bib_file:
                bib_file.write(bib_txt)

    elif args.add_doi:
        for doi in args.add_doi:
            bib_doi = doi_to_bib(doi)

            if args.bib_file:
                with open(f"{args.bib_file}", "a", encoding="utf-8") as bib_file:
                    bib_file.write(bib_doi)
            else:
                with open(f"{os.path.basename(os.getcwd())}.bib", "a", encoding="utf-8") as bib_file:
                    bib_file.write(bib_doi)

    else:
        print("Some arguments are expected.\nIntended use is \"pdf2biblatex -d DIRECTORY\" to generate a BibLaTeX"
              " file then \"pdf2biblatex -a DOI\" if some DOI are missing from the previous command.\n"
              "See \"pdf2biblatex --help\" for details.")


if __name__ == "__main__":
    main()
