#!/usr/bin/env python

from __future__ import print_function
import argparse
import glob
import os
import re
import signal
import sys
import textwrap
import traceback
import warnings

from backports.shutil_which import which
from backports.shutil_get_terminal_size import get_terminal_size
from natsort import natsorted, ns
from pygments import highlight
from pygments.formatters import HtmlFormatter
from pygments.lexers import guess_lexer, guess_lexer_for_filename
from pygments.lexers.special import TextLexer
import termcolor


# require python 2.7+
if sys.version_info < (2, 7):
    sys.exit("You have an old version of python. Install version 2.7 or higher.")
if sys.version_info < (3, 0):
    input = raw_input


def main():

    # Exit on ctrl-c
    def handler(signum, frame):
        print("")
        sys.exit(1)
    signal.signal(signal.SIGINT, handler) 
   
    # Parse command-line arguments
    parser = argparse.ArgumentParser(description="A command-line tool that "
                                                 "renders source code as a PDF.")
    parser.add_argument("-r", "--recursive", action="store_true", help="recurse into directories")
    parser.add_argument("-i", "--include", action="append", help="pattern to include")
    parser.add_argument("--no-color", action="store_true", help="disable syntax highlighting")
    parser.add_argument("-s", "--size", help="size of page")
    parser.add_argument("-x", "--exclude", action="append", help="pattern to exclude")
    parser.add_argument("output", help="file to output")
    parser.add_argument("input", help="input to render", nargs="+")
    args = parser.parse_args(sys.argv[1:])
 
    # Ensure output ends in .pdf
    output = args.output
    if not output.lower().endswith(".pdf"):
        output += ".pdf"

    # Check for size
    size = args.size
    if not size:
        size = "letter landscape"
    elif size == "A4" or size == "letter":
        size = "{} landscape".format(args.size.strip())

    # Create parent directory as needed
    dirname = os.path.dirname(os.path.realpath(output))
    if not os.path.isdir(dirname):
        while True:
            s = input("Create {}? ".format(dirname)).strip()
            if s.lower() in ["n", "no"]:
                raise RuntimeError()
            elif s.lower() in ["y", "yes"]:
                try:
                    os.makedirs(dirname)
                except Exception:
                    e = RuntimeError("Could not create {}.".format(dirname))
                    e.__cause__ = None
                    raise e

    # Prompt whether to overwrite file as needed
    if os.path.exists(output):
        while True:
            s = input("Overwrite {}? ".format(output))
            if s.lower() in ["y", "yes"]:
                break
            elif s.lower() in ["n", "no"]:
                raise RuntimeError()

    # Check for includes
    includes = []
    if args.include:
        for i in args.include:
            includes.append(re.escape(i).replace("\*", ".*"))

    # Check for excludes
    excludes = []
    if args.exclude:
        for x in args.exclude:
            excludes.append(re.escape(x).replace("\*", ".*"))

    # Check stdin for inputs else command line
    patterns = []
    if len(args.input) == 1 and args.input[0] == "-":
        patterns = sys.stdin.read().splitlines()
    else:
        patterns = args.input

    # Glob patterns lest shell (e.g., Windows) not have done so, ignoring empty patterns
    paths = []
    for pattern in patterns:
        if pattern:
            paths += natsorted(glob.glob(pattern), alg=ns.IGNORECASE)

    # Candidates to render
    candidates = []
    for path in paths:
        if not os.access(path, os.R_OK):
            raise RuntimeError("Could not read: {}".format(path))
        if os.path.isfile(path):
            candidates.append(path)
        elif os.path.isdir(path):
            files = []
            for dirpath, dirnames, filenames in os.walk(path):
                for filename in filenames:
                    files.append(os.path.join(dirpath, filename))
            natsorted(files, alg=ns.IGNORECASE)
            candidates += files
        else:
            raise RuntimeError("Unsupported: {}".format(path))

    # Filter candidates
    queue = []
    for candidate in candidates:

        # Skip implicit exclusions
        if includes and not re.search(r"^" + r"|".join(includes) + "$", candidate):
            continue

        # Skip explicit exclusions
        if excludes and re.search(r"^" + r"|".join(excludes) + "$", candidate):
            continue

        # Skip dotfiles
        if os.path.basename(candidate).startswith("."):
            continue

        # Queue candidate for rendering
        queue.append(candidate)

    # Render queued files
    documents = []
    for queued in queue:

        # Open file
        with open(queued, "rb") as file:

            # Read file
            code = file.read().decode("utf-8", "ignore")

            # Skip binary files
            if "\x00" in code:
                cprint("Skipping {} because binary...".format(queued), "yellow")
                continue

            # Highlight code unless file is empty, using inline line numbers to avoid
            # page breaks in tables, https://github.com/Kozea/WeasyPrint/issues/36
            string = ""
            if code.strip() and not args.no_color:
                try:
                    lexer = guess_lexer_for_filename(queued, code)
                except:
                    try:
                        lexer = guess_lexer(code)
                    except:
                        lexer = TextLexer()
                string = highlight(code, lexer, HtmlFormatter(linenos="inline", nobackground=True))
            else:
                string = highlight(code, TextLexer(), HtmlFormatter(linenos="inline", nobackground=True))

            # Stylize document
            stylesheets = [
                    CSS(string="@page {{ border-top: 1px #808080 solid; margin: 0.5in; size: {}; }}".format(size.replace(";", "\;"))),
                    CSS(string="@page {{ @top-right {{ color: #808080; content: '{}'; }} }}".format(queued.replace("'", "\'"))),
                    CSS(string="* { font-family: monospace; font-size: 10pt; }"),
                    CSS(string=HtmlFormatter().get_style_defs('.highlight')),
                    CSS(string=".highlight { background: initial; }"),
                    CSS(string=".lineno { color: #808080; }"),
                    CSS(string=".lineno:after { content: '  '; }")]

            # Render document
            cprint("Rendering {}...".format(queued))
            document = HTML(string=string).render(stylesheets=stylesheets)

            # Bookmark document
            document.pages[0].bookmarks = [(1, queued, (0, 0))]

            # Buffer this document
            documents.append(document)

    # Write documents to PDF
    # https://github.com/Kozea/WeasyPrint/issues/212#issuecomment-52408306
    if documents:
        pages = [page for document in documents for page in document.pages]
        documents[0].copy(pages).write_pdf(output)
        cprint("Rendered {}.".format(output), "green")
    else:
        cprint("Nothing to render.", "red")


