/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.jooby.run;

import java.io.File;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

import org.gradle.api.Project;
import org.gradle.api.internal.ConventionTask;
import org.gradle.api.tasks.InputFiles;
import org.gradle.api.tasks.TaskAction;
import org.gradle.tooling.GradleConnector;
import org.gradle.tooling.ProjectConnection;

public class JoobyTask extends ConventionTask {

  private static final Object LOCK = new Object();

  private static final List<String> XML_PROPS = Arrays.asList(
      "javax.xml.parsers.DocumentBuilderFactory",
      "javax.xml.parsers.SAXParserFactory",
      "javax.xml.stream.XMLInputFactory",
      "javax.xml.stream.XMLEventFactory",
      "javax.xml.transform.TransformerFactory",
      "javax.xml.stream.XMLOutputFactory",
      "javax.xml.datatype.DatatypeFactory",
      "org.xml.sax.driver");

  private List<String> includes;

  private List<String> excludes;

  private String logLevel;

  private boolean block;

  private Set<File> classpath;

  private Set<File> src;

  private List<String> watchDirs;

  private String mainClassName;

  private String compiler;

  @TaskAction
  public void run() throws Exception {
    System.setProperty("logLevel", getLogLevel());

    Project project = getProject();

    String mId = project.getName();

    List<File> cp = new ArrayList<>();

    // conf & public
    getClasspath().forEach(cp::add);

    Main app = new Main(mId, getMainClassName(), toFiles(watchDirs),
        cp.toArray(new File[cp.size()]));
    if (includes != null) {
      app.includes(includes.stream().collect(Collectors.joining(File.pathSeparator)));
    }

    if (excludes != null) {
      app.excludes(excludes.stream().collect(Collectors.joining(File.pathSeparator)));
    }

    String compiler = getCompiler();
    getLogger().info("compiler is {}", compiler);
    if ("on".equalsIgnoreCase(compiler)) {
      Path[] watchDirs = getSrc().stream()
          .map(File::toPath)
          .collect(Collectors.toList())
          .toArray(new Path[0]);
      // don't start watcher if continuous is ON
      new Watcher((k, path) -> {
        if (path.toString().endsWith(".java")) {
          runTask(project, path, "classes");
        } else if (path.toString().endsWith(".conf")
            || path.toString().endsWith(".properties")) {
          runTask(project, path, "classes");
        }
      }, watchDirs).start();
    }

    String[] args = project.getGradle().getStartParameter().getProjectProperties()
        .entrySet().stream().map(e -> e.toString()).collect(Collectors.toList())
        .toArray(new String[0]);
    app.run(isBlock(), args);
  }

  private List<File> toFiles(final List<String> watchDirs) {
    List<File> files = new ArrayList<>();
    if (watchDirs != null) {
      watchDirs.forEach(f -> files.add(new File(f)));
    }
    return files;
  }

  private void runTask(final Project project, final Path path, final String task) {
    synchronized (LOCK) {
      ProjectConnection connection = null;
      Map<String, String> xml = new HashMap<>();
      try {
        // clean jaxp
        XML_PROPS.forEach(p -> xml.put(p, (String) System.getProperties().remove(p)));

        connection = GradleConnector.newConnector()
            .useInstallation(project.getGradle().getGradleHomeDir())
            .forProjectDirectory(project.getRootDir())
            .connect();

        try {
          connection.newBuild()
              .setStandardError(System.err)
              .setStandardOutput(System.out)
              .forTasks(task)
              .run();
        } catch (Exception ex) {
          getLogger().debug("Execution of " + task + " resulted in exception", ex);
        }

      } finally {
        // restore jaxp
        xml.forEach((k, v) -> {
          if (v != null) {
            System.setProperty(k, v);
          }
        });
        if (connection != null) {
          connection.close();
        }
      }
    }
  }

  public void setIncludes(final List<String> includes) {
    this.includes = includes;
  }

  public List<String> getIncludes() {
    return includes;
  }

  public void setExcludes(final List<String> excludes) {
    this.excludes = excludes;
  }

  public List<String> getExcludes() {
    return excludes;
  }

  public void setLogLevel(final String logLevel) {
    this.logLevel = logLevel;
  }

  public String getLogLevel() {
    return logLevel;
  }

  @InputFiles
  public Set<File> getClasspath() {
    return classpath;
  }

  public void setClasspath(final Set<File> classpath) {
    this.classpath = classpath;
  }

  public void setBlock(final boolean block) {
    this.block = block;
  }

  public boolean isBlock() {
    return block;
  }

  public String getMainClassName() {
    return mainClassName;
  }

  public void setMainClassName(final String mainClassName) {
    this.mainClassName = mainClassName;
  }

  public Set<File> getSrc() {
    return src;
  }

  public void setSrc(final Set<File> watchDirs) {
    this.src = watchDirs;
  }

  public List<String> getWatchDirs() {
    return watchDirs;
  }

  public void setWatchDirs(final List<String> watchDirs) {
    this.watchDirs = watchDirs;
  }

  public String getCompiler() {
    return compiler;
  }

  public void setCompiler(final String compiler) {
    this.compiler = compiler;
  }
}
