/*
 * Copyright (c) 2009 IBM Corporation.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 */
package com.ibm.wala.cast.loader;

import com.ibm.wala.cast.ir.translator.AstTranslator;
import com.ibm.wala.cast.ir.translator.AstTranslator.AstLexicalInformation;
import com.ibm.wala.cast.ir.translator.AstTranslator.WalkContext;
import com.ibm.wala.cast.ir.translator.TranslatorToCAst;
import com.ibm.wala.cast.ir.translator.TranslatorToIR;
import com.ibm.wala.cast.loader.AstMethod.Retranslatable;
import com.ibm.wala.cast.tree.CAst;
import com.ibm.wala.cast.tree.CAstEntity;
import com.ibm.wala.cast.tree.CAstQualifier;
import com.ibm.wala.cast.tree.CAstSourcePositionMap;
import com.ibm.wala.cast.tree.impl.CAstImpl;
import com.ibm.wala.cast.types.AstMethodReference;
import com.ibm.wala.cast.util.CAstPrinter;
import com.ibm.wala.cfg.AbstractCFG;
import com.ibm.wala.cfg.IBasicBlock;
import com.ibm.wala.classLoader.IClass;
import com.ibm.wala.classLoader.IClassLoader;
import com.ibm.wala.classLoader.IMethod;
import com.ibm.wala.classLoader.Module;
import com.ibm.wala.classLoader.ModuleEntry;
import com.ibm.wala.classLoader.SourceFileModule;
import com.ibm.wala.classLoader.SourceModule;
import com.ibm.wala.core.util.warnings.Warning;
import com.ibm.wala.ipa.cha.IClassHierarchy;
import com.ibm.wala.ssa.SSAInstruction;
import com.ibm.wala.ssa.SymbolTable;
import com.ibm.wala.types.TypeName;
import com.ibm.wala.types.TypeReference;
import com.ibm.wala.types.annotations.Annotation;
import com.ibm.wala.util.collections.Iterator2Iterable;
import com.ibm.wala.util.collections.Pair;
import com.ibm.wala.util.debug.Assertions;
import com.ibm.wala.util.io.TemporaryFile;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * abstract class loader that performs CAst and IR generation for relevant entities in a list of
 * {@link Module}s. Subclasses provide the CAst / IR translators appropriate for the language.
 */
public abstract class CAstAbstractModuleLoader extends CAstAbstractLoader {

  private static final boolean DEBUG = false;

  public CAstAbstractModuleLoader(IClassHierarchy cha, IClassLoader parent) {
    super(cha, parent);
  }

  public CAstAbstractModuleLoader(IClassHierarchy cha) {
    this(cha, null);
  }

  /**
   * create the appropriate CAst translator for the language and source module
   *
   * @param modules all modules in the analysis
   */
  protected abstract TranslatorToCAst getTranslatorToCAst(
      CAst ast, ModuleEntry M, List<Module> modules) throws IOException;

  /** should IR be generated for entity? */
  protected abstract boolean shouldTranslate(CAstEntity entity);

  /**
   * create the appropriate IR translator for the language
   *
   * @param topLevelEntities the set of all modules being translated
   */
  protected abstract TranslatorToIR initTranslator(
      Set<Pair<CAstEntity, ModuleEntry>> topLevelEntities);

  protected File getLocalFile(SourceModule M) throws IOException {
    if (M instanceof SourceFileModule) {
      return ((SourceFileModule) M).getFile();
    } else {
      File f = File.createTempFile("module", ".txt");
      f.deleteOnExit();
      try (InputStream inputStream = M.getInputStream()) {
        TemporaryFile.streamToFile(f, inputStream);
      }
      return f;
    }
  }

  /**
   * subclasses should override to perform actions after CAst and IR have been generated. by
   * default, do nothing
   */
  protected void finishTranslation() {}

