/*
 * Copyright 2000-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 org.jetbrains.plugins.terminal;

import com.google.common.collect.Lists;
import com.intellij.execution.TaskExecutor;
import com.intellij.execution.configurations.EncodingEnvironmentUtil;
import com.intellij.execution.process.ProcessAdapter;
import com.intellij.execution.process.ProcessEvent;
import com.intellij.execution.process.ProcessHandler;
import com.intellij.execution.process.ProcessWaitFor;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.SystemInfo;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.CharsetToolkit;
import com.intellij.util.ArrayUtil;
import com.intellij.util.EnvironmentUtil;
import com.intellij.util.concurrency.AppExecutorUtil;
import com.intellij.util.containers.HashMap;
import com.jediterm.pty.PtyProcessTtyConnector;
import com.jediterm.terminal.TtyConnector;
import com.pty4j.PtyProcess;
import com.pty4j.util.PtyUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URI;
import java.net.URL;
import java.nio.charset.Charset;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;

/**
 * @author traff
 */
public class LocalTerminalDirectRunner extends AbstractTerminalRunner<PtyProcess> {
  private static final Logger LOG = Logger.getInstance(LocalTerminalDirectRunner.class);
  public static final String JEDITERM_USER_RCFILE = "JEDITERM_USER_RCFILE";
  public static final String ZDOTDIR = "ZDOTDIR";
  public static final String XDG_CONFIG_HOME = "XDG_CONFIG_HOME";


  private final Charset myDefaultCharset;

  public LocalTerminalDirectRunner(Project project) {
    super(project);
    myDefaultCharset = CharsetToolkit.UTF8_CHARSET;
  }

  private static boolean hasLoginArgument(String name) {
    return name.equals("bash") || name.equals("sh") || name.equals("zsh");
  }

  private static String getShellName(String path) {
    return new File(path).getName();
  }

  private static String findRCFile(String shellName) {
    if (shellName != null) {
      if ("sh".equals(shellName)) {
        shellName = "bash";
      }
      try {
        String rcfile = "jediterm-" + shellName + ".in";
        if ("zsh".equals(shellName)) {
          rcfile = ".zshrc";
        }
        else if ("fish".equals(shellName)) {
          rcfile = "fish/config.fish";
        }
        URL resource = LocalTerminalDirectRunner.class.getClassLoader().getResource(rcfile);
        if (resource != null && "jar".equals(resource.getProtocol())) {
          File file = new File(new File(PtyUtil.getJarContainingFolderPath(LocalTerminalDirectRunner.class)).getParent(), rcfile);
          if (file.exists()) {
            return file.getAbsolutePath();
          }
        }
        if (resource != null) {
          URI uri = resource.toURI();
          return uri.getPath();
        }
      }
      catch (Exception e) {
        LOG.warn("Unable to find " + "jediterm-" + shellName + ".in configuration file", e);
      }
    }
    return null;
  }

  @NotNull
  public static LocalTerminalDirectRunner createTerminalRunner(Project project) {
    return new LocalTerminalDirectRunner(project);
  }

  @Override
  protected PtyProcess createProcess(@Nullable String directory) throws ExecutionException {
    Map<String, String> envs = new HashMap<>(EnvironmentUtil.getEnvironmentMap());
    if (!SystemInfo.isWindows) {
      envs.put("TERM", "xterm-256color");
    }
    EncodingEnvironmentUtil.setLocaleEnvironmentIfMac(envs, myDefaultCharset);

    EncodingEnvironmentUtil.setLocaleEnvironmentIfMac(envs, myDefaultCharset);

    String[] command = getCommand(envs);

    for (LocalTerminalCustomizer customizer : LocalTerminalCustomizer.EP_NAME.getExtensions()) {
      try {
        command = customizer.customizeCommandAndEnvironment(myProject, command, envs);

        if (directory == null) {
          directory = customizer.getDefaultFolder(myProject);
        }
      }
      catch (Exception e) {
        LOG.error("Exception during customization of the terminal session", e);
      }
    }

    try {
      return PtyProcess.exec(command, envs, directory != null
                                            ? directory
                                            : TerminalProjectOptionsProvider.Companion.getInstance(myProject).getStartingDirectory());
    }
    catch (IOException e) {
      throw new ExecutionException(e);
    }
  }

  @Override
  protected ProcessHandler createProcessHandler(final PtyProcess process) {
    return new PtyProcessHandler(process, getShellPath());
  }

  @Override
  protected TtyConnector createTtyConnector(PtyProcess process) {
    return new PtyProcessTtyConnector(process, myDefaultCharset);
  }

  @Override
  public String runningTargetName() {
    return "Local Terminal";
  }

  @Override
  protected String getTerminalConnectionName(PtyProcess process) {
    return "Local Terminal";
  }