def cprint(text="", color=None, on_color=None, attrs=None, **kwargs):
    """Colorize text (and wraps to terminal's width)."""

    # Assume 80 in case not running in a terminal
    columns, _ = get_terminal_size()
    if columns == 0: columns = 80

    # Only python3 supports "flush" keyword argument
    if sys.version_info < (3, 0) and "flush" in kwargs:
        del kwargs["flush"]

    # Print text
    termcolor.cprint(textwrap.fill(text, columns, drop_whitespace=False, replace_whitespace=False),
                     color=color, on_color=on_color, attrs=attrs, **kwargs)


def excepthook(type, value, tb):
    """Report an exception."""
    excepthook.ignore = False
    if type is RuntimeError and str(value):
        cprint(str(value), "yellow")
    else:
        cprint("Sorry, something's wrong! Let sysadmins@cs50.harvard.edu know!", "yellow")
        traceback.print_exception(type, value, tb)
    cprint("Rendering cancelled.", "red")
sys.excepthook = excepthook


def _inline_min_content_width(context, box, outer=True, skip_stack=None,
                             first_line=False, is_line_start=False):
    """Return the min-content width for an ``InlineBox``.
    The width is calculated from the lines from ``skip_stack``. If
    ``first_line`` is ``True``, only the first line minimum width is
    calculated.
    """
    widths = weasyprint.layout.preferred.inline_line_widths(
        context, box, outer, is_line_start, minimum=True,
        skip_stack=skip_stack)

    if first_line:
        widths = [next(widths)]
    else:
        widths = list(widths)
        widths[-1] -= weasyprint.layout.preferred.trailing_whitespace_size(context, box)
    return weasyprint.layout.preferred.adjust(box, outer, max(widths))


# Check for dependencies
# http://weasyprint.readthedocs.io/en/latest/install.html
try:
    # Ignore warnings about outdated Cairo and Pango (on Ubuntu 14.04, at least)
    warnings.filterwarnings("ignore", category=UserWarning, module="weasyprint")
    from weasyprint import CSS, HTML
except OSError as e:
    if "pangocairo" in str(e):
        e = RuntimeError("Missing dependency. Install Pango.")
        e.__cause__ = None
        raise e
    elif "cairo" in str(e):
        e = RuntimeError("Missing dependency. Install cairo.")
        e.__cause__ = None
        raise e
    else:
        e = RuntimeError(str(e))
        e.__cause__ = None
        raise e
else:
    # Temporary until https://github.com/Kozea/WeasyPrint/milestone/7 deploys
    import weasyprint.layout.inlines
    weasyprint.layout.inlines.inline_min_content_width = _inline_min_content_width
    import weasyprint.layout.preferred
    weasyprint.layout.preferred.inline_min_content_width = _inline_min_content_width


if __name__ == "__main__":
    main()
