package annotations.tools;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;

import com.google.common.collect.HashMultimap;
import com.google.common.collect.SetMultimap;
import com.sun.tools.javac.main.CommandLine;

import plume.FileIOException;
import annotations.Annotation;
import annotations.Annotations;
import annotations.el.ABlock;
import annotations.el.AClass;
import annotations.el.ADeclaration;
import annotations.el.AElement;
import annotations.el.AExpression;
import annotations.el.AField;
import annotations.el.AMethod;
import annotations.el.AScene;
import annotations.el.ATypeElement;
import annotations.el.ATypeElementWithType;
import annotations.el.AnnotationDef;
import annotations.el.DefException;
import annotations.el.ElementVisitor;
import annotations.field.AnnotationFieldType;
import annotations.io.IndexFileParser;
import annotations.io.IndexFileWriter;

/**
 * Utility for merging index files, including multiple versions for the
 *  same class.
 *
 * @author dbro
 */
public class IndexFileMerger {
  public static void main(String[] args) {
    if (args.length < 1) { System.exit(0); }

    final SetMultimap<String, String> annotatedFor = HashMultimap.create();
    String[] inputArgs;

    // TODO: document assumptions
    // collect annotations into scene
    try {
      try {
        inputArgs = CommandLine.parse(args);
      } catch (IOException ex) {
        System.err.println(ex);
        System.err.println("(For non-argfile beginning with \"@\", use \"@@\" for initial \"@\".");
        System.err.println("Alternative for filenames: indicate directory, e.g. as './@file'.");
        System.err.println("Alternative for flags: use '=', as in '-o=@Deprecated'.)");
        System.exit(1);
        return;  // so compiler knows inputArgs defined after try/catch
      }

      File baseFile = new File(inputArgs[0]);
      boolean byDir = baseFile.isDirectory();
      String basePath = baseFile.getCanonicalPath();
      AScene scene = new AScene();

      for (int i = byDir ? 1 : 0; i < inputArgs.length; i++) {
        File inputFile = new File(inputArgs[i]);
        String inputPath = inputFile.getCanonicalPath();
        String filename = inputFile.getName();

        if (byDir) {
          if (!(filename.endsWith(".jaif") || filename.endsWith("jann"))) {
            System.err.println("WARNING: ignoring non-JAIF " + filename);
            continue;
          }
          if (!inputPath.startsWith(basePath)) {
            System.err.println("WARNING: ignoring file outside base directory "
                + filename);
            continue;
          }

          // note which base subdirectory JAIF came from
          String relPath = inputPath.substring(basePath.length()+1);  // +1 '/'
          int ix = relPath.indexOf(File.separator);
          String subdir = ix < 0 ? relPath : relPath.substring(0, ix);
          // trim .jaif or .jann and subdir, convert directory to package id
          String classname = relPath.substring(0, relPath.lastIndexOf('.'))
              .substring(relPath.indexOf('/')+1).replace(File.separator, ".");
          annotatedFor.put(classname, "\"" + subdir + "\"");
        }

        try {
          IndexFileParser.parseFile(inputPath, scene);
        } catch (FileNotFoundException e) {
          System.err.println("IndexFileMerger: can't read "
              + inputPath);
          System.exit(1);
        } catch (FileIOException e) {
          e.printStackTrace();  // TODO
          System.exit(1);
        }
      }

      if (!byDir) {
/*
        // collect defs
        Map<String, String> annoPkgs = new HashMap<String, String>();
        try {
          new DefCollector(scene) {
            @Override
            protected void visitAnnotationDef(AnnotationDef d) {
              String[] a = d.name.split("\\.");
              if (a.length > 2 && a[a.length-2].matches("quals?")) {
                String s = a[a.length-1];
                annoPkgs.put(s, d.name.substring(0));
              }
            }
          }.visit();
        } catch (DefException e) {
          System.err.println("DefCollector failed!");
          e.printStackTrace();
          System.exit(1);
        }
*/

        for (Map.Entry<String, AClass> entry : scene.classes.entrySet()) {
          // final String classname = entry.getKey();

          entry.getValue().accept(new ElementVisitor<Void, Void>() {
            // Map<String, String> annoPkgs = new HashMap<String, String>();

            // Void process(AElement el) {
            //  for (Annotation anno : el.tlAnnotationsHere) {
            //    AnnotationDef def = anno.def();
            //    String[] a = def.name.split("\\.");
            //    if ("AnnotatedFor".equals(a[a.length-1])) {
            //      @SuppressWarnings("unchecked")
            //      List<String> vals =
            //          (List<String>) anno.getFieldValue("value");
            //      for (String val : vals) {
            //        annotatedFor.put(classname, val);
            //      }
            //    } else if (a.length > 2 && a[a.length-2].matches("quals?")) {
            //      annotatedFor.put(classname, a[a.length-3]);
            //    }
            //  }
            //  return null;
            // }

            Void visit(AElement el) {
              if (el != null) { el.accept(this, null); }
              return null;
            }

            <T, E extends AElement> Void visitMap(Map<T, E> map) {
              if (map != null) {
                for (E el : map.values()) { visit(el); }
              }
              return null;
            }

            @Override
            public Void visitAnnotationDef(AnnotationDef d, Void v) {
              // String[] a = d.name.split("\\.");
              // if (a.length > 2 && a[a.length-2].matches("quals?")) {
              //  String s = a[a.length-1];
              //  annoPkgs.put(s, d.name.substring(0));
              // }
              return null;  // process(d);
            }

            @Override
            public Void visitBlock(ABlock el, Void v) {
              visitMap(el.locals);
              return visitExpression(el, v);
            }

            @Override
            public Void visitClass(AClass el, Void v) {
              visitMap(el.bounds);
              visitMap(el.extendsImplements);
              visitMap(el.instanceInits);
              visitMap(el.staticInits);
              visitMap(el.methods);
              visitMap(el.fields);
              visitMap(el.fieldInits);
              return visitDeclaration(el, v);
            }

            @Override
            public Void visitDeclaration(ADeclaration el, Void v) {
              visitMap(el.insertAnnotations);
              visitMap(el.insertTypecasts);
              return visitElement(el, v);
            }

            @Override
            public Void visitExpression(AExpression el, Void v) {
              visitMap(el.calls);
              visitMap(el.funs);
              visitMap(el.instanceofs);
              visitMap(el.news);
              visitMap(el.refs);
              visitMap(el.typecasts);
              return visitElement(el, v);
            }

            @Override
            public Void visitField(AField el, Void v) {
              visit(el.init);
              return visitDeclaration(el, v);
            }

            @Override
            public Void visitMethod(AMethod el, Void v) {
              visit(el.receiver);
              visitMap(el.parameters);
              visitMap(el.bounds);
              visit(el.returnType);
              visit(el.body);
              visitMap(el.throwsException);
              return visitDeclaration(el, v);
            }

            @Override
            public Void visitTypeElement(ATypeElement el, Void v) {
              visitMap(el.innerTypes);
              return visitElement(el, v);
            }

            @Override
            public Void visitTypeElementWithType(ATypeElementWithType el,
                Void v) {
              return visitTypeElement(el, v);
            }

            @Override
            public Void visitElement(AElement el, Void v) {
              visit(el.type);
              return null;  // process(el);
            }
          }, null);
        }
      }

      // add AnnotatedFor to each annotated class
      AnnotationFieldType stringArray =
          AnnotationFieldType.fromClass(new String[0].getClass(),
              Collections.<String, AnnotationDef>emptyMap());
      AnnotationDef afDef =
          Annotations.createValueAnnotationDef("AnnotatedFor",
              Collections.<Annotation>emptySet(), stringArray);
      for (Map.Entry<String, Collection<String>> entry :
          annotatedFor.asMap().entrySet()) {
        String key = entry.getKey();
        Collection<String> values = entry.getValue();
        Annotation afAnno = new Annotation(afDef, Collections
                .<String, Collection<String>>singletonMap("value", values));
        scene.classes.vivify(key).tlAnnotationsHere.add(afAnno);
      }
      scene.prune();
      annotatedFor.clear();  // for gc

      try {
        IndexFileWriter.write(scene, new PrintWriter(System.out, true));
      } catch (SecurityException e) {
        e.printStackTrace();
        System.exit(1);
      } catch (DefException e) {
        e.printStackTrace();
        System.exit(1);
      }
    } catch (IOException e) {
      e.printStackTrace();
    }
  }
}