/*
 * 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.apache.uima.ruta.ide.core.packages;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Arrays;
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.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

import org.apache.commons.io.IOUtils;
import org.apache.uima.internal.util.XMLUtils;
import org.apache.uima.ruta.ide.RutaIdeCorePlugin;
import org.apache.uima.ruta.ide.core.packages.DLTKRutaHelper.RutaPackage;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.Path;
import org.eclipse.dltk.core.DLTKCore;
import org.eclipse.dltk.core.IScriptProject;
import org.eclipse.dltk.launching.IInterpreterInstall;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

/**
 * This class persistently holds all required information for specified interpreter package to path
 * associations.
 */
public class PackagesManager {
  private static final String DEPENDENCY_TAG = "dependency"; //$NON-NLS-1$

  private static final String INTERPRETER_TAG = "interpreter"; //$NON-NLS-1$

  private static final String VALUE_ATTR = "value"; //$NON-NLS-1$

  private static final String PACKAGES_FILE = "packages.txt"; //$NON-NLS-1$

  private static final String PACKAGES_TAG = "packages"; //$NON-NLS-1$

  private static final String PACKAGE_TAG = "package"; //$NON-NLS-1$

  private static final String INTERPRETER_ATTR = INTERPRETER_TAG;

  private static final String NAME_ATTR = "name"; //$NON-NLS-1$

  private static final String PATH_TAG = "path"; //$NON-NLS-1$

  private static PackagesManager manager;

  /**
   * Contains association of PackageKey to PackageInformation
   */
  private Map packages = new HashMap();

  /**
   * Contains set of interpreter to list of packages association.
   */
  private Map interpreterToPackages = new HashMap();

  private Map packsWithDeps = new HashMap();

  public static synchronized PackagesManager getInstance() {
    if (manager == null) {
      manager = new PackagesManager();
    }
    return manager;
  }

  private PackagesManager() {
    initialize();
  }

  private static class PackageKey {
    private String packageName;

    private String interpreterPath;

    @Override
    public int hashCode() {
      final int prime = 31;
      int result = 1;
      result = prime * result + ((interpreterPath == null) ? 0 : interpreterPath.hashCode());
      result = prime * result + ((packageName == null) ? 0 : packageName.hashCode());
      return result;
    }

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

    public String getPackageName() {
      return packageName;
    }

    public void setPackageName(String packageName) {
      this.packageName = packageName;
    }

    public String getInterpreterPath() {
      return interpreterPath;
    }

    public void setInterpreterPath(String interpreterPath) {
      this.interpreterPath = interpreterPath;
    }
  }

  public static class PackageInformation {
    private final Set paths = new HashSet();

    private final Set dependencies = new HashSet();

    public Set getPaths() {
      return paths;
    }

    public Set getDependencies() {
      return dependencies;
    }
  }

