# This is -*- Python -*-

import sys, string, time, random
import gobject

import gnoetics
from poeticunit import *

class Poem(gobject.GObject):

    def __sanity_check(self):
        assert len(self.__units) > 0
        assert self.__units[0].is_head()
        assert self.__units[0].is_beginning_of_line()
        assert self.__units[0].is_beginning_of_stanza()
        assert self.__units[-1].is_tail()
        assert self.__units[-1].is_end_of_line()
        assert self.__units[-1].is_end_of_stanza()
        

    def __init__(self, form_name=None, units=None, magic=None, syllables=None, xml=None):
        gobject.GObject.__init__(self)

        self.__form_name = form_name
        self.__seqno = 0
        self.__freeze_changed_count = 0
        self.__freeze_changed_pending = False

        self.__units = []

        if xml:
            assert units is None
            assert magic is None
            self.__from_xml(xml)
        elif magic:
            assert units is None
            self.__units = _unit_magic(magic)
        elif syllables:
            assert units is None
            self.__units = _syllabic_magic(syllables)
        else:
            assert units
            self.__units = list(units)

        
        self.__sanity_check()

        
    def copy(self, clear=False):
        p = Poem(self.get_form_name(),
                 map(lambda x: x.copy(drop_binding=clear),
                     self.__units))
        if clear:
            p.combine_units()

        p.__seqno = self.__seqno

        return p

    def get_seqno(self):
        return self.__seqno

    def get_form_name(self):
        return self.__form_name

    def freeze_changed(self):
        assert self.__freeze_changed_count >= 0
        self.__freeze_changed_count += 1

    def thaw_changed(self):
        assert self.__freeze_changed_count > 0
        self.__freeze_changed_count -= 1
        if self.__freeze_changed_count == 0 and self.__freeze_changed_pending:
            self.__freeze_changed_pending = False
            self.emit_changed()

    def emit_changed(self):
        if self.__freeze_changed_count == 0:
            self.emit("changed")
        else:
            self.__freeze_changed_pending = True

    def form_name(self):
        return self.__form_name


    def __good_index(self, i):
        return 0 <= i < len(self.__units)


    def __fix_index(self, i):
        if i < 0:
            i += len(self.__units)
        return i


    def __len__(self):
        return len(self.__units)


    def __getitem__(self, i):
        i = self.__fix_index(i)
        assert self.__good_index(i)
        return self.__units[i]


    def is_fully_bound(self):
        for x in self.__units:
            if x.is_not_bound():
                return False
        return True

    def is_not_fully_bound(self):
        return not self.is_fully_bound()


    def find_first_unbound(self):
        for i, x in enumerate(self.__units):
            if x.is_not_bound():
                return i
        return None


    def combine_units(self):
        i = 0
        did_work = False
        while i < len(self.__units)-1:
            a = self.__units[i]
            b = self.__units[i+1]
            if a.can_combine(b):
                a.append(b)
                self.__units.pop(i+1)
                did_work = True
            else:
                i += 1
        if did_work:
            self.__seqno += 1

        self.__sanity_check()
        
        return did_work


    def bind(self, i, tok, is_left=None):
        i = self.__fix_index(i)
        assert self.__good_index(i)
        assert is_left is not None
        assert not tok.is_wildcard()

        orig = self.__units[i]
        assert orig.is_not_bound()

        if not (0 <= tok.get_syllables() <= orig.get_syllables()):
            print i, (is_left and "left") or "right"
            print tok
            print tok.get_syllables()
            print orig
            print orig.get_syllables()
            print
            for i, u in enumerate(self.__units):
                print i, u
                
                                    
        assert 0 <= tok.get_syllables() <= orig.get_syllables()

        if tok.get_syllables() == orig.get_syllables():
            site = orig
            repl = None
            bind_at = i
        elif is_left:
            site = orig.pop_left(tok.get_syllables())
            repl = (site, orig)
            bind_at = i
        else:
            site = orig.pop_right(tok.get_syllables())
            repl = (orig, site)
            bind_at = i+1

        site.bind(tok)
        if repl:
            self.__units[i:i+1] = repl

        self.__seqno += 1

        self.__sanity_check()
            
        self.emit_changed()
        return bind_at


    def bind_left(self, i, tok):
        self.bind(i, tok, is_left=True)


    def bind_right(self, i, tok):
        self.bind(i, tok, is_left=False)

    
    def bind_mandatory_breaks(self):
        i = 0
        while i < len(self.__units):
            u = self.__units[i]
            if u.is_not_bound() and u.is_head():
                self.bind_left(i, gnoetics.token_lookup_break())
                i += 1
            elif u.is_not_bound() and u.is_tail():
                self.bind_right(i, gnoetics.token_lookup_break())
            else:
                i += 1
                

    def unbind(self, i):
        i = self.__fix_index(i)
        assert self.__good_index(i)

        u = self.__units[i]
        assert u.is_bound()

        u.unbind()
        self.combine_units() # FIXME: could be optimized

        self.__seqno += 1

        self.__sanity_check()

        self.emit_changed()


    def clear(self):
        did_work = False
        for u in self.__units:
            if u.is_bound():
                u.unbind()
                did_work = True
        if did_work:
            self.combine_units()
            self.__seqno += 1
            self.emit_changed()


    def get_flag(self, i):
        i = self.__fix_index(i)
        assert self.__good_index(i)

        u = self.__units[i]
        return u.get_flag()
    

    def set_flag(self, i, x):
        i = self.__fix_index(i)
        assert self.__good_index(i)

        u = self.__units[i]

        if x != u.get_flag():
            u.set_flag(x)
            self.emit("changed_flag", i)


    def contains_flagged(self):
        for u in self.__units:
            if u.get_flag():
                return True
        return False


    def unbind_flagged(self):
        self.freeze_changed()

        # First, walk through and unflag zero-syllable "singletons"
        # (i.e. punctuation w/o flagged adjacent units and breaks)
        for i, u in enumerate(self.__units):
            if not u.get_flag():
                continue
            
            if u.is_break():
                u.set_flag(False)
            elif u.get_syllables() == 0:
                prev_flag = i > 0 and self.__units[i-1].get_flag() and self.__units[i-1].get_syllables() > 0
                next_flag = i < len(self.__units)-1 and self.__units[i+1].get_flag() and self.__units[i+1].get_syllables() > 0
                if not (next_flag or prev_flag):
                    u.set_flag(False)
                    self.emit("changed_flag", i)

        # Now actually unbind the flagged units.  If a bit of punctuation is
        # flagged, also unbind any breaks that follow.
        i = 0
        while i < len(self.__units):
            u = self.__units[i]
            if u.get_flag() and u.is_bound():
                if u.is_punctuation() and i+1 < len(self.__units):
                    u2 = self.__units[i+1]
                    if u2.is_break():
                        self.unbind(i+1)
                self.unbind(i)
            else:
                i += 1

        # Next, walk through and pop off any remaining unbound
        # zero-length units (since we know they are next to non-zero units,
        # so everything will work out OK in the end.
        i = 0
        while i < len(self.__units):
            u = self.__units[i]
            if u.get_flag() and u.is_not_bound() and u.get_syllables() == 0:
                self.__units.pop(i)
                self.emit_changed()
            else:
                i += 1

        self.thaw_changed()


    def get_bound_rhyme(self, key):
        if key is None:
            return None
        for u in self.__units:
            if u.get_rhyme() == key and u.is_bound():
                return u.get_binding()
        return None


    def extract_surrounding_tokens(self, i):

        assert 0 <= i < len(self.__units)

        u = self.__units[i]
        assert u.is_not_bound()

        leading_tokens = []
        trailing_tokens = []

        j = i-1
        while 0 <= j:
            u = self.__units[j]
            if u.is_not_bound():
                break
            tok = u.get_binding()
            leading_tokens.insert(0, tok)
            if tok.is_break():
                break
            j -= 1

        j = i+1
        while j < len(self.__units):
            u = self.__units[j]
            if u.is_not_bound():
                break
            tok = u.get_binding()
            trailing_tokens.append(tok)
            if tok.is_break():
                break
            j += 1

        return leading_tokens, trailing_tokens


    def to_list_with_breaks(self):
        L = []
        for u in self.__units:
            if u.is_bound():
                L.append(u.get_binding())
            else:
                L.append(u)
            if u.is_end_of_stanza():
                L.append("end of stanza")
            elif u.is_end_of_line():
                L.append("end of line")

        i = 0
        while i < len(L)-1:
            a = L[i]
            b = L[i+1]
            swap = False

            if a in ("end of stanza", "end of line"):
                if type(b) == gnoetics.Token and \
                   (b.is_punctuation() or b.is_break()):
                    swap = True

            if swap:
                L[i:i+2] = L[i+1], L[i]
                i -= 1
            else:
                i += 1

        return L

    
    def num_lines(self):
        count = 0
        for u in self.__units:
            if u.is_end_of_line():
                count += 1
        return count


    def num_stanzas(self):
        count = 0
        for u in self.__units:
            if u.is_end_of_stanza():
                count += 1
        return count


    def to_string(self,
                  add_timestamp=False,
                  add_latex_markup=False,
                  add_latex_wrapper=False):
        lines = []
        current_line = ""

        stanza_break = ""
        if add_latex_markup:
            stanza_break = "\\vskip 1ex plus 0.4ex minus 0.4ex"

        if add_latex_wrapper:
            lines.append("\\documentclass[12pt]{article}")
            lines.append("\\begin{document}")
            lines.append("\\pagestyle{empty}")

        if add_latex_markup:
            lines.append("{\\obeylines\\parindent=0in")

        if add_timestamp:
            time_str = time.asctime(time.localtime())
            if add_latex_markup:
                time_str = "{\\it " + time_str + "}"
            lines.append(time_str)
            lines.append(stanza_break)

        last_was_break = False

        for x in self.to_list_with_breaks():
            if x == "end of line":
                lines.append(current_line)
                current_line = ""
            elif x == "end of stanza":
                lines.append(current_line)
                lines.append(stanza_break)
                current_line = ""
            elif type(x) == gnoetics.Token:
                if x.is_break():
                    last_was_break = True
                else:
                    word = x.get_word()

                    if last_was_break:
                        word = word[0].upper() + word[1:]

                    if word == "i":
                        word = "I"

                    # extra space at beginning of sentence
                    if current_line and last_was_break:
                        current_line += " "
                        
                    if current_line and not x.has_left_glue():
                        current_line += " "

                    current_line += word

                    if not x.is_punctuation():
                        last_was_break = False

            else: # is a unit
                if current_line:
                    current_line += " "
                current_line += "_" * x.get_syllables()

        if current_line:
            lines.append(current_line)

        if add_latex_markup:
            lines.append("}")

        if add_latex_wrapper:
            lines.append("\\end{document}")

        def quote_latex_special_chars(line):
            line = line.replace("%", "\%")
            line = line.replace("&", "\&")
            line = line.replace("$", "\$")
            line = line.replace("#", "\#")
            return line

        if add_latex_markup:
            lines = map(quote_latex_special_chars, lines)

        return string.join(lines, "\n")


    def to_xml(self, doc):

        poem_node = doc.createElement("poem")

        units_node = doc.createElement("units")
        poem_node.appendChild(units_node)

        for u in self.__units:
            node = u.to_xml(doc)
            units_node.appendChild(node)

        return poem_node


    def __from_xml(self, poem_node):

        assert poem_node.localName == "poem"

        for node in poem_node.childNodes:

            if node.localName == "units":
                units = map(lambda x: gnoetics.PoeticUnit(xml=x), node.childNodes)
                self.__units = units
            else:
                assert 0



