/*
 * 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.php.editor.completion;

import java.util.Collections;
import java.util.List;
import java.util.Set;
import javax.swing.ImageIcon;
import org.netbeans.api.lexer.TokenHierarchy;
import org.netbeans.api.lexer.TokenSequence;
import org.netbeans.modules.csl.api.CompletionProposal;
import org.netbeans.modules.csl.api.ElementHandle;
import org.netbeans.modules.csl.api.ElementKind;
import org.netbeans.modules.csl.api.HtmlFormatter;
import org.netbeans.modules.csl.api.Modifier;
import org.netbeans.modules.php.editor.CodeUtils;
import org.netbeans.modules.php.editor.api.NameKind;
import org.netbeans.modules.php.editor.api.elements.FieldElement;
import org.netbeans.modules.php.editor.api.elements.MethodElement;
import org.netbeans.modules.php.editor.api.elements.TypeConstantElement;
import org.netbeans.modules.php.editor.api.elements.TypeElement;
import org.netbeans.modules.php.editor.api.elements.TypeMemberElement;
import org.netbeans.modules.php.editor.completion.PHPCompletionItem.CompletionRequest;
import org.netbeans.modules.php.editor.index.PHPDOCTagElement;
import org.netbeans.modules.php.editor.lexer.LexUtilities;
import org.netbeans.modules.php.editor.lexer.PHPDocCommentTokenId;
import org.netbeans.modules.php.editor.lexer.PHPTokenId;
import org.netbeans.modules.php.editor.model.ModelUtils;
import org.netbeans.modules.php.editor.parser.api.Utils;
import org.netbeans.modules.php.editor.parser.astnodes.ASTNode;
import org.netbeans.modules.php.editor.parser.astnodes.FieldsDeclaration;
import org.netbeans.modules.php.editor.parser.astnodes.FunctionDeclaration;
import org.netbeans.modules.php.editor.parser.astnodes.MethodDeclaration;
import org.netbeans.modules.php.editor.parser.astnodes.TypeDeclaration;
import org.netbeans.modules.php.project.api.PhpAnnotations;
import org.netbeans.modules.php.spi.annotation.AnnotationCompletionTag;
import org.netbeans.modules.php.spi.annotation.AnnotationCompletionTagProvider;
import org.openide.util.ImageUtilities;

/**
 *
 * @author tomslot
 */
public final class PHPDOCCodeCompletion {

    private static final String TAG_PREFIX = "@"; //NOI18N

    private PHPDOCCodeCompletion() {
    }

    private interface FetchedTextCallback {
        void call(String fetchedText);
    }

    private static final class TypeContextChecker implements FetchedTextCallback {
        private boolean isTypeContext = true;

        @Override
        public void call(String fetchedTxt) {
            // remove nullable type prefixes
            String fetchedText = CodeUtils.removeNullableTypePrefix(fetchedTxt);
            if (fetchedText.endsWith(CodeUtils.NULLABLE_TYPE_PREFIX)) {
                fetchedText = fetchedText.substring(0, fetchedText.length() - 1);
            }

            String trimmedText = fetchedText.trim();
            if (!trimmedText.isEmpty() && fetchedText.charAt(fetchedText.length() - 1) == '|') { //NOI18N
                // expect that user wants to complete mixed type
                boolean textPartHasWhitespace = false;
                for (int i = 0; i < trimmedText.length(); i++) {
                    if (Character.isWhitespace(trimmedText.charAt(i))) {
                        textPartHasWhitespace = true;
                        break;
                    }
                }
                isTypeContext = !textPartHasWhitespace;
            } else {
                for (int i = 0; i < fetchedText.length(); i++) {
                    if (!Character.isWhitespace(fetchedText.charAt(i))) {
                        isTypeContext = false;
                        break;
                    }
                }
            }
        }

        public boolean isTypeContext() {
            return isTypeContext;
        }

    }

    private static final class MemberCompletion implements FetchedTextCallback {
        private final PHPCompletionResult completionResult;
        private final CompletionRequest request;

        public MemberCompletion(PHPCompletionResult completionResult, CompletionRequest request) {
            this.completionResult = completionResult;
            this.request = request;
        }

        @Override
        public void call(String possibleType) {
            if (!possibleType.trim().isEmpty() && possibleType.endsWith("::")) { //NOI18N
                String type = possibleType.substring(0, possibleType.length() - 2);
                Set<TypeElement> types = request.index.getTypes(NameKind.exact(type));
                TypeElement typeElement = ModelUtils.getFirst(types);
                if (typeElement != null) {
                    completeTypeMembers(typeElement);
                }
            }
        }

        private void completeTypeMembers(TypeElement typeElement) {
            Set<TypeMemberElement> accessibleTypeMembers = request.index.getAccessibleTypeMembers(typeElement, null);
            for (TypeMemberElement typeMemberElement : accessibleTypeMembers) {
                completeTypeMember(typeMemberElement);
            }
        }

        private void completeTypeMember(TypeMemberElement typeMemberElement) {
            if (typeMemberElement instanceof MethodElement) {
                completeMethods((MethodElement) typeMemberElement);
            } else if (typeMemberElement instanceof FieldElement) {
                completeFields((FieldElement) typeMemberElement);
            } else if (typeMemberElement instanceof TypeConstantElement) {
                completeConstants((TypeConstantElement) typeMemberElement);
            }
        }

        private void completeMethods(MethodElement method) {
            List<PHPCompletionItem.MethodElementItem> items = PHPCompletionItem.MethodElementItem.getItems(method, request);
            for (PHPCompletionItem.MethodElementItem methodItem : items) {
                completionResult.add(methodItem);
            }
        }

