!7

concept AIList {
  S name;
  S text;
  bool botFlag, mighty;
  long modified;
  S status;
}

static ConceptFieldIndexCI<AIList> nameIndex;
static volatile long totalChars = -1;

p {
  dbSaveEvery(60);
  dbIndexing(AIList, 'name);
  nameIndex = indexConceptFieldCI(AIList, 'name);
  thread { mech_guessLanguage(""); } // preload
  calcOnConceptChanges(10000, f calcTotalSize, true);
}

html {
  temp countDispatch('html);
  O response;

  bool authed = webAuthed(params);
  
  if (eq(uri, "/list-count")) ret serveText(countConcepts(AIList));
  ret if not null response = serveListText(uri, params, authed);
  
  if (!authed) ret redirectToWebAuth();
  
  try object html_serveDispatches(uri);
  
  new Matches m;
  
  // API
  if (eq(uri, "/download"))
    ret subBot_serveFileWithName("mechlists-" + ymd_minus_hms() + ".gz", conceptsFile()); // TODO: concurrent writes?
  
  if (eq(uri, "/list-names"))
    ret serveText(jsonEncode(sortedIC(collect(list(AIList), 'name))));

  if (eq(uri, "/bot-edited-lists"))
    ret serveText(jsonEncode(sortedIC(collect(conceptsWhere(AIList, botFlag := true), 'name))));

  // Create API
  if (eq(uri, "/bot-list-create")) {
    lock dbLock();
    S name = params.get("name");
    getList(name);
    ret "OK";
  }

  // Edit API
  if (eq(uri, "/bot-list-edit")) {
    lock dbLock();
    S name = params.get("name"), text = params.get("text");
    AIList l = getList(name);
    if (eq(l.text, text)) ret "No change";
    logStructure("versions.log", litmap(id := l.id, +text, date := now(), mode := "bot edit"));
    cset(l, +text, botFlag := true, modified := now());
    ret "Changed";
  }

  // Append API
  if (eq(uri, "/bot-list-append")) {
    S mode = params.get('mode);
    lock dbLock();
    S name = params.get("name"), text = params.get("text");
    AIList l = getList(name);
    
    if (eq(mode, "uniqCI")) {
      text = lines(listMinusSet(tlft(text), asCISet(tlft(l.text))));
      if (empty(text)) ret "No change";
    }
        
    logStructure("versions.log", litmap(id := l.id, +text, date := now(), mode := "bot append"));
    cset(l, text := appendNewLineIfNempty(rtrim(l.text)) + text, botFlag := true, modified := now());
    ret "Changed";
  }

  // Show list
  if (startsWith(uri, "/list/", m)) {
    S name = urldecode(m.get(0));
    AIList l = eq("1", params.get("create")) ? getList(name) : getList_noCreate(name);
    if (l == null) ret "List not found";
    
    // Update text
    S text = params.get("human");
    S status = params.get("status");
    if (text != null) {
      lock dbLock();
      //printStruct(ll(+text, +status));
      if (neqAny(text, l.text, status, unnull(l.status))) {
        logStructure("versions.log", litmap(id := l.id, +text, +status, date := now()));
        if (eqic(status, "Delete me")) {
          deleteConcept(l);
          ret "List deleted";
        }
        if (status != null) cset(l, +status);
        cset(l, +text, botFlag := false, modified := now());
        //printStruct("l.text=" + sfu(text));
      }
      ret hrefresh(0, rawLink(uri) + "?random=" + randomID());
    }
    
    ret nav() + htitle_h2(htmlencode(l.name))
      + hpostform(
        "Status: " + htextinput('status, value := l.status)
      + h3("Contents (" + (l.botFlag ? "bot" : "human") + "-edited)")
      + p(hsubmit("Save"))
      + htextarea(l.text, name := 'human, cols := 80, rows := 30));
  }
  
  // New list
  S newList = trim(params.get('newList));
  if (nempty(newList)) {
    AIList l = getList(newList);
    ret hrefresh(rawLink("/list/" + urlencode(l.name)));
  }
  
  // Search results
  S q = params.get('q);
  if (nempty(q))
    ret hmobilefix()
      + htitle_h3("mech.tinybrain.de: Search results for " + htmlencode(singleQuote(q)))
      + listLists(scoredSearch_AIList(q, list(AIList)));
  
  // Overview
  bool alphabetical = eq(uri, "/alphabetical");
  
  L<AIList> list = alphabetical
    ? sortedByFieldIC('name, list(AIList))
    : sortedByFieldDesc('modified, list(AIList));
  
  ret hmobilefix()
    + htitle_h3("mech.tinybrain.de [" + n2(l(list), "list")
      + (totalChars >= 0 ? ", " + toK(totalChars) + "K chars" : "")
    + "]")
    + htableRaw_singleRow(ll(
      hform("Search: " + htextinput('q, style := "width: 200px") + " " + hsubmit("Search")),
      hpostform("| New list: " + htextinput('newList) + " " + hsubmit("Create list"))))
    + hBoolSelector(alphabetical, "/", "By date", "/alphabetical", "Alphabetical")
    + listLists(list);
}

sS nav() {
  ret hmobilefix() + p(ahref(rawLink(), "&lt;&lt; back"));
}

static AIList getList_noCreate(S name) {
  ret getList(name, false);
}

static AIList getList(S name) {
  ret getList(name, true);
}

static AIList getList(S name, bool create) {
  lock dbLock();
  //AIList l = uniq_sync(AIList, +name);
  AIList l = findConcept(AIList, +name);
  if (l == null) l = nameIndex.get(name);
  if (l == null) if (!create) null; else {
    l = cnew(AIList, +name);
    logStructure("versions.log", litmap(id := l.id, +name, date := now(), mode := "List created"));
  }
  if (l.modified == 0) cset(l, modified := now());
  ret l;
}

sS listLists(L<AIList> list) {
  ret ul(map(list, func(AIList l) -> S {
    int n = countLines(l.text);
    new L<S> status;
    bool german = eq('german, mech_guessLanguage_quick(l.name));
    if (l.mighty) status.add(german ? "GENUTZT" : "USED");
    addIfNempty(status, htmlencode(l.status));
    if (n != 0) status.add(n2(n, german ? "Zeile" : "line", german ? "Zeilen": "lines"));
    ret ahref(rawLink("/list/" + urlencode(l.name)),
        htmlencode(l.name)) + appendBracketed(joinWithComma(status));
  }));
}

static L<AIList> scoredSearch_AIList(S query, Iterable<AIList> data) {
  new Map<AIList, Int> scores;
  L<S> prepared = scoredSearch_prepare(query);
  for (AIList l : data)
    putUnlessZero(scores, l,
        3*scoredSearch_score(l.name, prepared)
      + 2*scoredSearch_score(l.status, prepared)
      +   scoredSearch_score(l.text, prepared));
  ret keysSortedByValuesDesc(scores);
}

svoid calcTotalSize {
  long total = 0;
  for (AIList l)
    total += l(l.text);
  totalChars = total;
}

sS export_getListText(S listName) {
  AIList l = getList_noCreate(listName);
  ret l == null ? "" : l.text;
}

sS export_setListText(S listName, S text) {
  AIList l = getList(listName);
  if (eq(l.text, text)) ret "No change";
  logStructure("versions.log", litmap(id := l.id, +text, date := now(), mode := "bot edit"));
  cset(l, +text, botFlag := true, modified := now());
  ret "Changed";
}

sS serveListText(S uri, SS params, bool authed) {
  new Matches m;
  if (startsWith(uri, "/list-text/", m)) {
    bool opt = eq("1", params.get("opt"));
    bool create = authed && eq("1", params.get("create"));
    S name = urldecode(m.get(0));
    AIList l = create ? getList(name) : getList_noCreate(name);
    if (!authed && !cic(l.status, "PUBLIC READ")) ret serve404("Not logged in");
    cset(l, mighty := true);
    if (l == null)
      if (opt)
        ret serveText(jsonEncode(litmap("Error", ll("List not found", name))));
      else
        ret serveText(jsonEncode(litmap("Text" := "")));
    else
      ret serveText(jsonEncode(litmap("Name" := l.name, "Text" := l.text)));
  }
  null;
}