/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package org.netbeans.modules.web.el.completion;

import com.sun.el.parser.AstDotSuffix;
import com.sun.el.parser.AstIdentifier;
import com.sun.el.parser.AstMethodArguments;
import com.sun.el.parser.Node;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.NestingKind;
import javax.lang.model.element.PackageElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.util.ElementScanner6;
import org.netbeans.api.java.source.ClassIndex;
import org.netbeans.api.java.source.CompilationController;
import org.netbeans.modules.csl.api.CodeCompletionContext;
import org.netbeans.modules.csl.api.CompletionProposal;
import org.netbeans.modules.csl.api.HtmlFormatter;
import org.netbeans.modules.csl.spi.DefaultCompletionProposal;
import org.netbeans.modules.web.el.AstPath;
import org.netbeans.modules.web.el.CompilationContext;
import org.netbeans.modules.web.el.ELElement;
import org.netbeans.modules.web.el.completion.ELCodeCompletionHandler.PrefixMatcher;

/**
 * Provides java completion inside the EL expressions - for calls of static field, methods and constructors.
 *
 * @author Martin Fousek <marfous@netbeans.org>
 */
public class ELJavaCompletion {

    private ELJavaCompletion() {
    }

    private static final Set<Modifier> PUBLIC_STATIC = EnumSet.<Modifier>of(Modifier.PUBLIC, Modifier.STATIC);
    private static final Set<ElementKind> FIELD_METHOD = EnumSet.<ElementKind>of(ElementKind.FIELD, ElementKind.METHOD);
    private static final String JAVA_LANG_PACKAGE = "java.lang"; //NOI18N
    private static final String JAVA_LANG_PREFIX = JAVA_LANG_PACKAGE + "."; //NOI18N
    private static final PackageEntry DEFAULT_PACKAGE = new PackageEntry(JAVA_LANG_PACKAGE, true);

    protected static void propose(
            CompilationContext ccontext,
            CodeCompletionContext context,
            ELElement element,
            Node targetNode,
            List<CompletionProposal> proposals) throws IOException {
        String prefix = getPrefixForNode(context, element, targetNode);
        PrefixMatcher prefixMatcher = PrefixMatcher.create(prefix, context);
        doJavaCompletion(ccontext, prefixMatcher, context.getCaretOffset() - prefix.length(), proposals);
    }

    private static String getPrefixForNode(CodeCompletionContext context, ELElement element, Node targetNode) {
        if (targetNode instanceof AstDotSuffix) {
            AstPath path = new AstPath(element.getNode());
            return extractPrefixFromPath(path.rootToNode(targetNode)) + "."; //NOI18N
        } else if (targetNode instanceof AstMethodArguments) {
            AstPath path = new AstPath(targetNode);
            return extractPrefixFromPath(path.rootToLeaf()); //NOI18N
        }
        return context.getPrefix() != null ? context.getPrefix() : ""; //NOI18N
    }

