/**
 * A JavaScript wrapper for {@linkplain Path}
 *
 * @author B. Piwowarski <*****@*****.**>
 */
@Exposed
public class ScriptingList extends WrapperObject<List> {
  static final Logger LOGGER = Logger.getLogger();

  public ScriptingList(List object) {
    super(object);
  }

  @Expose(mode = ExposeMode.PROPERTY, value = "length")
  int length() {
    return object.size();
  }

  @Expose(mode = ExposeMode.FIELDS)
  public Object get(int index) {
    return object.get(index);
  }

  @Expose(context = true)
  public void sort(JavaScriptContext jcx, NativeFunction f) {
    LOGGER.debug("Sorting list");
    Collections.sort(
        object,
        (a, b) -> {
          final double result =
              (Double) f.call(jcx.context(), jcx.scope(), null, new Object[] {a, b});
          if (result > 0) return 1;
          if (result < 0) return -1;
          return 0;
        });
  }
}
  /**
   * Delete everything recursively
   *
   * @param path
   */
  public static void recursiveDelete(Path path) throws IOException {
    logger.debug("Deleting %s", path);
    final Iterator<Path> it = Files.list(path).iterator();
    while (it.hasNext()) {
      final Path entry = it.next();

      logger.debug("Considering " + entry);

      if (Files.isDirectory(entry)) {
        recursiveDelete(entry);
      } else {
        Files.delete(entry);
      }
    }

    // Deleting self
    Files.delete(path);
  }
 @Expose(context = true)
 public void sort(JavaScriptContext jcx, NativeFunction f) {
   LOGGER.debug("Sorting list");
   Collections.sort(
       object,
       (a, b) -> {
         final double result =
             (Double) f.call(jcx.context(), jcx.scope(), null, new Object[] {a, b});
         if (result > 0) return 1;
         if (result < 0) return -1;
         return 0;
       });
 }
/**
 * Static methods for files
 *
 * @author B. Piwowarski
 * @date 17/11/2006
 */
public class FileSystem {
  /** A filter for directories */
  public static final FileFilter DIRECTORY_FILTER = pathname -> pathname.isDirectory();
  /** A filter for files */
  public static final FileFilter FILE_FILTER = pathname -> pathname.isFile();

  private static final Logger logger = Logger.getLogger();

  /**
   * Get a file filter
   *
   * @param extFilter
   * @param skipRegExp
   * @return
   */
  public static FileFilter newRegexpFileFilter(final String extFilter, final String skipRegExp) {
    return new FileFilter() {
      final Pattern pattern = skipRegExp != null ? Pattern.compile(skipRegExp) : null;

      public boolean accept(File file) {
        return (file.getName().endsWith(extFilter)
            && (pattern == null || !pattern.matcher(file.getName()).find()));
      }
    };
  }

  /**
   * Create a new file object from a list of names
   *
   * @param names A list of strings
   */
  public static File createFileFromPath(String... names) {
    return createFileFromPath(null, names);
  }

  /**
   * Creates a file from a list of strings and a base directory
   *
   * @param baseDirectory
   * @param names
   * @return
   */
  public static File createFileFromPath(File baseDirectory, String... names) {
    for (String name : names)
      if (baseDirectory == null) baseDirectory = new File(name);
      else baseDirectory = new File(baseDirectory, name);
    return baseDirectory;
  }

  /**
   * Delete everything recursively
   *
   * @param directory The directory to delete
   */
  public static void recursiveDelete(File directory) throws IOException {
    recursiveDelete(directory.toPath());
  }

  /**
   * Delete everything recursively
   *
   * @param path
   */
  public static void recursiveDelete(Path path) throws IOException {
    logger.debug("Deleting %s", path);
    final Iterator<Path> it = Files.list(path).iterator();
    while (it.hasNext()) {
      final Path entry = it.next();

      logger.debug("Considering " + entry);

      if (Files.isDirectory(entry)) {
        recursiveDelete(entry);
      } else {
        Files.delete(entry);
      }
    }

    // Deleting self
    Files.delete(path);
  }
}
/** Global context when executing a javascript */
public class PythonRunner implements AutoCloseable {

  private static final Logger LOGGER = Logger.getLogger();

  private static Map<String, PythonType> TYPES;

  private static Map<Class, Constructor> WRAPPERS;

  private final Map<String, String> environment;

  private final StaticContext staticContext;

  private final PythonInterpreter interpreter;

  private final ScriptContext scriptContext;

