/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright 2010 Oracle and/or its affiliates. All rights reserved.
 *
 * Oracle and Java are registered trademarks of Oracle and/or its affiliates.
 * Other names may be trademarks of their respective owners.
 *
 * The contents of this file are subject to the terms of either the GNU
 * General Public License Version 2 only ("GPL") or the Common
 * Development and Distribution License("CDDL") (collectively, the
 * "License"). You may not use this file except in compliance with the
 * License. You can obtain a copy of the License at
 * http://www.netbeans.org/cddl-gplv2.html
 * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
 * specific language governing permissions and limitations under the
 * License.  When distributing the software, include this License Header
 * Notice in each file and include the License file at
 * nbbuild/licenses/CDDL-GPL-2-CP.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the GPL Version 2 section of the License file that
 * accompanied this code. If applicable, add the following below the
 * License Header, with the fields enclosed by brackets [] replaced by
 * your own identifying information:
 * "Portions Copyrighted [year] [name of copyright owner]"
 *
 * If you wish your version of this file to be governed by only the CDDL
 * or only the GPL Version 2, indicate your decision by adding
 * "[Contributor] elects to include this software in this distribution
 * under the [CDDL or GPL Version 2] license." If you do not indicate a
 * single choice of license, a recipient has the option to distribute
 * your version of this file under either the CDDL, the GPL Version 2 or
 * to extend the choice of license to its licensees as provided above.
 * However, if you add GPL Version 2 code and therefore, elected the GPL
 * Version 2 license, then the option applies only if the new code is
 * made subject to such option by the copyright holder.
 *
 * Contributor(s):
 *
 * Portions Copyrighted 2008 Sun Microsystems, Inc.
 */
package org.netbeans.modules.maven.repository;

import java.awt.Image;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.swing.Action;
import javax.swing.SwingUtilities;
import org.apache.lucene.search.BooleanQuery;
import org.netbeans.api.annotations.common.StaticResource;
import org.netbeans.modules.maven.indexer.api.NBVersionInfo;
import org.netbeans.modules.maven.indexer.api.QueryField;
import org.netbeans.modules.maven.indexer.api.RepositoryInfo;
import org.netbeans.modules.maven.indexer.api.RepositoryPreferences;
import org.netbeans.modules.maven.indexer.api.RepositoryQueries;
import org.netbeans.modules.maven.indexer.api.RepositoryQueries.Result;
import static org.netbeans.modules.maven.repository.Bundle.*;
import org.netbeans.modules.maven.repository.M2RepositoryBrowser.QueryRequest;
import org.openide.actions.DeleteAction;
import org.openide.nodes.AbstractNode;
import org.openide.nodes.ChildFactory;
import org.openide.nodes.Children;
import org.openide.nodes.FilterNode;
import org.openide.nodes.Node;
import org.openide.util.ImageUtilities;
import org.openide.util.NbBundle.Messages;
import org.openide.util.RequestProcessor;
import org.openide.util.actions.SystemAction;
import org.openide.util.lookup.Lookups;

/**
 *
 * @author  mkleint
 */
public class FindResultsNode extends AbstractNode {

    private static final @StaticResource String FIND_IN_REPO = "org/netbeans/modules/maven/repository/FindInRepo.png";
    private static final @StaticResource String ARTIFACT_BADGE = "org/netbeans/modules/maven/repository/ArtifactBadge.png";
    private static final @StaticResource String EMPTY_ICON = "org/netbeans/modules/maven/repository/empty.png";
    private static final @StaticResource String WAIT_ICON = "org/netbeans/modules/maven/repository/wait.gif";

    
    private static final RequestProcessor queryRP = new RequestProcessor(FindResultsNode.class.getName(), 10);
    private final QueryRequest queryRequest;
    private String toAppend;
    private final HtmlDisplayNameChanger changer = new HtmlDisplayNameChanger();

    FindResultsNode(QueryRequest request) {
        super(Children.LEAF, Lookups.singleton(request));
        changer.setNode(this);
        setChildren(Children.create(new FindResultsChildren(request.fields, request.infos, changer), true));
        setDisplayName(request.fields.get(0).getValue());
        setIconBaseWithExtension(FIND_IN_REPO);
        queryRequest = request;
    }

    @Override public boolean canDestroy() {
        return true;
    }
    
    private void changeHDM(String toAppend) {
        this.toAppend = toAppend;
        fireDisplayNameChange(null, null);
    }

    @Override public void destroy() throws IOException {
        M2RepositoryBrowser.remove(queryRequest);
    }

    @Override public Action[] getActions(boolean context) {
        return new Action[] {SystemAction.get(DeleteAction.class)};
    }

    @Override
    public String getHtmlDisplayName() {
        StringBuilder base = new StringBuilder().append(getDisplayName());
        if (toAppend != null) {
            base.append(" <font color='!controlShadow'>[");
            base.append(toAppend);
            base.append("]</font>");
        }
        return base.toString();
    }
    
    private static class HtmlDisplayNameChanger {
        private FindResultsNode node;
        void changeHtmlDisplayName(String toAppend) {
            assert node != null;
            node.changeHDM(toAppend);
        }
        