    private static void doJavaCompletion(CompilationContext ccontext, PrefixMatcher pm, final int offset, final List<CompletionProposal> proposals) throws IOException {
        CompilationController cc = (CompilationController) ccontext.info();
        String packName = pm.getPrefix();
        int dotIndex = pm.getPrefix().lastIndexOf('.'); // NOI18N
        if (dotIndex != -1) {
            packName = pm.getPrefix().substring(0, dotIndex);
        }

        // adds packages to the CC
        addPackages(cc, pm.getPrefix(), offset, proposals);

        Set<PackageEntry> packages = new HashSet<>();
        if (dotIndex == -1) {
            // java.lang package is imported by default
            packages.add(DEFAULT_PACKAGE);
        }
        if (!packName.isEmpty()) {
            packages.add(new PackageEntry(packName, false));
        }

        // adds types to the CC
        addTypesFromPackages(cc, pm, packages, offset + dotIndex + 1, proposals);

        // adds element type fields and methods
        TypeElement typeElement = cc.getElements().getTypeElement(packName);
        if (typeElement == null && dotIndex != -1) {
            typeElement = cc.getElements().getTypeElement(JAVA_LANG_PREFIX + pm.getPrefix().substring(0, dotIndex));
        }
        if (typeElement != null) {
            proposals.addAll(getFieldsAndMethods(typeElement, offset + dotIndex + 1));
        }
    }

//    private static void addAllTypes(final CompilationContext ccontext, CompilationController controler, final List<CompletionProposal> proposals) {
//        String EMPTY = ""; //NOI18N
//        String prefix = ""; //NOI18N
//        ClassIndex.NameKind kind = ClassIndex.NameKind.CASE_INSENSITIVE_PREFIX;
//        Set<ElementHandle<TypeElement>> declaredTypes = controler.getClasspathInfo().getClassIndex().getDeclaredTypes(prefix != null ? prefix : EMPTY, kind, EnumSet.allOf(ClassIndex.SearchScope.class));
//        for (ElementHandle<TypeElement> name : declaredTypes) {
//            proposals.add(new JavaTypeCompletionItem(ccontext, name));
//        }
//    }
    private static void addTypesFromPackages(CompilationController cc, PrefixMatcher pm, Set<PackageEntry> packages, int offset, List<CompletionProposal> proposals) {
        for (PackageEntry packageEntry : packages) {
            PackageElement pkgElem = cc.getElements().getPackageElement(packageEntry.packageName);
            if (pkgElem == null) {
                continue;
            }
            List<TypeElement> tes = new TypeScanner().scan(pkgElem);
            for (TypeElement te : tes) {
                if (packageEntry.imported) {
                    if (pm.matches(te.getSimpleName().toString())) {
                        JavaTypeCompletionItem item = new JavaTypeCompletionItem(te, offset);
                        proposals.add(item);
                    }
                } else {
                    if (pm.matches(te.getQualifiedName().toString())) {
                        JavaTypeCompletionItem item = new JavaTypeCompletionItem(te, offset);
                        proposals.add(item);
                    }
                }
            }
        }
    }

    private static Collection<DefaultCompletionProposal> getFieldsAndMethods(TypeElement typeElement, int i) {
        Map<String, DefaultCompletionProposal> classProposals = new HashMap<>();
        for (Element element : typeElement.getEnclosedElements()) {
            if (element.getModifiers().containsAll(PUBLIC_STATIC) && FIELD_METHOD.contains(element.getKind())) {
                classProposals.put(element.getSimpleName().toString(), new JavaFieldCompletionItem(element, typeElement.getSimpleName().toString(), i));
            }
        }
        return classProposals.values();
    }

    private static void addPackages(CompilationController controler, String fqnPrefix, int offset, final List<CompletionProposal> proposals) {
        if (fqnPrefix == null || fqnPrefix.isEmpty()) {
            fqnPrefix = "java"; //NOI18N
        }
        for (String pkgName : controler.getClasspathInfo().getClassIndex().getPackageNames(
                fqnPrefix, true, EnumSet.allOf(ClassIndex.SearchScope.class))) {
            if (isImportedPackage(pkgName)) {
                proposals.add(new JavaPackageCompletionItem(pkgName, offset));
            }
        }
    }

    private static String extractPrefixFromPath(List<Node> pathToNode) {
        StringBuilder sb = new StringBuilder();
        for (Node node : pathToNode) {
            if (node instanceof AstIdentifier) {
                sb.append(node.getImage());
            } else if (node instanceof AstDotSuffix) {
                sb.append(".").append(node.getImage()); //NOI18N
            }
        }
        return sb.toString(); //NOI18N
    }

    private static boolean isImportedPackage(String pkgName) {
        // TODO - get imported classes and allow them to be completed too. For now we show only classes
        // enabled by default
        return pkgName.startsWith("java.") || pkgName.startsWith("javax.")
                || pkgName.equals("java") || pkgName.equals("javax");
    }

    private static final class JavaTypeCompletionItem extends DefaultCompletionProposal {

        private final String simpleName;
        private final String fqn;
        private final int offset;

        public JavaTypeCompletionItem(TypeElement te, int offset) {
            this.simpleName = te.getSimpleName().toString();
            this.fqn = te.getQualifiedName().toString();
            this.offset = offset;
        }