  public PythonRunner(
      Map<String, String> environment,
      Repositories repositories,
      Scheduler scheduler,
      Hierarchy loggerRepository,
      String pythonPath,
      BufferedWriter out,
      BufferedWriter err)
      throws Exception {

    init();

    this.staticContext = new StaticContext(scheduler, loggerRepository).repository(repositories);
    this.environment = environment;

    final PySystemState systemState = new PySystemState();
    interpreter = new PythonInterpreter(null, systemState);
    for (String path : pythonPath.split(":")) {
      systemState.path.add(new PyString(path));
    }
    interpreter.setOut(out);
    interpreter.setErr(err);
    scriptContext = staticContext.scriptContext();

    // XPM module
    final PyModule xpmModule = new PyModule("xpm", (PyObject) null);
    systemState.modules.__setitem__("xpm", xpmModule);

    // Add classes
    for (PyType type : TYPES.values()) {
      xpmModule.__setattr__(type.getName(), type);
    }

    // Add constants
    Scripting.forEachConstant(
        (name, value) -> {
          xpmModule.__setattr__(name, wrap(value));
        });

    // Add functions
    Scripting.forEachFunction(
        m -> {
          xpmModule.__setattr__(m.getKey(), new PythonMethod(null, m));
        });

    // Add Python specific functions
    for (MethodFunction m : Scripting.getMethodFunctions(PythonFunctions.class)) {
      xpmModule.__setattr__(m.getKey(), new PythonMethod(null, m));
    }

    // XPM object: wrap properties
    final XPM xpm = new XPM();
    ClassDescription xpmDescription = ClassDescription.analyzeClass(XPM.class);
    for (Map.Entry<Object, MethodFunction> x : xpmDescription.getMethods().entrySet()) {
      final Object key = x.getKey();
      if (key instanceof String) {
        xpmModule.__setattr__((String) key, new PythonMethod(xpm, x.getValue()));
      } else {
        throw new XPMRuntimeException("Could not handle key ", key);
      }
    }

    // Add properties
    xpmModule.__setattr__("tasks", wrap(new Tasks()));
    xpmModule.__setattr__("logger", wrap(new ScriptingLogger("xpm")));
    xpmModule.__setattr__("env", wrap(environment));
  }

  /** Gather types, etc. */
  public static void init() {
    if (TYPES == null) {
      TYPES = new HashMap<>();
      WRAPPERS = new HashMap<>();
      Scripting.forEachType(
          Functional.propagate(
              aClass -> {
                final PythonType type = new PythonType(aClass);
                TYPES.put(type.getName(), type);
                if (WrapperObject.class.isAssignableFrom(aClass)) {
                  final Class<?> wrappedClass =
                      (Class<?>)
                          ((ParameterizedType)
                                  TypeToken.of(aClass).getSupertype(WrapperObject.class).getType())
                              .getActualTypeArguments()[0];
                  final Constructor constructor = aClass.getConstructor(wrappedClass);
                  WRAPPERS.put(wrappedClass, constructor);
                }
              }));
    }
  }

  public static PyObject wrap(Object object) {
    // Simple case
    if (object == null) return Py.None;

    if (object instanceof PyObject) return (PyObject) object;

    final Class<?> objectClass = object.getClass();

    if (object.getClass().isArray()) {
      final PyList pyList = new PyList();
      for (int i = Array.getLength(object); --i >= 0; ) {
        pyList.add(wrap(Array.get(object, i)));
      }
      return pyList;
    }

    // Wrapper case
    // Wrapper case -- go up in the hierarchy
    for (Map.Entry<Class, Constructor> entry : WRAPPERS.entrySet()) {
      if (entry.getKey().isAssignableFrom(objectClass)) {
        try {
          return new PythonObject(entry.getValue().newInstance(object));
        } catch (Exception e) {
          throw new UnsupportedOperationException(
              "Could not wrap object of class " + objectClass, e);
        }
      }
    }

    // Simple types
    if (object instanceof String) {
      return new PyString((String) object);
    }

    if (object instanceof Boolean) {
      return new PyBoolean(((Boolean) object).booleanValue());
    }

    if (object instanceof Long) {
      return new PyLong((long) object);
    }

    if (object instanceof Number) {
      return new PyFloat(((Number) object).doubleValue());
    }

    // Exposed objects
    final Exposed exposed = objectClass.getAnnotation(Exposed.class);
    if (exposed != null) {
      return new PythonObject(object);
    }

    // Map entry as tuple
    if (object instanceof Map.Entry) {
      Map.Entry entry = (Map.Entry) object;
      return new PyTuple(wrap(entry.getKey()), wrap(entry.getValue()));
    }

    // Entry set
    if (object instanceof Set) {
      Set set = (Set) object;

      final AbstractSet wrappedSet =
          new AbstractSet() {
            @Override
            public Iterator iterator() {
              return new Iterator() {
                Iterator iterator = set.iterator();

                @Override
                public boolean hasNext() {
                  return iterator.hasNext();
                }

                @Override
                public Object next() {
                  return wrap(iterator.next());
                }
              };
            }

            @Override
            public int size() {
              return 0;
            }
          };
      return new PySet(wrappedSet, null);
    }

    if (object instanceof Iterator) {
      return PythonUtils.wrapIterator((Iterator) object);
    }

    throw new IllegalArgumentException(
        format("Cannot wrap class %s into python object", objectClass));
  }

  public static Object unwrap(PyObject arg) {
    return arg.__tojava__(Object.class);
  }

  public static Object[] unwrap(PyObject[] args) {
    Object[] unwrapped = new Object[args.length];
    for (int i = 0; i < args.length; ++i) {
      unwrapped[i] = unwrap(args[i]);
    }
    return unwrapped;
  }

  @Override
  public void close() throws Exception {
    interpreter.close();
    scriptContext.close();
    staticContext.close();
  }

  public Object evaluateReader(
      LocalhostConnector connector,
      Path locator,
      FileReader reader,
      String filename,
      int lineno,
      Object security)
      throws Exception {
    ScriptContext.get().setCurrentScriptPath(Paths.get(filename));
    final PyCode code = interpreter.compile(reader, filename);
    return interpreter.eval(code);
  }

  public Object evaluateString(
      LocalhostConnector connector,
      Path locator,
      String content,
      String name,
      int lineno,
      Object security)
      throws Exception {
    final PyCode code = interpreter.compile(content, name);
    return interpreter.eval(code);
  }
}