        void setNode(FindResultsNode node) {
            this.node = node;
        }
    }
    
    

    // XXX clumsy, use a real key instead (NBGroupInfo?) and replace no results/too general nodes with status line notifications
    private static class FindResultsChildren extends ChildFactory.Detachable<Node> {

        private List<Node> nodes;
        private final List<QueryField> fields;
        private final List<RepositoryInfo> infos;
        private final HtmlDisplayNameChanger changer;

        @Messages("MSG_Narrow={0} of {1} results shown. Narrow your search.")
        FindResultsChildren(List<QueryField> fields, List<RepositoryInfo> infos, HtmlDisplayNameChanger changer) {
            this.fields = fields;
            this.infos = infos;
            this.changer = changer;
        }

        @Override protected Node createNodeForKey(Node key) {
            return key;
        }

        @Override 
        protected boolean createKeys(List<Node> toPopulate) {
            if (nodes != null) {
                toPopulate.addAll(nodes);
            } else {
        queryRP.post(new Runnable() {

            @Override
            public void run() {
                try {
                    Result<NBVersionInfo> result = RepositoryQueries.findResult(fields, infos);
                    update(result, result.isPartial());
                    if (result.isPartial()) {
                        result.waitForSkipped();
                        update(result, false);
                    }
                    if (result.getReturnedResultCount() != result.getTotalResultCount()) {
                        changer.changeHtmlDisplayName(MSG_Narrow(result.getReturnedResultCount(), result.getTotalResultCount()));
                    }
                } catch (BooleanQuery.TooManyClauses exc) {
                    SwingUtilities.invokeLater(new Runnable() {
                        @Override
                        public void run() {
                            nodes = Collections.singletonList(getTooGeneralNode());
                        }
                    });
                } catch (final OutOfMemoryError oome) {
                    // running into OOME may still happen in Lucene despite the fact that
                    // we are trying hard to prevent it in NexusRepositoryIndexerImpl
                    // (see #190265)
                    // in the bad circumstances theoretically any thread may encounter OOME
                    // but most probably this thread will be it
                    // trying to indicate the condition to the user here
                    SwingUtilities.invokeLater(new Runnable() {
                        @Override
                        public void run() {
                            nodes = Collections.singletonList(getTooGeneralNode());
                        }
                    });
                }
            }
        });
    }
            return true; // XXX queryRequest.isFinished() unsuitable here
        }

