#! /usr/bin/python
from __future__ import print_function
from bs4 import BeautifulSoup
import re
import sys
import os
import time
import datetime
import collections

SOUP = None
DEBUGGING = False
INPUT_FILE = ""
OUTPUT_FILE = ""
HELP = '''
Convert a Twine (v 2) HTML file to a format suitable for inclusion 
in an Inform7 game, using AW Freyr's Hybrid Choices extension.

Usage: twineconvert <input file>
       twineconvert -i <input file> [-o <output file>]

The input file should be an html file generated by Twine.

If no ouput file is given, the program writes to stdout. 
WARNING: the program will overrwrite the output file. You 
shouldn't specify your inform story as the output file, but
you should write to a separate file and then copy and paste!

Other options:

-d, --debug : print extensive debugging information describing what
             is happening
-h, --help : print this text

Within your Twine, you can use the following conventions:

* A link in the form [[@FLIP->page]] will convert to a flip to that 
  page
* Put @dead-end in the text to produce a dead-end
* Put @one-off in the text to produce a one-off

More complex rules will require editing the final code. The program 
will not touch brackets (so if you want square brackets to be 
printed you should use [bracket] and [close bracket]), but will 
convert " to '.
'''

# Regular expressions
LINK_REGEX = "\[\[(.*?)\]\]"
SEP_REGEX  = "(.*?)->(.*?)$"
REV_REGEX  = "(.*?)<-(.*?)$"
PB_REGEX   = "\n\n+"
SP_REGEX   = "\s\s+"
QUOTE_REGEX = "\""
FPR_REGEX  = "\[paragraph break\]$"
ONE_OFF_REGEX = "@ONE-OFF"
DEAD_END_REGEX = "@DEAD-END"


# Key data structure
# We are going to work with a global structure and alter it in place
# {KEY} -> {text} (a string)
#          {links-in}: [(from, text)]
#          {flips}: to
#          {links-out}: number
#          {dead-end}: Boolean
#          {one-off}: Boolean

PASSAGES = {}

def load_passage_data ():
    '''
    In the first pass, we simply load all the data into the text portion,
    passage by passage
    '''
    global PASSAGES
    for page in SOUP.find_all("tw-passagedata"):
        PASSAGES[page["name"]] = { "text" : page.string,
                                   "links-in" : [],
                                   "flips" : None,
                                   "links-out" : 0,
                                   "one-off" : False,
                                   "dead-end" : False
        }
        if DEBUGGING:
            print("Loaded passage data for", page["name"])

def convert_to_links_in ():
    '''
    Use regular expression to identify links /out/ in the passages,
    converting them to links /in/, in list form. At the same time
    we locate and mark one-offs and dead-ends
    '''
    global PASSAGES
    lr = re.compile(LINK_REGEX)
    sr = re.compile(SEP_REGEX)
    rr = re.compile(REV_REGEX)
    dr = re.compile(DEAD_END_REGEX, re.IGNORECASE)
    oor = re.compile(ONE_OFF_REGEX, re.IGNORECASE)

    for page in PASSAGES:
        t = PASSAGES[page]["text"]
        if oor.search(t) != None:
            PASSAGES[page]["one-off"] = True
            if DEBUGGING:
                print("Page", page, "marked as one-off.")
        if dr.search(t) != None:
            PASSAGES[page]["dead-end"] = True
            if DEBUGGING:
                print("Page", page, "marked as dead-end.")
        if DEBUGGING:
            print ("Identifying links from page", page);
        j = lr.findall(t)
        for a in j:
            prompt = None
            target = None
            m = sr.match(a)
            if m != None:
                prompt = m.group(1)
                target = m.group(2)
            else:
                m = rr.match(a)
                if m != None:
                    prompt = m.group(2)
                    target = m.group(1)
            if prompt != None and target != None:
                if DEBUGGING:
                    print("Link to ", target, ":", prompt)
                if target in PASSAGES:
                    PASSAGES[target.strip()]["links-in"].append((prompt.strip(), page))
                    PASSAGES[page]["links-out"] += 1
                else:
                    sys.exit("ERROR: Bad link format in passage %s: %s" % (page, a))

