/**
 * MVEL 2.0
 * Copyright (C) 2007 The Codehaus
 * Mike Brock, Dhanji Prasanna, John Graham, Mark Proctor
 *
 * 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.mvel2.sh;

import org.mvel2.MVELInterpretedRuntime;
import org.mvel2.ParserContext;
import org.mvel2.integration.VariableResolverFactory;
import org.mvel2.integration.impl.MapVariableResolverFactory;
import org.mvel2.sh.command.basic.BasicCommandSet;
import org.mvel2.sh.command.file.FileCommandSet;
import org.mvel2.templates.TemplateRuntime;
import org.mvel2.util.StringAppender;

import java.io.*;
import java.util.*;

import static java.lang.Boolean.parseBoolean;
import static java.lang.Runtime.getRuntime;
import static java.lang.System.arraycopy;
import static java.lang.System.getProperty;
import static java.util.ResourceBundle.getBundle;
import static org.mvel2.MVEL.compileExpression;
import static org.mvel2.MVEL.executeExpression;
import static org.mvel2.util.PropertyTools.contains;

/**
 * A shell session.
 */
public class ShellSession {
  public static final String PROMPT_VAR = "$PROMPT";
  private static final String[] EMPTY = new String[0];

  private final Map<String, Command> commands = new HashMap<String, Command>();
  private Map<String, Object> variables;
  private Map<String, String> env;
  private Object ctxObject;

  ParserContext pCtx = new ParserContext();
  VariableResolverFactory lvrf;

  private int depth;
  private int cdepth;

  private boolean multi = false;
  private int multiIndentSize = 0;
  private PrintStream out = System.out;
  private String prompt;
  private String commandBuffer;
  StringAppender inBuffer = new StringAppender();

  final BufferedReader readBuffer = new BufferedReader(new InputStreamReader(System.in));

  public ShellSession() {
    System.out.println("Starting session...");

    variables = new HashMap<String, Object>();
    env = new HashMap<String, String>();

    commands.putAll(new BasicCommandSet().load());
    commands.putAll(new FileCommandSet().load());

    env.put(PROMPT_VAR, DefaultEnvironment.PROMPT);
    env.put("$OS_NAME", getProperty("os.name"));
    env.put("$OS_VERSION", getProperty("os.version"));
    env.put("$JAVA_VERSION", getProperty("java.version"));
    env.put("$CWD", new File(".").getAbsolutePath());
    env.put("$COMMAND_PASSTRU", "false");
    env.put("$PRINTOUTPUT", "true");
    env.put("$ECHO", "false");
    env.put("$SHOW_TRACES", "true");
    env.put("$USE_OPTIMIZER_ALWAYS", "false");
    env.put("$PATH", "");

    try {
      ResourceBundle bundle = getBundle(".mvelsh.properties");

      Enumeration<String> enumer = bundle.getKeys();
      String key;
      while (enumer.hasMoreElements()) {
        env.put(key = enumer.nextElement(), bundle.getString(key));
      }
    }
    catch (MissingResourceException e) {
      System.out.println("No config file found.  Loading default config.");

      if (!contains(getProperty("os.name").toLowerCase(), "windows")) {
        env.put("$PATH", "/bin:/usr/bin:/sbin:/usr/sbin");
      }

    }

    lvrf = new MapVariableResolverFactory(variables, new MapVariableResolverFactory(env));

  }

  public ShellSession(String init) {
    this();
    exec(init);
  }

