import re
import unittest

import gourmand.convert as convert
from gourmand.i18n import _


def parse_group(match, text, group_number, tag):
    start, end = match.span(group_number)
    if start == -1:
        return None
    else:
        retv = []
        if start > 0:
            retv.append((text[0:start], None))
        retv.append((text[start:end], tag))
        if end < len(text):
            retv.append((text[end:], None))
        return retv


class RecipeParser:
    """A generic parser for doing rough mark up of unformatted recipes

    We pre-parse the text based on a set of rules which look a bit
    like the rules we use for HTML parsing (see
    html_plugins/__init__.py).

    Each rule is
    [recipe_part,regexp_matcher,post_processing]

    recipe_part is a string describing what part of the recipe
    ('ingredient','ingredients', 'serving', etc.)

    regexp_matcher is a compiled regular expression to match our part.

    By default, we simply mark the whole line as being the recipe_part.

    post_processing can refine our match:

    If post_processing is an integer, it is the number of the regexp
    group which should actually be labelled (the rest of the line will
    be marked"ignore")

    Otherwise, post_processing should be a function of the following form.

    def (match_object, full_text, attribute):
        ...
        return [(chunk, tag),(chunk, tag),...]
    the matcher and text as arguments and should return a list of
    chunks/tag with marked up text from our line.

    [(chunk, tag),(chunk, tag),...]
    """

    LONG_LINE = 80
    SHORT_LINE = 40

    ATTRIBUTES = [
        "servings",
        "category",
        "cuisine",
        "rating",
        "source",
        "ignore",
        "yields",
        "yield_unit",
        "modifications",
    ]

    ALIASES = [
        ("cooking time", "cooktime"),
        ("preparation time", "preptime"),
        ("time", "preptime"),
        ("author", "source"),
        ("by", "source"),
        ("yield", "yields"),
        ("notes", "modifications"),
        ("note", "modifications"),
    ]

    IGNORE_ON_OWN = ["instructions", "ingredients", "directions"]

    joinable_tags = ["instructions", "ingredient", "ingredients", None]
    change_on_join = {"ingredient": "ingredients"}

    ing_matcher = re.compile(
        r"^\s*\u2022?\u2023?\u2043?\u204C?\u204D?\u2219?\u25C9?\u25D8?\u25E6?\u2619?\u2765?\u2767?\u29BE?\u29BF?\s*(%s\s+\w+.*)" % convert.NUMBER_REGEXP
    )

    def __init__(self):
        self.title_parsed = False
        self.make_rules()

    # INGREDIENT = 'ING'
    # INSTRUCTIONS = 'INSTRUCTIONS'
    # ATTRIBUTE = 'ATT'
    # TITLE = 'TIT'
    # IGNORE = 'IGN'
    def make_rules(self):
        self.rules = [
            ["ingredients", self.ing_matcher, 1],
            [
                "servings",
                re.compile(r"serv(ing|e)s?:?\s*%(num)s|%(num)s\s*servings?" % {"num": convert.NUMBER_REGEXP}, re.IGNORECASE),
                lambda m, txt, attr: (parse_group(m, txt, 2, attr) or parse_group(m, txt, 3, attr)),
            ],
        ]
        for a in self.ATTRIBUTES:
            self.rules.append([a, re.compile(r"\s*%s\s*:\s*(.*)" % a, re.IGNORECASE), 1])
        for name, attr in self.ALIASES:
            self.rules.append([attr, re.compile(r"\s*%s\s*:\s*(.*)" % name, re.IGNORECASE), 1])
        for ig in self.IGNORE_ON_OWN:
            self.rules.append([None, re.compile(r"^\W*%s\W*$" % ig, re.IGNORECASE), None])
        self.rules.append(
            [
                # instructions are our generic fallback
                "instructions",
                re.compile(".*"),
                None,
            ]
        )

        def parse_yield(match_obj, full_text, attr):
            colon, amt, unit = match_obj.groups()
            return [(amt.strip(), "yields"), (unit.strip(), "yield_unit")]

        self.rules = [
            ["yield", re.compile(r"%(yield)s(:|s|\s-)\s-*%(num)s\s-*(.*)" % {"yield": _("yield"), "num": convert.NUMBER_REGEXP}, re.IGNORECASE), parse_yield]
        ] + self.rules

    def break_into_paras(self):
        self.long_lines = False
        for line in self.txt.split("\n"):
            if len(line) > self.LONG_LINE:
                self.long_lines = True
                break
        if self.long_lines:
            self.paras = self.txt.split("\n")
        else:
            # Try to deal with wrapped lines reasonably...
            self.paras = []
            start_new_para = True
            for line in self.txt.split("\n"):
                if start_new_para or self.ing_matcher.match(line):
                    self.paras.append(line)
                    if len(line) > self.SHORT_LINE:
                        start_new_para = False
                else:
                    self.paras[-1] = self.paras[-1] + " " + line
                    start_new_para = len(line) < self.SHORT_LINE

    def parse(self, txt, parentThread=None):
        self.txt = txt
        self.parsed = []
        self.break_into_paras()
        tot = len(self.paras)
        n = 1
        for p in self.paras:
            # update a progress bar if necessary...
            if parentThread:
                parentThread.emit("progress", float(n) / tot, "Parsing unformatted recipe")
                n += 1
            self.parsed.append(("\n", None))
            # genericly guess that the title is the first line!
            if not self.title_parsed and p.strip():
                self.parsed.append((p, "title"))
                self.title_parsed = True
            elif not p.strip():
                self.parsed.append((p, None))
            else:
                for attr, regexp, postproc in self.rules:
                    m = regexp.search(p)
                    if m:
                        if postproc:
                            if isinstance(postproc, int):
                                proced = parse_group(m, p, postproc, attr)
                                if not proced:
                                    continue
                            else:
                                proced = postproc(m, p, attr)
                            if proced:
                                self.parsed.extend(proced)
                                break
                        else:
                            self.parsed.append((p, attr))
                            break
        self.join_the_joinable()
        return self.parsed

    def join_the_joinable(self):
        """Go through self.parsed and join joinable elements.

        This means: produce fewer elements to jump through for the
        user if possible.
        """
        parsed = self.parsed[0:]
        self.parsed = []
        for chunk, tag in parsed:
            if tag not in self.joinable_tags or len(self.parsed) == 0:
                self.parsed.append([chunk, tag])
                continue
            if tag in self.change_on_join:
                look_for = [tag, self.change_on_join[tag]]
            else:
                look_for = [tag]
            add_on = ""
            added = False
            for n in range(1, len(self.parsed) + 1):
                oldchunk, oldtag = self.parsed[-n]
                if oldtag in look_for:
                    self.parsed[-n][0] = oldchunk + add_on + chunk
                    added = True
                    if oldtag in self.change_on_join:
                        self.parsed[-n][1] = self.change_on_join[oldtag]
                    # Strip off any added junk...
                    if n > 1:
                        self.parsed = self.parsed[0 : -(n - 1)]
                    break
                if oldtag is None:
                    add_on += oldchunk
                else:
                    break
            if not added:
                self.parsed.append([chunk, tag])


class RecipeTestCase(unittest.TestCase):
    def setUp(self):
        self.recipe = """
My Recipe

This is a test recipe. I hope it is really good.

This recipe serves 8
Category: dessert, quick, snack
Cuisine: Classic American!
Yield: 2 cups

   1 tbs. milk
   3 tbs. unsweetened bakers chocolate
   2 tbs. sugar
   1/4 tsp. almond extract

   1 c. milk

   2 tbs. whipped cream (for garnish)

   Mix the first four ingredients together into a thick slurry.
   Add the milk. Heat and stir.

   Enjoy!!!
   """
        self.rp = RecipeParser()

    def testParser(self):
        parsed = self.rp.parse(self.recipe)
        self.assertEqual("", parsed)


if __name__ == "__main__":
    unittest.main()
