/*
 * Copyright 2000-2016 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 org.jetbrains.jps.model.serialization.library;

import com.intellij.openapi.util.JDOMUtil;
import com.intellij.util.containers.MultiMap;
import org.jdom.Element;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.jps.model.*;
import org.jetbrains.jps.model.java.JpsJavaLibraryType;
import org.jetbrains.jps.model.library.*;
import org.jetbrains.jps.model.library.sdk.JpsSdkType;
import org.jetbrains.jps.model.module.JpsModuleReference;
import org.jetbrains.jps.model.serialization.JpsModelSerializerExtension;

import java.util.*;

/**
 * @author nik
 */
public class JpsLibraryTableSerializer {
  private static final JpsLibraryRootTypeSerializer[] PREDEFINED_ROOT_TYPES_SERIALIZERS = {
    new JpsLibraryRootTypeSerializer("CLASSES", JpsOrderRootType.COMPILED, true),
    new JpsLibraryRootTypeSerializer("SOURCES", JpsOrderRootType.SOURCES, true)
  };
  private static final String NAME_ATTRIBUTE = "name";
  private static final String TYPE_ATTRIBUTE = "type";
  private static final String PROPERTIES_TAG = "properties";
  private static final String JAR_DIRECTORY_TAG = "jarDirectory";
  private static final String URL_ATTRIBUTE = "url";
  private static final String ROOT_TAG = "root";
  private static final String RECURSIVE_ATTRIBUTE = "recursive";
  private static final String LIBRARY_TAG = "library";
  private static final JpsLibraryPropertiesSerializer<JpsDummyElement> JAVA_LIBRARY_PROPERTIES_SERIALIZER =
    new JpsLibraryPropertiesSerializer<JpsDummyElement>(JpsJavaLibraryType.INSTANCE, null) {
      @Override
      public JpsDummyElement loadProperties(@Nullable Element propertiesElement) {
        return JpsElementFactory.getInstance().createDummyElement();
      }

      @Override
      public void saveProperties(JpsDummyElement properties, Element element) {
      }
    };
  private static final String MODULE_LEVEL = "module";
  private static final String PROJECT_LEVEL = "project";
  private static final String APPLICATION_LEVEL = "application";

  public static void loadLibraries(@Nullable Element libraryTableElement, JpsLibraryCollection result) {
    for (Element libraryElement : JDOMUtil.getChildren(libraryTableElement, LIBRARY_TAG)) {
      result.addLibrary(loadLibrary(libraryElement));
    }
  }

  public static void saveLibraries(JpsLibraryCollection libraryCollection, Element libraryTableElement) {
    List<JpsLibrary> libraries = new ArrayList<>();
    for (JpsLibrary library : libraryCollection.getLibraries()) {
      if (!(library.getType() instanceof JpsSdkType<?>)) {
        libraries.add(library);
      }
    }

    libraries.sort((o1, o2) -> o1.getName().compareToIgnoreCase(o2.getName()));

    for (JpsLibrary library : libraries) {
      Element libraryTag = new Element(LIBRARY_TAG);
      saveLibrary(library, libraryTag, library.getName());
      libraryTableElement.addContent(libraryTag);
    }
  }

  public static JpsLibrary loadLibrary(Element libraryElement) {
    return loadLibrary(libraryElement, libraryElement.getAttributeValue(NAME_ATTRIBUTE));
  }

