
"""
listsearch
~~~~~~~~~~

List based search tools.
"""

import os
import re

import monostyle.util.monostylestd as monostylestd
from monostyle.util.monostylestd import Report
from monostyle.rst_parser.core import RSTParser
import monostyle.rst_parser.walker as rst_walker
from monostyle.util.segmenter import Segmenter
from monostyle.util.porter_stemmer import Porterstemmer

PorterStemmer = Porterstemmer()
Segmenter = Segmenter()

def compile_searchlist(searchlist, re_conf):
    """Make search list one dimensional.

    searchlist -- a list with one or two columns. The first column contains the terms,
                  the second the message string(s) outputted to the user.
                  If only one column is given the message is
                  either the "msg" conf variable or is generated by joining the terms.

                  Inline:
                  If the first terms starts with a '#' the entry is skipped.
                  If a terms starts with a '?' the term is skipped.
                  These can be escaped with a backslash.
                  If a terms ends with a '|' the term will not be stemed.
    re_conf -- regex flags.
        stem -- stem the terms.
        overline -- match over whitespace including line wraps.
        boundary -- pattern start and end with word boundaries.
    """
    comlist = []
    flags = 0
    if re_conf["ignorecase"]:
        flags = re.IGNORECASE
    if re_conf["mulitline"] or re_conf["overline"]:
        flags = flags | re.MULTILINE
    if re_conf["dotall"]:
        flags = flags | re.DOTALL

    for search_ent in searchlist:
        if isinstance(search_ent, str):
            terms = search_ent
            if "msg" in re_conf:
                msg = re_conf["msg"]
            else:
                msg = search_ent
        elif len(search_ent) == 1:
            terms = search_ent[0]
            if "msg" in re_conf:
                msg = re_conf["msg"]
            else:
                msg = search_ent[0]
        elif len(search_ent) == 2:
            terms = search_ent[0]
            msg = search_ent[1]
        else:
            print("list: wrong form:", search_ent)
            continue

        if isinstance(terms, str):
            terms = [terms]

        for term in terms:
            if term == "":
                continue
            pattern_str = term
            # comment
            if pattern_str.startswith('#'):
                # skip entire entry
                break
            # ignore this single term
            if pattern_str.startswith('?'):
                continue

            # remove escape
            if pattern_str.startswith('\\#') or pattern_str.startswith('\\?'):
                pattern_str = pattern_str[1:]

            if re_conf["stem"]:
                if not pattern_str.endswith('|'):
                    pattern_str = PorterStemmer.stem(pattern_str, 0, len(pattern_str)-1)
                else:
                    pattern_str = pattern_str[:-1]
                    term = pattern_str
            else:
                if re_conf["overline"]:
                    pattern_str = pattern_str.replace(' ', r'(?s\s+)')
                if re_conf["boundary"]:
                    pattern_str = r'\b' + pattern_str + r'\b'

            if not re_conf["word"]:
                pattern = re.compile(pattern_str, flags)
            else:
                pattern = pattern_str

            if not isinstance(msg, str):
                msg = '/'.join(msg)
            ent = [pattern, term, msg]
            comlist.append(ent)

    return comlist


def parse_config(re_conf_str):
    """Parse config to Booleans."""
    re_conf_str = re_conf_str.upper()
    re_conf = {
        "boundary": "B",
        "dotall": "D",
        "ignorecase": "I",
        "mulitline": "M",
        "overline": "O",
        "stem": "S",
        "word": "W"
    }
    for key in re_conf.keys():
        re_conf[key] = bool(re_conf[key] in re_conf_str)

    return re_conf



def search_free(document, reports, comlist):
    """Search terms in document."""
    toolname = "list search"

    instr_pos = {
        "sect": {"*": ["name"]},
        "field": {"*": ["name", "body"]},
        "*": {"*": ["head", "body"]}
    }
    instr_neg = {
        "dir": {
            "figure": ["head", "attr"],
            "code-block": "*", "default":"*", "include":"*",
            "math":"*", "youtube":"*", "vimeo":"*"
        },
        "substdef": {"image": ["head", "attr"], "unicode":"*", "replace":"*"},
        "target": "*",
        "role": {
            "doc": ["id"], "ref": ["id"], "term": ["id"],
            "kbd": "*", "class": "*", "mod": "*", "math": "*"
        },
        "hyperlink": {"*": ["id"]},
        "standalone": "*"
    }

    for part in rst_walker.iter_nodeparts_instr(document.body, instr_pos, instr_neg):
        part_str = str(part.code)
        for pattern, term, msg in comlist:
            for m in re.finditer(pattern, part_str):
                out = part.code.slice_match_obj(m, 0, True)
                out.content = [term]
                line = monostylestd.getline_punc(document.body.code,
                                                 part.code.start_pos + m.start(),
                                                 len(m.group(0)), 50, 30)
                reports.append(Report('I', toolname, out, msg, line))

    return reports


