/*
 * Copyright 2003-2011 JetBrains s.r.o.
 *
 * Licensed 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 jetbrains.mps.ide.findusages.view.treeholder.tree;

import jetbrains.mps.ide.findusages.CantLoadSomethingException;
import jetbrains.mps.ide.findusages.CantSaveSomethingException;
import jetbrains.mps.ide.findusages.IExternalizeable;
import jetbrains.mps.ide.findusages.view.treeholder.tree.nodedatatypes.BaseNodeData;
import jetbrains.mps.ide.findusages.view.treeholder.tree.nodedatatypes.ModelNodeData;
import jetbrains.mps.ide.findusages.view.treeholder.tree.nodedatatypes.NodeNodeData;
import jetbrains.mps.project.Project;
import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;
import org.jdom.Element;
import org.jetbrains.mps.openapi.model.SModel;
import org.jetbrains.mps.openapi.model.SModelReference;
import org.jetbrains.mps.openapi.model.SNodeReference;
import org.jetbrains.mps.openapi.module.SRepository;

import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Stream;

public class DataNode implements IExternalizeable {
  private static final Logger LOG = LogManager.getLogger(DataNode.class);

  private static final String CHILDREN = "children";
  private static final String CHILD = "child";
  private static final String DATA = "data";
  private static final String DATA_CLASS = "data_class";

  private BaseNodeData myData;
  private ArrayList<DataNode> myChildren = new ArrayList<DataNode>();

  public DataNode(BaseNodeData data) {
    myData = data;
  }

  public DataNode(Element element, Project project) throws CantLoadSomethingException {
    read(element, project);
  }

  public BaseNodeData getData() {
    return myData;
  }

  public boolean containsNodes(Class dataClass) {
    return getNodeDataStream().anyMatch(dataClass::isInstance);
  }

  //-------DATA QUERY--------

  // flatten elements associated with the node and its children, recursively.
  /*package*/ Stream<BaseNodeData> getNodeDataStream() {
    return Stream.concat(Stream.of(myData), myChildren.stream().flatMap(DataNode::getNodeDataStream));
  }

  private static Set<SModel> resolve(Stream<SModelReference> models, SRepository repo) {
    return new HashSet<>(Arrays.asList(models.map(modelReference -> modelReference.resolve(repo)).filter(m -> m != null).toArray(SModel[]::new)));
  }

  public Set<SModel> getIncludedModels(SRepository repo) {
    return resolve(getNodeDataStream().filter(nd -> nd instanceof ModelNodeData && !nd.isInvalid() && !nd.isExcluded()).map(
        nd -> ((ModelNodeData) nd).getModelReference()), repo);
  }

  public Set<SModel> getAllModels(SRepository repo) {
    return resolve(getNodeDataStream().filter(nd -> nd instanceof ModelNodeData).map(nd -> ((ModelNodeData) nd).getModelReference()), repo);
  }

  public List<SNodeReference> getIncludedResultNodes() {
    return Arrays.asList(getNodeDataStream().filter(nd -> nd instanceof NodeNodeData && !nd.isInvalid() && !nd.isExcluded() && nd.isResultNode()).map(nd -> ((NodeNodeData) nd).getNodePointer()).toArray(SNodeReference[]::new));
  }

  public List<SNodeReference> getAllResultNodes() {
    return Arrays.asList(getNodeDataStream().filter(nd -> nd instanceof NodeNodeData && nd.isResultNode()).map(nd -> ((NodeNodeData) nd).getNodePointer()).toArray(SNodeReference[]::new));
  }

  //-------CHILD MANIPULATION--------

  public void add(DataNode o) {
    myChildren.add(o);
  }

  public List<DataNode> getChildren() {
    return Collections.unmodifiableList(myChildren);
  }

  //-------PERSISTENCE--------

  @Override
  public void read(Element element, Project project) throws CantLoadSomethingException {
    Element dataXML = element.getChild(DATA);
    String dataClass = dataXML.getAttributeValue(DATA_CLASS);
    try {
      Class<?> cls = Class.forName(dataClass);
      myData = (BaseNodeData) cls.getConstructor(Element.class, Project.class).newInstance(dataXML, project);
    } catch (InvocationTargetException e) {
      if (e.getCause() instanceof CantLoadSomethingException) {
        throw (CantLoadSomethingException) e.getCause();
      } else {
        throwLoadException(e);
      }
    } catch (ClassNotFoundException e) {
      throwLoadException(e);
    } catch (NoSuchMethodException e) {
      throwLoadException(e);
    } catch (IllegalAccessException e) {
      throwLoadException(e);
    } catch (InstantiationException e) {
      throwLoadException(e);
    }

    myChildren.clear();
    Element childrenXML = element.getChild(CHILDREN);
    for (Element nodeXML : (List<Element>) childrenXML.getChildren(CHILD)) {
      myChildren.add(new DataNode(nodeXML, project));
    }
  }

  @Override
  public void write(Element element, Project project) throws CantSaveSomethingException {
    Element dataXML = new Element(DATA);
    dataXML.setAttribute(DATA_CLASS, myData.getClass().getName());
    myData.write(dataXML, project);
    element.addContent(dataXML);

    Element childrenXML = new Element(CHILDREN);
    for (DataNode node : getChildren()) {
      Element childXML = new Element(CHILD);
      node.write(childXML, project);
      childrenXML.addContent(childXML);
    }
    element.addContent(childrenXML);
  }

  private void throwLoadException(Throwable t) throws CantLoadSomethingException {
    LOG.warn("can't instantiate node", t);
    throw new CantLoadSomethingException("can't instantiate node", t);
  }
}
