com.hichinaschool.flashcards.libanki.Models.java Source code

Java tutorial

Introduction

Here is the source code for com.hichinaschool.flashcards.libanki.Models.java

Source

/****************************************************************************************
 * Copyright (c) 2009 Daniel Svrd <daniel.svard@gmail.com>                             *
 * Copyright (c) 2010 Rick Gruber-Riemer <rick@vanosten.net>                            *
 * Copyright (c) 2011 Norbert Nagold <norbert.nagold@gmail.com>                         *
 * Copyright (c) 2011 Kostas Spyropoulos <inigo.aldana@gmail.com>                       *
 *                                                                                      *
 * This program is free software; you can redistribute it and/or modify it under        *
 * the terms of the GNU General Public License as published by the Free Software        *
 * Foundation; either version 3 of the License, or (at your option) any later           *
 * version.                                                                             *
 *                                                                                      *
 * This program is distributed in the hope that it will be useful, but WITHOUT ANY      *
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A      *
 * PARTICULAR PURPOSE. See the GNU General Public License for more details.             *
 *                                                                                      *
 * You should have received a copy of the GNU General Public License along with         *
 * this program.  If not, see <http://www.gnu.org/licenses/>.                           *
 ****************************************************************************************/

package com.hichinaschool.flashcards.libanki;

import android.content.ContentValues;
import android.database.Cursor;
import android.util.Log;

