package com.taobao.weex.annotator;

import com.intellij.lang.annotation.Annotation;
import com.intellij.lang.annotation.AnnotationHolder;
import com.intellij.lang.annotation.Annotator;
import com.intellij.lang.javascript.psi.*;
import com.intellij.openapi.util.TextRange;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiWhiteSpace;
import com.intellij.psi.html.HtmlTag;
import com.intellij.psi.xml.*;
import com.taobao.weex.lint.*;
import com.taobao.weex.quickfix.QuickFixAction;
import com.taobao.weex.utils.CodeUtil;
import com.taobao.weex.utils.WeexFileUtil;
import org.jetbrains.annotations.NotNull;

import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;

/**
 * Created by moxun on 16/10/11.
 */
public class WeexAnnotator implements Annotator {

    private JSEmbeddedContent script;
    private JSObjectLiteralExpression moduleExports;

    @Override
    public void annotate(@NotNull PsiElement psiElement, @NotNull AnnotationHolder annotationHolder) {
        if (!psiElement.getContainingFile().getVirtualFile().getName().toLowerCase().endsWith(".we")) {
            return;
        }
        if (psiElement instanceof XmlDocument) {
            checkStructure(psiElement, annotationHolder);
        }

        if (psiElement instanceof XmlTag && ((XmlTag) psiElement).getName().equals("script")) {
            label:
            for (PsiElement element : psiElement.getChildren()) {
                if (element instanceof JSEmbeddedContent) {
                    script = (JSEmbeddedContent) element;
                    for (PsiElement element1 : script.getChildren()) {
                        if (element1 instanceof JSExpressionStatement) {
                            for (PsiElement element2 : element1.getChildren()) {
                                if (element2 instanceof JSAssignmentExpression) {
                                    PsiElement[] children = element2.getChildren();
                                    if (children.length == 2) {
                                        if (children[0].getText().equals("module.exports")) {
                                            moduleExports = (JSObjectLiteralExpression) children[1];
                                            break label;
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }

    private void checkAttributes(XmlTag tag, @NotNull AnnotationHolder annotationHolder) {
        if (tag.getLocalName().toLowerCase().equals("template")) {
            for (XmlTag child : tag.getSubTags()) {
                checkAttributeValue(child, annotationHolder);
            }
        }
    }

    private
    @NotNull
    LintResult verifyDataType(String type, String s) {

        if ("function".equals(type.toLowerCase())) {
            return verifyFunction(s);
        }

        LintResult result = new LintResult();

        if (s.contains(".") || Pattern.compile("\\[\\d+\\]").matcher(s).matches()) {
            result.setCode(LintResultType.PASSED);
            result.setDesc("Skip analyse array or object");
            return result;
        }

        s = CodeUtil.getVarNameFromMustache(s);

        if (type != null && type.equalsIgnoreCase("boolean")) {
            if (CodeUtil.maybeBooleanExpression(s)) {
                result.setCode(LintResultType.PASSED);
                result.setDesc("Boolean Expression");
                return result;
            }
        }

        if (moduleExports == null) {
            result.setCode(LintResultType.UNRESOLVED_VAR);
            result.setDesc("Unresolved property '" + s + "'");
            return result;
        }
        JSProperty data = moduleExports.findProperty("data");

        if (data == null || data.getValue() == null) {
            result.setCode(LintResultType.UNRESOLVED_VAR);
            result.setDesc("Unresolved property '" + s + "'");
            return result;
        }

        for (PsiElement element : data.getValue().getChildren()) {

            if (!(element instanceof JSProperty)) {
                result.setCode(LintResultType.PASSED);
                result.setDesc("Unsupported data type: " + element.getClass().getSimpleName());
                return result;
            }

            String varName = ((JSProperty) element).getName();
            if (varName == null) {
                result.setCode(LintResultType.UNRESOLVED_VAR);
                result.setDesc("Unresolved property '" + s + "'");
                return result;
            }
            if (varName.equals(s)) {
                String typeString = WeexFileUtil.getJSPropertyType((JSProperty) element);
                if (match(type, typeString)) {
                    result.setCode(LintResultType.PASSED);
                    result.setDesc("Lint passed");
                    return result;
                } else {
                    JSExpression expression = ((JSProperty) element).getValue();
                    if (expression != null) {
                        String value = expression.getText();
                        String guessedType = CodeUtil.guessStringType(value);
                        if (match(type, guessedType)) {
                            result.setCode(LintResultType.PASSED);
                            result.setDesc("Lint passed");
                            return result;
                        }
                    }
                    result.setCode(LintResultType.WRONG_VALUE_TYPE);
                    result.setDesc("Wrong property type. expect " + type + ", found " + typeString);
                    return result;
                }
            }
        }
        result.setCode(LintResultType.UNRESOLVED_VAR);
        result.setDesc("Unresolved property '" + s + "'");
        return result;
    }

    private
    @NotNull
    LintResult verifyFunction(String s) {

        s = CodeUtil.getFunctionNameFromMustache(s);

        LintResult result = new LintResult();

        if (moduleExports == null) {
            result.setCode(LintResultType.UNRESOLVED_VAR);
            result.setDesc("Unresolved function '" + s + "'");
            return result;
        }
        JSProperty data = moduleExports.findProperty("methods");

        if (data == null || data.getValue() == null) {
            result.setCode(LintResultType.UNRESOLVED_VAR);
            result.setDesc("Unresolved function '" + s + "'");
            return result;
        }

        for (PsiElement e : data.getValue().getChildren()) {
            if (e instanceof JSProperty) {
                for (PsiElement e1 : e.getChildren()) {
                    if (e1 instanceof JSFunctionExpression) {
                        if (s.equals(((JSFunctionExpression) e1).getName())) {
                            result.setCode(LintResultType.PASSED);
                            result.setDesc("Lint passed");
                            return result;
                        }
                    }
                }
            }
        }

        result.setCode(LintResultType.UNRESOLVED_VAR);
        result.setDesc("Unresolved function '" + s + "'");
        return result;
    }

    private LintResult verifyVarAndFunction(String type, String s) {
        LintResult l1 = verifyDataType(type, s);
        if (!l1.passed()) {
            LintResult l2 = verifyFunction(s);
            if (l2.passed()) {
                return l2;
            } else {
                return l1;
            }
        } else {
            return l1;
        }
    }

    private boolean match(String type, String jsType) {
        if (type.equals(jsType.toLowerCase())) {
            return true;
        }
        if (type.equals("var")) {
            return true;
        }
        return false;
    }

    private void checkAttributeValue(XmlTag xmlTag, @NotNull AnnotationHolder annotationHolder) {
        WeexTag tag = DirectiveLint.getWeexTag(xmlTag.getName());
        if (tag != null) {
            //check self attrs

            List<String> parent = tag.parent;
            if (xmlTag.getParentTag() != null) {
                String name = xmlTag.getParentTag().getName();
                if (parent != null && !parent.contains(name)) {
                    annotationHolder.createErrorAnnotation(xmlTag, "Element '" + xmlTag.getName()
                            + "' only allowed " + tag.parent.toString() + " as parent element");
                }
            }

            Set<String> extAttrs = tag.getExtAttrs();
            for (XmlAttribute attr : xmlTag.getAttributes()) {

                String attrName = attr.getName();
                String attrValue = attr.getValue();
                Attribute validAttr = tag.getAttribute(attrName);

                if (WeexFileUtil.isMustacheValue(attrValue)) {
                    String bindVar = attrValue.replaceAll("\\{+", "").replaceAll("\\}+", "");
                    LintResult ret = null;
                    String type = "var";
                    if (validAttr == null) {
                        ret = verifyVarAndFunction("var", bindVar);
                    } else {
                        if (!inRepeat(xmlTag) || "repeat".equals(attrName)) {
                            type = validAttr.valueType;
                            ret = verifyDataType(validAttr.valueType, bindVar);
                        }
                    }
                    if (inRepeat(xmlTag) && !"repeat".equals(attrName)) {
                        //repeat 绑定数组内的数据在lint时可能不存在, 跳过检测
                        ret = new LintResult(LintResultType.PASSED, "Skip repeat tag");
                    }
                    if (!ret.passed()) {
                        Annotation annotation = annotationHolder
                                .createErrorAnnotation(attr.getValueElement(), ret.getDesc());
                        if (ret.getCode() == LintResultType.UNRESOLVED_VAR) {
                            annotation.registerFix(new QuickFixAction(bindVar, type));
                        }
                    }
                }

                if (extAttrs.contains(attrName)) {
                    if (!WeexFileUtil.isMustacheValue(attrValue) && validAttr != null && !CodeUtil.maybeInLineExpression(attrValue)) {

                        //正则匹配
                        if (validAttr.valuePattern != null) {
                            if (!validAttr.match(attrValue)) {
                                annotationHolder.createErrorAnnotation(attr.getValueElement(), "Attribute '" + attr.getName()
                                        + "' only allowed value that match '" + validAttr.valuePattern + "'");
                            }
                        } else {
                            //枚举匹配
                            if (validAttr.valueEnum != null) {
                                if (!validAttr.valueEnum.contains(attr.getValue()) && attr.getValueElement() != null) {
                                    annotationHolder.createErrorAnnotation(attr.getValueElement(), "Attribute '" + attr.getName()
                                            + "' only allowed " + validAttr.valueEnum.toString() + " or mustache template as value");
                                }
                            }
                        }
                    }
                } else {
                    if (CodeUtil.maybeInLineExpression(attrValue)) {
                        //TODO: checking expression
                    } else {
                        annotationHolder.createInfoAnnotation(attr, "Not weex defined attribute '" + attrName + "'");
                    }
                }
            }

            if (xmlTag.getSubTags().length == 0 && !inRepeat(xmlTag)) {
                String value = xmlTag.getValue().getText();
                if (WeexFileUtil.containsMustacheValue(value)) {
                    Map<String, TextRange> vars = WeexFileUtil.getVars(value);
                    for (Map.Entry<String, TextRange> entry : vars.entrySet()) {
                        String bindVar = entry.getKey();
                        LintResult ret = verifyDataType("var", bindVar);
                        if (!ret.passed()) {
                            TextRange base = xmlTag.getValue().getTextRange();
                            TextRange range = new TextRange(base.getStartOffset() + entry.getValue().getStartOffset() + 2,
                                    base.getStartOffset() + entry.getValue().getEndOffset() - 2);
                            Annotation annotator = annotationHolder.
                                    createErrorAnnotation(range, ret.getDesc());
                            if (ret.getCode() == LintResultType.UNRESOLVED_VAR) {
                                annotator.registerFix(new QuickFixAction(bindVar, "var"));
                            }
                        }
                    }
                }
            }

            //check sub tags
            XmlTag[] subTags = xmlTag.getSubTags();
            for (XmlTag t : subTags) {
                List<String> validChild = tag.child;
                if (validChild != null && !validChild.contains(t.getName().toLowerCase())) {
                    annotationHolder.createErrorAnnotation(t, "Element '" + xmlTag.getName()
                            + "' only allowed " + tag.child.toString() + " as child element");
                }
                checkAttributeValue(t, annotationHolder);
            }
        } else {
            //unsupported tag
        }
    }

    private boolean inRepeat(XmlTag tag) {
        if (tag == null) {
            return false;
        }

        if ("template".equals(tag.getName())) {
            return false;
        }

        if (tag.getAttribute("repeat") != null) {
            return true;
        } else {
            return inRepeat(tag.getParentTag());
        }
    }

    private void checkStructure(PsiElement document, @NotNull AnnotationHolder annotationHolder) {
        PsiElement[] children = document.getChildren();
        List<String> acceptedTag = Arrays.asList("template","element","script", "style");
        for (PsiElement element : children) {
            if (element instanceof HtmlTag) {
                if (!acceptedTag.contains(((HtmlTag) element).getName().toLowerCase())) {
                    annotationHolder.createErrorAnnotation(element, "Invalid tag '"
                            + ((HtmlTag) element).getName() + "', only the [template, element, script, style] tags are allowed here.");
                }
                checkAttributes((XmlTag) element, annotationHolder);
            } else {
                if (!(element instanceof PsiWhiteSpace)
                        && !(element instanceof XmlProlog)
                        && !(element instanceof XmlText)
                        && !(element instanceof XmlComment)) {
                    String s = element.getText();
                    if (s.length() > 20) {
                        s = s.substring(0, 20);
                    }
                    annotationHolder.createErrorAnnotation(element, "Invalid content '" + s +
                            "', only the [template, script, style] tags are allowed here.");
                }
            }
        }
    }
}