from pathlib import Path
import os
import re
from lxml import etree
import tempfile
from .odoo_config import customs_dir
from .odoo_config import plaintextfile
from .odoo_config import translate_path_relative_to_customs_root
modified_filename = ""
cache_models = {}
cache_xml_ids = {}

SEP_FILE = ":::"
SEP_LINENO = ":"

def try_to_get_filepath(filepath):
    filepath = Path(filepath)
    if not filepath.is_file():
        candidates = []
        if str(filepath)[0] != '/':
            filepath = customs_dir() / filepath
        root = customs_dir()
        candidates.append((root / filepath))
        for candidate in candidates:
            if candidate.is_file():
                filepath = candidate
                break
    if not filepath.is_file():
        raise Exception(u"not found: {}".format(filepath))
    filepath = filepath.resolve()
    return filepath

def get_file_lineno(line):
    path, lineno = line.split(SEP_FILE)[1].split(SEP_LINENO)
    path = path.replace("//", "/")
    path = try_to_get_filepath(path)
    return path, int(lineno)

def get_view(inherit_id):
    with plaintextfile().open('r') as f:
        lines = f.readlines()
        lines = filter(lambda line: inherit_id in line, lines)
        lines = filter(lambda line: re.search(r"\D\ {}\ \D".format(inherit_id), line), lines)
        lines = list(lines)
        if lines:
            return get_file_lineno(lines[0])
    return None, None

def get_qweb_template(name):
    with plaintextfile().open('r') as f:
        lines = f.readlines()
        lines = list(filter(lambda line: '~qweb' in line and name in line, lines))
        if lines:
            return get_file_lineno(lines[0])
    return None, None

def walk_files(on_match, pattern):
    from . import module_tools
    from .module_tools import Module, Modules

    if modified_filename:
        try:
            mod = Module(modified_filename)
        except Module.IsNot:
            modules = Modules().modules.values()
        else:
            modules = [mod]
    else:
        modules = Modules().modules.values()

    for mod in modules:
        for file in mod.path.glob("**/" + pattern):

            rel_file = file.relative_to(mod.path)
            if rel_file.parts[0] in ['migrations', 'migration']: # ignore migrations folder that contain OpenUpgrade
                continue
            if '.git' in file.parts:
                continue
            if file.name.startswith('.'):
                continue

            lines = file.read_text(encoding='utf-8', errors='ignore').split("\n")
            on_match(file, mod, lines)


def _get_methods():

    result = []

    def on_match(filename, module, lines):
        for linenumber, line in enumerate(lines):
            linenumber += 1
            methodname = re.search(r"def\ ([^\(]*)", line)
            if methodname:
                methodname = methodname.group(1)
                model = None
                if filename in cache_models:
                    if 'lines' in cache_models[filename]:
                        linenums = list(reversed(list(filter(lambda x: x < linenumber, cache_models[filename]['lines'].keys()))))
                        model = None
                        if len(linenums) > 0:
                            model = cache_models[filename]['lines'][linenums[0]]

                    result.append({
                            'model': model,
                            'module': module.name,
                            'type': 'N/A',
                            'filename': os.path.basename(filename),
                            'filepath': filename,
                            'line': linenumber,
                            'method': methodname,
                    })
    walk_files(on_match, "*.py")

    return result

def _get_fields():

    result = []

    def on_match(filename, module, lines):
        for linenumber, line in enumerate(lines):
            linenumber += 1
            if "#" in line:
                line = line.split("#")[0]

            match = re.search(r".*=.*fields\..*\(", line)
            if match:
                fieldname = match.group(0).split("=")[0].strip()
            else:
                # V8
                match = re.search(r"[\'\"]([^\'^\"]*)[\'\"].*fields\.", line)
                if not match:
                    continue
                fieldname = match.group(1)
            if filename in cache_models and 'lines' in cache_models[filename]:
                linenums = list(reversed(list(filter(lambda x: x < linenumber, cache_models[filename]['lines'].keys()))))
                model = None
                if len(linenums) > 0:
                    model = cache_models[filename]['lines'][linenums[0]]

                result.append({
                    'model': model,
                    'module': module.name,
                    'type': 'N/A',
                    'filename': os.path.basename(filename),
                    'filepath': filename,
                    'line': linenumber,
                    'field': fieldname
                })

    walk_files(on_match, "*.py")

    return result