        @Override
        public org.netbeans.modules.csl.api.ElementHandle getElement() {
            return null;
        }

        @Override
        public org.netbeans.modules.csl.api.ElementKind getKind() {
            return org.netbeans.modules.csl.api.ElementKind.CLASS;
        }

        @Override
        public String getName() {
            return simpleName;
        }

        @Override
        public String getRhsHtml(HtmlFormatter formatter) {
            return fqn;
        }

        @Override
        public int getAnchorOffset() {
            return offset;
        }
    }

    private static final class JavaFieldCompletionItem extends DefaultCompletionProposal {

        private final String name;
        private final String enclosingName;
        private final org.netbeans.modules.csl.api.ElementKind kind;
        private final int offset;

        public JavaFieldCompletionItem(Element e, String enclosingName, int offset) {
            this.name = e.getSimpleName().toString();
            this.enclosingName = enclosingName;
            this.kind = e.getKind() == ElementKind.METHOD
                    ? org.netbeans.modules.csl.api.ElementKind.METHOD
                    : org.netbeans.modules.csl.api.ElementKind.CONSTANT;
            this.offset = offset;
        }

        @Override
        public org.netbeans.modules.csl.api.ElementHandle getElement() {
            return null;
        }

        @Override
        public org.netbeans.modules.csl.api.ElementKind getKind() {
            return kind;
        }

        @Override
        public String getName() {
            return name;
        }

        @Override
        public String getRhsHtml(HtmlFormatter formatter) {
            return enclosingName;
        }

        @Override
        public int getAnchorOffset() {
            return offset;
        }

        @Override
        public String getCustomInsertTemplate() {
            if (isMethod()) {
                return super.getCustomInsertTemplate() + "(${cursor})"; //NOI18N
            }
            return super.getCustomInsertTemplate();
        }

        private boolean isMethod() {
            return kind == org.netbeans.modules.csl.api.ElementKind.METHOD;
        }
    }

    private static final class JavaPackageCompletionItem extends DefaultCompletionProposal {

        private final String name;
        private final int offset;

        public JavaPackageCompletionItem(String name, int offset) {
            this.name = name;
            this.offset = offset;
        }

        @Override
        public org.netbeans.modules.csl.api.ElementHandle getElement() {
            return null;
        }

        @Override
        public org.netbeans.modules.csl.api.ElementKind getKind() {
            return org.netbeans.modules.csl.api.ElementKind.PACKAGE;
        }

        @Override
        public int getAnchorOffset() {
            return offset;
        }

        @Override
        public String getName() {
            return name;
        }

        @Override
        public String getRhsHtml(HtmlFormatter formatter) {
            return name;
        }
    }

    private static final class TypeScanner extends ElementScanner6<List<TypeElement>, Void> {

        public TypeScanner() {
            super(new ArrayList<TypeElement>());
        }

        private static boolean isAccessibleClass(TypeElement te) {
            NestingKind nestingKind = te.getNestingKind();
            return (nestingKind == NestingKind.TOP_LEVEL && te.getModifiers().contains(Modifier.PUBLIC));
        }

        @Override
        public List<TypeElement> visitType(TypeElement typeElement, Void arg) {
            if (typeElement.getKind() == ElementKind.CLASS && isAccessibleClass(typeElement)) {
                DEFAULT_VALUE.add(typeElement);
            }
            return super.visitType(typeElement, arg);
        }
    }

    private static final class PackageEntry {
        private final String packageName;
        private final boolean imported;

        public PackageEntry(String packageName, boolean imported) {
            this.packageName = packageName;
            this.imported = imported;
        }

        @Override
        public int hashCode() {
            int hash = 7;
            hash = 23 * hash + packageName.hashCode();
            hash = 23 * hash + Boolean.valueOf(imported).hashCode();
            return hash;
        }

        @Override
        public boolean equals(Object obj) {
            if (obj == null) {
                return false;
            }
            if (this.getClass() != obj.getClass()) {
                return false;
            }
            PackageEntry other = (PackageEntry) obj;
            if (!this.packageName.equals(other.packageName)) {
                return false;
            }
            if (this.imported != other.imported) {
                return false;
            }
            return true;
        }
    }
}