  private void initialize() {
    IPath packagesPath = RutaIdeCorePlugin.getDefault().getStateLocation().append(PACKAGES_FILE);
    File packagesFile = packagesPath.toFile();
    if (packagesFile.exists()) {
      try {
        DocumentBuilderFactory documentBuilderFactory = XMLUtils.createDocumentBuilderFactory();
        DocumentBuilder builder = documentBuilderFactory.newDocumentBuilder();
        try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream(packagesFile),
                2048)) {
          Document document = builder.parse(bis);
          populate(document.getDocumentElement());
        }
      } catch (SAXException | IOException | ParserConfigurationException e) {
        if (DLTKCore.DEBUG) {
          e.printStackTrace();
        }
      }
    }
  }

  private void save() {
    IPath packagesPath = RutaIdeCorePlugin.getDefault().getStateLocation().append(PACKAGES_FILE);
    File packagesFile = packagesPath.toFile();

    OutputStream os = null;
    try {
      DocumentBuilderFactory documentBuilderFactory = XMLUtils.createDocumentBuilderFactory();
      DocumentBuilder builder = documentBuilderFactory.newDocumentBuilder();
      Document document = builder.newDocument();
      save(document);

      os = new BufferedOutputStream(new FileOutputStream(packagesFile, false), 2048);

      TransformerFactory transformerFactory = XMLUtils.createTransformerFactory();
      Transformer transformer = transformerFactory.newTransformer();
      transformer.setOutputProperty(OutputKeys.METHOD, "xml"); //$NON-NLS-1$
      transformer.setOutputProperty(OutputKeys.INDENT, "yes"); //$NON-NLS-1$

      DOMSource source = new DOMSource(document);
      StreamResult outputTarget = new StreamResult(os);
      transformer.transform(source, outputTarget);

    } catch (IOException | TransformerException | ParserConfigurationException e) {
      if (DLTKCore.DEBUG) {
        e.printStackTrace();
      }
    } finally {
      IOUtils.closeQuietly(os);
    }
  }

  private synchronized void save(Document doc) {
    Element packagesElement = doc.createElement(PACKAGES_TAG);
    doc.appendChild(packagesElement);
    for (Iterator iterator = this.packages.keySet().iterator(); iterator.hasNext();) {
      PackageKey key = (PackageKey) iterator.next();

      Element packageElement = doc.createElement(PACKAGE_TAG);
      packageElement.setAttribute(NAME_ATTR, key.getPackageName());
      packageElement.setAttribute(INTERPRETER_ATTR, key.getInterpreterPath());

      PackageInformation info = (PackageInformation) this.packages.get(key);
      Set paths = info.getPaths();
      for (Iterator iterator2 = paths.iterator(); iterator2.hasNext();) {
        IPath path = (IPath) iterator2.next();
        Element pathElement = doc.createElement(PATH_TAG);
        pathElement.setAttribute(VALUE_ATTR, path.toOSString());
        packageElement.appendChild(pathElement);
      }
      Set deps = info.getDependencies();
      for (Iterator iterator2 = deps.iterator(); iterator2.hasNext();) {
        String pkgName = (String) iterator2.next();
        Element pkgElement = doc.createElement(DEPENDENCY_TAG);
        pkgElement.setAttribute(NAME_ATTR, pkgName);
        packageElement.appendChild(pkgElement);
      }
      packagesElement.appendChild(packageElement);
    }
    for (Iterator iterator = this.interpreterToPackages.keySet().iterator(); iterator.hasNext();) {
      String interpreter = (String) iterator.next();

      Element interpreterElement = doc.createElement(INTERPRETER_TAG);
      interpreterElement.setAttribute(NAME_ATTR, interpreter);
      Set pkgs = (Set) this.interpreterToPackages.get(interpreter);
      for (Iterator iterator2 = pkgs.iterator(); iterator2.hasNext();) {
        String pkgName = (String) iterator2.next();
        Element pathElement = doc.createElement(PACKAGE_TAG);
        pathElement.setAttribute(VALUE_ATTR, pkgName);
        interpreterElement.appendChild(pathElement);
      }
      packagesElement.appendChild(interpreterElement);
    }
  }

  private synchronized void populate(Element documentElement) {
    NodeList childNodes = documentElement.getChildNodes();
    int length = childNodes.getLength();
    for (int i = 0; i < length; i++) {
      Node child = childNodes.item(i);
      if (child.getNodeType() == Node.ELEMENT_NODE) {
        if (child.getNodeName().equalsIgnoreCase(PACKAGE_TAG)) {
          Element e = (Element) child;
          String packageName = e.getAttribute(NAME_ATTR);
          String interpreter = e.getAttribute(INTERPRETER_ATTR);
          PackageInformation packageInfo = new PackageInformation();
          NodeList childrens = e.getChildNodes();
          for (int j = 0; j < childrens.getLength(); j++) {
            Node path = childrens.item(j);
            if (path.getNodeType() == Node.ELEMENT_NODE) {
              if (path.getNodeName().equalsIgnoreCase(PATH_TAG)) {
                String pathValue = ((Element) path).getAttribute(VALUE_ATTR);
                packageInfo.getPaths().add(new Path(pathValue));
              } else if (path.getNodeName().equalsIgnoreCase(DEPENDENCY_TAG)) {
                String pkgName = ((Element) path).getAttribute(NAME_ATTR);
                packageInfo.getDependencies().add(pkgName);
              }
            }
          }
          this.packages.put(makeKey(packageName, interpreter), packageInfo);
        } else if (child.getNodeName().equalsIgnoreCase(INTERPRETER_TAG)) {
          Element e = (Element) child;
          String interpreter = e.getAttribute(NAME_ATTR);
          NodeList paths = e.getChildNodes();
          Set packagesSet = new HashSet();
          for (int j = 0; j < paths.getLength(); j++) {
            Node packageNode = paths.item(j);
            if (packageNode.getNodeType() == Node.ELEMENT_NODE) {
              if (packageNode.getNodeName().equalsIgnoreCase(PACKAGE_TAG)) {
                String packageNameValue = ((Element) packageNode).getAttribute(VALUE_ATTR);
                packagesSet.add(packageNameValue);
              }
            }
          }
          this.interpreterToPackages.put(interpreter, packagesSet);
        }
      }
    }
  }

  private PackageKey makeKey(String packageName, String interpreter) {
    PackageKey key = new PackageKey();
    key.setPackageName(packageName);
    key.setInterpreterPath(interpreter);
    return key;
  }

  public synchronized IPath[] getPathsForPackage(IInterpreterInstall install, String packageName) {
    PackageKey key = makeKey(packageName, getInterpreterKey(install));
    if (this.packages.containsKey(key)) {
      PackageInformation info = (PackageInformation) this.packages.get(key);
      Set els = info.getPaths();
      return (IPath[]) els.toArray(new IPath[els.size()]);
    }
    // Retrieve paths from interpreter with all dependencies.
    RutaPackage[] srcs = DLTKRutaHelper.getSrcs(install.getExecEnvironment(),
            install.getInstallLocation(), install.getEnvironmentVariables(), packageName);

    PackageInformation resultInfo = null;
    for (int i = 0; i < srcs.length; i++) {
      Set paths2 = srcs[i].getPaths();
      PackageKey okey = makeKey(srcs[i].getName(), getInterpreterKey(install));
      PackageInformation info;
      if (this.packages.containsKey(okey)) {
        info = (PackageInformation) this.packages.get(okey);
      } else {
        info = new PackageInformation();
      }
      info.getPaths().addAll(paths2);
      info.getDependencies().addAll(srcs[i].getDependencies());
      this.packages.put(okey, info);
      if (okey.equals(key)) {
        resultInfo = info;
      }
    }
    // Follow all dependencies
    if (resultInfo == null) {
      this.packages.put(key, new PackageInformation());
      return new IPath[0];
    }

    Set resultPaths = new HashSet();
    resultPaths.addAll(resultInfo.getPaths());

    save();
    return (IPath[]) resultPaths.toArray(new IPath[resultPaths.size()]);
  }

  public synchronized Map getDependencies(String pkgName, IInterpreterInstall install) {
    Set checkedPackages = new HashSet();
    Map packagesSet = new HashMap();
    PackageKey key = makeKey(pkgName, install);
    PackageInformation info = (PackageInformation) this.packages.get(key);
    if (info != null) {
      traverseDependencies(packagesSet, checkedPackages, info, install);
    }
    return packagesSet;
  }

  private PackageKey makeKey(String pkgName, IInterpreterInstall install) {
    return makeKey(pkgName, getInterpreterKey(install));
  }

  private synchronized void traverseDependencies(Map packagesSet, Set checkedPackages,
          PackageInformation resultInfo, IInterpreterInstall install) {
    Set dependencies = resultInfo.getDependencies();
    for (Iterator iterator = dependencies.iterator(); iterator.hasNext();) {
      String pkgName = (String) iterator.next();
      if (!checkedPackages.contains(pkgName)) {
        checkedPackages.add(pkgName);
        PackageKey pkgKey = makeKey(pkgName, getInterpreterKey(install));
        if (this.packages.containsKey(pkgKey)) {
          PackageInformation depInfo = (PackageInformation) this.packages.get(pkgKey);
          packagesSet.put(pkgName, depInfo);
          traverseDependencies(packagesSet, checkedPackages, depInfo, install);
        }
      }
    }
  }

  public synchronized Set getPackageNames(IInterpreterInstall install) {
    String key = getInterpreterKey(install);
    if (this.interpreterToPackages.containsKey(key)) {
      Set set = (Set) this.interpreterToPackages.get(key);
      return set;
    }
    // Evaluate
    Set packs = DLTKRutaHelper.getPackages(install);
    this.interpreterToPackages.put(key, packs);
    save();
    return packs;
  }

  private String getInterpreterKey(IInterpreterInstall install) {
    if (install == null) {
      return "";
    }
    return install.getInstallLocation().toOSString() + ":" //$NON-NLS-1$
            + install.getEnvironment().getId();
  }

  private String getInterpreterProjectKey(IInterpreterInstall install, String projectName) {
    return "internal|||" + projectName + "|||" //$NON-NLS-1$ //$NON-NLS-2$
            + getInterpreterKey(install);
  }

  public Set getInternalPackageNames(IInterpreterInstall install, IScriptProject project) {
    return getInternalPackageNames(install, project.getElementName());
  }

  public synchronized Set getInternalPackageNames(IInterpreterInstall install, IProject project) {
    return getInternalPackageNames(install, project.getName());
  }

  public synchronized Set getInternalPackageNames(IInterpreterInstall install, String projectName) {
    final String key = getInterpreterProjectKey(install, projectName);
    if (this.interpreterToPackages.containsKey(key)) {
      return (Set) this.interpreterToPackages.get(key);
    }
    return Collections.EMPTY_SET;
  }

  public synchronized void setInternalPackageNames(IInterpreterInstall install,
          IScriptProject project, Set names) {
    String key = getInterpreterProjectKey(install, project.getElementName());
    // TODO compare and save only if there are changes
    this.interpreterToPackages.put(key, new HashSet(names));
    save();
  }

  public synchronized IPath[] getPathsForPackages(IInterpreterInstall install,
          Set packagesInBuild) {

    StringBuffer buf = new StringBuffer();
    String[] pkgs = (String[]) packagesInBuild.toArray(new String[packagesInBuild.size()]);
    for (int i = 0; i < pkgs.length; i++) {
      buf.append(pkgs[i]).append(" "); //$NON-NLS-1$
    }
    PackageKey key = makeKey(buf.toString(), getInterpreterKey(install));

    if (this.packages.containsKey(key)) {
      PackageInformation info = (PackageInformation) this.packages.get(key);
      Set paths = info.getPaths();
      return (IPath[]) paths.toArray(new IPath[paths.size()]);
    }
    // Retrieve paths from interpreter with all dependencies.
    RutaPackage[] srcs = DLTKRutaHelper.getSrcs(install.getExecEnvironment(),
            install.getInstallLocation(), install.getEnvironmentVariables(), buf.toString());
    Set result = new HashSet();
    if (srcs == null) {
      return new IPath[0];
    }
    for (int i = 0; i < srcs.length; i++) {
      Set paths2 = srcs[i].getPaths();
      PackageKey okey = makeKey(srcs[i].getName(), getInterpreterKey(install));
      PackageInformation info = null;
      if (this.packages.containsKey(okey)) {
        info = (PackageInformation) this.packages.get(okey);
      } else {
        info = new PackageInformation();
      }
      result.addAll(paths2);
      info.getPaths().addAll(paths2);
      info.getDependencies().addAll(srcs[i].getDependencies());
      this.packages.put(okey, info);
    }

    PackageInformation info = new PackageInformation();
    info.getPaths().addAll(result);
    this.packages.put(key, info);

    for (int i = 0; i < pkgs.length; i++) {
      PackageKey lkey = makeKey(pkgs[i], getInterpreterKey(install));
      if (!this.packages.containsKey(lkey)) {
        this.packages.put(lkey, new PackageInformation());
      }
    }
    save();

    return (IPath[]) result.toArray(new IPath[result.size()]);
  }

  public IPath[] getAllPaths(String pkgName, IInterpreterInstall install) {
    Set result = new HashSet();
    IPath[] paths = this.getPathsForPackage(install, pkgName);
    result.addAll(Arrays.asList(paths));
    Map dependencies = this.getDependencies(pkgName, install);
    for (Iterator iterator = dependencies.keySet().iterator(); iterator.hasNext();) {
      String packageName = (String) iterator.next();
      PackageInformation info = (PackageInformation) dependencies.get(packageName);
      result.addAll(info.getPaths());
    }
    return (IPath[]) result.toArray(new IPath[result.size()]);
  }

  public synchronized void removeInterprterInfo(IInterpreterInstall install) {
    // Remove interpreter to packages set
    String interpreterPath = getInterpreterKey(install);
    this.interpreterToPackages.remove(interpreterPath);
    // Remove all values stored for interpreter packages
    for (Iterator iterator = this.packages.keySet().iterator(); iterator.hasNext();) {
      PackageKey key = (PackageKey) iterator.next();
      String path = key.getInterpreterPath();
      if (path.equals(interpreterPath)) {
        iterator.remove();
      }
    }
    save();
  }

  /**
   * Clears all cached information.
   */
  public synchronized void clearCache() {
    this.interpreterToPackages.clear();
    this.packages.clear();
    this.packsWithDeps.clear();
    save();
  }

  public IPath[] getPathsForPackageWithDeps(IInterpreterInstall install, String name) {
    Set result = new HashSet();
    IPath[] paths = this.getPathsForPackage(install, name);
    result.addAll(Arrays.asList(paths));

    Map dependencies = getDependencies(name, install);
    for (Iterator iterator = dependencies.keySet().iterator(); iterator.hasNext();) {
      String pkgName = (String) iterator.next();
      result.addAll(Arrays.asList(getPathsForPackage(install, pkgName)));
    }
    return (IPath[]) result.toArray(new IPath[result.size()]);
  }

  public IPath[] getPathsForPackagesWithDeps(IInterpreterInstall install, Set packagesSet) {

    String pkey = makePKey(packagesSet);
    if (this.packsWithDeps.containsKey(pkey)) {
      return (IPath[]) this.packsWithDeps.get(pkey);
    }

    Set result = new HashSet();
    IPath[] paths = this.getPathsForPackages(install, packagesSet);
    result.addAll(Arrays.asList(paths));

    for (Iterator jiterator = packagesSet.iterator(); jiterator.hasNext();) {
      String name = (String) jiterator.next();
      Map dependencies = getDependencies(name, install);
      for (Iterator iterator = dependencies.keySet().iterator(); iterator.hasNext();) {
        String pkgName = (String) iterator.next();
        result.addAll(Arrays.asList(getPathsForPackage(install, pkgName)));
      }
    }
    IPath[] array = (IPath[]) result.toArray(new IPath[result.size()]);
    this.packsWithDeps.put(pkey, array);
    return array;
  }

  private String makePKey(Set packagesSet) {
    StringBuffer buffer = new StringBuffer();
    List l = new ArrayList();
    l.addAll(packagesSet);
    Collections.sort(l);
    for (Iterator iterator = l.iterator(); iterator.hasNext();) {
      String object = (String) iterator.next();
      buffer.append(object);
    }
    return buffer.toString();
  }
}
