/*
 * Copyright 2003-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 jetbrains.mps.make;

import com.intellij.compiler.instrumentation.FailSafeClassReader;
import com.intellij.compiler.instrumentation.InstrumentationClassFinder;
import com.intellij.compiler.instrumentation.InstrumenterClassWriter;
import com.intellij.compiler.notNullVerification.NotNullVerifyingInstrumenter;
import jetbrains.mps.make.CompilationErrorsHandler.ClassesErrorsTracker;
import jetbrains.mps.project.MPSExtentions;
import jetbrains.mps.reloading.IClassPathItem;
import jetbrains.mps.reloading.RealClassPathItem;
import jetbrains.mps.util.NameUtil;
import jetbrains.mps.vfs.IFile;
import org.eclipse.jdt.internal.compiler.ClassFile;
import org.eclipse.jdt.internal.compiler.CompilationResult;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.mps.openapi.module.SModule;
import org.jetbrains.org.objectweb.asm.ClassWriter;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import static jetbrains.mps.project.SModuleOperations.getJavaFacet;

/**
 * Write compiled java classes to disk, also instruments the notnull annotations
 *
 * fixme use bundle for this package
 * Created by apyshkin on 5/26/16.
 */
public class ClassFileWriter {
  private final static String OUTPUT_DIR_CANNOT_BE_CREATED = "Can't create %s directory";
  private final static String MODULE_FOR_CLASS_NOT_FOUND = "It cannot be calculated in which module's output path the class file for %s must be placed";
  private final static String OUTPUT_DIR_IS_NOT_WRITEABLE = "Can't write to %s";
  private final static String OUTPUT_CANNOT_BE_DELETED = "Can't delete %s";

  private final ModulesContainer myModulesContainer;
  private final MessageSender mySender;
  private final ChangedModulesTracker myChangedModulesTracker = new ChangedModulesTracker();
  private final InstrumentationClassFinder myFinder;
  private final Map<String, InputStream> myClassFile2Bytes = new LinkedHashMap<>();

  // fixme think about class path
  public ClassFileWriter(ModulesContainer modulesContainer, CompositeTracer tracer, IClassPathItem classPath) {
    myModulesContainer = modulesContainer;
    mySender = tracer.getSender();
    myFinder = createInstrumentationClassFinder(classPath);
  }

  @NotNull
  private InstrumentationClassFinder createInstrumentationClassFinder(final IClassPathItem classPath) {
    final URL[] urlsArr = convertClassPathToUrls(classPath);
    return new InstrumentationClassFinder(urlsArr) { // fixme separate platform cp from usual cp
      @Override
      protected InputStream lookupClassBeforeClasspath(String internalClassName) {
        return myClassFile2Bytes.get(internalClassName);
      }
    };
  }

  @NotNull
  private static URL[] convertClassPathToUrls(IClassPathItem classPath) {
    final List<URL> urls = new ArrayList<>();
    for (RealClassPathItem flatten : classPath.flatten()) {
      try {
        urls.add(new File(flatten.getPath()).toURI().toURL());
      } catch (MalformedURLException e) {
        e.printStackTrace();
      }
    }
    return urls.toArray(new URL[urls.size()]);
  }

  private void updateClassFile2BytesMap(List<CompilationResult> results) {
    for (CompilationResult result : results) {
      for (ClassFile classFile : result.getClassFiles()) {
        String path = convertCompoundToPath(classFile.getCompoundName());
        myClassFile2Bytes.put(path, new ByteArrayInputStream(classFile.getBytes()));
      }
    }
  }

  /**
   * @return a set of changed modules
   */
  @NotNull
  public Set<SModule> write(List<CompilationResult> results, ClassesErrorsTracker errorsTracker) {
    updateClassFile2BytesMap(results);
    for (CompilationResult result : results) {
      for (ClassFile cf : result.getClassFiles()) {
        writeClassFile(cf, errorsTracker);
      }
    }
    return myChangedModulesTracker.getModules();
  }