        private void completeFields(FieldElement field) {
            PHPCompletionItem.FieldItem fieldItem = PHPCompletionItem.FieldItem.getItem(field, request, true);
            completionResult.add(fieldItem);
        }

        private void completeConstants(TypeConstantElement constant) {
            PHPCompletionItem.TypeConstantItem constantItem = PHPCompletionItem.TypeConstantItem.getItem(constant, request);
            completionResult.add(constantItem);
        }

    }

    private static void fetchDocText(CompletionRequest request, FetchedTextCallback callback) {
        TokenHierarchy<?> th = request.info.getSnapshot().getTokenHierarchy();
        TokenSequence<PHPTokenId> phpTS = (th != null) ? LexUtilities.getPHPTokenSequence(th, request.anchor) : null;
        if (phpTS != null) {
            phpTS.move(request.anchor);
            if (phpTS.moveNext()) {
                TokenSequence<PHPDocCommentTokenId> tokenSequence = phpTS.embedded(PHPDocCommentTokenId.language());
                if (tokenSequence != null) {
                    tokenSequence.move(request.anchor);
                    if (tokenSequence.movePrevious()) {
                        int offset = tokenSequence.offset() + tokenSequence.token().length();
                        if (tokenSequence.moveNext()) {
                            CharSequence tokenText = tokenSequence.token().text();
                            String text = tokenText.subSequence(0, request.anchor - offset).toString().trim();
                            callback.call(text);
                        }
                    }
                }
            }
        }
    }

    static boolean isTypeCtx(PHPCompletionItem.CompletionRequest request) {
        TypeContextChecker typeContextChecker = new TypeContextChecker();
        fetchDocText(request, typeContextChecker);
        return typeContextChecker.isTypeContext();
    }

    public static void complete(final PHPCompletionResult completionResult, final CompletionRequest request) {
        if (request.prefix.startsWith(TAG_PREFIX)) {
            completeAnnotation(completionResult, request);
        }
        fetchDocText(request, new MemberCompletion(completionResult, request));
    }

    private static void completeAnnotation(final PHPCompletionResult completionResult, final CompletionRequest request) {
        String prefix = request.prefix.substring(TAG_PREFIX.length());
        List<AnnotationCompletionTagProvider> providers = PhpAnnotations.getDefault().getCompletionTagProviders(request.info.getSnapshot().getSource().getFileObject());
        ASTNode nodeAfterOffset = Utils.getNodeAfterOffset(request.result, request.anchor);
        int priority = 0;
        for (AnnotationCompletionTagProvider annotationProvider : providers) {
            priority++;
            List<AnnotationCompletionTag> annotations;
            if (nodeAfterOffset instanceof TypeDeclaration) {
                annotations = annotationProvider.getTypeAnnotations();
            } else if (nodeAfterOffset instanceof MethodDeclaration) {
                annotations = annotationProvider.getMethodAnnotations();
            } else if (nodeAfterOffset instanceof FunctionDeclaration) {
                annotations = annotationProvider.getFunctionAnnotations();
            } else if (nodeAfterOffset instanceof FieldsDeclaration) {
                annotations = annotationProvider.getFieldAnnotations();
            } else {
                annotations = annotationProvider.getAnnotations();
            }
            for (AnnotationCompletionTag tag : annotations) {
                if (tag.getName().startsWith(prefix)) {
                    completionResult.add(new PHPDOCCodeCompletionItem(request.anchor, tag, annotationProvider.getName(), priority));
                }
            }
        }
    }

    public static class PHPDOCCodeCompletionItem implements CompletionProposal {
        private static final String PHP_ANNOTATION_ICON = "org/netbeans/modules/php/editor/resources/annotation.png"; //NOI18N
        private static final ImageIcon ANNOTATION_ICON = new ImageIcon(ImageUtilities.loadImage(PHP_ANNOTATION_ICON));
        private final AnnotationCompletionTag tag;
        private final int anchorOffset;
        private final PHPDOCTagElement elem;
        private final String providerName;
        private final int priority;

        public PHPDOCCodeCompletionItem(int anchorOffset, AnnotationCompletionTag tag, String providerName, int priority) {
            this.tag = tag;
            this.anchorOffset = anchorOffset;
            this.providerName = providerName;
            this.priority = priority;
            elem = new PHPDOCTagElement(tag.getName(), tag.getDocumentation());
        }

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

        @Override
        public ElementHandle getElement() {
            return elem;
        }

        @Override
        public String getName() {
            return TAG_PREFIX + tag.getName(); //NOI18N
        }

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

        @Override
        public String getSortText() {
            return priority + providerName + getName();
        }

        @Override
        public int getSortPrioOverride() {
            return 0;
        }

        @Override
        public String getLhsHtml(HtmlFormatter formatter) {
            formatter.name(getKind(), true);
            formatter.appendText(getName());
            formatter.name(getKind(), false);
            tag.formatParameters(formatter);
            return formatter.getText();
        }

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

        @Override
        public ElementKind getKind() {
            return elem.getKind();
        }

        @Override
        public ImageIcon getIcon() {
            return ANNOTATION_ICON;
        }

        @Override
        public Set<Modifier> getModifiers() {
            return Collections.<Modifier>emptySet();
        }

        @Override
        public boolean isSmart() {
            return false;
        }

        @Override
        public String getCustomInsertTemplate() {
            return tag.getInsertTemplate();
        }
    }
}