##############################################################################

def _unit_magic(scheme):
    scheme = scheme.strip()
    units = []
    for i, x in enumerate(scheme):

        if x == " ":
            continue
        
        is_first = (i == 0)
        is_last = (i == len(scheme)-1)

        args = {}

        if x in "123456789":
            args["syllables"] = int(x)
        else:
            args["iambs"] = 5
            if x != "*":
                args["rhyme"] = x

        args["is_beginning_of_line"] = True
        args["is_end_of_line"] = True
        if is_first:
            args["is_head"] = True
        if is_last:
            args["is_tail"] = True
        if is_first or scheme[i-1] == " ":
            args["is_beginning_of_stanza"] = True
        if is_last or scheme[i+1] == " ":
            args["is_end_of_stanza"] = True

        units.append(PoeticUnit(**args))

    return units

def _syllabic_magic(syllable_list):
    units = []
    for i, x in enumerate(syllable_list):

        if x == 0:
            continue
        
        is_first = (i == 0)
        is_last = (i == len(syllable_list)-1)

        args = {}

        args["syllables"] = x

        args["is_beginning_of_line"] = True
        args["is_end_of_line"] = True
        if is_first:
            args["is_head"] = True
        if is_last:
            args["is_tail"] = True
        if is_first or syllable_list[i-1] == 0:
            args["is_beginning_of_stanza"] = True
        if is_last or syllable_list[i+1] == 0:
            args["is_end_of_stanza"] = True

        units.append(PoeticUnit(**args))

    return units

