# This is -*- Python -*-

import time, os

import gnoetics

class SolveFailed:
    pass


def find_leading_trailing(poem, i):

    assert 0 <= i < len(poem)

    u = poem[i]
    assert u.is_not_bound()

    leading_tokens = []
    trailing_tokens = []

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

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

    return leading_tokens, trailing_tokens



def solve_unit(poem, model, leading_tokens, unit, trailing_tokens,
               weights=None,
               allow_left_queries=True,
               allow_right_queries=False,
               verbose=None,
               extra_filters=True):

    # Copy list of leading and trailing tokens, so we can
    # manipulate them freely
    leading_tokens = list(leading_tokens)
    trailing_tokens = list(trailing_tokens)

    if verbose is None:
        verbose = os.getenv("GNOETRY_DEBUG") is not None

    def token_list_is_valid(token_list, allow_break_at):
        if allow_break_at < 0:
            allow_break_at += len(token_list)
        for i, tok in enumerate(token_list):
            if tok.is_wildcard():
                return False # No wildcards!
            if tok.is_break() and i != allow_break_at:
                return False # Breaks only where we say it is OK!
        return True

    lead_ok = token_list_is_valid(leading_tokens, allow_break_at=0)
    trail_ok = token_list_is_valid(trailing_tokens, allow_break_at=-1)

    if (not lead_ok) and verbose:
        print "Bad leading tokens!"
        print leading_tokens
        print

    if (not trail_ok) and verbose:
        print "Bad trailing tokens!"
        print trailing_tokens
        print

    assert lead_ok
    assert trail_ok

    # Double leading/trailing breaks
    
    if leading_tokens and leading_tokens[0].is_break():
        leading_tokens.insert(0, gnoetics.token_lookup_break())

    if trailing_tokens and trailing_tokens[-1].is_break():
        trailing_tokens.append(gnoetics.token_lookup_break())

    assert len(leading_tokens) + len(trailing_tokens) >= 2

    assert unit.is_not_bound()

    syl = unit.get_syllables()
    if syl == 0:
        print "*** Ouch!"
        print leading_tokens
        print unit
        print trailing_tokens
        print "*** Ouch!"
    assert syl > 0
    
    meter = unit.get_meter()
    rhyme = unit.get_rhyme()
    rhymes_with = poem.get_bound_rhyme(rhyme)

    word_count = 0
    punct_count = 0
    for tok in leading_tokens:
        if tok.is_punctuation():
            punct_count += 1
        elif not tok.is_break():
            word_count += 1
    for tok in trailing_tokens:
        if tok.is_punctuation():
            punct_count += 1
        elif not tok.is_break():
            word_count += 1

    discourage_punct = False
    if word_count > 2 and word_count < 3 * punct_count:
        discourage_punct = True

    def punctution_between(token_list, i0, i1):
        for i in range(i0, i1+1):
            if -len(token_list) <= i < len(token_list) \
                   and token_list[i].is_punctuation():
                return True
        return False

    forbid_punct = False
    if extra_filters and \
       (punctution_between(leading_tokens, -2, -1) \
        or punctution_between(trailing_tokens, 0, 1)):
        forbid_punct = True


    have_both = False
    def single_both(s, b):
        if have_both:
            return b
        else:
            return s
        
    left_queries = []
    right_queries = []

    if leading_tokens and trailing_tokens:
        have_both = True

        if trailing_tokens[0].is_break():
            allow_left_queries = False
            allow_right_queries = True
            discourage_punct = False
            forbid_punct = False

        else:

            Q_filter = { "min_syllables": syl,
                         "max_syllables": syl,
                         "meter_left":    meter,
                         }

            if rhymes_with:
                Q_filter["rhymes_with"] = rhymes_with
            elif rhyme:
                Q_filter["has_rhyme_threshold"] = gnoetics.RHYME_FALSE
                Q_filter["rhyme_p_threshold"] = 0.20
                
            Q = (leading_tokens[-1],
                 gnoetics.token_lookup_wildcard(),
                 trailing_tokens[0],
                 Q_filter)

            left_queries.append(Q)  # or right


    if leading_tokens and allow_left_queries:
        
        Q_filter = { "min_syllables": 0,
                     "max_syllables": syl-1,
                     "meter_left":    meter,
                     }

        Q = (leading_tokens[-2],
             leading_tokens[-1],
             gnoetics.token_lookup_wildcard(),
             Q_filter)
        left_queries.append(Q)


        if not have_both:
            Q_filter = { "min_syllables": syl,
                         "max_syllables": syl,
                         "meter_left":    meter,
                         }
            if rhymes_with:
                Q_filter["rhymes_with"] = rhymes_with
            elif rhyme:
                Q_filter["has_rhyme_threshold"] = gnoetics.RHYME_FALSE
                Q_filter["rhyme_p_threshold"] = 0.20
                
            Q = (leading_tokens[-2],
                 leading_tokens[-1],
                 gnoetics.token_lookup_wildcard(),
                 Q_filter)
            left_queries.append(Q)


    if trailing_tokens and allow_right_queries:

        Q_filter = { "min_syllables": 0,
                     "max_syllables": syl-1,
                     "meter_right":   meter,
                     }
        
        Q = (gnoetics.token_lookup_wildcard(),
             trailing_tokens[0],
             trailing_tokens[1],
             Q_filter)
        right_queries.append(Q)


        if not have_both:
            Q_filter = { "min_syllables": syl,
                         "max_syllables": syl,
                         "meter_left":    meter,
                         }
            if rhymes_with:
                Q_filter["rhymes_with"] = rhymes_with
            elif rhyme:
                Q_filter["has_rhyme_threshold"] = gnoetics.RHYME_FALSE
                Q_filter["rhyme_p_threshold"] = 0.20

            Q = (gnoetics.token_lookup_wildcard(),
                 trailing_tokens[0],
                 trailing_tokens[1],
                 Q_filter)
            right_queries.append(Q)




    if discourage_punct or forbid_punct:
        if discourage_punct:
            pref = 0
        else:
            pref = -1
        for Q in left_queries:
            Q[-1]["punctuation_preference"] = pref
        for Q in right_queries:
            Q[-1]["punctuation_preference"] = pref


    if left_queries:
        if verbose:
            print "Left queries:", left_queries
        ranker = gnoetics.Ranker()
        if weights is not None:
            for txt, wt in weights.items():
                ranker.set_weight(txt, wt)
        for t1, t2, t3, filter in left_queries:
            model.query(t1, t2, t3, filter, ranker)
        left_solns = ranker.get_solutions()
        if verbose:
            print "Left solns:", map(lambda x: x.get_word(), left_solns)
    else:
        left_solns = []

    if right_queries:
        if verbose:
            print "Right queries:", right_queries
        ranker = gnoetics.Ranker()
        if weights is not None:
            for txt, wt in weights.items():
                ranker.set_weight(txt, wt)
        for t1, t2, t3, filter in right_queries:
            model.query(t1, t2, t3, filter, ranker)
        right_solns = ranker.get_solutions()
        if verbose:
            print "Right solns:", map(lambda x: x.get_word(), right_solns)
    else:
        right_solns = []

    return left_solns, right_solns

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