  private void _exec() {
    String[] inTokens;
    Object outputBuffer;

    final PrintStream sysPrintStream = System.out;
    final PrintStream sysErrorStream = System.err;
    final InputStream sysInputStream = System.in;

    File execFile;

    if ("true".equals(env.get("$ECHO"))) {
      out.println(">" + commandBuffer);
      out.flush();
    }

    inTokens = inBuffer.append(commandBuffer).toString().split("\\s");

    if (inTokens.length != 0 && commands.containsKey(inTokens[0])) {

      commandBuffer = null;

      String[] passParameters;
      if (inTokens.length > 1) {
        arraycopy(inTokens, 1, passParameters = new String[inTokens.length - 1], 0, passParameters.length);
      }
      else {
        passParameters = EMPTY;
      }

      try {
        commands.get(inTokens[0]).execute(this, passParameters);
      }
      catch (CommandException e) {
        out.append("Error: ").append(e.getMessage()).append("\n");
      }
    }
    else {
      commandBuffer = null;

      try {
        if (shouldDefer(inBuffer)) {
          multi = true;
          return;
        }
        else {
          multi = false;
        }

        if (parseBoolean(env.get("$USE_OPTIMIZER_ALWAYS"))) {
          outputBuffer = executeExpression(compileExpression(inBuffer.toString()), ctxObject, lvrf);
        }
        else {
          MVELInterpretedRuntime runtime = new MVELInterpretedRuntime(inBuffer.toString(), ctxObject, lvrf);
          runtime.newContext(pCtx);
          outputBuffer = runtime.parse();
        }
      }
      catch (Exception e) {
        if ("true".equals(env.get("$COMMAND_PASSTHRU"))) {

          String[] paths;
          String s;
          if ((s = inTokens[0]).startsWith("./")) {
            s = new File(env.get("$CWD")).getAbsolutePath() + s.substring(s.indexOf('/'));

            paths = new String[]{s};
          }
          else {
            paths = env.get("$PATH").split("(:|;)");
          }

          boolean successfulExec = false;


          for (String execPath : paths) {
            if ((execFile = new File(execPath + "/" + s)).exists() && execFile.isFile()) {
              successfulExec = true;

              String[] execString = new String[inTokens.length];
              execString[0] = execFile.getAbsolutePath();

              System.arraycopy(inTokens, 1, execString, 1, inTokens.length - 1);

              try {
                final Process p = getRuntime().exec(execString);
                final OutputStream outStream = p.getOutputStream();

                final InputStream inStream = p.getInputStream();
                final InputStream errStream = p.getErrorStream();

                final RunState runState = new RunState(this);

                final Thread pollingThread = new Thread(new Runnable() {
                  public void run() {
                    byte[] buf = new byte[25];
                    int read;

                    while (true) {
                      try {
                        while ((read = inStream.read(buf)) > 0) {
                          for (int i = 0; i < read; i++) {
                            sysPrintStream.print((char) buf[i]);
                          }
                          sysPrintStream.flush();
                        }

                        if (!runState.isRunning()) break;
                      }
                      catch (Exception e) {
                        break;
                      }
                    }

                    sysPrintStream.flush();

                    if (!multi) {
                      multiIndentSize = (prompt = String.valueOf(TemplateRuntime.eval(env.get("$PROMPT"), variables))).length();
                      out.append(prompt);
                    }
                    else {
                      out.append(">").append(indent((multiIndentSize - 1) + (depth * 4)));
                    }

                  }
                });


                final Thread watchThread = new Thread(new Runnable() {
                  public void run() {

                    Thread runningThread = new Thread(new Runnable() {
                      public void run() {
                        try {
                          String read;
                          while (runState.isRunning()) {
                            while ((read = readBuffer.readLine()) != null) {
                              if (runState.isRunning()) {
                                for (char c : read.toCharArray()) {
                                  outStream.write((byte) c);
                                }
                              }
                              else {
                                runState.getSession().setCommandBuffer(read);
                                break;
                              }
                            }
                          }

                          outStream.write((byte) '\n');
                          outStream.flush();
                        }
                        catch (Exception e2) {

                        }
                      }

                    });

                    runningThread.setPriority(Thread.MIN_PRIORITY);
                    runningThread.start();

                    try {
                      p.waitFor();
                    }
                    catch (InterruptedException e) {
                      // nothing;
                    }

                    sysPrintStream.flush();
                    runState.setRunning(false);

                    try {
                      runningThread.join();
                    }
                    catch (InterruptedException e) {
                      // nothing;�
                    }
                  }
                });

                pollingThread.setPriority(Thread.MIN_PRIORITY);
                pollingThread.start();

                watchThread.setPriority(Thread.MIN_PRIORITY);
                watchThread.start();
                watchThread.join();


                try {
                  pollingThread.notify();
                }
                catch (Exception ne) {

                }

              }
              catch (Exception e2) {
                // fall through;
              }
            }
          }

          if (successfulExec) {
            inBuffer.reset();
            return;
          }
        }

        ByteArrayOutputStream stackTraceCap = new ByteArrayOutputStream();
        PrintStream capture = new PrintStream(stackTraceCap);

        e.printStackTrace(capture);
        capture.flush();

        env.put("$LAST_STACK_TRACE", new String(stackTraceCap.toByteArray()));
        if (parseBoolean(env.get("$SHOW_TRACE"))) {
          out.println(env.get("$LAST_STACK_TRACE"));
        }
        else {
          out.println(e.toString());
        }

        inBuffer.reset();

        return;
      }


      if (outputBuffer != null && "true".equals(env.get("$PRINTOUTPUT"))) {
        if (outputBuffer.getClass().isArray()) {
          out.println(Arrays.toString((Object[]) outputBuffer));
        }
        else {
          out.println(String.valueOf(outputBuffer));
        }
      }


    }

    inBuffer.reset();


  }

