package com.pluscubed.plustimer.model; import android.content.Context; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.WorkerThread; import com.couchbase.lite.CouchbaseLiteException; import com.couchbase.lite.Database; import com.couchbase.lite.Document; import com.couchbase.lite.Query; import com.couchbase.lite.QueryRow; import com.couchbase.lite.View; import com.fasterxml.jackson.annotation.JsonAutoDetect; import com.fasterxml.jackson.annotation.JsonProperty; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.JsonDeserializer; import com.pluscubed.plustimer.R; import com.pluscubed.plustimer.utils.PrefUtils; import com.pluscubed.plustimer.utils.Utils; import net.gnehzr.tnoodle.scrambles.Puzzle; import net.gnehzr.tnoodle.scrambles.PuzzlePlugins; import net.gnehzr.tnoodle.utils.BadLazyClassDescriptionException; import net.gnehzr.tnoodle.utils.LazyInstantiatorException; import java.io.File; import java.io.IOException; import java.text.Collator; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; import rx.Completable; import rx.Observable; import rx.Single; import rx.android.schedulers.AndroidSchedulers; import rx.exceptions.Exceptions; import rx.schedulers.Schedulers; /** * Puzzle Type object */ @JsonAutoDetect(creatorVisibility = JsonAutoDetect.Visibility.NONE, fieldVisibility = JsonAutoDetect.Visibility.NONE, getterVisibility = JsonAutoDetect.Visibility.NONE, isGetterVisibility = JsonAutoDetect.Visibility.NONE, setterVisibility = JsonAutoDetect.Visibility.NONE ) public class PuzzleType extends CbObject { public static final String TYPE_PUZZLETYPE = "puzzletype"; public static final String VIEW_PUZZLETYPES = "puzzletypes"; @Nullable private static Completable sInitialization; @Nullable private static List<PuzzleType> sPuzzleTypes; private static Set<CurrentSessionChangeListener> sCurrentChangeListeners; private Puzzle mPuzzle; @JsonProperty("scrambler") private String mScrambler; @JsonProperty("currentSessionId") private String mCurrentSessionId; @JsonProperty("enabled") private boolean mEnabled; @JsonProperty("inspection") private boolean mInspectionOn; @JsonProperty("name") private String mName; @JsonProperty("sessions") @NonNull private List<String> mSessions; //Pre-SQL legacy code @Deprecated private String mCurrentSessionFileName; @Deprecated private String mHistoryFileName; @Deprecated private HistorySessions mHistorySessionsLegacy; @Deprecated private String mLegacyName; public PuzzleType() { mSessions = new ArrayList<>(); } @WorkerThread public PuzzleType(Context context, String scrambler, String name, String currentSessionId, boolean inspectionOn, String id) throws CouchbaseLiteException, IOException { super(context, id); mScrambler = scrambler; mName = name; mEnabled = true; mCurrentSessionId = currentSessionId; mInspectionOn = inspectionOn; mSessions = new ArrayList<>(); updateCb(context); } public static void addCurrentChangeListener(CurrentSessionChangeListener listener) { getListeners().add(listener); } public static void removeCurrentChangeListener(CurrentSessionChangeListener listener) { getListeners().remove(listener); } private static Set<CurrentSessionChangeListener> getListeners() { if (sCurrentChangeListeners == null) { sCurrentChangeListeners = new HashSet<>(); } return sCurrentChangeListeners; } private static void notifyChangeCurrentListeners(String oldType) { notifyChangeCurrentListenersDeferred(oldType).subscribe(); } @NonNull private static Completable notifyChangeCurrentListenersDeferred(String oldType) { return Completable.fromCallable(() -> { for (CurrentSessionChangeListener listener : getListeners()) { listener.notifyChange(oldType); } return null; }).subscribeOn(AndroidSchedulers.mainThread()); } public static Observable<PuzzleType> getPuzzleTypes(Context context) { return initialize(context) .andThen(Observable.defer(() -> Observable.from(sPuzzleTypes))); } public static Single<PuzzleType> get(Context context, String id) { return initialize(context) .andThen(getInternal(id).toObservable()) .toSingle(); } @NonNull private static Single<PuzzleType> getInternal(String id) { return Single.fromCallable(() -> { for (PuzzleType type : sPuzzleTypes) { if (type.getId().equals(id)) { return type; } } return sPuzzleTypes.get(0); }); } public static Single<PuzzleType> getCurrent(Context context) { return initialize(context) .andThen(getInternal(getCurrentId(context)).toObservable()) .toSingle(); } public static String getCurrentId(Context context) { return PrefUtils.getCurrentPuzzleType(context); } public static Observable<PuzzleType> getEnabledPuzzleTypes(Context context) { return getPuzzleTypes(context) .flatMap(puzzleType -> { if (puzzleType.isEnabled()) { return Observable.just(puzzleType); } else { return Observable.empty(); } }); } public static boolean isInitialized() { return sPuzzleTypes != null; } public static Completable initialize(Context context) { if (isInitialized()) { return Completable.complete(); } if (sInitialization == null) { try { int savedVersionCode = PrefUtils.getVersionCode(context); Database database = CouchbaseInstance.get(context).getDatabase(); View puzzletypesView = database.getView(VIEW_PUZZLETYPES); puzzletypesView.setMap((document, emitter) -> { if (document.get("type").equals(TYPE_PUZZLETYPE)) { emitter.emit(document.get("name"), document.get("scrambler")); } }, "1"); sInitialization = initializePuzzleTypes(context, database) .doOnCompleted(() -> { for (PuzzleType puzzleType : sPuzzleTypes) { //TODO: upgrade database //puzzleType.upgradeDatabase(context); } sInitialization = null; }).toObservable() .publish().autoConnect() .toCompletable(); } catch (CouchbaseLiteException | IOException e) { return Completable.error(e); } } return sInitialization; } @NonNull private static Completable initializePuzzleTypes(Context context, Database database) { return Completable.create(completableSubscriber -> { Query puzzleTypesQuery = database.getView(VIEW_PUZZLETYPES).createQuery(); puzzleTypesQuery.runAsync((rows, error) -> { if (!rows.hasNext()) { try { initializeFirstRun(context); completableSubscriber.onCompleted(); } catch (CouchbaseLiteException | IOException e) { completableSubscriber.onError(e); } } else { List<PuzzleType> puzzleTypes = new ArrayList<>(); for (QueryRow row : rows) { PuzzleType type = PuzzleType.fromDoc(row.getDocument(), PuzzleType.class); puzzleTypes.add(type); } sPuzzleTypes = puzzleTypes; completableSubscriber.onCompleted(); } }); }).subscribeOn(Schedulers.io()); } private static void initializeFirstRun(Context context) throws CouchbaseLiteException, IOException { //Generate default puzzle types from this... String[] scramblers = context.getResources().getStringArray(R.array.scramblers); //and this, with the appropriate UI names... String[] defaultCustomPuzzleTypes = context.getResources() .getStringArray(R.array.default_custom_puzzletypes); //from this String[] puzzles = context.getResources().getStringArray(R.array.scrambler_names); //and the legacy names from this String[] legacyNames = context.getResources().getStringArray(R.array.legacy_names); List<PuzzleType> puzzleTypes = new ArrayList<>(); for (int i = 0; i < scramblers.length + defaultCustomPuzzleTypes.length; i++) { String scrambler; String defaultCustomType = null; String uiName; if (scramblers.length > i) { scrambler = scramblers[i]; } else { defaultCustomType = defaultCustomPuzzleTypes[i - scramblers.length]; scrambler = defaultCustomType.substring(0, defaultCustomType.indexOf(",")); } String id = scrambler; if (puzzles.length > i) { uiName = puzzles[i]; } else { int order = Integer.parseInt(scrambler.substring(0, 1)); String addon = null; if (scrambler.contains("ni")) { addon = context.getString(R.string.bld); } if (defaultCustomType != null) { if (defaultCustomType.contains("feet")) { addon = context.getString(R.string.feet); id += "-feet"; } else if (defaultCustomType.contains("oh")) { addon = context.getString(R.string.oh); id += "-oh"; } } if (addon != null) { uiName = order + "x" + order + "-" + addon; } else { uiName = order + "x" + order; } } PuzzleType newPuzzleType = new PuzzleType(context, scrambler, uiName, null, true, id/*, legacyNames[i]*/); if (uiName.equals("3x3")) { //Default current puzzle type newPuzzleType.newSession(context); PrefUtils.setCurrentPuzzleType(context, newPuzzleType.getId()); } puzzleTypes.add(newPuzzleType); } Collections.sort(puzzleTypes, (lhs, rhs) -> Collator.getInstance().compare(lhs.getName(), rhs.getName())); sPuzzleTypes = puzzleTypes; } public static Completable setCurrent(Context context, String puzzleTypeId) { String oldId = getCurrentId(context); if (!puzzleTypeId.equals(oldId)) { PrefUtils.setCurrentPuzzleType(context, puzzleTypeId); return get(context, puzzleTypeId) .doOnSuccess(puzzleType -> { if (puzzleType.getCurrentSessionId() == null) { try { puzzleType.newSession(context); } catch (IOException | CouchbaseLiteException e) { throw Exceptions.propagate(e); } } }) .flatMapObservable(puzzleType -> notifyChangeCurrentListenersDeferred(oldId).toObservable()) .toCompletable(); } else { return Completable.complete(); } } private Session newSession(Context context) throws IOException, CouchbaseLiteException { mCurrentSessionId = getId() + "-" + Session.ID_CURRENT_PREFIX + "default"; Session session = new Session(context, mCurrentSessionId); mSessions.add(session.getId()); updateCb(context); return session; } public String getCurrentSessionId() { return mCurrentSessionId; } public boolean isBld() { return mScrambler.contains("ni"); } public boolean isScramblerOfficial() { return !mScrambler.contains("fast"); } public String getScrambler() { return mScrambler; } public String getName() { return mName; } boolean isEnabled() { return mEnabled; } public Completable setEnabled(Context context, boolean enabled) { return initialize(context) .concatWith(Completable.defer(() -> { mEnabled = enabled; if (mId.equals(getCurrentId(context)) && !mEnabled) { for (PuzzleType puzzleType : sPuzzleTypes) { if (puzzleType.mEnabled) { return setCurrent(context, puzzleType.getId()) .doOnCompleted(() -> updateCb(context)); } } } return Completable.complete(); })); } public String getId() { return mId; } @Deprecated public String getHistoryFileName() { return mHistoryFileName; } @Deprecated public String getCurrentSessionFileName() { return mCurrentSessionFileName; } //TODO /*@Deprecated public HistorySessions getHistorySessionsSorted() { return mHistorySessionsLegacy; }*/ public void deleteSession(Context context, String sessionId) throws CouchbaseLiteException, IOException { getSession(context, sessionId) .getDocument(context) .delete(); mSessions.remove(sessionId); updateCb(context); } private void upgradeDatabase(Context context) { mHistoryFileName = mLegacyName + ".json"; mCurrentSessionFileName = mLegacyName + "-current.json"; mHistorySessionsLegacy = new HistorySessions(mHistoryFileName); //TODO //AFTER UPDATING APP//////////// int savedVersionCode = PrefUtils.getVersionCode(context); if (savedVersionCode <= 10) { //Version <=10: Set up history sessions with old // name first if (!mScrambler.equals("333") || mLegacyName.equals("THREE")) { mHistorySessionsLegacy.setFilename(mScrambler + ".json"); mHistorySessionsLegacy.init(context); mHistorySessionsLegacy.setFilename(mHistoryFileName); if (mHistorySessionsLegacy.getList().size() > 0) { mHistorySessionsLegacy.save(context); } File oldFile = new File(context.getFilesDir(), mScrambler + ".json"); oldFile.delete(); } } if (savedVersionCode <= 13) { //Version <=13: ScrambleAndSvg json structure changes Gson gson = new GsonBuilder() .registerTypeAdapter(Session.class, (JsonDeserializer<Session>) (json, typeOfT, sessionJsonDeserializer) -> { Gson gson1 = new GsonBuilder() .registerTypeAdapter(ScrambleAndSvg.class, (JsonDeserializer<ScrambleAndSvg>) (json1, typeOfT1, context1) -> new ScrambleAndSvg(json1.getAsJsonPrimitive().getAsString(), null)) .create(); Session s = gson1.fromJson(json, typeOfT); /*for (final Solve solve : s.getSolves()) { //TODO: Legacy //solve.attachSession(s); }*/ return s; }) .create(); Utils.updateData(context, mHistoryFileName, gson); Utils.updateData(context, mCurrentSessionFileName, gson); } mHistorySessionsLegacy.setFilename(null); //////////////////////////// } public void submitCurrentSession(Context context) throws IOException, CouchbaseLiteException { //newSession(context); getSessionDeferred(context, mCurrentSessionId) .subscribe(session -> { try { Document doc = CouchbaseInstance.get(context).getDatabase().createDocument(); session.setId(doc.getId()); session.updateCb(context); mSessions.add(doc.getId()); newSession(context); notifyChangeCurrentListeners(getCurrentId(context)); } catch (CouchbaseLiteException | IOException e) { e.printStackTrace(); } }); } public Puzzle getPuzzle() { if (mPuzzle == null) { try { mPuzzle = PuzzlePlugins.getScramblers().get(mScrambler) .cachedInstance(); } catch (LazyInstantiatorException | BadLazyClassDescriptionException | IOException e) { e.printStackTrace(); } } return mPuzzle; } public Single<Session> getCurrentSessionDeferred(Context context) { return getSessionDeferred(context, mCurrentSessionId); } public Session getCurrentSession(Context context) throws CouchbaseLiteException, IOException { return getSession(context, mCurrentSessionId); } public Single<Session> getSessionDeferred(Context context, String id) { return Single.defer(() -> Single.just(getSession(context, id))) .subscribeOn(Schedulers.io()); } public Session getSession(Context context, String id) throws CouchbaseLiteException, IOException { return fromDocId(context, id, Session.class); } public Observable<Session> getHistorySessionsSorted(Context context) { return getHistorySessions(context) .toSortedList((session, session2) -> { return Utils.compare(session.getLastSolve(context).toBlocking().first().getTimestamp(), session2.getLastSolve(context).toBlocking().first().getTimestamp()); }).flatMap(Observable::from); } public Observable<Session> getHistorySessions(Context context) { return Observable.from(new ArrayList<>(mSessions)) .filter(id -> !id.equals(mCurrentSessionId)) .subscribeOn(Schedulers.io()) .flatMap(id -> { try { return Observable.just(getSession(context, id)); } catch (CouchbaseLiteException | IOException e) { return Observable.error(e); } }); } @Override public boolean equals(Object o) { return o instanceof PuzzleType && ((PuzzleType) o).getId().equals(mId); } @Override protected String getType() { return TYPE_PUZZLETYPE; } public interface CurrentSessionChangeListener { void notifyChange(String oldType); } }