  public static JpsLibrary loadLibrary(Element libraryElement, String name) {
    String typeId = libraryElement.getAttributeValue(TYPE_ATTRIBUTE);
    final JpsLibraryPropertiesSerializer<?> loader = getLibraryPropertiesSerializer(typeId);
    JpsLibrary library = createLibrary(name, loader, libraryElement.getChild(PROPERTIES_TAG));

    MultiMap<JpsOrderRootType, String> jarDirectories = new MultiMap<>();
    MultiMap<JpsOrderRootType, String> recursiveJarDirectories = new MultiMap<>();
    for (Element jarDirectory : JDOMUtil.getChildren(libraryElement, JAR_DIRECTORY_TAG)) {
      String url = jarDirectory.getAttributeValue(URL_ATTRIBUTE);
      String rootTypeId = jarDirectory.getAttributeValue(TYPE_ATTRIBUTE);
      final JpsOrderRootType rootType = rootTypeId != null ? getRootType(rootTypeId) : JpsOrderRootType.COMPILED;
      boolean recursive = Boolean.parseBoolean(jarDirectory.getAttributeValue(RECURSIVE_ATTRIBUTE));
      jarDirectories.putValue(rootType, url);
      if (recursive) {
        recursiveJarDirectories.putValue(rootType, url);
      }
    }
    for (Element rootsElement : libraryElement.getChildren()) {
      final String rootTypeId = rootsElement.getName();
      if (!rootTypeId.equals(JAR_DIRECTORY_TAG) && !rootTypeId.equals(PROPERTIES_TAG)) {
        final JpsOrderRootType rootType = getRootType(rootTypeId);
        for (Element rootElement : JDOMUtil.getChildren(rootsElement, ROOT_TAG)) {
          String url = rootElement.getAttributeValue(URL_ATTRIBUTE);
          JpsLibraryRoot.InclusionOptions options;
          if (jarDirectories.get(rootType).contains(url)) {
            final boolean recursive = recursiveJarDirectories.get(rootType).contains(url);
            options = recursive ? JpsLibraryRoot.InclusionOptions.ARCHIVES_UNDER_ROOT_RECURSIVELY : JpsLibraryRoot.InclusionOptions.ARCHIVES_UNDER_ROOT;
          }
          else {
            options = JpsLibraryRoot.InclusionOptions.ROOT_ITSELF;
          }
          library.addRoot(url, rootType, options);
        }
      }
    }
    return library;
  }

  public static void saveLibrary(JpsLibrary library, Element libraryElement, final String libraryName) {
    if (libraryName != null) {
      libraryElement.setAttribute(NAME_ATTRIBUTE, libraryName);
    }
    saveProperties((JpsTypedLibrary<?>)library, libraryElement);
    List<Element> jarDirectoryElements = new ArrayList<>();
    for (JpsLibraryRootTypeSerializer serializer : getSortedSerializers()) {
      List<JpsLibraryRoot> roots = library.getRoots(serializer.getType());
      if (roots.isEmpty() && !serializer.isWriteIfEmpty()) continue;

      Element typeElement = new Element(serializer.getTypeId());
      for (JpsLibraryRoot root : roots) {
        typeElement.addContent(new Element(ROOT_TAG).setAttribute(URL_ATTRIBUTE, root.getUrl()));
        if (root.getInclusionOptions() != JpsLibraryRoot.InclusionOptions.ROOT_ITSELF) {
          Element jarDirectoryElement = new Element(JAR_DIRECTORY_TAG).setAttribute(URL_ATTRIBUTE, root.getUrl());
          boolean recursive = root.getInclusionOptions() == JpsLibraryRoot.InclusionOptions.ARCHIVES_UNDER_ROOT_RECURSIVELY;
          jarDirectoryElement.setAttribute(RECURSIVE_ATTRIBUTE, Boolean.toString(recursive));
          if (!serializer.getType().equals(JpsOrderRootType.COMPILED)) {
            jarDirectoryElement.setAttribute(TYPE_ATTRIBUTE, serializer.getTypeId());
          }
          jarDirectoryElements.add(jarDirectoryElement);
        }
      }
      libraryElement.addContent(typeElement);
    }
    libraryElement.addContent(jarDirectoryElements);
  }

  private static <P extends JpsElement> void saveProperties(JpsTypedLibrary<P> library, Element libraryElement) {
    JpsLibraryType<P> type = library.getType();
    if (!type.equals(JpsJavaLibraryType.INSTANCE)) {
      JpsLibraryPropertiesSerializer<P> serializer = getLibraryPropertiesSerializer(type);
      libraryElement.setAttribute(TYPE_ATTRIBUTE, serializer.getTypeId());
      Element element = new Element(PROPERTIES_TAG);
      serializer.saveProperties(library.getProperties(), element);
      if (!element.getContent().isEmpty() || !element.getAttributes().isEmpty()) {
        libraryElement.addContent(element);
      }
    }
  }