  private void writeClassFile(@NotNull ClassFile cf, ClassesErrorsTracker errorsTracker) {
    String fqName = convertCompoundToFqName(cf.getCompoundName());
    String containerClassName = getContainerClassName(fqName); // the name up to dollar sign
    SModule moduleForClass = myModulesContainer.getModuleContainingClass(containerClassName);
    if (moduleForClass == null) {
      mySender.error(String.format(MODULE_FOR_CLASS_NOT_FOUND, fqName));
    } else {
      myChangedModulesTracker.addChanged(moduleForClass);
      File outputDir = createOutputDir(fqName, moduleForClass);
      String className = NameUtil.shortNameFromLongName(fqName);
      File output = new File(outputDir, className + MPSExtentions.DOT_CLASSFILE);
      if (!errorsTracker.hasError(containerClassName)) {
        writeClassFile(cf, output);
      } else {
        if (output.exists() && !output.delete()) {
          String errMsg = String.format(OUTPUT_CANNOT_BE_DELETED, output.getPath());
          mySender.error(errMsg);
        }
      }
    }
  }

  private void writeClassFile(ClassFile classFile, File output) {
    FileOutputStream os = null;
    try {
      os = new FileOutputStream(output);
      byte[] classContent = instrumentNotNull(classFile.getBytes());
      os.write(classContent);
    } catch (IOException e) {
      mySender.error(String.format(OUTPUT_DIR_IS_NOT_WRITEABLE, output.getAbsolutePath()));
    } finally {
      assert os != null;
      try {
        os.close();
      } catch (IOException e) {
        mySender.error("IOException: ", e);
      }
    }
  }

  @NotNull
  private File createOutputDir(String fqName, SModule m) {
    File classesGen = getClassesGen(m);
    String packageName = NameUtil.namespaceFromLongName(fqName);
    File outputDir = new File(classesGen, NameUtil.pathFromNamespace(packageName));
    if (!outputDir.exists() && !outputDir.mkdirs()) {
      throw new RuntimeException(String.format(OUTPUT_DIR_CANNOT_BE_CREATED, outputDir.getPath()));
    }
    return outputDir;
  }

  @NotNull
  private File getClassesGen(@NotNull SModule m) {
    IFile classesGen = getJavaFacet(m).getClassesGen();
    assert classesGen != null;
    return new File(classesGen.getPath());
  }

  /**
   * cuts the name up to the $ sign
   */
  @NotNull
  private static String getContainerClassName(String fqName) {
    String containerClassName = fqName;
    if (containerClassName.contains("$")) {
      int index = containerClassName.indexOf('$');
      containerClassName = containerClassName.substring(0, index);
    }
    return containerClassName;
  }

  // FIXME
  @NotNull
  private byte[] instrumentNotNull(@NotNull byte[] classContent) throws MalformedURLException {
    FailSafeClassReader reader = new FailSafeClassReader(classContent, 0, classContent.length);
    ClassWriter writer = new InstrumenterClassWriter(reader, ClassWriter.COMPUTE_FRAMES, myFinder);
    // To understand why last parameter was added - see commits 250331a & 490d4e6 in IDEA Community
    NotNullVerifyingInstrumenter.processClassFile(reader, writer, new String[]{NotNull.class.getName()});
    return writer.toByteArray();
//    return classContent;
  }

  @NotNull
  public static String convertCompoundToFqName(char[][] compoundName) {
    return convertCompoundToStringWithSep(compoundName, '.');
  }

  private static String convertCompoundToPath(char[][] compoundName) {
    return convertCompoundToStringWithSep(compoundName, '/');
  }

  private static String convertCompoundToStringWithSep(char[][] compoundName, char separator) {
    StringBuilder result = new StringBuilder();
    for (int i = 0; i < compoundName.length; i++) {
      char[] part = compoundName[i];
      result.append(part);
      if (i != compoundName.length - 1) {
        result.append(separator);
      }
    }
    return result.toString();
  }

  private static class ChangedModulesTracker {
    private final Set<SModule> myModules = new HashSet<SModule>();

    public void addChanged(@NotNull SModule module) {
      myModules.add(module);
    }

    public Set<SModule> getModules() {
      return myModules;
    }
  }
}
