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

import java.util.Map.Entry;
import java.util.*;
import javax.swing.SwingUtilities;
import javax.swing.text.BadLocationException;
import javax.swing.text.Document;
import org.netbeans.api.editor.mimelookup.MimeRegistration;
import org.netbeans.api.html.lexer.HTMLTokenId;
import org.netbeans.api.lexer.*;
import org.netbeans.lib.editor.util.CharSequenceUtilities;
import org.netbeans.modules.csl.api.DeclarationFinder.DeclarationLocation;
import org.netbeans.modules.csl.api.Error;
import org.netbeans.modules.csl.api.HintsProvider.HintsManager;
import org.netbeans.modules.csl.api.*;
import org.netbeans.modules.csl.spi.ParserResult;
import org.netbeans.modules.editor.NbEditorDocument;
import org.netbeans.modules.html.editor.api.gsf.HtmlExtension;
import org.netbeans.modules.html.editor.api.gsf.HtmlParserResult;
import org.netbeans.modules.html.editor.lib.api.elements.*;
import org.netbeans.modules.parsing.api.*;
import org.netbeans.modules.parsing.spi.SchedulerEvent;
import org.netbeans.modules.web.jsf.api.editor.JsfFacesComponentsProvider.FacesComponentLibrary;
import org.netbeans.modules.web.jsf.editor.completion.JsfAttributesCompletionHelper;
import org.netbeans.modules.web.jsf.editor.completion.JsfCompletionItem;
import org.netbeans.modules.web.jsf.editor.facelets.AbstractFaceletsLibrary;
import org.netbeans.modules.web.jsf.editor.facelets.CompositeComponentLibrary;
import org.netbeans.modules.web.jsf.editor.hints.HintsRegistry;
import org.netbeans.modules.web.jsfapi.api.DefaultLibraryInfo;
import org.netbeans.modules.web.jsfapi.api.JsfVersion;
import org.netbeans.modules.web.jsfapi.api.Library;
import org.netbeans.modules.web.jsfapi.api.LibraryComponent;
import org.netbeans.modules.web.jsfapi.api.NamespaceUtils;
import org.netbeans.spi.editor.completion.CompletionItem;
import org.netbeans.spi.lexer.MutableTextInput;

/**
 * XXX should be rather done by dynamic artificial embedding creation. The
 * support then can be implemented by CSL language mapped to the language
 * mimetype.
 *
 * @author marekfukala
 */
@MimeRegistration(mimeType=org.netbeans.modules.web.jsfapi.api.JsfUtils.JSF_XHTML_FILE_MIMETYPE, service=HtmlExtension.class)
public class JsfHtmlExtension extends HtmlExtension {

    private static final String EL_ENABLED_KEY = "el_enabled"; //NOI18N

    @Override
    public Map<OffsetRange, Set<ColoringAttributes>> getHighlights(HtmlParserResult result, SchedulerEvent event) {
        final Map<OffsetRange, Set<ColoringAttributes>> highlights = new HashMap<>();

        //highlight JSF tags
        highlightJsfTags(result, highlights);

        //check if the EL is enabled in the file and enables it if not
        checkELEnabled(result);

        return highlights;

    }

    public void checkELEnabled(HtmlParserResult result) {
        Document doc = result.getSnapshot().getSource().getDocument(true);
        InputAttributes inputAttributes = (InputAttributes) doc.getProperty(InputAttributes.class);
        if (inputAttributes == null) {
            inputAttributes = new InputAttributes();
            doc.putProperty(InputAttributes.class, inputAttributes);
        }
        Language xhtmlLang = Language.find(JsfUtils.XHTML_MIMETYPE); //NOI18N
        if (inputAttributes.getValue(LanguagePath.get(xhtmlLang), EL_ENABLED_KEY) == null) {
            inputAttributes.setValue(LanguagePath.get(xhtmlLang), EL_ENABLED_KEY, new Object(), false);

            //refresh token hierarchy so the EL becomes lexed
            recolor(doc);
        }
    }