  private static <P extends JpsElement> JpsLibrary createLibrary(String name, JpsLibraryPropertiesSerializer<P> loader,
                                                                           final Element propertiesElement) {
    return JpsElementFactory.getInstance().createLibrary(name, loader.getType(), loader.loadProperties(propertiesElement));
  }

  private static JpsOrderRootType getRootType(String rootTypeId) {
    for (JpsLibraryRootTypeSerializer serializer : PREDEFINED_ROOT_TYPES_SERIALIZERS) {
      if (serializer.getTypeId().equals(rootTypeId)) {
        return serializer.getType();
      }
    }
    for (JpsModelSerializerExtension extension : JpsModelSerializerExtension.getExtensions()) {
      for (JpsLibraryRootTypeSerializer serializer : extension.getLibraryRootTypeSerializers()) {
        if (serializer.getTypeId().equals(rootTypeId)) {
          return serializer.getType();
        }
      }
    }
    return JpsOrderRootType.COMPILED;
  }

  private static Collection<JpsLibraryRootTypeSerializer> getSortedSerializers() {
    List<JpsLibraryRootTypeSerializer> serializers = new ArrayList<>();
    Collections.addAll(serializers, PREDEFINED_ROOT_TYPES_SERIALIZERS);
    for (JpsModelSerializerExtension extension : JpsModelSerializerExtension.getExtensions()) {
      serializers.addAll(extension.getLibraryRootTypeSerializers());
    }
    Collections.sort(serializers);
    return serializers;
  }

  private static JpsLibraryPropertiesSerializer<?> getLibraryPropertiesSerializer(@Nullable String typeId) {
    if (typeId != null) {
      for (JpsModelSerializerExtension extension : JpsModelSerializerExtension.getExtensions()) {
        for (JpsLibraryPropertiesSerializer<?> loader : extension.getLibraryPropertiesSerializers()) {
          if (loader.getTypeId().equals(typeId)) {
            return loader;
          }
        }
      }
    }
    return JAVA_LIBRARY_PROPERTIES_SERIALIZER;
  }

  private static <P extends JpsElement> JpsLibraryPropertiesSerializer<P> getLibraryPropertiesSerializer(@NotNull JpsLibraryType<P> type) {
    for (JpsModelSerializerExtension extension : JpsModelSerializerExtension.getExtensions()) {
      for (JpsLibraryPropertiesSerializer<?> loader : extension.getLibraryPropertiesSerializers()) {
        if (loader.getType().equals(type)) {
          //noinspection unchecked
          return (JpsLibraryPropertiesSerializer<P>)loader;
        }
      }
    }
    throw new IllegalArgumentException("unknown type library:" + type);
  }

  public static JpsElementReference<? extends JpsCompositeElement> createLibraryTableReference(String level) {
    JpsElementFactory elementFactory = JpsElementFactory.getInstance();
    if (level.equals(PROJECT_LEVEL)) {
      return elementFactory.createProjectReference();
    }
    if (level.equals(APPLICATION_LEVEL)) {
      return elementFactory.createGlobalReference();
    }
    for (JpsModelSerializerExtension extension : JpsModelSerializerExtension.getExtensions()) {
      final JpsElementReference<? extends JpsCompositeElement> reference = extension.createLibraryTableReference(level);
      if (reference != null) {
        return reference;
      }
    }
    throw new UnsupportedOperationException();
  }

  public static String getLevelId(JpsElementReference<? extends JpsCompositeElement> reference) {
    if (reference instanceof JpsModuleReference) {
      return MODULE_LEVEL;
    }
    JpsCompositeElement element = reference.resolve();
    if (element instanceof JpsProject) {
      return PROJECT_LEVEL;
    }
    else if (element instanceof JpsGlobal) {
      return APPLICATION_LEVEL;
    }
    for (JpsModelSerializerExtension extension : JpsModelSerializerExtension.getExtensions()) {
      String levelId = extension.getLibraryTableLevelId(reference);
      if (levelId != null) {
        return levelId;
      }
    }
    return null;
  }
}