  //todo: fix this
  public void run() {
    final BufferedReader readBuffer = new BufferedReader(new InputStreamReader(System.in));

    try {
      //noinspection InfiniteLoopStatement
      while (true) {
        printPrompt();

        if (commandBuffer == null) {
          commandBuffer = readBuffer.readLine();
        }

        _exec();
      }
    }
    catch (Exception e) {
      e.printStackTrace();
      System.out.println("unexpected exception. exiting.");
    }

  }

  public void printPrompt() {
    if (!multi) {
      multiIndentSize = (prompt = String.valueOf(TemplateRuntime.eval(env.get("$PROMPT"), variables))).length();
      out.append(prompt);
    }
    else {
      out.append(">").append(indent((multiIndentSize - 1) + (depth * 4)));
    }
  }


  public boolean shouldDefer(StringAppender inBuf) {
    char[] buffer = new char[inBuf.length()];
    inBuf.getChars(0, inBuf.length(), buffer, 0);

    depth = cdepth = 0;
    for (int i = 0; i < buffer.length; i++) {
      switch (buffer[i]) {
        case '/':
          if (i + 1 < buffer.length && buffer[i + 1] == '*') {
            cdepth++;
          }
          break;
        case '*':
          if (i + 1 < buffer.length && buffer[i + 1] == '/') {
            cdepth--;
          }
          break;

        case '{':
          depth++;
          break;
        case '}':
          depth--;
          break;
      }
    }

    return depth + cdepth > 0;
  }

  public String indent(int size) {
    StringBuffer sbuf = new StringBuffer();
    for (int i = 0; i < size; i++) sbuf.append(" ");
    return sbuf.toString();
  }

  public Map<String, Command> getCommands() {
    return commands;
  }

  public Map<String, Object> getVariables() {
    return variables;
  }

  public Map<String, String> getEnv() {
    return env;
  }

  public Object getCtxObject() {
    return ctxObject;
  }

  public void setCtxObject(Object ctxObject) {
    this.ctxObject = ctxObject;
  }

  public String getCommandBuffer() {
    return commandBuffer;
  }

  public void setCommandBuffer(String commandBuffer) {
    this.commandBuffer = commandBuffer;
  }

  public void exec(String command) {
    for (String c : command.split("\n")) {
      inBuffer.append(c);
      _exec();
    }
  }

  public static final class RunState {
    private boolean running = true;
    private ShellSession session;


    public RunState(ShellSession session) {
      this.session = session;
    }

    public ShellSession getSession() {
      return session;
    }

    public void setSession(ShellSession session) {
      this.session = session;
    }

    public boolean isRunning() {
      return running;
    }

    public void setRunning(boolean running) {
      this.running = running;
    }
  }
}
