sclass CleanFluidTextFile implements Runnable {
  srecord Entry(S text, SS params) {
    long targetIndex;
    S actualText() { ret unquote(text); }
  }

  // set by user
  int maxRefLength = 8;
  bool verbose;
  Reader reader;
  Writer writer;
  
  // set by class
  new L<Entry> entries;
  new Map<S, Entry> relocationMap;

  long maxFileSize() {
    ret rangeOfNHexNumberWithNDigits(maxRefLength);
  }
  
  public void run() ctex {
    FluidTextFileEntry e;
    while ((e = readFluidTextFileEntry(reader)) != null) {
      entries.add(new Entry(e.params, e.quotedContent));
    }
    //pnlStruct(entries);

    // make a map and pessimistically assign byte indices to all entries

    for (Entry e : entries)
      mapPut(relocationMap, e.params.get("i"), e);
    makeIndices(entries);

    // rewrite references

    for (Entry e : entries)
      if (hasReferences(e))
        e.text = replaceRefs(e, r -> lookupInRelocationMap(r));

    // write out

    long idx = 0; // index into file
    for (Entry e : entries) {
      assertEquals(e.targetIndex, idx);
      e.params.put("i", renderReference(e.targetIndex));
      S line = entryToString(e, idx);
      if (verbose)
        print(quote(line));
      writer.write(line);
      int l = lUtf8(line);
      if (verbose)
        print("Actual entry length: " + l);
      idx += l;
    }
  }

  S lookupInRelocationMap(S ref) {
    Entry e = relocationMap.get(ref);
    //print("lookupInRelocationMap: " + ref + " => " + e);
    ret e == null ? null : renderReference(e.targetIndex);
  }

  S renderReference(long idx) {
    ret takeLast(maxRefLength, longToHex(idx));
  }

  void makeIndices(L<Entry> entries) {
    long idx = 0; // index into file
    for (Entry e : entries) {
      e.targetIndex = idx;
      int l = pessimisticEntryLength(e);
      idx += l;
      if (verbose)
        print("Pessimistic entry length: " + l);
    }
  }

  S entryToString(Entry e, long idx) {
    ret entryToString(e, renderReference(idx));
  }
  
  S entryToString(Entry e, S idx) {
    SS params = litorderedmap(
      i := idx,
      l := intToHex_flexLength(lUtf8(e.text)));
    mapPutAll_noOverwrite(params, e.params);
    ret renderEqualsCommaProperties(params) + " " + e.text + "\n";
  }  

  S dummyRef() {
    ret rep('0', maxRefLength);
  }

  bool hasReferences(Entry e) {
    ret eq(e.params.get("refs"), quote("at *"));
  }

  int pessimisticEntryLength(Entry e) {
    Entry e2 = e;
    if (hasReferences(e)) {
      e2 = cloneObject(e);
      e2.text = replaceRefs(e, r -> dummyRef());
      if (verbose)
        print("Pessimistic: rewrote text to " + quote(e2.text));
    }
    S s = entryToString(e2, dummyRef());
    if (verbose)
      print(quote(s));
    ret lUtf8(s);
  }

  S replaceRefs(Entry e, IF1<S> f) {
    LS tok = javaTokWithUnifiedNumbersAndIdentifiers(e.actualText());
    for (int i : jfindAll(tok, "at *")) {
      S x = f.get(tok.get(i+2));
      if (nempty(x))
        tok.set(i+2, x);
    }
    ret multiLineQuote(join(tok));
  }
}