package falgout.jrepl;

import java.io.BufferedReader;
import java.io.Closeable;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.Reader;
import java.io.Writer;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Member;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;

import com.google.inject.Inject;
import com.google.inject.Singleton;

import falgout.jrepl.command.execute.codegen.GeneratedSourceCode;
import falgout.jrepl.guice.Stderr;
import falgout.jrepl.guice.Stdout;
import falgout.jrepl.reflection.NestedClass;

@Singleton
public final class Environment implements Closeable {
    private final BufferedReader in;
    private final PrintWriter out;
    private final PrintWriter err;
    
    private final Set<Import> imports = new ImportSet();
    {
        imports.add(Import.create(false, "java.lang", true));
    }
    private final EnvironmentClassLoader cl = new EnvironmentClassLoader(this);
    private final Map<String, Variable<?>> variables = new LinkedHashMap<>();
    private final Map<String, Method> methods = new LinkedHashMap<>();
    private final Map<String, NestedClass<?>> classes = new LinkedHashMap<>();
    
    @Inject
    public Environment(Reader in, @Stdout Writer out, @Stderr Writer err) {
        this.in = in instanceof BufferedReader ? (BufferedReader) in : new BufferedReader(in);
        this.out = createPrintWriter(out);
        this.err = createPrintWriter(err);
        
        Thread.currentThread().setContextClassLoader(cl);
    }
    
    private PrintWriter createPrintWriter(Writer w) {
        return w instanceof PrintWriter ? (PrintWriter) w : new PrintWriter(w, true);
    }
    
    public BufferedReader getInput() {
        return in;
    }
    
    public PrintWriter getOutput() {
        return out;
    }
    
    public PrintWriter getError() {
        return err;
    }
    
    public void printStackTrace(Throwable t) {
        if (t instanceof InvocationTargetException) {
            t = t.getCause();
            StackTraceElement[] st = t.getStackTrace();
            int i;
            for (i = 0; i < st.length; i++) {
                if (!st[i].getClassName().contains(GeneratedSourceCode.TEMPLATE)) {
                    break;
                }
            }
            t.setStackTrace(Arrays.copyOf(st, i));
            t.printStackTrace(err);
        } else {
            while (t != null) {
                String message = t.getLocalizedMessage();
                if (!message.isEmpty()) {
                    err.println(message);
                }
                t = t.getCause();
            }
        }
    }
    
    public boolean addVariable(Variable<?> variable) {
        if (containsVariable(variable.getIdentifier())) {
            return false;
        } else if (variable.isFinal() && !variable.isInitialized()) {
            throw new IllegalArgumentException("Uninitialized final variables are not allowed.");
        }
        
        variables.put(variable.getIdentifier(), variable);
        return true;
    }
    
    public boolean containsVariable(String variableName) {
        return variables.containsKey(variableName);
    }
    
    public Variable<?> getVariable(String variableName) {
        return variables.get(variableName);
    }
    
    public Collection<? extends Variable<?>> getVariables() {
        return Collections.unmodifiableCollection(variables.values());
    }
    
    public Set<Import> getImports() {
        return imports;
    }
    
    public EnvironmentClassLoader getImportClassLoader() {
        return cl;
    }
    
    public boolean addMethod(Method method) {
        if (methods.containsKey(method.getName())) {
            return false;
        }
        
        methods.put(method.getName(), method);
        return true;
    }
    
    public boolean addClass(NestedClass<?> clazz) {
        if (classes.containsKey(clazz.getName())) {
            return false;
        }
        
        classes.put(clazz.getName(), clazz);
        return true;
    }
    
    public boolean containsClass(String className) {
        return classes.containsKey(className);
    }
    
    public NestedClass<?> getClass(String className) {
        return classes.get(className);
    }
    
    public Set<? extends Member> getMembers() {
        Set<Member> members = new LinkedHashSet<>(methods.size() + classes.size());
        members.addAll(methods.values());
        members.addAll(classes.values());
        
        return Collections.unmodifiableSet(members);
    }
    
    @Override
    public void close() throws IOException {
        cl.close();
    }
}

@@ -2205,16 +2205,24 @@
         
+truncate
 StackTra
@@ -2227,27 +2227,31 @@
 race
-Element%5B%5D st = t.ge
+(t);%0A            t.prin
 tSta
@@ -2258,16 +2258,19 @@
 ckTrace(
+err
 );%0A     
@@ -2276,144 +2276,144 @@
     
-    int i;%0A            for (i = 0; i %3C st.length; i++) %7B%0A                if (!st%5Bi%5D.getClassName().contains(GeneratedSourceCode.TEMPLATE
+%7D else %7B%0A            while (t != null) %7B%0A                String message = t.getLocalizedMessage();%0A                if (!message.isEmpty(
 )) %7B
@@ -2437,13 +2437,28 @@
     
-break
+err.println(message)
 ;%0A  
@@ -2489,145 +2489,170 @@
     
-%7D%0A       
     
+t =
  t.
-setStackTrace(Arrays.copyOf(st, i));%0A            t.printStackTrace(err);%0A        %7D else %7B%0A            while (t != null) %7B
+getCause();%0A            %7D%0A        %7D%0A    %7D%0A    %0A    private void truncateStackTrace(Throwable t) %7B%0A        if (t == null) %7B%0A            return;%0A        %7D
 %0A   
@@ -2648,32 +2648,33 @@
       %7D%0A        
+%0A
         String m
@@ -2671,43 +2671,45 @@
   St
-ring message = t.getLocalizedMessag
+ackTraceElement%5B%5D st = t.getStackTrac
 e();
@@ -2713,32 +2713,39 @@
 ();%0A        
+int i;%0A
         
 if (!message
@@ -2736,33 +2736,105 @@
     
-if (!message.isEmpty()
+for (i = 0; i %3C st.length && st%5Bi%5D.getClassName().contains(GeneratedSourceCode.TEMPLATE); i++
 ) %7B
+%7D
 %0A   
@@ -2842,39 +2842,76 @@
     
-            err.println(message
+t.setStackTrace(Arrays.copyOf(st, i));%0A        t.printStackTrace(err
 );%0A 
@@ -2913,32 +2913,33 @@
 r);%0A        
+%0A
         
 %7D%0A          
@@ -2930,43 +2930,89 @@
     
-%7D%0A                t = t.getCau
+truncateStackTrace(t.getCause());%0A        for (Throwable s : t.getSuppres
 se
+d
 ()
-;
+) %7B
 %0A   
@@ -3012,33 +3012,54 @@
 ) %7B%0A            
-%7D
+truncateStackTrace(s);
 %0A        %7D%0A    %7D

Truncate the stack trace elements for all of the exceptions that were thrown.