package yuku.alkitab.base.sync;
import android.support.annotation.NonNull;
import android.util.Pair;
import com.google.gson.reflect.TypeToken;
import gnu.trove.map.hash.TObjectLongHashMap;
import gnu.trove.set.TIntSet;
import yuku.alkitab.base.App;
import yuku.alkitab.base.S;
import yuku.alkitab.base.U;
import yuku.alkitab.base.model.ReadingPlan;
import yuku.alkitab.base.model.SyncShadow;
import yuku.alkitab.base.util.Literals;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Reading plan sync
*/
public class Sync_Rp {
/**
* @return base revno, delta of shadow -> current.
*/
public static Pair<Sync.ClientState<Content>, List<Sync.Entity<Content>>> getClientStateAndCurrentEntities() {
final SyncShadow ss = S.getDb().getSyncShadowBySyncSetName(SyncShadow.SYNC_SET_RP);
final List<Sync.Entity<Content>> srcs = ss == null? Literals.List(): entitiesFromShadow(ss);
final List<Sync.Entity<Content>> dsts = getEntitiesFromCurrent();
final Sync.Delta<Content> delta = new Sync.Delta<>();
// additions and modifications
for (final Sync.Entity<Content> dst : dsts) {
final Sync.Entity<Content> existing = findEntity(srcs, dst.gid, dst.kind);
if (existing == null) {
delta.operations.add(new Sync.Operation<>(Sync.Opkind.add, dst.kind, dst.gid, dst.content));
} else {
if (!isSameContent(dst, existing)) { // only when it changes
delta.operations.add(new Sync.Operation<>(Sync.Opkind.mod, dst.kind, dst.gid, dst.content));
}
}
}
// deletions
for (final Sync.Entity<Content> src : srcs) {
final Sync.Entity<Content> still_have = findEntity(dsts, src.gid, src.kind);
if (still_have == null) {
delta.operations.add(new Sync.Operation<>(Sync.Opkind.del, src.kind, src.gid, null));
}
}
return Pair.create(new Sync.ClientState<>(ss == null ? 0 : ss.revno, delta), dsts);
}
private static boolean isSameContent(final Sync.Entity<Content> a, final Sync.Entity<Content> b) {
if (!U.equals(a.gid, b.gid)) return false;
if (!U.equals(a.kind, b.kind)) return false;
return U.equals(a.content, b.content);
}
private static Sync.Entity<Content> findEntity(final List<Sync.Entity<Content>> list, final String gid, final String kind) {
for (final Sync.Entity<Content> entity : list) {
if (U.equals(gid, entity.gid) && U.equals(kind, entity.kind)) {
return entity;
}
}
return null;
}
private static List<Sync.Entity<Content>> entitiesFromShadow(@NonNull final SyncShadow ss) {
final BufferedReader reader = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(ss.data), Charset.forName("utf-8")));
final Sync.SyncShadowDataJson<Content> data = App.getDefaultGson().fromJson(reader, new TypeToken<Sync.SyncShadowDataJson<Content>>() {}.getType());
return data.entities;
}
@NonNull public static SyncShadow shadowFromEntities(@NonNull final List<Sync.Entity<Content>> entities, final int revno) {
final Sync.SyncShadowDataJson<Content> data = new Sync.SyncShadowDataJson<>();
data.entities = entities;
final ByteArrayOutputStream baos = new ByteArrayOutputStream();
final BufferedWriter w = new BufferedWriter(new OutputStreamWriter(baos, Charset.forName("utf-8")));
App.getDefaultGson().toJson(data, new TypeToken<Sync.SyncShadowDataJson<Content>>() {}.getType(), w);
U.wontThrow(() -> w.flush());
final SyncShadow res = new SyncShadow();
res.data = baos.toByteArray();
res.syncSetName = SyncShadow.SYNC_SET_RP;
res.revno = revno;
return res;
}
@NonNull public static List<Sync.Entity<Content>> getEntitiesFromCurrent() {
final List<Sync.Entity<Content>> res = new ArrayList<>();
// lookup map for startTime
final List<ReadingPlan.ReadingPlanInfo> infos = S.getDb().listAllReadingPlanInfo();
final TObjectLongHashMap<String /* gid */> /* long startTime */ startTimes = new TObjectLongHashMap<>(infos.size());
for (final ReadingPlan.ReadingPlanInfo info : infos) {
startTimes.put(ReadingPlan.gidFromName(info.name), info.startTime);
}
// The only source of data is from ReadingPlanProgress table,
// but since reading plans with no done is not listed in ReadingPlanProgress,
// we need to consult ReadingPlan table to know what they are.
final Map<String /* gid */, TIntSet /* done reading codes */> map = S.getDb().getReadingPlanProgressSummaryForSync();
for (final Map.Entry<String, TIntSet> e : map.entrySet()) {
final String gid = e.getKey();
final Content content = new Content();
content.startTime = startTimes.containsKey(gid)? startTimes.get(gid): null;
final TIntSet set = e.getValue();
final Set<Integer> done = content.done = new LinkedHashSet<>(set.size());
set.forEach(value -> {
done.add(value);
return true;
});
final Sync.Entity<Content> entity = new Sync.Entity<>(Sync.Entity.KIND_RP_PROGRESS, gid, content);
res.add(entity);
}
// add remaining reading plans without any done
startTimes.forEachEntry((gid, startTime) -> {
if (!map.containsKey(gid)) {
final Content content = new Content();
content.startTime = startTime;
content.done = new LinkedHashSet<>();
final Sync.Entity<Content> entity = new Sync.Entity<>(Sync.Entity.KIND_RP_PROGRESS, gid, content);
res.add(entity);
}
return true;
});
return res;
}
public static class Content {
public Long startTime; // time in millis when the reading plan has started. Can be null, if no such data is found. Server should always prioritize entities with non-null startTime.
public Set<Integer> done; // reading codes that are checked
//region boilerplate equals and hashCode methods
@Override
public boolean equals(final Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
final Content content = (Content) o;
if (startTime != null ? !startTime.equals(content.startTime) : content.startTime != null) return false;
if (done != null ? !done.equals(content.done) : content.done != null) return false;
return true;
}
@Override
public int hashCode() {
int result = startTime != null ? startTime.hashCode() : 0;
result = 31 * result + (done != null ? done.hashCode() : 0);
return result;
}
//endregion
@Override
public String toString() {
return "Content{" +
"startTime=" + startTime +
", done=" + done +
'}';
}
}
}