def _get_views():

    result = []
    for id in cache_xml_ids['ids']:
        e = cache_xml_ids['ids'][id]
        if e['model'] == 'ir.ui.view':
            if not e['type'] and e['inherit_id']:
                parent = e
                BARRIER = 0
                while parent and parent['inherit_id'] and BARRIER < 10:
                    parent = cache_xml_ids['ids'].get(e['inherit_id'], None)
                    BARRIER += 1
                if parent:
                    e['type'] = parent['type']
            result.append(e)

    return result

def _get_qweb_templates():
    result = []

    def on_match(filename, module, lines):
        if filename.relative_to(module.path).parts[0] != 'static':
            return

        try:
            tree = etree.parse(filename)
        except Exception:
            return

        # get all records
        for r in tree.xpath("/templates/*"):
            if "t-name" in r.attrib:
                id = r.attrib["t-name"]
                extends = r.get('t-extend', '')

                if '.' not in id:
                    id = "%s.%s" % (module.name, id)

                r = {
                    'type': 'qweb',
                    'module': module.name,
                    'id': id,
                    'filename': os.path.basename(filename),
                    'filepath': filename,
                    'line': r.sourceline,
                    "name": id,
                    "inherit_id": extends,
                }

                result.append(r)

    walk_files(on_match, "*.xml")
    sorted(result, key=lambda x: x['name'])

    return result

def _get_xml_ids():
    result = []

    cache_xml_ids.setdefault('files', {})
    cache_xml_ids.setdefault('ids', {})

    def on_match(filename, module, lines):
        try:
            tree = etree.parse(str(filename))
        except TypeError:
            return
        except etree.XMLSyntaxError:
            return

        if filename not in cache_xml_ids["files"]:
            cache_xml_ids["files"][filename] = []

        def append_result(model, xmlid, line, res_model, name="", ttype="", inherit_id=""):

            if '.' not in xmlid:
                xmlid = "%s.%s" % (module.name, xmlid)

            # find res_models of view:
            if model and xmlid and '.' in xmlid:
                r = {
                    'module': module.name,
                    'model': model,
                    'id': xmlid,
                    'filename': os.path.basename(filename),
                    'filepath': filename,
                    'line': line,
                    'res_model': res_model,
                    "name": name,
                    "type": ttype,
                    "inherit_id": inherit_id,
                }

                cache_xml_ids["ids"][xmlid] = r
                cache_xml_ids["files"][filename].append(r)
                result.append(r)

        # get all records
        for r in tree.xpath("//record"):
            if "id" in r.attrib and "model" in r.attrib:
                id = r.attrib["id"]
                model = r.attrib["model"]

                res_model = r.xpath("field[@name='model' or @name='res_model']")
                if len(res_model) > 0:
                    res_model = res_model[0].text
                else:
                    res_model = ""

                if model == "ir.ui.menuitem":
                    name = r.xpath("field[@name='name']")[0].text
                    append_result(model, id, r.sourceline, '', name)
                elif model == "ir.ui.view":
                    name = ""
                    inherit_id = ""
                    if r.xpath("field[@name='name']"):
                        name = r.xpath("field[@name='name']")[0].text
                    if r.xpath("field[@name='inherit_id']"):
                        if r.xpath("field[@name='inherit_id']/@ref"):
                            inherit_id = r.xpath("field[@name='inherit_id']/@ref")[0]
                            if '.' not in inherit_id:
                                inherit_id = '{}.{}'.format(module, inherit_id)
                    ttype = ""
                    if not inherit_id:
                        if r.xpath("field[@name='arch']"):
                            arch = etree.tostring(r.xpath("field[@name='arch']")[0]).decode('utf-8')
                            lines = [x.strip() for x in arch.split("\n")]
                            lines = [l for l in lines if l]
                            lines = lines[:5]

                            for line in lines:
                                for _t in ['form', 'tree', 'calendar', 'search', 'kanban']:
                                    token = "<{} ".format(_t)
                                    if token in line:
                                        ttype = _t
                    append_result(model, id, r.sourceline, '', name, ttype=ttype, inherit_id=inherit_id)
                else:
                    append_result(model, id, r.sourceline, res_model)

        for r in tree.xpath("//menuitem"):
            if "id" in r.attrib:
                id = r.attrib["id"]
                model = "ir.ui.menuitem"
                name = id
                try:
                    name = r.attrib["name"]
                except Exception:
                    # if there is no name, then name comes from
                    # associated action
                    try:
                        action = r.attrib['action']
                    except Exception:
                        action = ""

                    if action in cache_xml_ids:
                        name = cache_xml_ids[action].get("name")

                append_result(model, id, r.sourceline, '', name)

        for r in tree.xpath("//report"):
            if "id" in r.attrib:
                id = r.attrib["id"]
                model = "report"
                append_result(model, id, r.sourceline, '')

        for r in tree.xpath("//template"):
            if "id" in r.attrib:
                id = r.attrib["id"]
                model = "ir.ui.view"
                inherit_id = ""
                if r.get('inherit_id'):
                    inherit_id = r.get('inherit_id')
                append_result(model, id, r.sourceline, 'qweb', inherit_id=inherit_id)

    walk_files(on_match, "*.xml")
    result.sort(key=lambda x: x['id'])

    return result