    private void recolor(final Document doc) {
        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                NbEditorDocument nbdoc = (NbEditorDocument) doc;
                nbdoc.extWriteLock();
                try {
                    MutableTextInput mti = (MutableTextInput) doc.getProperty(MutableTextInput.class);
                    if (mti != null) {
                        mti.tokenHierarchyControl().rebuild();
                    }
                } finally {
                    nbdoc.extWriteUnlock();
                }
            }
        });
    }

    private void highlightJsfTags(HtmlParserResult result, final Map<OffsetRange, Set<ColoringAttributes>> highlights) {
        final Snapshot snapshot = result.getSnapshot();
        Source source = snapshot.getSource();
        JsfSupportImpl jsfs = JsfSupportImpl.findFor(source);
        if (jsfs == null) {
            return;
        }
        Map<String, Library> libs = jsfs.getLibraries();
        Map<String, String> nss = result.getNamespaces();

        //1. resolve which declared libraries are available on classpath

        //2. resolve which tag prefixes are registered for libraries, either available or missing
        // add hint for missing library

        for (String namespace : nss.keySet()) {
            Node root = result.root(namespace);
            if (root != null) {
                final Library tldl = NamespaceUtils.getForNs(libs, namespace);
                ElementUtils.visitChildren(root, new ElementVisitor() {
                    @Override
                    public void visit(Element element) {
                        if (element.type() == ElementType.OPEN_TAG
                                || element.type() == ElementType.CLOSE_TAG) {
                            Named named = (Named) element;

                            if (named.namespacePrefix() != null) {
                                Set<ColoringAttributes> coloring = tldl == null ? ColoringAttributes.CLASS_SET : ColoringAttributes.METHOD_SET;
                                try {
                                    highlight(snapshot, named, highlights, coloring);
                                } catch (BadLocationException ex) {
                                    //just ignore
                                }
                            }
                        }
                    }
                });
            }
        }

    }

    private void highlight(Snapshot s, Named node, Map<OffsetRange, Set<ColoringAttributes>> hls, Set<ColoringAttributes> cas) throws BadLocationException {
        // "<div" id='x'> part
        int prefixLen = node.type() == ElementType.OPEN_TAG ? 1 : 2; //"<" open; "</" close
        hls.put(getDocumentOffsetRange(s, node.from(), node.from() + node.name().length() + prefixLen /* tag open symbol len */),
                cas);
        // <div id='x'">" part
        hls.put(getDocumentOffsetRange(s, node.to() - 1, node.to()),
                cas);

    }

    private OffsetRange getDocumentOffsetRange(Snapshot s, int astFrom, int astTo) throws BadLocationException {
        int from = s.getOriginalOffset(astFrom);
        int to = s.getOriginalOffset(astTo);

        if (from == -1 || to == -1) {
            throw new BadLocationException("Cannot convert snapshot offset to document offset", -1); //NOI18N
        }

        return new OffsetRange(from, to);
    }

    @Override
    public List<CompletionItem> completeOpenTags(CompletionContext context) {
        HtmlParserResult result = context.getResult();
        Source source = result.getSnapshot().getSource();
        JsfSupportImpl jsfs = JsfSupportImpl.findFor(source);
        if (jsfs == null) {
            return Collections.emptyList();
        }
        Map<String, Library> libs = jsfs.getLibraries();
        Set<Library> librariesSet = new HashSet<>(libs.values());
        //uri to prefix map
        Map<String, String> declaredNS = result.getNamespaces();

        List<CompletionItem> items = new ArrayList<>();

        int colonIndex = context.getPrefix().indexOf(':');
        if (colonIndex == -1) {
            //editing namespace or tag w/o ns
            //offer all tags
            for (Library lib : librariesSet) {
                String declaredPrefix = NamespaceUtils.getForNs(declaredNS, lib.getNamespace());
                if (declaredPrefix == null) {
                    //undeclared prefix, try to match with default library prefix
                    if (lib.getDefaultPrefix() != null && lib.getDefaultPrefix().startsWith(context.getPrefix())) {
                        items.addAll(queryLibrary(context, lib, lib.getDefaultPrefix(), true, jsfs.getJsfVersion().isAtLeast(JsfVersion.JSF_2_2)));
                    }
                } else {
                    items.addAll(queryLibrary(context, lib, declaredPrefix, false, jsfs.getJsfVersion().isAtLeast(JsfVersion.JSF_2_2)));
                }
            }
        } else {
            String tagNamePrefix = context.getPrefix().substring(0, colonIndex);
            //find a namespace according to the prefix
            String namespace = getUriForPrefix(tagNamePrefix, declaredNS);
            if (namespace == null) {
                //undeclared prefix, check if a taglib contains it as
                //default prefix. If so, offer it in the cc w/ tag autoimport function
                for (Library lib : librariesSet) {
                    if (lib.getDefaultPrefix() != null && lib.getDefaultPrefix().equals(tagNamePrefix)) {
                        //match
                        items.addAll(queryLibrary(context, lib, tagNamePrefix, true, jsfs.getJsfVersion().isAtLeast(JsfVersion.JSF_2_2)));
                    }
                }

            } else {
                //query only associated lib
                Library lib = NamespaceUtils.getForNs(libs, namespace);
                if (lib == null) {
                    //no such lib, exit
                    return Collections.emptyList();
                } else {
                    //query the library
                    items.addAll(queryLibrary(context, lib, tagNamePrefix, false, jsfs.getJsfVersion().isAtLeast(JsfVersion.JSF_2_2)));
                }
            }
        }

        //filter the items according to the prefix
        Iterator<CompletionItem> itr = items.iterator();
        while (itr.hasNext()) {
            if (!CharSequenceUtilities.startsWith(itr.next().getInsertPrefix(), context.getPrefix())) {
                itr.remove();
            }
        }

        return items;

    }

    private String getUriForPrefix(String prefix, Map<String, String> namespaces) {
        for (Entry<String, String> entry : namespaces.entrySet()) {
            if (prefix.equals(entry.getValue())) {
                return entry.getKey();
            }
        }
        return null;
    }

    private Collection<CompletionItem> queryLibrary(CompletionContext context, Library lib, String nsPrefix, boolean undeclared, boolean isJsf22Plus) {
        Collection<CompletionItem> items = new ArrayList<>();
        for (LibraryComponent component : lib.getComponents()) {
            if (!(component instanceof AbstractFaceletsLibrary.Function)) {
                items.add(JsfCompletionItem.createTag(context.getCCItemStartOffset(), component, nsPrefix, undeclared, isJsf22Plus));
            }
        }

        return items;
    }

    @Override
    public List<CompletionItem> completeAttributes(CompletionContext context) {
        HtmlParserResult result = context.getResult();
        Source source = result.getSnapshot().getSource();
        JsfSupportImpl jsfs = JsfSupportImpl.findFor(source);
        if (jsfs == null) {
            return Collections.emptyList();
        }
        Map<String, Library> libs = jsfs.getLibraries();
        //uri to prefix map
        Map<String, String> declaredNS = result.getNamespaces();

        List<CompletionItem> items = new ArrayList<>();

        Element queriedNode = context.getCurrentNode();
        if (queriedNode.type() != ElementType.OPEN_TAG) {
            return Collections.emptyList();
        }
        OpenTag ot = (OpenTag) queriedNode;
        CharSequence nsPrefix = ot.namespacePrefix();
        if (nsPrefix == null) {
            // this must be at attribute from JSF namespace or not JSF tag (without the prefiex)
            String jsfPrefix = declaredNS.get(DefaultLibraryInfo.JSF.getNamespace());
            if (context.getItemText().startsWith(jsfPrefix + ":")) { //NOI18N
                Library htmlLibrary = NamespaceUtils.getForNs(libs, DefaultLibraryInfo.HTML.getNamespace());
                return JsfAttributesCompletionHelper.getJsfItemsForHtmlElement(context, htmlLibrary, context.getItemText());
            } else {
                return Collections.emptyList();
            }
        }
        String tagName = ot.unqualifiedName().toString();

        String namespace = getUriForPrefix(nsPrefix.toString(), declaredNS);
        Library flib = null;
        if (namespace == null) {
            // the namespace is not imported, try find library according the default prefix
            for (Library lib : libs.values()) {
                if (lib.getDefaultPrefix() != null && lib.getDefaultPrefix().equals(nsPrefix.toString())) {
                    flib = lib;
                    break;
                }
            }
        } else {
            flib = NamespaceUtils.getForNs(libs, namespace);
        }

        if (flib == null) {
            //The facelets library not found. This happens if one declares
            //a namespace which is not matched to any existing library
            return Collections.emptyList();
        }

        JsfAttributesCompletionHelper.completeAttributes(context, items, "", flib, tagName, context.getPrefix());

        return items;
    }

    @Override
    public List<CompletionItem> completeAttributeValue(CompletionContext context) {
        List<CompletionItem> items = new ArrayList<>();

        JsfSupportImpl jsfs = JsfSupportImpl.findFor(context.getResult().getSnapshot().getSource());
        String ns = ElementUtils.getNamespace(context.getCurrentNode());
        OpenTag openTag = context.getCurrentNode().type() == ElementType.OPEN_TAG 
                ? (OpenTag) context.getCurrentNode() : null;

        //complete xmlns attribute value
        if(jsfs != null) {
            JsfAttributesCompletionHelper.completeXMLNSAttribute(context, items, jsfs);
        }
        
        if(ns == null || openTag == null) {
            return items;
        }
        
        //first try to complete using special metadata
        JsfAttributesCompletionHelper.completeTagLibraryMetadata(context, items, ns, openTag);

        if(jsfs == null) {
            return items;
        }

        //then try to complete according to the attribute type (taken from the library descriptor)
        JsfAttributesCompletionHelper.completeValueAccordingToType(context, items, ns, openTag, jsfs);

        // completion for files in cases of ui:include src attribute
        JsfAttributesCompletionHelper.completeFaceletsFromProject(context, items, ns, openTag);

        // completion for sections in cases of ui:define name attribute
        JsfAttributesCompletionHelper.completeSectionsOfTemplate(context, items, ns, openTag);

        // completion for java classes in <cc:attribute type="com.example.|
        JsfAttributesCompletionHelper.completeJavaClasses(context, items, ns, openTag);

        //facets
        JsfAttributesCompletionHelper.completeFacetsInCCImpl(context, items, ns, openTag, jsfs);
        JsfAttributesCompletionHelper.completeFacets(context, items, ns, openTag, jsfs);

        return items;
    }

    @Override
    public DeclarationLocation findDeclaration(ParserResult result, final int caretOffset) {
        assert result instanceof HtmlParserResult;
        HtmlParserResult htmlresult = (HtmlParserResult) result;
        int embeddedOffset = result.getSnapshot().getEmbeddedOffset(caretOffset);
        if (embeddedOffset == -1) {
            return DeclarationLocation.NONE;
        }

        Element leaf = htmlresult.findByPhysicalRange(embeddedOffset, true);
        if (leaf == null || leaf.type() != ElementType.OPEN_TAG) {
            return DeclarationLocation.NONE;
        }

        JsfSupportImpl jsfs = JsfSupportImpl.findFor(result.getSnapshot().getSource());
        if (jsfs == null) {
            return DeclarationLocation.NONE;
        }

        String ns = ElementUtils.getNamespace(leaf);
        if (ns == null) {
            return DeclarationLocation.NONE;
        }

        Library lib = jsfs.getLibrary(ns);
        if (lib == null) {
            return DeclarationLocation.NONE;
        }

        TokenSequence ts = JsfNavigationHelper.getTokenSequenceAtCaret(result.getSnapshot().getTokenHierarchy(), embeddedOffset);
        if (ts == null) {
            return DeclarationLocation.NONE;
        }

        Token t = ts.token();
        if (t.id() == HTMLTokenId.VALUE) {
            String value = CharSequenceUtilities.toString(ts.token().text()).replaceAll("[\"']", ""); //NOI18N
            String attribute = ""; //NOI18N
            while (ts.movePrevious()) {
                if (ts.token().id() == HTMLTokenId.TAG_OPEN) {
                    String tag = CharSequenceUtilities.toString(ts.token().text());
                    return JsfNavigationHelper.goToReferencedFile(htmlresult, embeddedOffset, tag, attribute, value);
                } else if (ts.token().id() == HTMLTokenId.ARGUMENT && attribute.isEmpty()) {
                    attribute = CharSequenceUtilities.toString(ts.token().text());
                }
            }
        } else {
            if (lib instanceof CompositeComponentLibrary) {
                return JsfNavigationHelper.goToCompositeComponentLibrary(htmlresult, embeddedOffset, lib);
            } else if (lib instanceof FacesComponentLibrary) {
                return JsfNavigationHelper.goToFacesComponentLibrary(htmlresult, embeddedOffset, (FacesComponentLibrary) lib);
            }
        }

        return DeclarationLocation.NONE;

    }

    @Override
    public OffsetRange getReferenceSpan(final Document doc, final int caretOffset) {
        TokenHierarchy th = TokenHierarchy.get(doc);
        TokenSequence ts = JsfNavigationHelper.getTokenSequenceAtCaret(th, caretOffset);
        if (ts == null) {
            return OffsetRange.NONE;
        }

        Token t = ts.token();
        if (t.id() == HTMLTokenId.TAG_OPEN) {
            if (CharSequenceUtilities.indexOf(t.text(), ':') != -1) {
                return new OffsetRange(ts.offset(), ts.offset() + t.length());
            }
        } else if (t.id() == HTMLTokenId.ARGUMENT) {
            int from = ts.offset();
            int to = from + t.text().length();
            //try to find the tag and check if there is a prefix
            while (ts.movePrevious()) {
                if (ts.token().id() == HTMLTokenId.TAG_OPEN) {
                    if (CharSequenceUtilities.indexOf(ts.token().text(), ':') != -1) {
                        return new OffsetRange(from, to);
                    } else {
                        break;
                    }
                }
            }
        } else if (t.id() == HTMLTokenId.VALUE) {
            CharSequence value = ts.token().text();
            int from = ts.offset();
            int to = from + t.text().length();
            //try to find the tag and check if there is a prefix
            while (ts.movePrevious()) {
                if (ts.token().id() == HTMLTokenId.TAG_OPEN) {
                    if (CharSequenceUtilities.indexOf(ts.token().text(), "include") != -1) {
                        if (CharSequenceUtilities.indexOf(value, "'") != -1 || CharSequenceUtilities.indexOf(value, "\"") != -1) {
                            from++; to--;
                        }
                        return new OffsetRange(from, to);
                    }
                    break;
                }
            }
        }

        return OffsetRange.NONE;
    }

    @Override
    public void computeErrors(HintsManager manager, RuleContext context, List<Hint> hints, List<Error> unhandled) {
        //just delegate to the hints registry and add all gathered results
        hints.addAll(HintsRegistry.getDefault().gatherHints(context));
    }

    @Override
    public void computeSelectionHints(HintsManager manager, RuleContext context, List<Hint> hints, int start, int end) {
        //inject composite component support
        Hint injectCC = InjectCompositeComponent.getHint(context, start, end);
        if (injectCC != null) {
            hints.add(injectCC);
        }
    }

}