def convert_flips ():
    '''
    After conversion, flips are recorded as links in, but they are not. 
    We need to change them to a special sort of link out.
    '''
    global PASSAGES
    for page in PASSAGES:
        if DEBUGGING:
            print("Considering flips for page", page, "with links", PASSAGES[page]["links-in"])
        links_in = []
        for link in PASSAGES[page]["links-in"]:
            (prompt, target) = link
            if prompt == "@FLIPS" or prompt == "@flips" or prompt == "@Flips":
                if target in PASSAGES:
                    PASSAGES[target]["flips"] = page
                    if DEBUGGING:
                        print("Reconfiguring flips for pages", page, "and", target)
                else:
                    sys.exit("ERROR: Bad continuation reference on page %s: %s" % (target, page))
            else:
                links_in.append(link)
        PASSAGES[page]["links-in"] = links_in

def eliminate_multiples ():
    '''
    If a passage has more than one link in, with different text, we need to
    set up "alternative" pages with flips. Each alternative page inherits the
    prompt text, and flips to the ultimate target.
    '''
    global PASSAGES
    keys = PASSAGES.keys()
    for page in keys:
        current_links_in = PASSAGES[page]["links-in"]
        if len(current_links_in) > 1 and not uniform(current_links_in):
            for i in range (0, len(current_links_in)):
                (prompt, origin) = current_links_in[i];
                newpage = page + "__" + str(i)
                if newpage in PASSAGES:
                    sys.exit("ERROR: cannot create new page %s because there is an existing page with the same name. Please rename that page." % newpage)
                else:
                    PASSAGES[newpage] = {
                        "text" : "",
                        "links-in" : [(prompt, origin)],
                        "links-out" : 1,
                        "flips" : page,
                        "one-off" : PASSAGES[page]["one-off"],
                        "dead-end" : PASSAGES[page]["dead-end"]}
                    if DEBUGGING:
                        print("Created new page", newpage, "which flips to",
                              page, "from", origin, "with link text of:", prompt)
            PASSAGES[page]["links-in"] = []
            PASSAGES[page]["one-off"] = False
                

def uniform (link_lst):
    '''
    link_lst is a list in the form [(prompt, origin) ...]. Return true
    if all prompts are the same.
    '''
    template_prompt = link_lst[0][0]
    for i in range (1, len(link_lst)):
        if link_lst[i][0] != template_prompt:
            return False
    return True

def clean_up_text ():
    '''
    We need to do various things:
    1) Remove links, dead-ends, one-offs
    2) Convert paragraphs into [paragraph break]
    3) Convert double apostrophes to single
    4) Remove extra spaces
    5) Trim
    6) Remove trailing [paragraph break]
    '''
    global PASSAGES
    qr = re.compile(QUOTE_REGEX)
    lr = re.compile(LINK_REGEX)
    dr = re.compile(DEAD_END_REGEX, re.IGNORECASE)
    oor = re.compile(ONE_OFF_REGEX, re.IGNORECASE)
    pr = re.compile(PB_REGEX)
    sr = re.compile(SP_REGEX)
    fpr = re.compile(FPR_REGEX)
    for page in PASSAGES:
        t = PASSAGES[page]["text"]
        t = qr.sub("'", t)
        t = lr.sub("", t)
        t = dr.sub("", t)
        t = oor.sub("", t)
        t = pr.sub("[paragraph break]", t)
        t = sr.sub(" ", t)
        t = t.strip()
        t = fpr.sub("", t)
        PASSAGES[page]["text"] = t.strip()