def _get_models():
    result = []

    def on_match(filename, module, lines):
        with open(filename, "r") as f:
            lines = f.read().split("\n")

            def append_model(name, name_linenum, inherit, inherit_linenum):
                if name == "" and inherit == "":
                    return

                if name == "" and len(inherit) != "":
                    model = inherit
                    linenum = inherit_linenum
                    inherited = True
                else:
                    model = name
                    linenum = name_linenum
                    inherited = False

                global cache_models
                if filename not in cache_models:
                    cache_models[filename] = {'lines': {}, 'models': {}}
                if model not in cache_models[filename]['models']:
                    cache_models[filename]['models'][model] = []
                if "models" not in cache_models:
                    cache_models["models"] = {}
                if model not in cache_models["models"]:
                    cache_models["models"][model] = []
                cache_models[filename]['models'][model].append(linenum)
                cache_models[filename]['lines'][linenum] = model
                cache_models["models"][model].append({'file': filename, 'line': linenum, 'inherited': inherited, 'module': module})

            for linenum, line in enumerate(lines):
                linenum += 1

                osvregex = [
                    r"class.*\(.*osv.*\)",
                    r"class.*\(.*TransientModel.*\)",
                    r"class.*\(.*Model.*\)",
                ]
                if any(re.match(x, line) for x in osvregex):

                    _name = ""
                    _inherit = ""

                    for linenum1 in range(linenum, len(lines)):
                        line1 = lines[linenum1]
                        linenum1 += 1

                        if re.search(r"[\\\t\ ]_name.?=", line1):
                            _name = re.search("[\\\'\\\"]([^\\\'^\\\"]*)[\\\'\\\"]", line1)
                            if not _name:
                                # print "classname not found in: %s"%lines[i]
                                pass
                            else:
                                _name = _name.group(1)
                        elif re.search(r"[\\\t\ ]_inherit.?=", line1):
                            match = re.search("[\\\'\\\"]([^\\\'^\\\"]*)[\\\'\\\"]", line1)
                            if match:
                                _inherit = match.group(1)
                        elif any(re.match(x, line1) for x in osvregex):
                            # reached new class so append it
                            break

                    linenum_class = linenum # Zeilennummer der Klasse verwenden; es gibt Faelle z.B. stock.move, in denen _columns oberhalb von _name steht
                    append_model(_name, linenum_class, _inherit, linenum_class)
                    linenum = linenum1 - 1

    walk_files(on_match, "*.py")

    cache_models.setdefault("models", {})
    for m in cache_models["models"]:
        for l in cache_models["models"][m]:
            result.append({
                'model': m,
                'line': l['line'],
                'filepath': l['file'],
                'filename': l['file'].name,
                'module': l['module'].name,
                'inherited': l['inherited']
            })
    return result