  @Override
  public void init(final List<Module> modules) {

    final CAst ast = new CAstImpl();

    // convert everything to CAst
    final Set<Pair<CAstEntity, ModuleEntry>> topLevelEntities = new LinkedHashSet<>();
    for (Module module : modules) {
      translateModuleToCAst(module, ast, topLevelEntities, modules);
    }

    // generate IR as needed
    final TranslatorToIR xlatorToIR = initTranslator(topLevelEntities);

    for (Pair<CAstEntity, ModuleEntry> p : topLevelEntities) {
      if (shouldTranslate(p.fst)) {
        xlatorToIR.translate(p.fst, p.snd);
      }
    }

    if (DEBUG) {
      for (Map.Entry<TypeName, IClass> entry : types.entrySet()) {
        try {
          final IClass value = entry.getValue();
          System.err.println(
              ("found type " + entry.getKey() + " : " + value + " < " + value.getSuperclass()));
        } catch (Exception e) {
          System.err.println(e);
        }
      }
    }

    finishTranslation();
  }

  /**
   * translate moduleEntry to CAst and store result in topLevelEntities
   *
   * @param modules all mofules in the analysis
   */
  private void translateModuleEntryToCAst(
      ModuleEntry moduleEntry,
      CAst ast,
      Set<Pair<CAstEntity, ModuleEntry>> topLevelEntities,
      List<Module> modules) {
    try {
      if (moduleEntry.isModuleFile()) {
        // nested module
        translateModuleToCAst(moduleEntry.asModule(), ast, topLevelEntities, modules);
      } else {
        TranslatorToCAst xlatorToCAst = getTranslatorToCAst(ast, moduleEntry, modules);

        CAstEntity fileEntity = null;
        try {
          fileEntity = xlatorToCAst.translateToCAst();

          if (DEBUG) {
            CAstPrinter.printTo(fileEntity, new PrintWriter(System.err));
          }
          topLevelEntities.add(Pair.make(fileEntity, moduleEntry));

        } catch (TranslatorToCAst.Error e) {
          addMessages(moduleEntry, e.warning);
        }
      }
    } catch (final IOException e) {
      addMessage(
          moduleEntry,
          new Warning(Warning.SEVERE) {
            @Override
            public String getMsg() {
              return "I/O issue: " + e.getMessage();
            }
          });
    } catch (final RuntimeException e) {
      final ByteArrayOutputStream s = new ByteArrayOutputStream();
      PrintStream ps = new PrintStream(s);
      e.printStackTrace(ps);
      addMessage(
          moduleEntry,
          new Warning(Warning.SEVERE) {
            @Override
            public String getMsg() {
              return "Parsing issue: " + new String(s.toByteArray());
            }
          });
    }
  }

  /**
   * translate all relevant entities in the module to CAst, storing the results in topLevelEntities
   *
   * @param modules all modules in the analysis
   */
  private void translateModuleToCAst(
      Module module,
      CAst ast,
      Set<Pair<CAstEntity, ModuleEntry>> topLevelEntities,
      List<Module> modules) {
    for (ModuleEntry me : Iterator2Iterable.make(module.getEntries())) {
      translateModuleEntryToCAst(me, ast, topLevelEntities, modules);
    }
  }

  public class DynamicCodeBody extends AstFunctionClass {
    private final WalkContext translationContext;
    private final CAstEntity entity;

    public DynamicCodeBody(
        TypeReference codeName,
        TypeReference parent,
        IClassLoader loader,
        CAstSourcePositionMap.Position sourcePosition,
        CAstEntity entity,
        WalkContext context) {
      super(codeName, parent, loader, sourcePosition);
      types.put(codeName.getName(), this);
      this.translationContext = context;
      this.entity = entity;
    }

    @Override
    public IClassHierarchy getClassHierarchy() {
      return cha;
    }

    public IMethod setCodeBody(DynamicMethodObject codeBody) {
      this.functionBody = codeBody;
      codeBody.entity = entity;
      codeBody.translationContext = translationContext;
      return codeBody;
    }