  public String[] getCommand(Map<String, String> envs) {

    String shellPath = getShellPath();

    return getCommand(shellPath, envs, TerminalOptionsProvider.Companion.getInstance().shellIntegration());
  }

  private static String getShellPath() {
    return TerminalOptionsProvider.Companion.getInstance().getShellPath();
  }

  @NotNull
  public static String[] getCommand(String shellPath, Map<String, String> envs, boolean shellIntegration) {
    if (SystemInfo.isUnix) {
      List<String> command = Lists.newArrayList(shellPath.split(" "));

      String shellCommand = command.get(0);
      String shellName = command.size() > 0 ? getShellName(shellCommand) : null;


      if (shellName != null) {
        command.remove(0);

        List<String> result = Lists.newArrayList(shellCommand);

        String rcFilePath = findRCFile(shellName);

        if (rcFilePath != null &&
            shellIntegration) {
          if (shellName.equals("bash") || (SystemInfo.isMac && shellName.equals("sh"))) {
            addRcFileArgument(envs, command, result, rcFilePath, "--rcfile");
          }
          else if (shellName.equals("zsh")) {
            String zdotdir = EnvironmentUtil.getEnvironmentMap().get(ZDOTDIR);
            if (StringUtil.isNotEmpty(zdotdir)) {
              envs.put("_OLD_ZDOTDIR", zdotdir);
              File zshRc = new File(FileUtil.expandUserHome(zdotdir), ".zshrc");
              if (zshRc.exists()) {
                envs.put(JEDITERM_USER_RCFILE, zshRc.getAbsolutePath());
              }
            }
            envs.put(ZDOTDIR, new File(rcFilePath).getParent());
          }
          else if (shellName.equals("fish")) {
            String xdgConfig = EnvironmentUtil.getEnvironmentMap().get(XDG_CONFIG_HOME);
            if (StringUtil.isNotEmpty(xdgConfig)) {
              File fishConfig = new File(new File(FileUtil.expandUserHome(xdgConfig), "fish"), "config.fish");
              if (fishConfig.exists()) {
                envs.put(JEDITERM_USER_RCFILE, fishConfig.getAbsolutePath());
              }
              envs.put("OLD_" + XDG_CONFIG_HOME, xdgConfig);
            }

            envs.put(XDG_CONFIG_HOME, new File(rcFilePath).getParentFile().getParent());
          }
        }

        if (!loginOrInteractive(command)) {
          if (hasLoginArgument(shellName) && SystemInfo.isMac) {
            result.add("--login");
          }
          result.add("-i");
        }

        if (isLogin(command)) {
          envs.put("LOGIN_SHELL", "1");
        }

        result.addAll(command);
        return ArrayUtil.toStringArray(result);
      }
      else {
        return ArrayUtil.toStringArray(command);
      }
    }
    else {
      return new String[]{shellPath};
    }
  }

  private static void addRcFileArgument(Map<String, String> envs,
                                        List<String> command,
                                        List<String> result,
                                        String rcFilePath, String rcfileOption) {
    result.add(rcfileOption);
    result.add(rcFilePath);
    int idx = command.indexOf(rcfileOption);
    if (idx >= 0) {
      command.remove(idx);
      if (idx < command.size()) {
        envs.put(JEDITERM_USER_RCFILE, FileUtil.expandUserHome(command.get(idx)));
        command.remove(idx);
      }
    }
  }

  private static boolean loginOrInteractive(List<String> command) {
    return command.contains("-i") || isLogin(command);
  }

  private static boolean isLogin(List<String> command) {
    return command.contains("--login") || command.contains("-l");
  }

  private static class PtyProcessHandler extends ProcessHandler implements TaskExecutor {

    private final PtyProcess myProcess;
    private final ProcessWaitFor myWaitFor;

    public PtyProcessHandler(PtyProcess process, @NotNull String presentableName) {
      myProcess = process;
      myWaitFor = new ProcessWaitFor(process, this, presentableName);
    }

    @Override
    public void startNotify() {
      addProcessListener(new ProcessAdapter() {
        @Override
        public void startNotified(ProcessEvent event) {
          try {
            myWaitFor.setTerminationCallback(integer -> notifyProcessTerminated(integer));
          }
          finally {
            removeProcessListener(this);
          }
        }
      });

      super.startNotify();
    }

    @Override
    protected void destroyProcessImpl() {
      myProcess.destroy();
    }

    @Override
    protected void detachProcessImpl() {
      destroyProcessImpl();
    }

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

    @Override
    public boolean isSilentlyDestroyOnClose() {
      return true;
    }

    @Nullable
    @Override
    public OutputStream getProcessInput() {
      return myProcess.getOutputStream();
    }

    @NotNull
    @Override
    public Future<?> executeTask(@NotNull Runnable task) {
      return AppExecutorUtil.getAppExecutorService().submit(task);
    }
  }
}