def _remove_entries(plain_text_file, rel_path):
    """
    Removes entries pointing to the relative path
    """
    with open(plain_text_file, 'r') as f:
        lines = f.readlines()
    match = "{}{}{}".format(SEP_FILE, rel_path, SEP_LINENO)
    temp = tempfile.mktemp(suffix='.tmp')
    os.system("cat '{plain_text_file}' | grep -v '{match}' > '{temp}'; cp '{temp}' '{plain_text_file}'".format(**locals()))

def update_cache(arg_modified_filename=None):
    """
    param: modified_filename - if given, then only this filename is parsed;
    """
    from . import module_tools
    from .module_tools import Module, Modules
    if arg_modified_filename:
        arg_modified_filename = Path(arg_modified_filename).resolve().absolute()
    plainfile = plaintextfile()
    if not plainfile.is_file():
        arg_modified_filename = None

    if arg_modified_filename:
        try:
            Module(arg_modified_filename)
        except Module.IsNot:
            return

    global cache_models
    global modified_filename
    modified_filename = arg_modified_filename

    try:
        rel_path = translate_path_relative_to_customs_root(modified_filename) if modified_filename else None
    except Exception:
        # suck errors - called from vim for all files
        return

    if arg_modified_filename and plainfile.is_file():
        _remove_entries(plainfile, rel_path)

    cache_models = {}
    qwebtemplates = _get_qweb_templates()
    xml_ids = _get_xml_ids()
    models = _get_models()
    methods = _get_methods()
    fields = _get_fields()
    views = _get_views()

    if os.path.isfile(plainfile) and arg_modified_filename:
        f = open(plainfile, 'a')
    else:
        if os.path.isdir(os.path.dirname(plainfile)):
            f = open(plainfile, 'w')
        else:
            return

    try:
        TEMPLATE = "{type}\t[{module}]\t{name}\t" + SEP_FILE + "{filepath}" + SEP_LINENO + "{line}"
        for model in models:
            f.write(TEMPLATE.format(type="model", module=model['module'], name=model['model'], filepath=translate_path_relative_to_customs_root(model['filepath']), line=model['line']))
            f.write("\n")
        for xmlid in xml_ids:
            if '.' in xmlid['id']:
                name = xmlid['id']
            else:
                name = u"{}.{}".format(xmlid['module'], xmlid['id'])
            name += " model:" + xmlid['model']
            f.write(TEMPLATE.format(type="xmlid", module=xmlid['module'], name=name, filepath=translate_path_relative_to_customs_root(xmlid['filepath']), line=xmlid['line']))
            f.write("\n")
        for method in methods:
            name = u"{model}.{method}".format(**method)
            f.write(TEMPLATE.format(type="def", module=method['module'], name=name, filepath=translate_path_relative_to_customs_root(method['filepath']), line=method['line']))
            f.write("\n")
        for field in fields:
            name = u"{model}.{field}".format(**field)
            f.write(TEMPLATE.format(type="field", module=field['module'], name=name, filepath=translate_path_relative_to_customs_root(field['filepath']), line=field['line']))
            f.write("\n")
        for view in views:
            name = u"{res_model} ~{type} {id} [inherit_id={inherit_id}]".format(**view)
            f.write(TEMPLATE.format(type="view", module=view['module'], name=name, filepath=translate_path_relative_to_customs_root(view['filepath']), line=view['line']))
            f.write("\n")
        for qwebtemplate in qwebtemplates:
            name = u"~{type} {id} [inherit_id={inherit_id}]".format(**qwebtemplate)
            f.write(TEMPLATE.format(type="qweb", module=qwebtemplate['module'], name=name, filepath=translate_path_relative_to_customs_root(qwebtemplate['filepath']), line=qwebtemplate['line']))
            f.write("\n")
    finally:
        f.close()

    return plainfile