gobject.type_register(Poem)

gobject.signal_new("changed",
                   Poem,
                   gobject.SIGNAL_RUN_LAST,
                   gobject.TYPE_NONE,
                   ())

gobject.signal_new("changed_flag",
                   Poem,
                   gobject.SIGNAL_RUN_LAST,
                   gobject.TYPE_NONE,
                   (gobject.TYPE_INT,))

##############################################################################

class SonnetStanza(Poem):
    def __init__(self):
        Poem.__init__(self, "Sonnet Stanza", magic="ABAB")

class Sonnet(Poem):
    def __init__(self):
        Poem.__init__(self, "Sonnet", magic="ABAB CDCD EFEF GG")

class Haiku(Poem):
    def __init__(self):
        Poem.__init__(self, "Haiku", magic="575")

class Tanka(Poem):
    def __init__(self):
        Poem.__init__(self, "Tanka", magic="575 77")

class Renga(Poem):
    def __init__(self):
        Poem.__init__(self, "Renga", magic="575 77 575")

class BlankVerse(Poem):
    def __init__(self, stanzas, lines_per_stanza):
        magic = string.join(("*"*lines_per_stanza,)*stanzas, " ")
        Poem.__init__(self,
                      "Blank Verse (%d/%d)" % (stanzas, lines_per_stanza),
                      magic=magic)

class RandomSyllabic(Poem):
    def generate_stanza(self, lines, min_syllables, max_syllables):
        stanza = []
        for i in range(lines):
            stanza.append(random.randint(min_syllables, max_syllables))
        return stanza
                          
    def __init__(self, stanzas, lines_per_stanza, min_syllables, max_syllables, identical_stanzas_flag):
        syllables = []
        stanza_syllables = None
        for i in range(stanzas):
            if i != 0:
                syllables.append(0)
            if stanza_syllables is None or not identical_stanzas_flag:
                stanza_syllables = self.generate_stanza(lines_per_stanza, min_syllables, max_syllables)
            syllables.extend(stanza_syllables)
        Poem.__init__(self, "Random Syllabic", syllables=syllables)
                                    
        
        


