// Some of the functions are dependent on the concepts field,
// others are global.

!include once #1035461 // Gazelle 22 Class synonyms Include

sclass G22Utils_Base {
  // event that is triggered every time a file or directory inside the
  // project changes. Starts the fileWatcher automatically when the
  // first listener is added.
  event projectFileChanged(File file);
}

sclass G22Utils > G22Utils_Base is AutoCloseable, TransientObject {
  settable IBackgroundProcesses backgroundProcessesUI;
  settable Enterable module;
  settable G22MasterStuff masterStuff;
  settable Concepts concepts;
  settable G22ProjectActions projectActions;
  gettable G22AutoStarter autoStarter = new(this);
  settable new FunctionTimings<S> functionTimings;
  
  // TODO: set this to a few seconds if your variables are expensive to render
  // settable Seconds variableUpdateIntervalInUI;
  
  FileWatchService fileWatcher; // general file watch service
  bool projectFileListenerInitiated;

  gettable CombinedStringifier stringifier = new(
    o -> o cast BufferedImage ? "Image (" + o.getWidth() + "*" + o.getHeight() + " px)" : null
  );
  
  ILASClassLoader lasClassLoader() { ret masterStuff?.lasClassLoader(); }
  
  ImageSurface stdImageSurface_noToolTip(ImageSurface is default imageSurface()) {
    imageSurface_pixelated(is);
    is.setAutoZoomToDisplay(true).repaintInThread(false);
    is.defaultImageDir = -> dbDir();
    is.specialPurposed = true;
    ret is;
  }
    
  ImageSurface stdImageSurface(ImageSurface is default imageSurface()) {
    stdImageSurface_noToolTip(is);
    new ImageSurface_PositionToolTip(is);
    ret is;
  }
  
  L<ImageSurface> stdImageSurfaces(Cl images) {
    ret map stdImageSurface(toBufferedImages(images));
  }

  ImageSurface stdImageSurface(BufferedImage etc img) {
    var is = stdImageSurface();
    is.setImage(img);
    ret is;
  }
  
  ImageSurface stdImageSurface(File file) {
    var is = stdImageSurface();
    is.setImage(file);
    ret is;
  }
  
  ImageSurface stdImageSurface(G22GalleryImage img) {
    ret stdImageSurface(img?.path);
  }
  
  ImageSurface stdImageSurfaceWithSelection(BufferedImage etc img, Rect selection) {
    var is = stdImageSurface(img);
    is.setSelection(selection);
    ret is;
  }
  
  S stringify(O o) { ret stringifier.toString(o); }
  
  event settingUpParser(GazelleV_LeftArrowScriptParser parser);
  event settingUpScriptIDE(JLeftArrowScriptIDE ide);

  GazelleV_LeftArrowScriptParser leftArrowParser() {
    new GazelleV_LeftArrowScriptParser parser;
    parser.classNameResolver(classNameResolver());
    parser.lasClassLoader(lasClassLoader());
    settingUpParser(parser);
    parser.addClassAlias("Freq", "Frequency");
    parser.addClassAlias("MRUAndAllTimeTop", "MRUAndAllTimeTop_optimized");
    ret parser;
  }
  
  O leftArrow(S script) {
    ret leftArrowParse(script)!;
  }
  
  GazelleV_LeftArrowScript.Script leftArrowParse(S script) {
    ret leftArrowParser().parse(script);
  }
  
  O leftArrowWithVars(S script, O... vars) {
    var parser = leftArrowParser();
    for (int i = 0; i < l(vars); i += 2)
      parser.addVar((S) vars[i], or(_getClass(vars[i+1]), O), true);
      
    var parsed = parser.parse(script);
    new FlexibleVarContext varContext;
    for (int i = 0; i < l(vars); i += 2)
      varContext.set((S) vars[i], vars[i+1]);
    ret parsed.get(varContext);
  }
  
  void basicParserTest() {
    var parser = leftArrowParser();
    print(classContainerPrefixes := parser.classContainerPrefixes());
    assertEquals(pair(1, 2), parser.parse("new Pair 1 2")!);
  }

  ifclass JLeftArrowScriptIDE
  JLeftArrowScriptIDE leftArrowIDE() {
    new JLeftArrowScriptIDE ide;
    ide.g22utils(this);
    ide.scriptTimeout(projectWideScriptTimeout());
    settingUpScriptIDE(ide);
    ret ide;
  }
  endif
  
  File byteCodePath() {
    ret assertNotNull(getBytecodePathForClass(this));
  }

  simplyCached ClassNameResolver classNameResolver() {
    ret new ClassNameResolver().byteCodePath(byteCodePath()).init();
  }
  
  File databasesMotherDir() {
    ret masterStuff.databasesMotherDir();
  }
  
  File dirOfProjectNamed(S name) {
    assertNempty(name);
    ret newFile(databasesMotherDir(), name);
  }
  
  AutoCloseable enter() { ret module?.enter(); }
  
  S defaultDBName() { ret "Default"; }
  
  File lastOpenedDBsFile() {
    ret newFile(databasesMotherDir(), "Last Opened");
  }
  
  File recentlyOpenedDBsFile() {
    ret newFile(databasesMotherDir(), "Recently Opened");
  }
  
  File autoUpdateFile() {
    ret newFile(databasesMotherDir(), "Auto-Update");
  }
  
  bool autoUpdateEnabled() {
    ret fileExists(autoUpdateFile());
  }
  
  void setAutoUpdate(bool b) {
    createOrRemoveFile(autoUpdateFile(), b);
  }
  
  LS dbsToOpen() {
    ret loadRecentProjectsFile(lastOpenedDBsFile());
  }
  
  LS dbNamesRecentlyOpened() {
    ret loadRecentProjectsFile(recentlyOpenedDBsFile());
  }
  
  void addToRecentDBNames(S dbName, bool hidden) {
    LS list = dbNamesRecentlyOpened();
    LS list2 = cloneList(list);
    removeAll(list2, dbName, "*" + dbName);
    list2.add(0, hidden ? "*" + dbName : dbName);
    truncateList(list2, 100);
    if (!eq(list, list2))
      saveTextFile(recentlyOpenedDBsFile(), lines(list2));
  }
  
  // returns project names with optional "*" prefix for hidden projects
  LS loadRecentProjectsFile(File file) {
    new LS dbNames;
    for (S name : tlft(loadTextFile(file))) {
      S name2 = dropPrefix("*", name);
      if (fileExists(newFile(databasesMotherDir(), name2)))
        dbNames.add(name);
    }
        
    if (empty(dbNames)) dbNames.add(defaultDBName());
    ret dbNames;
  }
  
  void setOpenDBs(Cl<? extends IG22LoadedDB> dbs) {
    new LS dbNames;
    for (db : dbs) {
      var dbDir = conceptsDir(db.concepts());
      if (sameFile(databasesMotherDir(), dirOfFile(dbDir)))
        dbNames.add((db.hidden() ? "*" : "") + fileName(dbDir));
    }
      
    saveTextFile(lastOpenedDBsFile(), lines(dbNames));
  }
  
  ifclass SimpleCRUD_v2
  <A extends G22LeftArrowScript> void setupScriptCRUD(SimpleCRUD_v2<A> crud, bool allowRunOnProjectOpen default false) {
    crud.useNewChangeHandler(true);
    crud.editableFieldsForItem = x -> llNonNulls("description",
      allowRunOnProjectOpen ? "runOnProjectOpen" : null,
      allowRunOnProjectOpen ? "runOrder" : null);
    //G22LeftArrowScript.f_description().getName());
    crud.multiLineField("text");
    crud.multiLineField("editingText");
    crud.makeTextArea = text -> jMinHeight(200, crud.makeTextArea_base(text));
    crud.humanizeFieldNames = false;
    crud.iconButtons(true);
    crud.itemToMap_inner2 = c -> scriptToMap(c, allowRunOnProjectOpen);
    crud.dontDuplicateFields = litset("runOnProjectOpen", "runOrder", "runCount", "lastResultByMode");

  }
  endif

  MapSO scriptToMap(G22LeftArrowScript c, bool allowRunOnProjectOpen default false) {  
    ret litorderedmap(
      "Description" := str(c),
      "Status" := renderScriptStatus(c),
      "LoC" := renderScriptLoC(c),
      "Import note" := c.importNote,
      "Run on project open" := c.renderRunOnProjectOpenStatus());
  }
  
  S renderScriptStatus(G22LeftArrowScript c) {
    ret or2_rev("Empty", joinNemptiesWithSpacedPlus(
      c.isClearForAutoRun() ? "Clear for auto-run" : null,
      c.isSavedDistinctFromAutoRunVersion() ? "Saved (not cleared)" : null,
      c.isEditing() ? "Editing" : null
    ));
  }
  
  S renderScriptLoC(G22LeftArrowScript c) {
    ret n2(intMax(mapLL linesOfCode_javaTok(
      c.editingText,
      c.text,
      c.codeForAutoRun())));
  }
  
  // e.g. for an image file
  L<G22Label> labelsForFile(File file) {
    if (file == null) null;
    File labelsFile = appendToFileName(file, ".labels");
    LS labels = tlft(loadTextFile(labelsFile));
    ret map getLabel(labels);
  }

  File labelsFile(File file) {  
    if (file == null) null;
    ret appendToFileName(file, ".labels");
  }
  
  void setLabelsForFile(File file, L<G22Label> labels) {
    LS list = map(labels, label -> label.name);
    File f = labelsFile(file);
    saveTextFile(f, lines(list));
    print("Saved " + nLabels(list) + " (" + joinWithComma(list) + ") to " + f);
  }
  
  G22Label getLabel(S name) {
    if (empty(name)) null;
    if (containsNewLine(name)) fail("No newlines in label names allowed: " + name);
    ret uniqCI(concepts, G22Label, +name);
  }
  
  File dbDir aka projectDir() { ret conceptsDir(concepts); }
  
  File fileInDbDir aka projectFile(S name) { ret newFile(dbDir(), name); }
  
  record GazelleDB(S name, File dir) {
    S name() { ret name; }
    File dir() { ret dir; }
    
    bool loaded() {
      ret loadedDB() != null;
    }
    
    simplyCached IG22LoadedDB loadedDB() {
      ret masterStuff.getLoadedDBForConceptDir(dir);
    }
    
    File conceptsFile() { ret conceptsFileIn(dir); }
  }
  
  L<GazelleDB> gazelleDBs() {
    new L<GazelleDB> dbs;
    for (File dir : listDirsContainingFileNamed(databasesMotherDir(),
      "concepts.structure.gz"))
      dbs.add(new GazelleDB(fileName(dir), dir));
    ret dbs;
  }
  
  ItIt<Concepts> peekAllProjectsConcepts() {
    var classFinder = masterStuff.makeClassFinder();
    ret mapI_pcall(gazelleDBs(), db -> {
      var cc = newConceptsWithClassFinder(db.conceptsFile(), classFinder);
      cc.loadFromDisk();
      ret cc;
    });
  }
  
  ifclass RSyntaxTextAreaWithSearch
    RSyntaxTextAreaWithSearch newSyntaxTextArea(IF1<JComponent> wrapStatusLabel default (IF1) null) {
      RSyntaxTextAreaWithSearch ta = new(wrapStatusLabel);
      ta.textArea().setHighlightCurrentLine(false);
      ta.menuLessOperation();
      ret ta;
    }
    
    RSyntaxTextAreaWithSearch newSyntaxTextArea(S text) {
      var ta = newSyntaxTextArea();
      ta.setText(text);
      ret ta;
    }
  endif
  
  File projectStoryTextFile() { ret newFile(dbDir(), "story.txt"); }
  
  S projectName() { ret fileName(dbDir()); }
  
  close {
    autoStarter.close();
    dispose fileWatcher;
  }
  
  // project vars
  
  G22Variable findProjectVar(S name) {
    ret conceptWhereCI(concepts, G22Variable, +name);
  }
  
  Cl<S> projectVarNames() {
    ret collect name(list(concepts, G22Variable));
  }
  
  O getProjectVar(S name) {
    G22Variable var = findProjectVar(name);
    ret var?.value();
  }
  
  O getProjectVarOrCallScript(S name, long scriptID) {
    try object getProjectVar(name);
    ret callScript(scriptID);
  }
  
  O getProjectVarSetByScript(S name, long scriptID) {
    try object getProjectVar(name);
    callScript(scriptID);
    ret getProjectVar(name);
  }
  
  // gets first non-null value of project variable named "name"
  // in any open project (order not defined)
  O getProjectVarFromAnyProject(S name) {
    for (project : masterStuff().openProjects())
      try object project.g22utils().getProjectVar(name);
    null;
  }
  
  // gets all non-null values from project variable named "name"
  // in any open project
  L getProjectVarFromAllProjects aka getVarFromAnyProject(S name) {
    ret nonNulls(masterStuff().openProjects(),
      project -> project.g22utils().getProjectVar(name));
  }
  
  // Creates the variable if it's not there, otherwise
  // returns existing variable.
  G22Variable projectVarConcept(S name) {
    ret optimizedUniqCI(concepts, G22Variable, +name);
  }
  
  O waitForProjectVar(double timeoutSeconds default infinity(), S name) {
    G22Variable var = projectVarConcept(name);
    ret waitForCalculatedValueUsingChangeListener(timeoutSeconds, -> var.value(), var);
  }
  
  void setBigProjectVar(bool persistent, S name, O value) {
    if (persistent)
      setBigProjectVar(name, value);
    else
      setTransientProjectVar(name, value);
  }
  
  G22Variable setProjectVar(bool persistent, S name, O value) {
    G22Variable var = projectVarConcept(name);
    
    // defensive order
    if (!persistent) var.persistent(persistent);
    var.value(value);
    if (persistent) var.persistent(persistent);
    
    ret var;
  }
  
  // uncache a big project variable. Does not change the value
  void unloadProjectVar(S name) {
    var var = findProjectVar(name);
    var?.unload();
  }

  <A> A setBigProjectVar(S name, A value) {
    setPersistentProjectVar(name, value);
    G22Variable var = projectVarConcept(name);
    var.makeBig();
    ret value;
  }
  
  O getCachedProjectVar(S name) {
    var var = findProjectVar(name);
    ret var?.cachedValue();
  }
  
  bool isBigProjectVar(S name) {
    var v = findProjectVar(name);
    ret v != null && v.big();
  }
  
  <A> A setPersistentProjectVar(S name, A value) {
    setProjectVar(true, name, value);
    ret value;
  }
  
  G22Variable setTransientProjectVar(S name, O value) {
    ret setProjectVar(false, name, value);
  }
  
  void setAutoClosingProjectVar(S name, O value) {
    setTransientProjectVar(name, value).autoClose(true);
  }
  
  void deleteProjectVar(S name) {
    deleteConcept(findProjectVar(name));
  }
  
  // Note: unsynced. Wild west baby!
  O getOrCreateProjectVar(S name, IF0 maker) {
    ret getOrCreateProjectVar(projectVarConcept(name), maker);
  }
  
  O getOrCreatePersistentProjectVar(S name, IF0 maker) {
    var var = projectVarConcept(name);
    O value = getOrCreateProjectVar(var, maker);
    var.persistent(true);
    ret value;
  }
  
  O getOrCreateProjectVar(G22Variable var, IF0 maker) {
    if (!var.has()) {
      O value = maker!;
      if (value != null)
        var.setValueIfNull(value);
    }
    ret var!;
  }
  
  // variable is made persistent
  IVarWithNotify liveProjectVar(S name, O defaultValue default null) {
    G22Variable var = projectVarConcept(name);
    var.persistent(true);
    var.setValueIfNull(defaultValue);
    ret var.varValue();
  }
  
  IVarWithNotify liveTransientProjectVar(S name, O defaultValue default null) {
    G22Variable var = projectVarConcept(name);
    var.persistent(false);
    var.setValueIfNull(defaultValue);
    ret var.varValue();
  }
  
  void replaceCloseableProjectVar(S name, IF0<? extends AutoCloseable> calc) {
    O value = getProjectVar(name);
    if (value cast AutoCloseable) {
      main close(value);
      deleteProjectVar(name);
    }
    setTransientProjectVar(name, calc?!);
  }
  
  // notify of internal changes in the variable
  // even though the value pointer may have stayed the same.
  void projectVarChanged(S name) {
    var var = findProjectVar(name);
    var?.changeInsideOfValue();
  }

  // search by value
  Cl<G22Variable> projectVarChanged(O value) {
    var vars = conceptsWhere(concepts, G22Variable, +value);
    for (var : vars)
      var.change();
    ret vars;
  }
  
  // timeouts
  
  double defaultScriptTimeout() { ret 10.0; }
  double projectWideScriptTimeout() {
    ret or(toDoubleOrNull(getProjectVar("!Script Timeout")),
      defaultScriptTimeout());
  }
  
  // getting objects
  
  G22Analyzer getAnalyzer(long id) {
    var a = getConcept(concepts, G22Analyzer, id);
    if (a == null) fail("Analyzer not found: " + id);
    ret a;
  }

  G22LeftArrowScript getScript(long id) {
    var a = getConcept(concepts, G22LeftArrowScript, id);
    if (a == null) fail("Script not found: " + id);
    ret a;
  }

  // calling scripts
  
  // This is meant to be called from within another script.
  // No timeout (timeout is handled by calling script)
  // Uses "safest" version available (auto-run or saved)
  O callScript(long id) {
    var script = getScript(id);
    ret script.evaluateWithoutTimeout();
  }
  
  // Like callScript, but only uses auto-run version
  O callAutoRunnableScript(long id) {
    var script = getScript(id);
    ret script.evaluateAutoRunWithoutTimeout();
  }
  
  // no timeout by default
  double defaultTimeout() { ret infinity(); }
  
  // evaluate
  // -with timeout
  // -registered in background processes
  // -with module entered
  <A> A evalRegisteredCode(double timeoutSeconds default defaultTimeout(), S processName, IF0<A> code) {
    if (code == null) null;
    ret evalWithTimeoutOrTypedException(timeoutSeconds, -> {
      temp enter();
      temp var process = backgroundProcessesUI.tempAdd(processName);
      Thread myThread = currentThread();
      process.setInterruptAction(r { cancelThread(myThread) });
      ret code!;
    });
  }
  
  virtual GazelleHost host() {
    temp enter();
    ret dm_os();
  }
  
  <A> A timeFunction(S name, IF0<A> f) {
    ret functionTimings.get(name, f);
  }
  
  bool isConceptsDir(File dir) {
    ret isSameFile(conceptsDir(concepts), dir);
  }
  
  synchronized FileWatchService fileWatcher() {
    ret fileWatcher if null = new FileWatchService;
  }
  
  synchronized selfType onProjectFileChanged(IVF1<File> listener) {
    super.onProjectFileChanged(listener);
    if (!projectFileListenerInitiated) {
      set projectFileListenerInitiated;
      fileWatcher().addRecursiveListener(dbDir(), l1 projectFileChanged);
    }
    this;
  }
  
  simplyCached G22ProjectInfo projectInfo() {
    ret optimizedUniq(concepts, G22ProjectInfo);
  }
  
  RunnablesReferenceQueue runnablesReferenceQueue() {
    ret masterStuff.runnablesReferenceQueue();
  }
  
  EphemeralObjectIDs ephemeralObjectIDs() {
    ret masterStuff.ephemeralObjectIDs();
  }
  
  // get a remember ephemeral object by ID
  O eph(long id) { ret ephemeralObjectIDs().get(id); }
  
  void openInBrowser(S url) {
    if (Desktop.getDesktop().isSupported(Desktop.Action.BROWSE))
      main openInBrowser(url);
    else {
      S cmd;
      if (projectInfo().useFirefox() && isOnPATH("firefox")) 
        cmd = "firefox";
      else {
        cmd = chromeCmd();
        if (isRoot())
          cmd += " -no-sandbox";
      }
      cmd += " " + platformQuote(url);
      nohup(cmd);
    }
  }
  
  S compilationDate() {
    ret or2(compilationDateFromClassPath(this), "unknown");
  }
  
  // Global ID for project
  S projectID() {
    ret projectInfo().projectID();
  }
  
  IG22LoadedDB getLoadedDB() {
    ret masterStuff.getLoadedDB(concepts);
  }
  
  ConceptsComboBox<GalleryImage> galleryImagesComboBox() {
    ret swing(->
      new ConceptsComboBox<GalleryImage>(concepts, GalleryImage));
  }
  
  IBackgroundProcess tempAddBackgroundProcess(S name) {
    ret backgroundProcessesUI?.tempAdd(name);
  }
  
  IF1<S, Class> classFinder() {
    ret masterStuff.makeClassFinder();
  }
  
  O restructure(O o) {
    ret unstructure(structure(o), false, concepts.classFinder);
  }
  
  G22ProjectActions project() { ret projectActions(); }
  
  bool openPathInProject(S path) {
    ret projectActions().openPathInProject(path);
  }
  
  bool openUIURL aka showUIURL(S url) {
    ret projectActions().openUIURL(url);
  }
  
  BufferedImage loadProjectImage(S fileName) {
    ret loadImage2(projectFile(fileName));
  }
  
  IG22LoadedDB openDB(S name, bool hidden default false) {
    ret masterStuff().openDB(name, hidden);
  }
  
  G22Utils getProject(S name) {
    ret openDB(name, true).g22utils();
  }
  
  G22JavaObjectVisualizer visualizeObject(O o) {
    ret new G22JavaObjectVisualizer(this, o);
  }
  
  swappable G22VariablesPanel makeVariablesPanel() {
    ret new G22VariablesPanel().g22utils(this);
  }
  
  swappable JComponent jGazelleLogo() {
    ret main jGazelleLogo();
  }
  
  bool devMode() {
    ret masterStuff().devMode();
  }
  
  // specialize list() for our concepts
  
  <A extends Concept> L<A> nuLike list(Class<A> type, Concepts cc default concepts()) {
    ret main list(cc, type);
  }
  
  <A extends Concept> L<A> list(Concepts concepts, Class<A> type) {
  ret main list(type, concepts);
  }
  
  G22GalleryImage galleryImageForMD5(S md5) {
    ret firstThat(list(concepts, G22GalleryImage),
      img -> cic(fileName(img.path), md5));
  }
  
  // add the right kind of scrollpane to an image surface
  JComponent wrapImageSurface(ImageSurface is) {
    ret is == null ?: jscroll_centered_borderless(is);
  }
  
  JComponent wrap(O o) {
    var c = main wrap(o);
    if (c cast ImageSurface)
      ret wrapImageSurface(c);
    ret c;
  }
  
  swappable S byUser() {
    ret "";
  }
  
  Cl<? extends IG22LoadedDB> getLoadedDBs aka openProjects() {
    ret masterStuff().openProjects();
  }
  
  <A extends Concept> L<A> nuLike listInOpenProjects(Class<A> type) {
    ret concatMap(openProjects(), db -> db.g22utils().list(type));
  }
  
  transient simplyCached PicturesByMD5 picturesByMD5() {
    ret new PicturesByMD5(projectFile("Images"))
      .extension(".qoi");
  }
  
  JButton reloadButton(S toolTip, Runnable action) {
    ret jimageButton(#1101440, toolTip, action);
  }
  
  void registerConcept(Concept c) {
    main registerConcept(concepts, c);
  }
  
  void registerConcept(Concepts cc, Concept c) {
    main registerConcept(cc, c);
  }
  
  void addingAdditionalAutoCompletes(LeftArrowScriptAutoCompleter autoCompleter) {
    S token = autoCompleter.prevToken();
    if (isIdentifier(token) && contains(token, "ProjectVar"))
      autoCompleter.addToSearcher(quoteAll(projectVarNames()));
  }
  
  bool addLibrary(S libID) {
    if (!main addLibrary(libID)) false;
    masterStuff().newClassesDefined();
    true;
  }
} // end of G22Utils