/*
 * Copyright 2014 Google Inc.
 *
 * 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 com.google.gwt.dev.shell;

import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.core.ext.TreeLogger.Type;
import com.google.gwt.core.ext.UnableToCompleteException;
import com.google.gwt.core.ext.linker.ArtifactSet;
import com.google.gwt.core.ext.linker.impl.StandardLinkerContext;
import com.google.gwt.dev.DevMode.HostedModeOptions;
import com.google.gwt.dev.cfg.ModuleDef;
import com.google.gwt.dev.util.arg.OptionMethodNameDisplayMode;
import com.google.gwt.thirdparty.guava.common.annotations.VisibleForTesting;
import com.google.gwt.thirdparty.guava.common.base.Stopwatch;
import com.google.gwt.thirdparty.guava.common.collect.ListMultimap;
import com.google.gwt.thirdparty.guava.common.collect.Lists;

import java.io.File;
import java.io.IOException;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.ServerSocket;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;

/**
 * Starts a superdev-mode codeserver.
 */
public class SuperDevListener implements CodeServerListener {

  private final TreeLogger logger;
  private final int codeServerPort;
  private List<String> codeServerArgs;

  /**
   * Listens for new connections from browsers.
   */
  public SuperDevListener(TreeLogger treeLogger, HostedModeOptions options) {
    this.logger = treeLogger;
    this.codeServerPort = chooseCodeServerPort(treeLogger, options);

    // This directory must exist when the Code Server starts.
    ensureModuleBaseDir(options);

    codeServerArgs = makeCodeServerArgs(options, codeServerPort);
  }

  @Override
  public int getSocketPort() {
    return codeServerPort;
  }

  @VisibleForTesting
  List<String> getCodeServerArgs() {
    return codeServerArgs;
  }

  @Override
  public URL makeStartupUrl(String url) throws UnableToCompleteException {
    try {
      return new URL(url);
    } catch (MalformedURLException e) {
      logger.log(TreeLogger.ERROR, "Invalid URL " + url, e);
      throw new UnableToCompleteException();
    }
  }

  @Override
  public void writeCompilerOutput(StandardLinkerContext linkerStack, ArtifactSet artifacts,
      ModuleDef module, boolean isRelink) throws UnableToCompleteException {
    // The code server will do this.
  }

  @Override
  public void setIgnoreRemoteDeath(boolean b) {
  }

  @Override
  public void start() {
    try {
      Stopwatch watch = Stopwatch.createStarted();
      logger.log(Type.INFO, "Running CodeServer with parameters: " + codeServerArgs);
      runCodeServer(codeServerArgs.toArray(new String[0]));
      logger.log(Type.INFO, "Code server started in " + watch + " ms");
    } catch (Exception e) {
      logger.log(Type.INFO, "Unable to start Code server");
      throw new RuntimeException(e);
    }
  }

  private void runCodeServer(String[] mainArgs) throws Exception {
    // Using reflection so as we don't create a circular dependency between
    // dev.jar && codeserver.jar
    Method mainMethod;
    try {
      Class<?> clazz = Class.forName("com.google.gwt.dev.codeserver.CodeServer");
      mainMethod = clazz.getMethod("main", String[].class);
    } catch (ClassNotFoundException e) {
      logger.log(TreeLogger.ERROR, "Unable to find main() method for Super Dev Mode "
          + "code server. Hint: verify that gwt-codeserver.jar is in your classpath.");
      throw e;
    }

    mainMethod.invoke(null, new Object[] {mainArgs});
  }

  private static int chooseCodeServerPort(TreeLogger logger, HostedModeOptions options) {
    int port = options.getCodeServerPort();
    if (port == 0) {
      // Automatically choose an unused port.
      try {
        ServerSocket serverSocket = new ServerSocket(0);
        port = serverSocket.getLocalPort();
        serverSocket.close();
        return port;
      } catch (IOException e) {
        logger.log(TreeLogger.ERROR, "Unable to get an unnused port.");
        throw new RuntimeException(e);
      }
    } else if (port < 0 || port == 9997) {
      // 9997 is the default non-SuperDevMode port from DevModeBase. TODO: use constant.
      return 9876; // Default Super Dev Mode port
    } else {
      return port; // User-specified port
    }
  }

  private static void ensureModuleBaseDir(HostedModeOptions options) {
    File dir = options.getModuleBaseDir();
    if (!dir.isDirectory()) {
      dir.mkdirs();
      if (!dir.isDirectory()) {
        throw new RuntimeException("unable to create module base directory: " +
            dir.getAbsolutePath());
      }
    }
  }

  private static List<String> makeCodeServerArgs(HostedModeOptions options, int port) {
    List<String> args = new ArrayList<String>();
    args.add("-noprecompile");
    args.add("-port");
    args.add(String.valueOf(port));
    args.add("-sourceLevel");
    args.add(String.valueOf(options.getSourceLevel()));
    if (options.getBindAddress() != null) {
      args.add("-bindAddress");
      args.add(options.getBindAddress());
    }
    if (options.getWorkDir() != null) {
      args.add("-workDir");
      args.add(String.valueOf(options.getWorkDir()));
    }
    args.add("-launcherDir");
    args.add(options.getModuleBaseDir().getAbsolutePath());
    if (options.getLogLevel() != null) {
      args.add("-logLevel");
      args.add(String.valueOf(options.getLogLevel()));
    }
    if (options.shouldGenerateJsInteropExports()) {
      args.add("-generateJsInteropExports");
    }
    for (String regex : options.getJsInteropExportFilter().getValues()) {
      if (regex.startsWith("-")) {
        args.add("-excludeJsInteropExports");
        args.add(regex.substring(1));
      } else {
        args.add("-includeJsInteropExports");
        args.add(regex.startsWith("+") ? regex.substring(1) : regex);
      }
    }
    if (!options.isIncrementalCompileEnabled()) {
      args.add("-noincremental");
    }
    if (options.getMethodNameDisplayMode() != OptionMethodNameDisplayMode.Mode.NONE) {
      args.add("-XmethodNameDisplayMode");
      args.add(options.getMethodNameDisplayMode().name());
    }

    args.add("-style");
    args.add(options.getOutput().name());

    if (options.isStrict()) {
      args.add("-strict");
    }

    if (options.getProperties().size() > 0) {
      args.addAll(makeSetPropertyArgs(options.getProperties()));
    }
    for (String mod : options.getModuleNames()) {
      args.add(mod);
    }
    return args;
  }

  private static List<String> makeSetPropertyArgs(
      ListMultimap<String, String> properties) {
    List<String> propertyArgs = Lists.newArrayList();
    for (String propertyName : properties.keySet()) {
      propertyArgs.add("-setProperty");
      StringBuilder nameValues = new StringBuilder(propertyName + "=");
      for (String propertyValue : properties.get(propertyName)) {
        nameValues.append(propertyValue + ",");
      }
      propertyArgs.add(nameValues.substring(0, nameValues.length() - 1));
    }
    return propertyArgs;
  }
}