class Solver:

    def __init__(self, model):
        self.__model = model
        self.__actions = []
        self.__poem = None


    def set_poem(self, p):
        self.__poem = p
        self.__actions = []


    def get_poem(self):
        return self.__poem


    def __next_position(self):
        assert self.__poem
        assert not self.__poem.is_fully_bound()
        return self.__poem.find_first_unbound()


    def __solve_at(self, i, weights=None, verbose=None):

        if verbose is None:
            verbose = os.getenv("GNOETRY_DEBUG") is not None

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

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

        # If the stuff in our action queue isn't immediately
        # adjacent to where we are now, obviously rolling back
        # won't help us.
        # FIXME: Obviously this will have to be more subtle once
        # we have cross-line constraints.
        if self.__actions and abs(self.__actions[-1][0] - i) > 1:
            self.__actions = []

        if u.get_syllables() == 0 and verbose:
            print "Hit 0-length unit at %d" % i
            for j in range(len(self.__poem)):
                if i == j:
                    print "*****",
                print j, self.__poem[j]
            print

        leading_tokens, trailing_tokens = self.__poem.extract_surrounding_tokens(i)

        left_solns, right_solns = gnoetics.solve_unit(self.__poem,
                                                      self.__model,
                                                      leading_tokens,
                                                      u,
                                                      trailing_tokens,
                                                      weights=weights)

        # If we found no solutions, try again w/o the extra filters.
        if not left_solns and not right_solns:
            left_solns, right_solns = gnoetics.solve_unit(self.__poem,
                                                          self.__model,
                                                          leading_tokens,
                                                          u,
                                                          trailing_tokens,
                                                          extra_filters=False,
                                                          weights=weights)


        if verbose:
            print "pos=%d left_solns=%d right_solns=%d" % (i,
                                                           len(left_solns),
                                                           len(right_solns))

        if left_solns:
            self.__actions.append([i, "left", left_solns, right_solns])
        elif right_solns:
            self.__actions.append([i, "right", left_solns, right_solns])
        else:
            while 1:
                if not self.__actions:
                    raise SolveFailed
                act = self.__actions[-1]
                i = act[0]
                mode = act[1]
                if mode == "left":
                    self.__poem.unbind(i)
                    if verbose:
                        print "left-unbind at %d" % i
                else:
                    self.__poem.unbind(i+1)
                    if verbose:
                        print "right-unbind at %d" % i
                left_solns = act[2]
                right_solns = act[3]
                if right_solns and not left_solns:
                    act[1] = "right"
                if left_solns or right_solns:
                    break
                self.__actions.pop(-1)

        if left_solns:
            tok = left_solns.pop(0)
            if verbose:
                print "left-bind at %d" % i
            self.__poem.bind_left(i, tok)
        else:
            tok = right_solns.pop(0)
            if verbose:
                print "right-bind at %d" % i
            self.__poem.bind_right(i, tok)


    def step(self, weights=None):
        if not self.__poem:
            return

        if self.__poem.is_fully_bound():
            return

        self.__poem.bind_mandatory_breaks()

        verbose = os.getenv("GNOETRY_DEBUG") is not None

        i = self.__next_position()

        if verbose:
            print "Initial state (solving at %d):" % i
            for j in range(len(self.__poem)):
                if j == i:
                    print "*****",
                print j, self.__poem[j]
            print
    
        self.__solve_at(i, weights=weights)


    def multistep(self, count=10, timeout=0.2, weights=None):
        if not self.__poem:
            return

        t1 = time.time()
        N = 0
        
        self.__poem.freeze_changed()

        while self.__poem.is_not_fully_bound():

            if count is not None and count > 0 and N > count:
                break

            if timeout is not None:
                t2 = time.time()
                if t2 - t1 > timeout:
                    break

            self.step(weights=weights)

            N += 1

        self.__poem.thaw_changed()


    def full_solution(self, weights=None):
        verbose = os.getenv("GNOETRY_DEBUG") is not None
        if verbose:
            print
            print "-" * 50
            print
            
        self.__actions = []
        self.multistep(count=None, timeout=None, weights=weights)