def goto_inherited_view(filepath, line, current_buffer):
    line -= 1  # line ist einsbasiert
    sline = current_buffer[line]
    context = try_to_get_context(sline, current_buffer[:line + 1], filepath)

    filepath = None
    goto, filepath = None, None

    if isinstance(context, dict):
        if context["context"] in ["arch", "template"] and context.get('inherit_id', False):
            inherit_id = context["inherit_id"]
            filepath, goto = get_view(inherit_id)
        if context["context"] in ["qweb"] and context.get('inherit_id', False):
            inherit_id = context["inherit_id"]
            filepath, goto = get_qweb_template(inherit_id)

    return filepath, goto

def try_to_get_context(line_content, lines_before, filename):
    result = None
    ext = ""
    if filename:
        _, ext = os.path.splitext(filename)

    # determine the last attribute if applicable:
    last_attribute = None

    if " " in line_content:
        t = line_content.split(" ")[-1]
        if "=" in t:
            t = t.split("=")[0]
        t = t.replace("\"", "")
        t = t.replace("\'", "")
        last_attribute = t

    if ext == "":
        pass
    elif ext in (".py", ".mako"):
        pass
    elif ext in (".xml"):
        if last_attribute == "parent":
            return "menuitem"
        elif last_attribute in ["src_model", "res_model"]:
            return "model"
        elif last_attribute in ["groups"]:
            return "group"

    field = False
    if "<field " in line_content and re.search("name=['\"]inherit_id['\"]", line_content) and last_attribute == "ref":
        return "view"
    elif "<field " in line_content and re.search("name=['\"]group_id['\"]", line_content) and last_attribute == "ref":
        return "group"
    elif "<field " in line_content and re.search("name=['\"]model['\"]", line_content):
        return "model"
    elif "<field " in line_content and re.search("name=['\"]model_id['\"]", line_content):
        return "model_id"

    elif "<field " in line_content and re.search("name=['\"]menu_id['\"]", line_content) and last_attribute == "ref":
        return "menuitem"
    elif re.search("<field.*name=['\"]$", line_content):
        field = True

    # check if we are in a view architecture:
    is_arch = False
    model = None
    inherit_id = None
    for i in range(len(lines_before)):
        line = lines_before[- i - 1]
        if re.search(r"<template\ ", line) and "<templates " not in line:
            inherit_id = re.search(r"\ inherit_id=['\"]([^\"^']*)", line)
            if inherit_id:
                inherit_id = inherit_id.group(1)
                return {
                    'context': 'template',
                    'inherit_id': inherit_id,
                }
        if "<t " in line:
            inherit_id = re.search(r"\ t-extend=['\"]([^\"^']*)", line)
            if inherit_id:
                inherit_id = inherit_id.group(1)
                return {
                    'context': 'qweb',
                    'inherit_id': inherit_id,
                }

        if re.search("<field.*name=['\"]arch['\"]", line):
            is_arch = True
        if re.search("<field.*name=['\"]inherit_id['\"]", line):
            try:
                inherit_id = re.search(r"\ ref=['\"]([^\"^']*)", line).group(1)
            except Exception:
                pass
        if re.search("<field.*name=['\"]model['\"]", line):
            try:
                model = re.search(".*>([^<]*)<", line).group(1)
            except Exception:
                pass

        if re.search("<record", line):
            if is_arch:
                return {'context': 'arch', 'model': model, 'field': field, 'inherit_id': inherit_id}

    return result