    void update(Result<NBVersionInfo> result, final boolean partial) {
        final Map<String, List<NBVersionInfo>> map = new HashMap<String, List<NBVersionInfo>>();

        if (result.getResults() != null) {
            for (NBVersionInfo nbvi : result.getResults()) {
                String key = nbvi.getGroupId() + " : " + nbvi.getArtifactId(); //NOI18n
                List<NBVersionInfo> get = map.get(key);
                if (get == null) {
                    get = new ArrayList<NBVersionInfo>();
                    map.put(key, get);
                }
                get.add(nbvi);
            }
        }

        final List<String> keyList = new ArrayList<String>(map.keySet());
        Collections.sort(keyList);

        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                updateResultNodes(keyList, map, partial);
            }
        });
    }

    private void updateResultNodes(List<String> keyList, Map<String, List<NBVersionInfo>> map, boolean partial) {

            if (keyList.size() > 0) { // some results available
                
                Map<String, Node> currentNodes = new HashMap<String, Node>();
                if (nodes != null) {
                for (Node nd : nodes) {
                    currentNodes.put(nd.getName(), nd);
                }
                }
                List<Node> newNodes = new ArrayList<Node>(keyList.size());

                for (String key : keyList) {
                    Node nd;
                    nd = currentNodes.get(key);
                    if (null != nd) {
                        ((ArtifactNode)nd).setVersionInfos(map.get(key));
                    } else {
                        nd = new ArtifactNode(key, map.get(key));
                    }
                    newNodes.add(nd);
                }
                if (partial) { // still searching, no results yet
                    newNodes.add(getPartialNode());
                }
                
                nodes = newNodes;
                refresh(false);
            } else if (partial) { // still searching, no results yet
                nodes = Collections.singletonList(getPartialNode());
            } else { // finished searching with no results
                nodes = Collections.singletonList(getNoResultsNode());
            }
        }

    }

    private static class ArtifactNode extends AbstractNode {

        private List<NBVersionInfo> versionInfos;
        private ArtifactNodeChildren myChildren;

        public ArtifactNode(String name, List<NBVersionInfo> list) {
            super(new ArtifactNodeChildren(list));
            myChildren = (ArtifactNodeChildren)getChildren();
            this.versionInfos=list;
            setName(name);
            setDisplayName(name);
        }

        @Override
        public Image getIcon(int arg0) {
            Image badge = ImageUtilities.loadImage(ARTIFACT_BADGE, true);

            return badge;
        }

        @Override
        public Image getOpenedIcon(int arg0) {
            return getIcon(arg0);
        }

        public @Override Action[] getActions(boolean context) {
            return new Action[0];
        }

        public List<NBVersionInfo> getVersionInfos() {
            return new ArrayList<NBVersionInfo>(versionInfos);
        }

        public void setVersionInfos(List<NBVersionInfo> infos) {
            versionInfos = infos;
            myChildren.setNewKeys(infos);
        }

        static class ArtifactNodeChildren extends Children.Keys<NBVersionInfoTuple> {

            private List<NBVersionInfoTuple> keys;


            public ArtifactNodeChildren(List<NBVersionInfo> keys) {
                this.keys = processKeys(keys);
            }

            @Override
            protected Node[] createNodes(NBVersionInfoTuple info) {                
                return new Node[]{new VersionNode(info.repo, info.info, info.info.isJavadocExists(),
                            info.info.isSourcesExists(), true)
                        };
            }

            @Override
            protected void addNotify() {
                setKeys(keys);
            }

            protected void setNewKeys(List<NBVersionInfo> keys) {
                this.keys = processKeys(keys);
                setKeys(this.keys);
            }

            private List<NBVersionInfoTuple> processKeys(List<NBVersionInfo> keys) {
                List<NBVersionInfoTuple> toRet = new ArrayList<NBVersionInfoTuple>();
                HashMap<RepositoryInfo, Set<NBVersionInfo>> map = new HashMap<RepositoryInfo, Set<NBVersionInfo>>();
                for (NBVersionInfo k : keys) {
                    RepositoryInfo rinf = RepositoryPreferences.getInstance().getRepositoryInfoById(k.getRepoId());
                    NBVersionInfoTuple t = new NBVersionInfoTuple(k, rinf);
                    toRet.add(t);
                    Set<NBVersionInfo> set = map.get(rinf);
                    if (set == null) {
                        set = new HashSet<NBVersionInfo>();
                        map.put(rinf, set);
                    }
                    set.add(k);
                }
                Iterator<NBVersionInfoTuple> it = toRet.iterator();
                //this stuff is likely slow for large amount of data..
                LBL: while (it.hasNext()) {
                    NBVersionInfoTuple one = it.next();
                    if (one.repo.isLocal()) { //for local ones right now only..
                        for (java.util.Map.Entry<RepositoryInfo, Set<NBVersionInfo>> ent : map.entrySet()) {
                            if (ent.getKey().equals(one.repo)) {
                                continue;
                            }
                            for (NBVersionInfo ver : ent.getValue()) {
                                if (one.info.compareToWithoutRepoId(ver) == 0) {
                                    //do some kind of merging?
                                    it.remove();
                                    continue LBL;
                                }
                            }
                        }
                    }
                }
                return toRet;
            }
        }
        static class NBVersionInfoTuple {
            final NBVersionInfo info;
            final RepositoryInfo repo;

            public NBVersionInfoTuple(NBVersionInfo info, RepositoryInfo repo) {
                this.info = info;
                this.repo = repo;
            }
            
        }
    }

    private static Node noResultsNode, tooGeneralNode, partialNode;

    @Messages("LBL_Node_Empty=No matching items")
    private static Node getNoResultsNode() {
        if (noResultsNode == null) {
            AbstractNode nd = new AbstractNode(Children.LEAF) {

                @Override
                public Image getIcon(int arg0) {
                    return ImageUtilities.loadImage(WAIT_ICON);
                    }

                @Override
                public Image getOpenedIcon(int arg0) {
                    return getIcon(arg0);
                }
            };
            nd.setName("Empty"); //NOI18N

            nd.setDisplayName(LBL_Node_Empty()); //NOI18N

            noResultsNode = nd;
        }

        return new FilterNode (noResultsNode, Children.LEAF);
    }
    
    @Messages("LBL_Node_Partial=Partial result, waiting for indexing to finish.")
    private static Node getPartialNode() {
        if (partialNode == null) {
            AbstractNode nd = new AbstractNode(Children.LEAF) {

                @Override
                public Image getIcon(int arg0) {
                    return ImageUtilities.loadImage(EMPTY_ICON);
                }

                @Override
                public Image getOpenedIcon(int arg0) {
                    return getIcon(arg0);
                }
            };
            nd.setName("partial"); //NOI18N

            nd.setDisplayName(LBL_Node_Partial()); //NOI18N

            partialNode = nd;
        }

        return new FilterNode (partialNode, Children.LEAF);
    }    

    @Messages("LBL_Node_TooGeneral=Too general query")
    private static Node getTooGeneralNode() {
        if (tooGeneralNode == null) {
            AbstractNode nd = new AbstractNode(Children.LEAF) {

                @Override
                public Image getIcon(int arg0) {
                    return ImageUtilities.loadImage(EMPTY_ICON);
                    }

                @Override
                public Image getOpenedIcon(int arg0) {
                    return getIcon(arg0);
                }
            };
            nd.setName("Too General"); //NOI18N

            nd.setDisplayName(LBL_Node_TooGeneral()); //NOI18N

            tooGeneralNode = nd;
        }

        return new FilterNode (tooGeneralNode, Children.LEAF);
    }

    
}