    @Override
    public Collection<Annotation> getAnnotations() {
      return Collections.emptySet();
    }
  }

  public class DynamicMethodObject extends AstMethod implements Retranslatable {
    private WalkContext translationContext;
    private CAstEntity entity;

    public DynamicMethodObject(
        IClass cls,
        Collection<CAstQualifier> qualifiers,
        AbstractCFG<?, ?> cfg,
        SymbolTable symtab,
        boolean hasCatchBlock,
        Map<IBasicBlock<SSAInstruction>, TypeReference[]> caughtTypes,
        boolean hasMonitorOp,
        AstLexicalInformation lexicalInfo,
        DebuggingInformation debugInfo) {
      super(
          cls,
          qualifiers,
          cfg,
          symtab,
          AstMethodReference.fnReference(cls.getReference()),
          hasCatchBlock,
          caughtTypes,
          hasMonitorOp,
          lexicalInfo,
          debugInfo,
          null);

      // force creation of these constants by calling the getter methods
      symtab.getNullConstant();
    }

    @Override
    public CAstEntity getEntity() {
      return entity;
    }

    @Override
    public void retranslate(AstTranslator xlator) {
      xlator.translate(entity, translationContext);
    }

    @Override
    public IClassHierarchy getClassHierarchy() {
      return cha;
    }

    @Override
    public String toString() {
      return "<Code body of " + cls + '>';
    }

    @Override
    public TypeReference[] getDeclaredExceptions() {
      return null;
    }

    @Override
    public LexicalParent[] getParents() {
      if (lexicalInfo() == null) return new LexicalParent[0];

      final String[] parents = lexicalInfo().getScopingParents();

      if (parents == null) return new LexicalParent[0];

      LexicalParent result[] = new LexicalParent[parents.length];

      for (int i = 0; i < parents.length; i++) {
        final int hack = i;
        final AstMethod method =
            (AstMethod) lookupClass(parents[i], cha).getMethod(AstMethodReference.fnSelector);
        result[i] =
            new LexicalParent() {
              @Override
              public String getName() {
                return parents[hack];
              }

              @Override
              public AstMethod getMethod() {
                return method;
              }
            };

        if (AstTranslator.DEBUG_LEXICAL) {
          System.err.println(("parent " + result[i].getName() + " is " + result[i].getMethod()));
        }
      }

      return result;
    }

    @Override
    public String getLocalVariableName(int bcIndex, int localNumber) {
      return null;
    }

    @Override
    public boolean hasLocalVariableTable() {
      return false;
    }

    public int getMaxLocals() {
      Assertions.UNREACHABLE();
      return -1;
    }

    public int getMaxStackHeight() {
      Assertions.UNREACHABLE();
      return -1;
    }

    @Override
    public TypeReference getParameterType(int i) {
      if (i == 0) {
        return getDeclaringClass().getReference();
      } else {
        return getDeclaringClass().getClassLoader().getLanguage().getRootType();
      }
    }
  }

  public class CoreClass extends AstDynamicPropertyClass {
    private final TypeName superName;

    public CoreClass(
        TypeName name,
        TypeName superName,
        IClassLoader loader,
        CAstSourcePositionMap.Position sourcePosition) {
      super(
          sourcePosition,
          name,
          loader,
          (short) 0,
          Collections.emptyMap(),
          CAstAbstractModuleLoader.this.getLanguage().getRootType());
      types.put(name, this);
      this.superName = superName;
    }

    @Override
    public IClassHierarchy getClassHierarchy() {
      return cha;
    }

    @Override
    public String toString() {
      return "Core[" + getReference().getName().toString().substring(1) + ']';
    }

    @Override
    public Collection<IClass> getDirectInterfaces() {
      return Collections.emptySet();
    }

    @Override
    public IClass getSuperclass() {
      return superName == null ? null : types.get(superName);
    }

    @Override
    public Collection<Annotation> getAnnotations() {
      return Collections.emptySet();
    }
  }
}