import com.hichinaschool.flashcards.anki.AnkiDroidApp;
import com.hichinaschool.flashcards.anki.Pair;
import com.samskivert.mustache.Mustache;
import com.samskivert.mustache.Template;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class Models {
    private static final Pattern fClozePattern1 = Pattern.compile("(?:\\{\\{|<%)cloze:(.+?)(?:\\}\\}|%>)");
    private static final Pattern fClozePattern2 = Pattern.compile("\\{\\{c(\\d+)::.+?\\}\\}");

    public static final String defaultModel = "{'sortf': 0, " + "'did': 1, " + "'latexPre': \""
            + "\\\\documentclass[12pt]{article} " + "\\\\special{papersize=3in,5in} "
            + "\\\\usepackage[utf8]{inputenc} " + "\\\\usepackage{amssymb,amsmath} " + "\\\\pagestyle{empty} "
            + "\\\\setlength{\\\\parindent}{0in} " + "\\\\begin{document} " + "\", "
            + "'latexPost': \"\\\\end{document}\", " + "'mod': 0, " + "'usn': 0, " + "'vers': [], " // FIXME: remove when other clients have caught up 
            + "'type': " + Sched.MODEL_STD + ", " + "'css': \" .card {" + "font-familiy: arial; "
            + "font-size: 20px; " + "text-align: center; " + "color:black; " + "background-color: white; }\"" + "}";

    private static final String defaultField = "{'name': \"\", " + "'ord': null, " + "'sticky': False, " +
    // the following alter editing, and are used as defaults for the template wizard
            "'rtl': False, " + "'font': \"Arial\", " + "'size': 20, " +
            // reserved for future use
            "'media': [] }";

    private static final String defaultTemplate = "{'name': \"\", " + "'ord': null, " + "'qfmt': \"\", "
            + "'afmt': \"\", " + "'did': null, " + "'bqfmt': \"\"," + "'bafmt': \"\"," + "'bfont': \"Arial\","
            + "'bsize': 12 }";

    // /** Regex pattern used in removing tags from text before diff */
    // private static final Pattern sFactPattern = Pattern.compile("%\\([tT]ags\\)s");
    // private static final Pattern sModelPattern = Pattern.compile("%\\(modelTags\\)s");
    // private static final Pattern sTemplPattern = Pattern.compile("%\\(cardModel\\)s");

    private Collection mCol;
    private boolean mChanged;
    private HashMap<Long, JSONObject> mModels;

    // BEGIN SQL table entries
    private int mId;
    private String mName = "";
    private long mCrt = Utils.intNow();
    private long mMod = Utils.intNow();
    private JSONObject mConf;
    private String mCss = "";
    private JSONArray mFields;
    private JSONArray mTemplates;
    // BEGIN SQL table entries

    // private Decks mDeck;
    // private AnkiDb mDb;
    //
    /** Map for compiled Mustache Templates */
    private Map<String, Template> mCmpldTemplateMap = new HashMap<String, Template>();

    //
    // /** Map for convenience and speed which contains FieldNames from current model */
    // private TreeMap<String, Integer> mFieldMap = new TreeMap<String, Integer>();
    //
    // /** Map for convenience and speed which contains Templates from current model */
    // private TreeMap<Integer, JSONObject> mTemplateMap = new TreeMap<Integer, JSONObject>();
    //
    // /** Map for convenience and speed which contains the CSS code related to a Template */
    // private HashMap<Integer, String> mCssTemplateMap = new HashMap<Integer, String>();
    //
    // /**
    // * The percentage chosen in preferences for font sizing at the time when the css for the CardModels related to
    // this
    // * Model was calculated in prepareCSSForCardModels.
    // */
    // private transient int mDisplayPercentage = 0;
    // private boolean mNightMode = false;

    /**
     * Saving/loading registry
     * ***********************************************************************************************
     */

    public Models(Collection col) {
        mCol = col;
    }

    /**
     * Load registry from JSON.
     */
    public void load(String json) {
        mChanged = false;
        mModels = new HashMap<Long, JSONObject>();
        try {
            JSONObject modelarray = new JSONObject(json);
            JSONArray ids = modelarray.names();
            if (ids != null) {
                for (int i = 0; i < ids.length(); i++) {
                    String id = ids.getString(i);
                    JSONObject o = modelarray.getJSONObject(id);
                    mModels.put(o.getLong("id"), o);
                }
            }
        } catch (JSONException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * Mark M modified if provided, and schedule registry flush.
     */
    public void save() {
        save(null, false);
    }

    public void save(JSONObject m) {
        save(m, false);
    }

    public void save(JSONObject m, boolean templates) {
        if (m != null && m.has("id")) {
            try {
                m.put("mod", Utils.intNow());
                m.put("usn", mCol.usn());
                // TODO: fix empty id problem on _updaterequired (needed for model adding)
                if (m.getLong("id") != 0) {
                    _updateRequired(m);
                }
                if (templates) {
                    _syncTemplates(m);
                }
            } catch (JSONException e) {
                throw new RuntimeException(e);
            }
        }
        mChanged = true;
        // runHook("newModel")
    }

    /**
     * Flush the registry if any models were changed.
     */
    public void flush() {
        if (mChanged) {
            JSONObject array = new JSONObject();
            try {
                for (Map.Entry<Long, JSONObject> o : mModels.entrySet()) {
                    array.put(Long.toString(o.getKey()), o.getValue());
                }
            } catch (JSONException e) {
                throw new RuntimeException(e);
            }
            ContentValues val = new ContentValues();
            val.put("models", Utils.jsonToString(array));
            mCol.getDb().update("col", val);
            mChanged = false;
        }
    }

    /**
     * Retrieving and creating models
     * ***********************************************************************************************
     */

    /**
     * Get current model.
     * @return The JSONObject of the model, or null if not found in the deck and in the configuration.
     */
    public JSONObject current() {
        return current(true);
    }

    /**
     * Get current model.
     * @param forDeck If true, it tries to get the deck specified in deck by mid, otherwise or if the former is not
     *                found, it uses the configuration`s field curModel.
     * @return The JSONObject of the model, or null if not found in the deck and in the configuration.
     */
    public JSONObject current(boolean forDeck) {
        JSONObject m = null;
        if (forDeck) {
            m = get(mCol.getDecks().current().optLong("mid", -1));
        }
        if (m == null) {
            m = get(mCol.getConf().optLong("curModel", -1));
        }
        if (m == null) {
            if (!mModels.isEmpty()) {
                m = mModels.values().iterator().next();
            }
        }
        return m;
    }

    public void setCurrent(JSONObject m) {
        try {
            mCol.getConf().put("curModel", m.get("id"));
        } catch (JSONException e) {
            throw new RuntimeException(e);
        }
        mCol.setMod();
    }

    /** get model with ID, or none. */
    public JSONObject get(long id) {
        if (mModels.containsKey(id)) {
            return mModels.get(id);
        } else {
            return null;
        }
    }

    /** get all models */
    public ArrayList<JSONObject> all() {
        ArrayList<JSONObject> models = new ArrayList<JSONObject>();
        Iterator<JSONObject> it = mModels.values().iterator();
        while (it.hasNext()) {
            models.add(it.next());
        }
        return models;
    }

    /** get model with NAME. */
    public JSONObject byName(String name) {
        for (JSONObject m : mModels.values()) {
            try {
                if (m.getString("name").equals(name)) {
                    return m;
                }
            } catch (JSONException e) {
                throw new RuntimeException(e);
            }
        }
        return null;
    }

    /** Create a new model, save it in the registry, and return it. */
    public JSONObject newModel(String name) {
        // caller should call save() after modifying
        JSONObject m;
        try {
            m = new JSONObject(defaultModel);
            m.put("name", name);
            m.put("mod", Utils.intNow());
            m.put("flds", new JSONArray());
            m.put("tmpls", new JSONArray());
            m.put("tags", new JSONArray());
            m.put("id", 0);
        } catch (JSONException e) {
            throw new RuntimeException(e);
        }
        return m;
    }

    /** Delete model, and all its cards/notes. */
    public void rem(JSONObject m) {
        mCol.modSchema();
        try {
            long id = m.getLong("id");
            boolean current = current().getLong("id") == id;
            // delete notes/cards
            mCol.remCards(Utils.arrayList2array(mCol.getDb().queryColumn(Long.class,
                    "SELECT id FROM cards WHERE nid IN (SELECT id FROM notes WHERE mid = " + id + ")", 0)));
            // then the model
            mModels.remove(id);
            save();
            // GUI should ensure last model is not deleted
            if (current) {
                setCurrent(mModels.values().iterator().next());
            }
        } catch (JSONException e) {
            throw new RuntimeException(e);
        }
    }

    public void add(JSONObject m) {
        _setID(m);
        update(m);
        setCurrent(m);
        save(m);
    }

    /** Add or update an existing model. Used for syncing and merging. */
    public void update(JSONObject m) {
        try {
            mModels.put(m.getLong("id"), m);
        } catch (JSONException e) {
            throw new RuntimeException(e);
        }
        // mark registry changed, but don't bump mod time
        save();
    }

    private void _setID(JSONObject m) {
        long id = Utils.intNow(1000);
        while (mModels.containsKey(id)) {
            id = Utils.intNow(1000);
        }
        try {
            m.put("id", id);
        } catch (JSONException e) {
            throw new RuntimeException(e);
        }
    }

    public boolean have(long id) {
        return mModels.containsKey(id);
    }

    public long[] ids() {
        Iterator<Long> it = mModels.keySet().iterator();
        long[] ids = new long[mModels.size()];
        int i = 0;
        while (it.hasNext()) {
            ids[i] = it.next();
            i++;
        }
        return ids;
    }

    /**
     * Tools ***********************************************************************************************
     */

    /** Note ids for M */
    public ArrayList<Long> nids(JSONObject m) {
        try {
            return mCol.getDb().queryColumn(Long.class, "SELECT id FROM notes WHERE mid = " + m.getLong("id"), 0);
        } catch (JSONException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * Number of notes using m
     * @param m The model to the count the notes of.
     * @return The number of notes with that model.
     */
    public int useCount(JSONObject m) {
        try {
            return mCol.getDb().queryScalar("select count() from notes where mid = " + m.getLong("id"));
        } catch (JSONException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * Copying ***********************************************************************************************
     */

    /** Copy, save and return. */
    public JSONObject copy(JSONObject m) {
        JSONObject m2 = null;
        try {
            m2 = new JSONObject(Utils.jsonToString(m));
            m2.put("name", m2.getString("name") + " copy");
        } catch (JSONException e) {
            throw new RuntimeException(e);
        }
        add(m2);
        return m2;
    }

    /**
     * Fields ***********************************************************************************************
     */

    public JSONObject newField(String name) {
        JSONObject f;
        try {
            f = new JSONObject(defaultField);
            f.put("name", name);
        } catch (JSONException e) {
            throw new RuntimeException(e);
        }
        return f;
    }

    /** "Mapping of field name -> (ord, field). */
    public Map<String, Pair<Integer, JSONObject>> fieldMap(JSONObject m) {
        JSONArray ja;
        try {
            ja = m.getJSONArray("flds");
            // TreeMap<Integer, String> map = new TreeMap<Integer, String>();
            Map<String, Pair<Integer, JSONObject>> result = new HashMap<String, Pair<Integer, JSONObject>>();
            for (int i = 0; i < ja.length(); i++) {
                JSONObject f = ja.getJSONObject(i);
                result.put(f.getString("name"), new Pair<Integer, JSONObject>(f.getInt("ord"), f));
            }
            return result;
        } catch (JSONException e) {
            throw new RuntimeException(e);
        }
    }

    public ArrayList<String> fieldNames(JSONObject m) {
        JSONArray ja;
        try {
            ja = m.getJSONArray("flds");
            ArrayList<String> names = new ArrayList<String>();
            for (int i = 0; i < ja.length(); i++) {
                names.add(ja.getJSONObject(i).getString("name"));
            }
            return names;
        } catch (JSONException e) {
            throw new RuntimeException(e);
        }

    }

    public int sortIdx(JSONObject m) {
        try {
            return m.getInt("sortf");
        } catch (JSONException e) {
            throw new RuntimeException(e);
        }
    }

    // public int setSortIdx(JSONObject m, int idx) {
    // try {
    // mCol.modSchema();
    // m.put("sortf", idx);
    // mCol.updateFieldCache(nids(m));
    // save(m);
    // } catch (JSONException e) {
    // throw new RuntimeException(e);
    // }
    // }

    public void addField(JSONObject m, JSONObject field) {
        // only mod schema if model isn't new
        try {
            if (m.getLong("id") != 0) {
                mCol.modSchema();
            }
            JSONArray ja = m.getJSONArray("flds");
            ja.put(field);
            m.put("flds", ja);
            _updateFieldOrds(m);
            save(m);
            _transformFields(m, new TransformFieldAdd());
        } catch (JSONException e) {
            throw new RuntimeException(e);
        }

    }

    class TransformFieldAdd implements TransformFieldVisitor {
        @Override
        public String[] transform(String[] fields) {
            String[] f = new String[fields.length + 1];
            System.arraycopy(fields, 0, f, 0, fields.length);
            f[fields.length] = "";
            return f;
        }
    }

    public void remField(JSONObject m, JSONObject field) {
        mCol.modSchema();
        try {
            JSONArray ja = m.getJSONArray("flds");
            JSONArray ja2 = new JSONArray();
            int idx = -1;
            for (int i = 0; i < ja.length(); ++i) {
                if (field.equals(ja.getJSONObject(i))) {
                    idx = i;
                    continue;
                }
                ja2.put(ja.get(i));
            }
            m.put("flds", ja2);
            int sortf = m.getInt("sortf");
            if (sortf >= m.getJSONArray("flds").length()) {
                m.put("sortf", sortf - 1);
            }
            _updateFieldOrds(m);
            _transformFields(m, new TransformFieldDelete(idx));
            if (idx == sortIdx(m)) {
                // need to rebuild
                mCol.updateFieldCache(Utils.toPrimitive(nids(m)));
            }
            renameField(m, field, null);
        } catch (JSONException e) {
            throw new RuntimeException(e);
        }

    }

    class TransformFieldDelete implements TransformFieldVisitor {
        private int idx;

        public TransformFieldDelete(int _idx) {
            idx = _idx;
        }

        @Override
        public String[] transform(String[] fields) {
            ArrayList<String> fl = new ArrayList<String>(Arrays.asList(fields));
            fl.remove(idx);
            return fl.toArray(new String[] {});
        }
    }

    public void moveField(JSONObject m, JSONObject field, int idx) {
        mCol.modSchema();
        try {
            JSONArray ja = m.getJSONArray("flds");
            ArrayList<JSONObject> l = new ArrayList<JSONObject>();
            int oldidx = -1;
            for (int i = 0; i < ja.length(); ++i) {
                l.add(ja.getJSONObject(i));
                if (field.equals(ja.getJSONObject(i))) {
                    oldidx = i;
                    if (idx == oldidx) {
                        return;
                    }
                }
            }
            // remember old sort field
            String sortf = Utils.jsonToString(m.getJSONArray("flds").getJSONObject(m.getInt("sortf")));
            // move
            l.remove(oldidx);
            l.add(idx, field);
            m.put("flds", new JSONArray(l));
            // restore sort field
            ja = m.getJSONArray("flds");
            for (int i = 0; i < ja.length(); ++i) {
                if (Utils.jsonToString(ja.getJSONObject(i)).equals(sortf)) {
                    m.put("sortf", i);
                    break;
                }
            }
            _updateFieldOrds(m);
            save(m);
            _transformFields(m, new TransformFieldMove(idx, oldidx));
            renameField(m, field, null);
        } catch (JSONException e) {
            throw new RuntimeException(e);
        }

    }

    class TransformFieldMove implements TransformFieldVisitor {
        private int idx;
        private int oldidx;

        public TransformFieldMove(int _idx, int _oldidx) {
            idx = _idx;
            oldidx = _oldidx;
        }

        @Override
        public String[] transform(String[] fields) {
            String val = fields[oldidx];
            ArrayList<String> fl = new ArrayList<String>(Arrays.asList(fields));
            fl.remove(oldidx);
            fl.add(idx, val);
            return fl.toArray(new String[] {});
        }
    }

    public void renameField(JSONObject m, JSONObject field, String newName) {
        mCol.modSchema();
        try {
            String pat = String.format("\\{\\{([:#^/]|[^:#/^}][^:}]*?:|)%s\\}\\}",
                    Pattern.quote(field.getString("name")));
            if (newName == null) {
                newName = "";
            }
            String repl = "{{$1" + newName + "}}";

            JSONArray tmpls = m.getJSONArray("tmpls");
            for (int i = 0; i < tmpls.length(); ++i) {
                JSONObject t = tmpls.getJSONObject(i);
                for (String fmt : new String[] { "qfmt", "afmt" }) {
                    if (!newName.equals("")) {
                        t.put(fmt, t.getString(fmt).replaceAll(pat, repl));
                    } else {
                        t.put(fmt, t.getString(fmt).replaceAll(pat, ""));
                    }
                }
            }
            field.put("name", newName);
        } catch (JSONException e) {
            throw new RuntimeException(e);
        }
        save(m);
    }

    public void _updateFieldOrds(JSONObject m) {
        JSONArray ja;
        try {
            ja = m.getJSONArray("flds");
            for (int i = 0; i < ja.length(); i++) {
                JSONObject f = ja.getJSONObject(i);
                f.put("ord", i);
            }
        } catch (JSONException e) {
            throw new RuntimeException(e);
        }
    }

    interface TransformFieldVisitor {
        public String[] transform(String[] fields);
    }

    public void _transformFields(JSONObject m, TransformFieldVisitor fn) {
        // model hasn't been added yet?
        try {
            if (m.getLong("id") == 0) {
                return;
            }
            ArrayList<Object[]> r = new ArrayList<Object[]>();
            Cursor cur = null;

            try {
                cur = mCol.getDb().getDatabase()
                        .rawQuery("select id, flds from notes where mid = " + m.getLong("id"), null);
                while (cur.moveToNext()) {
                    r.add(new Object[] {
                            Utils.joinFields((String[]) fn.transform(Utils.splitFields(cur.getString(1)))),
                            Utils.intNow(), mCol.usn(), cur.getLong(0) });
                }
            } finally {
                if (cur != null) {
                    cur.close();
                }
            }
            mCol.getDb().executeMany("update notes set flds=?,mod=?,usn=? where id = ?", r);
        } catch (JSONException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * Templates ***********************************************************************************************
     */

    public JSONObject newTemplate(String name) {
        JSONObject t;
        try {
            t = new JSONObject(defaultTemplate);
            t.put("name", name);
        } catch (JSONException e) {
            throw new RuntimeException(e);
        }
        return t;
    }

    /** Note: should col.genCards() afterwards. */
    public void addTemplate(JSONObject m, JSONObject template) {
        try {
            if (m.getLong("id") != 0) {
                mCol.modSchema();
            }
            JSONArray ja = m.getJSONArray("tmpls");
            ja.put(template);
            m.put("tmpls", ja);
            _updateTemplOrds(m);
            save(m);
        } catch (JSONException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * Removing a template
     * 
     * @return False if removing template would leave orphan notes.
     */
    public boolean remTemplate(JSONObject m, JSONObject template) {
        try {
            assert (m.getJSONArray("tmpls").length() > 1);
            // find cards using this template
            JSONArray ja = m.getJSONArray("tmpls");
            int ord = -1;
            for (int i = 0; i < ja.length(); ++i) {
                if (ja.get(i).equals(template)) {
                    ord = i;
                    break;
                }
            }
            String sql = "select c.id from cards c, notes f where c.nid=f.id and mid = " + m.getLong("id")
                    + " and ord = " + ord;
            long[] cids = Utils.toPrimitive(mCol.getDb().queryColumn(Long.class, sql, 0));
            // all notes with this template must have at least two cards, or we could end up creating orphaned notes
            sql = "select nid, count() from cards where nid in (select nid from cards where id in "
                    + Utils.ids2str(cids) + ") group by nid having count() < 2 limit 1";
            if (mCol.getDb().queryScalar(sql, false) != 0) {
                return false;
            }
            // ok to proceed; remove cards
            mCol.modSchema();
            mCol.remCards(cids);
            // shift ordinals
            mCol.getDb().execute(
                    "update cards set ord = ord - 1, usn = ?, mod = ? where nid in (select id from notes where mid = ?) and ord > ?",
                    new Object[] { mCol.usn(), Utils.intNow(), m.getLong("id"), ord });
            JSONArray tmpls = m.getJSONArray("tmpls");
            JSONArray ja2 = new JSONArray();
            for (int i = 0; i < tmpls.length(); ++i) {
                if (template.equals(tmpls.getJSONObject(i))) {
                    continue;
                }
                ja2.put(tmpls.get(i));
            }
            m.put("tmpls", ja2);
        } catch (JSONException e) {
            throw new RuntimeException(e);
        }
        _updateTemplOrds(m);
        save(m);
        return true;
    }

    public void _updateTemplOrds(JSONObject m) {
        JSONArray ja;
        try {
            ja = m.getJSONArray("tmpls");
            for (int i = 0; i < ja.length(); i++) {
                JSONObject f = ja.getJSONObject(i);
                f.put("ord", i);
            }
        } catch (JSONException e) {
            throw new RuntimeException(e);
        }
    }

    public void moveTemplate(JSONObject m, JSONObject template, int idx) {
        try {
            JSONArray ja = m.getJSONArray("tmpls");
            int oldidx = -1;
            ArrayList<JSONObject> l = new ArrayList<JSONObject>();
            HashMap<Integer, Integer> oldidxs = new HashMap<Integer, Integer>();
            for (int i = 0; i < ja.length(); ++i) {
                if (ja.get(i).equals(template)) {
                    oldidx = i;
                    if (idx == oldidx) {
                        return;
                    }
                }
                JSONObject t = ja.getJSONObject(i);
                oldidxs.put(t.hashCode(), t.getInt("ord"));
                l.add(t);
            }
            l.remove(oldidx);
            l.add(idx, template);
            m.put("tmpls", new JSONArray(l));
            _updateTemplOrds(m);
            // generate change map - We use StringBuilder
            StringBuilder sb = new StringBuilder();
            ja = m.getJSONArray("tmpls");
            for (int i = 0; i < ja.length(); ++i) {
                JSONObject t = ja.getJSONObject(i);
                sb.append("when ord = ").append(oldidxs.get(t.hashCode())).append(" then ").append(t.getInt("ord"));
                if (i != ja.length() - 1) {
                    sb.append(" ");
                }
            }
            // apply
            save(m);
            mCol.getDb().execute(
                    "update cards set ord = (case " + sb.toString()
                            + " end),usn=?,mod=? where nid in (select id from notes where mid = ?)",
                    new Object[] { mCol.usn(), Utils.intNow(), m.getLong("id") });
        } catch (JSONException e) {
            throw new RuntimeException(e);
        }
    }

    private void _syncTemplates(JSONObject m) {
        ArrayList<Long> rem = mCol.genCards(Utils.arrayList2array(nids(m)));
    }

    // public TreeMap<Integer, JSONObject> getTemplates() {
    // return mTemplateMap;
    // }
    //
    //
    // public JSONObject getTemplate(int ord) {
    // return mTemplateMap.get(ord);
    // }

    /**
     * Get a compiled template, create it if missing or if args != null
     * 
     * @param modelId
     * @param ord
     * @param args Pass it as [qfmt, afmt] to use custom format, or [] to use the format from model
     * @return
     */
    public Template getCmpldTemplate(String format) {
        if (!mCmpldTemplateMap.containsKey(format)) {
            mCmpldTemplateMap.put(format, Mustache.compiler().compile(format));
        }

        return mCmpldTemplateMap.get(format);
    }

    // not in libanki
    // Handle fields fetched from templates and any anki-specific formatting
    protected static final String clozeReg = "\\{\\{c%s::(.*?)(::(.*?))?\\}\\}";

    protected static class fieldParser implements Mustache.VariableFetcher {
        private Map<String, String> _fields;

        public fieldParser(Map<String, String> fields) {
            _fields = fields;
        }

        public Object get(Object ctx, String tag_name) throws Exception {
            if (tag_name.length() == 0) {
                return null;
            }
            String txt = _fields.get(tag_name);
            if (txt != null) {
                return txt;
            }

            // field modifiers
            String[] parts = tag_name.split(":", 3);
            String mod = null, extra = null, tag = null;
            if (parts.length == 1 || parts[0].equals("")) {
                return null;
            } else if (parts.length == 2) {
                mod = parts[0];
                tag = parts[1];
            } else if (parts.length == 3) {
                mod = parts[0];
                extra = parts[1];
                tag = parts[2];
            }

            txt = _fields.get(tag);

            // Log.d(AnkiDroidApp.TAG, "Processing field modifier " + mod + ": extra = " + extra + ", field " + tag + " = " + txt);

            // built-in modifiers
            if (mod.equals("text")) {
                // strip html
                if (txt != null && txt.length() > 0) {
                    return Utils.stripHTML(txt);
                }
                return "";
            } else if (mod.equals("type")) {
                // type answer field; convert it to [[type:...]] for the gui code to process
                return "[[" + tag_name + "]]";
            } else if (mod.equals("cq") || mod.equals("ca")) {
                // cloze deletion
                if (txt != null && txt.length() != 0 && extra != null && extra.length() != 0) {
                    return clozeText(txt, extra, mod.charAt(1));
                } else {
                    return "";
                }
            } else {
                // hook-based field modifier
                if (txt == null) {
                    txt = (String) AnkiDroidApp.getHooks().runFilter("fmod_" + mod, "", extra,
                            AnkiDroidApp.getAppResources(), tag, tag_name);
                } else {
                    txt = (String) AnkiDroidApp.getHooks().runFilter("fmod_" + mod, txt, extra,
                            AnkiDroidApp.getAppResources(), tag, tag_name);
                }
                if (txt == null) {
                    return "{unknown field " + tag_name + "}";
                }
                return txt;
            }
        }

        private static String clozeText(String txt, String ord, char type) {
            Matcher m = Pattern.compile(String.format(Locale.US, clozeReg, ord)).matcher(txt);
            if (!m.find()) {
                return "";
            }
            // replace chozen cloze with type
            if (type == 'q') {
                if (m.group(3) != null && m.group(3).length() != 0) {
                    txt = m.replaceAll("<span class=cloze>[$3]</span>");
                } else {
                    txt = m.replaceAll("<span class=cloze>[...]</span>");
                }
            } else {
                txt = m.replaceAll("<span class=cloze>$1</span>");
            }
            // and display other clozes normally
            return txt.replaceAll(String.format(Locale.US, clozeReg, ".*?"), "$1");
        }
    }

    /**
     * Model changing ***********************************************************************************************
     */

    /**
     * Change a model
     * @param m The model to change.
     * @param nids The list of notes that the change applies to.
     * @param newModel For replacing the old model with another one. Should be self if the model is not changing
     * @param fmap Field map for switching fields. This is ord->ord and there should not be duplicate targets 
     * @param cmap Field map for switching fields. This is ord->ord and there should not be duplicate targets
     */
    public void change(JSONObject m, long[] nids, JSONObject newModel, Map<Integer, Integer> fmap,
            Map<Integer, Integer> cmap) {
        mCol.modSchema();
        try {
            assert (newModel.getLong("id") == m.getLong("id")) || (fmap != null && cmap != null);
        } catch (JSONException e) {
            throw new RuntimeException(e);
        }
        if (fmap != null) {
            _changeNotes(nids, newModel, fmap);
        }
        if (cmap != null) {
            _changeCards(nids, m, newModel, cmap);
        }
        mCol.genCards(nids);
    }

    private void _changeNotes(long[] nids, JSONObject newModel, Map<Integer, Integer> map) {
        List<Object[]> d = new ArrayList<Object[]>();
        int nfields;
        long mid;
        try {
            nfields = newModel.getJSONArray("flds").length();
            mid = newModel.getLong("id");
        } catch (JSONException e) {
            throw new RuntimeException(e);
        }
        Cursor cur = null;
        try {
            cur = mCol.getDb().getDatabase()
                    .rawQuery("select id, flds from notes where id in ".concat(Utils.ids2str(nids)), null);
            while (cur.moveToNext()) {
                long nid = cur.getLong(0);
                String[] flds = Utils.splitFields(cur.getString(1));
                Map<Integer, String> newflds = new HashMap<Integer, String>();

                for (Integer old : map.keySet()) {
                    newflds.put(map.get(old), flds[old]);
                }
                List<String> flds2 = new ArrayList<String>();
                for (int c = 0; c < nfields; ++c) {
                    if (newflds.containsKey(c)) {
                        flds2.add(newflds.get(c));
                    } else {
                        flds2.add("");
                    }
                }
                String joinedFlds = Utils.joinFields(flds2.toArray(new String[] {}));
                d.add(new Object[] { joinedFlds, mid, Utils.intNow(), mCol.usn(), nid });
            }
        } finally {
            if (cur != null) {
                cur.close();
            }
        }
        mCol.getDb().executeMany("update notes set flds=?,mid=?,mod=?,usn=? where id = ?", d);
        mCol.updateFieldCache(nids);
    }

    private void _changeCards(long[] nids, JSONObject oldModel, JSONObject newModel, Map<Integer, Integer> map) {
        List<Object[]> d = new ArrayList<Object[]>();
        List<Long> deleted = new ArrayList<Long>();
        Cursor cur = null;
        int omType;
        int nmType;
        int nflds;
        try {
            omType = oldModel.getInt("type");
            nmType = newModel.getInt("type");
            nflds = newModel.getJSONArray("tmpls").length();
        } catch (JSONException e) {
            throw new RuntimeException(e);
        }
        try {
            cur = mCol.getDb().getDatabase()
                    .rawQuery("select id, ord from cards where nid in ".concat(Utils.ids2str(nids)), null);
            while (cur.moveToNext()) {
                // if the src model is a cloze, we ignore the map, as the gui doesn't currently
                // support mapping them
                Integer newOrd;
                long cid = cur.getLong(0);
                int ord = cur.getInt(1);
                if (omType == Sched.MODEL_CLOZE) {
                    newOrd = cur.getInt(1);
                    if (nmType != Sched.MODEL_CLOZE) {
                        // if we're mapping to a regular note, we need to check if
                        // the destination ord is valid
                        if (nflds <= ord) {
                            newOrd = null;
                        }
                    }
                } else {
                    // mapping from a regular note, so the map should be valid
                    newOrd = map.get(ord);
                }
                if (newOrd != null) {
                    d.add(new Object[] { newOrd, mCol.usn(), Utils.intNow(), cid });
                } else {
                    deleted.add(cid);
                }
            }
        } finally {
            if (cur != null) {
                cur.close();
            }
        }
        mCol.getDb().executeMany("update cards set ord=?,usn=?,mod=? where id=?", d);
        mCol.remCards(Utils.toPrimitive(deleted));
    }

    /**
     * Schema hash ***********************************************************************************************
     */

    /** Return a hash of the schema, to see if models are compatible. */
    public String scmhash(JSONObject m) {
        String s = "";
        try {
            JSONArray flds = m.getJSONArray("flds");
            for (int i = 0; i < flds.length(); ++i) {
                s += flds.getJSONObject(i).getString("name");
            }
            JSONArray tmpls = m.getJSONArray("tmpls");
            for (int i = 0; i < tmpls.length(); ++i) {
                JSONObject t = tmpls.getJSONObject(i);
                s += t.getString("name");
                s += t.getString("qfmt");
                s += t.getString("afmt");
            }
        } catch (JSONException e) {
            throw new RuntimeException(e);
        }
        return Utils.checksum(s);
    }

    /**
     * Required field/text cache
     * ***********************************************************************************************
     */

    private void _updateRequired(JSONObject m) {
        try {
            if (m.getInt("type") == Sched.MODEL_CLOZE) {
                // nothing to do
                return;
            }
            JSONArray req = new JSONArray();
            ArrayList<String> flds = new ArrayList<String>();
            JSONArray fields;
            fields = m.getJSONArray("flds");
            for (int i = 0; i < fields.length(); i++) {
                flds.add(fields.getJSONObject(i).getString("name"));
            }
            JSONArray templates = m.getJSONArray("tmpls");
            for (int i = 0; i < templates.length(); i++) {
                JSONObject t = templates.getJSONObject(i);
                Object[] ret = _reqForTemplate(m, flds, t);
                JSONArray r = new JSONArray();
                r.put(t.getInt("ord"));
                r.put(ret[0]);
                r.put(ret[1]);
                req.put(r);
            }
            m.put("req", req);
        } catch (JSONException e) {
            throw new RuntimeException(e);
        }
    }

    private Object[] _reqForTemplate(JSONObject m, ArrayList<String> flds, JSONObject t) {
        try {
            ArrayList<String> a = new ArrayList<String>();
            ArrayList<String> b = new ArrayList<String>();
            for (String f : flds) {
                a.add("ankiflag");
                b.add("");
            }
            Object[] data;
            data = new Object[] { 1l, 1l, m.getLong("id"), 1l, t.getInt("ord"), "",
                    Utils.joinFields(a.toArray(new String[a.size()])) };
            String full = mCol._renderQA(data).get("q");
            data = new Object[] { 1l, 1l, m.getLong("id"), 1l, t.getInt("ord"), "",
                    Utils.joinFields(b.toArray(new String[b.size()])) };
            String empty = mCol._renderQA(data).get("q");
            // if full and empty are the same, the template is invalid and there is no way to satisfy it
            if (full.equals(empty)) {
                return new Object[] { "none", new JSONArray(), new JSONArray() };
            }
            String type = "all";
            JSONArray req = new JSONArray();
            ArrayList<String> tmp = new ArrayList<String>();
            for (int i = 0; i < flds.size(); i++) {
                tmp.clear();
                tmp.addAll(a);
                tmp.set(i, "");
                data[6] = Utils.joinFields(tmp.toArray(new String[tmp.size()]));
                // if no field content appeared, field is required
                if (!mCol._renderQA(data, new ArrayList<String>()).get("q").contains("ankiflag")) {
                    req.put(i);
                }
            }
            if (req.length() > 0) {
                return new Object[] { type, req };
            }
            // if there are no required fields, switch to any mode
            type = "any";
            req = new JSONArray();
            for (int i = 0; i < flds.size(); i++) {
                tmp.clear();
                tmp.addAll(b);
                tmp.set(i, "1");
                data[6] = Utils.joinFields(tmp.toArray(new String[tmp.size()]));
                // if not the same as empty, this field can make the card non-blank
                if (!mCol._renderQA(data).get("q").equals(empty)) {
                    req.put(i);
                }
            }
            return new Object[] { type, req };
        } catch (JSONException e) {
            throw new RuntimeException(e);
        }
    }

    /** Given a joined field string, return available template ordinals */
    public ArrayList<Integer> availOrds(JSONObject m, String flds) {
        try {
            if (m.getInt("type") == Sched.MODEL_CLOZE) {
                return _availClozeOrds(m, flds);
            }
            String[] fields = Utils.splitFields(flds);
            for (String f : fields) {
                f = f.trim();
            }
            ArrayList<Integer> avail = new ArrayList<Integer>();
            JSONArray reqArray = m.getJSONArray("req");
            for (int i = 0; i < reqArray.length(); i++) {
                JSONArray sr = reqArray.getJSONArray(i);

                int ord = sr.getInt(0);
                String type = sr.getString(1);
                JSONArray req = sr.getJSONArray(2);

                if (type.equals("none")) {
                    // unsatisfiable template
                    continue;
                } else if (type.equals("all")) {
                    // AND requirement?
                    boolean ok = true;
                    for (int j = 0; j < req.length(); j++) {
                        int idx = req.getInt(j);
                        if (fields[idx] == null || fields[idx].length() == 0) {
                            // missing and was required
                            ok = false;
                            break;
                        }
                    }
                    if (!ok) {
                        continue;
                    }
                } else if (type.equals("any")) {
                    // OR requirement?
                    boolean ok = false;
                    for (int j = 0; j < req.length(); j++) {
                        int idx = req.getInt(j);
                        if (fields[idx] != null && fields[idx].length() != 0) {
                            // missing and was required
                            ok = true;
                            break;
                        }
                    }
                    if (!ok) {
                        continue;
                    }
                }
                avail.add(ord);
            }
            return avail;
        } catch (JSONException e) {
            throw new RuntimeException(e);
        }
    }

    public ArrayList<Integer> _availClozeOrds(JSONObject m, String flds) {
        return _availClozeOrds(m, flds, true);
    }

    public ArrayList<Integer> _availClozeOrds(JSONObject m, String flds, boolean allowEmpty) {
        String[] sflds = Utils.splitFields(flds);
        Map<String, Pair<Integer, JSONObject>> map = fieldMap(m);
        Set<Integer> ords = new HashSet<Integer>();
        Matcher matcher1 = null;
        try {
            matcher1 = fClozePattern1.matcher(m.getJSONArray("tmpls").getJSONObject(0).getString("qfmt"));
            // Libanki makes two finds for each case of the cloze tags, but we embed both in the pattern.
            // Please note, that this approach is not 100% correct, as we allow cases like {{cloze:...%>
        } catch (JSONException e) {
            throw new RuntimeException(e);
        }
        while (matcher1.find()) {
            String fname = matcher1.group(1);
            if (!map.containsKey(fname)) {
                continue;
            }
            int ord = map.get(fname).first;
            Matcher matcher2 = fClozePattern2.matcher(sflds[ord]);
            while (matcher2.find()) {
                ords.add(Integer.parseInt(matcher2.group(1)) - 1);
            }
        }
        if (ords.contains(-1)) {
            ords.remove(-1);
        }
        if (ords.isEmpty() && allowEmpty) {
            // empty clozes use first ord
            return new ArrayList<Integer>(Arrays.asList(new Integer[] { 0 }));
        }
        return new ArrayList<Integer>(ords);
    }

    /**
     * Sync handling ***********************************************************************************************
     */

    public void beforeUpload() {
        try {
            for (JSONObject m : all()) {
                m.put("usn", 0);
            }
        } catch (JSONException e) {
            throw new RuntimeException(e);
        }
        save();
    }

    /**
     * Routines from Stdmodels.py
     * ***********************************************************************************************
     */

    public static JSONObject addBasicModel(Collection col) {
        return addBasicModel(col, "Basic");
    }

    public static JSONObject addBasicModel(Collection col, String name) {
        Models mm = col.getModels();
        JSONObject m = mm.newModel(name);
        JSONObject fm = mm.newField("Front");
        mm.addField(m, fm);
        fm = mm.newField("Back");
        mm.addField(m, fm);
        JSONObject t = mm.newTemplate("Card 1");
        try {
            t.put("qfmt", "{{Front}}");
            t.put("afmt", "{{FrontSide}}\n\n<hr id=answer>\n\n{{Back}}");
        } catch (JSONException e) {
            throw new RuntimeException(e);
        }
        mm.addTemplate(m, t);
        mm.add(m);
        return m;
    }

    /* Forward & Reverse */

    public static JSONObject addForwardReverse(Collection col) {
        String name = "Basic (and reversed card)";
        Models mm = col.getModels();
        JSONObject m = addBasicModel(col);
        try {
            m.put("name", name);
            JSONObject t = mm.newTemplate("Card 2");
            t.put("qfmt", "{{Back}}");
            t.put("afmt", "{{FrontSide}}\n\n<hr id=answer>\n\n{{Front}}");
            mm.addTemplate(m, t);
        } catch (JSONException e) {
            throw new RuntimeException(e);
        }
        return m;
    }

    /* Forward & Optional Reverse */

    public static JSONObject addForwardOptionalReverse(Collection col) {
        String name = "Basic (optional reversed card)";
        Models mm = col.getModels();
        JSONObject m = addBasicModel(col);
        try {
            m.put("name", name);
            JSONObject fm = mm.newField("Add Reverse");
            mm.addField(m, fm);
            JSONObject t = mm.newTemplate("Card 2");
            t.put("qfmt", "{{#Add Reverse}}{{Back}}{{/Add Reverse}}");
            t.put("afmt", "{{FrontSide}}\n\n<hr id=answer>\n\n{{Front}}");
            mm.addTemplate(m, t);
        } catch (JSONException e) {
            throw new RuntimeException(e);
        }
        return m;
    }

    public static JSONObject addClozeModel(Collection col) {
        Models mm = col.getModels();
        JSONObject m = mm.newModel("Cloze");
        try {
            m.put("type", Sched.MODEL_CLOZE);
            String txt = "Text";
            JSONObject fm = mm.newField(txt);
            mm.addField(m, fm);
            fm = mm.newField("Extra");
            mm.addField(m, fm);
            JSONObject t = mm.newTemplate("Cloze");
            String fmt = "{{cloze:" + txt + "}}";
            m.put("css", m.getString("css") + ".cloze {" + "font-weight: bold;" + "color: blue;" + "}");
            t.put("qfmt", fmt);
            t.put("afmt", fmt + "<br>\n{{Extra}}");
            mm.addTemplate(m, t);
            mm.add(m);
        } catch (JSONException e) {
            throw new RuntimeException(e);
        }
        return m;
    }

    /**
     * Other stuff NOT IN LIBANKI
     * ***********************************************************************************************
     */

    public void setChanged() {
        mChanged = true;
    }

    public HashMap<Long, HashMap<Integer, String>> getTemplateNames() {
        HashMap<Long, HashMap<Integer, String>> result = new HashMap<Long, HashMap<Integer, String>>();
        for (JSONObject m : mModels.values()) {
            JSONArray templates;
            try {
                templates = m.getJSONArray("tmpls");
                HashMap<Integer, String> names = new HashMap<Integer, String>();
                for (int i = 0; i < templates.length(); i++) {
                    JSONObject t = templates.getJSONObject(i);
                    names.put(t.getInt("ord"), t.getString("name"));
                }
                result.put(m.getLong("id"), names);
            } catch (JSONException e) {
                throw new RuntimeException(e);
            }
        }
        return result;
    }

    /**
     * @return the ID
     */
    public int getId() {
        return mId;
    }

    /**
     * @return the name
     */
    public String getName() {
        return mName;
    }

    public HashMap<Long, JSONObject> getModels() {
        return mModels;
    }

    /** Validate model entries. */
    public boolean validateModel() {
        Iterator<Entry<Long, JSONObject>> iterator = mModels.entrySet().iterator();
        while (iterator.hasNext()) {
            if (!validateBrackets(iterator.next().getValue())) {
                return false;
            }
        }
        return true;
    }

    /** Check if there is a right bracket for every left bracket. */
    private boolean validateBrackets(JSONObject value) {
        String s = value.toString();
        int count = 0;
        boolean inQuotes = false;
        char[] ar = s.toCharArray();
        for (int i = 0; i < ar.length; i++) {
            char c = ar[i];
            // if in quotes, do not count
            if (c == '"' && (i == 0 || (ar[i - 1] != '\\'))) {
                inQuotes = !inQuotes;
                continue;
            }
            if (inQuotes) {
                continue;
            }
            switch (c) {
            case '{':
                count++;
                break;
            case '}':
                count--;
                if (count < 0) {
                    return false;
                }
                break;
            }
        }
        return (count == 0);
    }
}