def search_word(document, reports, comlist, config):
    """Search terms in document within word boundaries."""
    toolname = "search word"

    instr_pos = {
        "sect": {"*": ["name"]},
        "field": {"*": ["name", "body"]},
        "*": {"*": ["head", "body"]}
    }
    instr_neg = {
        "dir": {
            "figure": ["head", "attr"],
            "code-block": "*", "default":"*", "include":"*",
            "math":"*", "youtube":"*", "vimeo":"*"
        },
        "substdef": {"image": ["head", "attr"], "unicode":"*", "replace":"*"},
        "target": "*",
        "role": {
            "doc": ["id"], "ref": ["id"], "term": ["id"],
            "kbd": "*", "class": "*", "mod": "*", "math": "*"
        },
        "hyperlink": {"*": ["id"]},
        "standalone": "*"
    }

    for part in rst_walker.iter_nodeparts_instr(document.body, instr_pos, instr_neg):
        for word in Segmenter.iter_word(part.code):
            word_str = str(word)
            if config["ignorecase"]:
                word_str = word_str.lower()
            if config["stem"]:
                word_stem = PorterStemmer.stem(word_str, 0, len(word_str)-1)

            for pattern, term, msg in comlist:
                if config["stem"]:
                    if not pattern.endswith('|'):
                        if pattern != word_stem:
                            continue
                    else:
                        if pattern[:-1] != word_str:
                            continue

                if pattern != word_str:
                    continue

                word.content = [term]
                line = monostylestd.getline_punc(document.body.code, word.start_pos,
                                                 word.span_len(), 50, 30)
                reports.append(Report('I', toolname, word, msg, line))

    return reports


def search(document, reports, data, config):
    """Switch between free and with boundary."""
    if not config["word"]:
        reports = search_free(document, reports, data)
    else:
        reports = search_word(document, reports, data, config)

    return reports


OPS = (
    ("test", search, "BIS"),
    ("be_eng/Be", search, "BIS"),
    ("be_eng/Rules", search, "IS"),
    ("confuse/Div", search, "BIS"),
    ("blender/UI", search, "I"),
    ("blender/Editors", search, ""),
    ("blender/Modes", search, ""),
    ("avoid/*", search, "BI"),
    ("simplify", search, "BI")
)

def init(op_names):
    def wildcard_leaf(data_src):
        for key, value in data_src.items():
            if isinstance(value, dict):
                yield from wildcard_leaf(value)
            elif isinstance(value, list):
                yield key, value

    ops = []
    if isinstance(op_names, str):
        op_names = [op_names]

    for op_name in op_names:
        for op in OPS:
            if op_name == op[0]:
                config = parse_config(op[2])
                if not op[0].endswith("/*"):
                    data_src = monostylestd.get_data_file(op[0])
                    # last path segment as default message.
                    config["msg"] = op[0].split('/')[-1]
                    data_comp = compile_searchlist(data_src, config)
                else:
                    data_src = monostylestd.get_data_file(op[0][:-2])
                    data_comp = []
                    for key, value in wildcard_leaf(data_src):
                        # key as default message.
                        config["msg"] = key
                        data_comp.extend(compile_searchlist(value, config))

                ops.append((op[1], {"data": data_comp, "config": config}))
                break
        else:
            print("Unknown operation: " + op_name)
            return None

    return ops


def hub(op_names):
    rst_parser = RSTParser()
    ops = init(op_names)

    reports = []
    for fn, text in monostylestd.rst_texts():
        document = rst_parser.parse_full(rst_parser.document(fn, text))

        for op, arg in ops:
            reports = op(document, reports, **arg)

    return reports


def main():
    import argparse

    descr = __doc__.replace('~', '')
    parser = argparse.ArgumentParser(description=descr)
    parser.add_argument("ops", nargs='+', help="")

    parser.add_argument("-r", "--root",
                        dest="root", nargs='?', const="",
                        help="defines the ROOT directory of the working copy or "
                             "if left empty the root defined in the config")

    args = parser.parse_args()

    if args.root is None:
        root_dir = os.getcwd()
    else:
        if len(args.root.strip()) == 0:
            root_dir = monostylestd.ROOT_DIR
        else:
            root_dir = os.path.normpath(args.root)

        if not os.path.exists(root_dir):
            print('Error: root {0} does not exists'.format(args.root))
            return 2

    root_dir = monostylestd.replace_windows_path_sep(root_dir)
    monostylestd.ROOT_DIR = root_dir

    reports = hub(args.ops)
    monostylestd.print_reports(reports)


if __name__ == "__main__":
    main()