def construct_inform_format (name, data):
    '''
    Take given data and put it into a suitable format.
    '''
    built = name
    built += " is a page."
    if data["text"] != "":
        if data["text"] == "Double-click this passage to edit it.":
            built += '"[bold type]**FIXME? (' + name + ')**[roman type]".'
        else:
            built += ' "' + data["text"] + '".'
    if data["links-in"] != []:
        built += ' The cdesc is "' + data["links-in"][0][0] + '". It is for ' + data["links-in"][0][1]
        for i in range (1, len(data["links-in"])):
            built += ", " + data["links-in"][i][1]
        built += ". "
    else:
        if data["links-out"] == 0 and not data["dead-end"]:
            built += ' The cdesc is "[bold type]**FIXME? (' + name + ')**[roman type]".'
            built += " [This page appears to be an end-page, but it has no cdesc. I've automatically added one, but you should check this. Is the page reachable?]"
            if DEBUGGING:
                print("WARNING: page", name,
                      "has no cdesc, but appears to be an end-page.")
    if data["flips"] != None:
        built += " It flips to " + data["flips"] + "."
    if data["links-out"] == 0 and not data["dead-end"]:
        built += " It is an end-page."
    if data["one-off"] == True:
        built += " It is one-off."
    if data["dead-end"] == True:
        built += " It is a dead-end."
    return built

def process_command_line_args ():
    global INPUT_FILE
    global OUTPUT_FILE
    global DEBUGGING
    argno = len(sys.argv)
    if argno < 2:
        sys.exit('Usage: %s filename' % sys.argv[0])
    else:
        i = 1
        while i < argno:
            tok_1 = sys.argv[i]
            if tok_1 == '-i' or tok_1 == '--input':
                if argno == i:
                    sys.exit('Bad option %s: no input file given' % sys.argv[i])
                else:
                    INPUT_FILE = sys.argv[i+1]
                    i += 2
            elif tok_1 == '-o' or tok_1 == '--output':
                if argno == i:
                    sys.exit('Bad option %s: no output file given' % sys.argv[i])
                else:
                    OUTPUT_FILE = sys.argv[i+1]
                    i +=2
            elif tok_1 == "-h" or tok_1 == "--help":
                print(HELP)
                exit(0)
            elif tok_1 == "-d" or tok_1 == "--debug"or tok_1 == "-v" or tok_1 == "--verbose":
                DEBUGGING = True
                i += 1
            else:
                INPUT_FILE = sys.argv[i]
                i += 1
    if not os.path.exists(INPUT_FILE):
        sys.exit('ERROR: input file %s was not found.' % INPUT_FILE)

def read_data_in():
    '''
    Read in the data, converting it to SOUP
    '''
    global SOUP
    try:
        input_file = open(INPUT_FILE, "r")
        html_doc = input_file.read()
    except IOError as e:
        sys.exit("Error reading input file %s: %s" % (INPUT_FILE, e))
    try:
        input_file.close()
    except IOError as e:
        sys.exit("Error closing input file %s: %s" % (INPUT_FILE, e))
    SOUP = BeautifulSoup(html_doc, 'html.parser')

def generate_output():
    '''
    Generate output, either to stdout (if there is no output file selected), or to
    the selected output file.
    '''
    if OUTPUT_FILE == "":
        print("[Automatically generated from ", INPUT_FILE, " on ",
              datetime.datetime.now().strftime("%Y-%m-%d %H:%M"), "]\n\n", sep='')
        for page in sorted(PASSAGES):
            p = construct_inform_format(page, PASSAGES[page])
            print(p, "\n\n")
        print("[Automatically generated content ends here]")
    else:
        try:
            fh = open(OUTPUT_FILE, "w")
        except IOError as e:
            sys.exit("Error opening output file %s: %s" % (OUTPUT_FILE, e))
        fh.write("[Automatically generated from " + INPUT_FILE +
                 " on " +
                 datetime.datetime.now().strftime("%Y-%m-%d %H:%M") + "]\n\n")
        for page in sorted(PASSAGES):
            p = construct_inform_format(page, PASSAGES[page]) + "\n\n"
            fh.write(p)
        fh.write("[Automatically generated content ends here]")
        try:
            fh.close()
        except IOError as e:
            sys.exit("Error closing output file %s: %s" % (OUTPUT_FILE, e))
        print("Converted %s. Output written to %s. Have a good day!" %
              (INPUT_FILE, OUTPUT_FILE))
    
process_command_line_args()
read_data_in()
load_passage_data()
convert_to_links_in()
convert_flips()
eliminate_multiples()
clean_up_text()
generate_output()

