public class ZulipApp extends Application {
    private static final String API_KEY = "api_key";
    private static final String EMAIL = "email";
    private static final String USER_AGENT = "ZulipAndroid";
    private static final String DEFAULT_SERVER_URL = "https://api.zulip.com/";
    private static ZulipApp instance;
    /**
     * Mapping of email address to presence information for that user. This is
     * updated every 2 minutes by a background thread (see AsyncStatusUpdate)
     */
    public final Map<String, Presence> presences = new ConcurrentHashMap<>();
    /**
     * Queue of message ids to be marked as read. This queue should be emptied
     * every couple of seconds
     */
    public final Queue<Integer> unreadMessageQueue = new ConcurrentLinkedQueue<>();

    // This object's intrinsic lock is used to prevent multiple threads from
    // making conflicting updates to ranges
    public Object updateRangeLock = new Object();
    public String tester;
    private Person you;
    private SharedPreferences settings;
    private String api_key;

    private int max_message_id;
    private DatabaseHelper databaseHelper;
    private ZulipServices zulipServices;
    private ZulipServices uploadServices;
    private ReferenceObjectCache objectCache;
    private ZulipActivity zulipActivity;
    /**
     * Handler to manage batching of unread messages
     */
    private Handler unreadMessageHandler;
    private String eventQueueId;
    private int lastEventId;
    private int pointer;
    private Gson gson;

    public static ZulipApp get() {
        return instance;
    }

    private static void setInstance(ZulipApp instance) {
        ZulipApp.instance = instance;
    }

    public ZulipActivity getZulipActivity() {
        return zulipActivity;
    }

    public void setZulipActivity(ZulipActivity zulipActivity) {
        this.zulipActivity = zulipActivity;
    }

    @Override
    public void onCreate() {
        super.onCreate();
        if (!BuildConfig.DEBUG)
            Fabric.with(this, new Crashlytics());
        ZulipApp.setInstance(this);
        // This used to be from HumbugActivity.getPreferences, so we keep that
        // file name.
        this.settings = getSharedPreferences("HumbugActivity", Context.MODE_PRIVATE);
        //check for auto theme is enabled or not
        if (getSettings().getBoolean(Constants.AUTO_NIGHT_THEME, false)) {
            AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_AUTO);
        }
        max_message_id = settings.getInt("max_message_id", -1);
        eventQueueId = settings.getString("eventQueueId", null);
        lastEventId = settings.getInt("lastEventId", -1);
        pointer = settings.getInt("pointer", -1);


        this.api_key = settings.getString(API_KEY, null);

        if (api_key != null) {
            afterLogin();
        }

        // create unread message queue
        unreadMessageHandler = new Handler(new Handler.Callback() {
            @Override
            public boolean handleMessage(android.os.Message message) {
                if (message.what == 0) {
                    AsyncUnreadMessagesUpdate task = new AsyncUnreadMessagesUpdate(ZulipApp.this);
                    task.execute();
                }

                // documentation doesn't say what this value does for
                // Handler.Callback,
                // and Handler.handleMessage returns void
                // so this just returns true.
                return true;
            }
        });
    }

    public int getAppVersion() {
        try {
            PackageInfo packageInfo = this.getPackageManager().getPackageInfo(
                    this.getPackageName(), 0);
            return packageInfo.versionCode;
        } catch (NameNotFoundException e) {
            throw new RuntimeException("Could not get package version: " + e);
        }
    }

    public ObjectCache getObjectCache() {
        if (objectCache == null) {
            objectCache = new ReferenceObjectCache(true);
        }
        return objectCache;
    }

    private void afterLogin() {
        String email = settings.getString(EMAIL, null);
        setEmail(email);
        setupEmoji();
    }

    public ZulipServices getZulipServices() {
        if (zulipServices == null) {
            HttpLoggingInterceptor logging = new HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY);
            zulipServices = new Retrofit.Builder()
                    .client(new OkHttpClient.Builder().readTimeout(60, TimeUnit.SECONDS)
                            .addInterceptor(new ZulipInterceptor())
                            .addInterceptor(logging)
                            .build())
                    .addConverterFactory(GsonConverterFactory.create(getGson()))
                    .baseUrl(getServerURI())
                    .build()
                    .create(ZulipServices.class);
        }
        return zulipServices;
    }

    public void setZulipServices(ZulipServices zulipServices) {
        this.zulipServices = zulipServices;
    }

    public ZulipServices getUploadServices() {
        if (uploadServices == null) {
            HttpLoggingInterceptor logging = new HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.HEADERS);
            uploadServices = new Retrofit.Builder()
                    .client(new OkHttpClient.Builder().readTimeout(60, TimeUnit.SECONDS)
                            .addInterceptor(new ZulipInterceptor())
                            .addInterceptor(logging)
                            .build())
                    .addConverterFactory(GsonConverterFactory.create(getGson()))
                    .baseUrl(getServerURI())
                    .build()
                    .create(ZulipServices.class);
        }
        return uploadServices;
    }

    public Gson getGson() {
        if (gson == null) {
            gson = buildGson();
        }
        return gson;
    }

    private Gson buildGson() {
        final Gson naiveGson = new GsonBuilder()
                .registerTypeAdapter(Date.class, new JsonDeserializer<Date>() {
                    public Date deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
                        return new Date(json.getAsJsonPrimitive().getAsLong() * 1000);
                    }
                })
                .registerTypeAdapter(MessageType.class, new JsonDeserializer<MessageType>() {
                    @Override
                    public MessageType deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
                        return json.getAsString().equalsIgnoreCase("stream") ? MessageType.STREAM_MESSAGE : MessageType.PRIVATE_MESSAGE;
                    }
                })
                .create();
        final Gson nestedGson = new GsonBuilder()
                .registerTypeAdapter(Message.class, new JsonDeserializer<Message>() {
                    @Override
                    public Message deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {

                        if (BuildConfig.DEBUG) {
                            Log.d("RAW MESSAGES", json.toString());
                        }
                        Message genMess;
                        if ("stream".equalsIgnoreCase(json.getAsJsonObject().get("type").getAsString())) {
                            Message.ZulipStreamMessage msg = naiveGson.fromJson(json, Message.ZulipStreamMessage.class);
                            msg.setRecipients(msg.getDisplayRecipient());
                            genMess = msg;
                        } else {
                            Message.ZulipDirectMessage msg = naiveGson.fromJson(json, Message.ZulipDirectMessage.class);
                            if (msg.getDisplayRecipient() != null) {
                                // set the correct person id for message recipients
                                List<Person> people = msg.getDisplayRecipient();
                                for (Person person : people) {
                                    // this is needed as the json field name for person id is
                                    // "user_id" in realm configuration response and "id" in
                                    // GET v1/messages response.
                                    person.setId(person.getRecipientId());
                                }

                                msg.setRecipients(people.toArray(new Person[people.size()]));
                            }

                            msg.setContent(Message.formatContent(msg.getFormattedContent(), ZulipApp.get()).toString());
                            genMess = msg;
                        }
                        if (genMess._history != null && genMess._history.size() != 0) {
                            genMess.updateFromHistory(genMess._history.get(0));
                        }
                        return genMess;
                    }
                }).create();

        return new GsonBuilder()
                .registerTypeAdapter(UserConfigurationResponse.class, new TypeAdapter<UserConfigurationResponse>() {

                    @Override
                    public void write(JsonWriter out, UserConfigurationResponse value) throws IOException {
                        nestedGson.toJson(nestedGson.toJsonTree(value), out);
                    }

                    @Override
                    public UserConfigurationResponse read(JsonReader in) throws IOException {
                        return nestedGson.fromJson(in, UserConfigurationResponse.class);
                    }
                })
                .registerTypeAdapter(EventsBranch.class, new JsonDeserializer<EventsBranch>() {
                    @Override
                    public EventsBranch deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
                        EventsBranch invalid = nestedGson.fromJson(json, EventsBranch.class);
                        if (BuildConfig.DEBUG) {
                            Log.d("RAW EVENTS", json.toString());
                        }
                        Class<? extends EventsBranch> event = EventsBranch.BranchType.fromRawType(invalid);
                        if (event != null) {
                            if (event.getSimpleName().equalsIgnoreCase("SubscriptionWrapper")) {
                                // check operation
                                if (SubscriptionWrapper.OPERATION_ADD.equalsIgnoreCase(json.getAsJsonObject().get("op").getAsString()) ||
                                        SubscriptionWrapper.OPERATION_REMOVE.equalsIgnoreCase(json.getAsJsonObject().get("op").getAsString()) ||
                                        SubscriptionWrapper.OPERATION_UPDATE.equalsIgnoreCase(json.getAsJsonObject().get("op").getAsString())) {
                                    Type type = new TypeToken<SubscriptionWrapper<Stream>>() {
                                    }.getType();
                                    return nestedGson.fromJson(json, type);
                                } else {
                                    Type type = new TypeToken<SubscriptionWrapper<String>>() {
                                    }.getType();
                                    return nestedGson.fromJson(json, type);
                                }
                            }
                            return nestedGson.fromJson(json, event);
                        }
                        Log.w("GSON", "Attempted to deserialize and unregistered EventBranch... See EventBranch.BranchType");
                        return invalid;
                    }
                })
                .registerTypeAdapter(Message.class, nestedGson.getAdapter(Message.class))
                .create();
    }

    /**
     * Fills the Emoji Table with the existing emoticons saved in the assets folder.
     */
    private void setupEmoji() {
        try {
            final RuntimeExceptionDao<Emoji, Object> dao = getDao(Emoji.class);
            if (dao.queryForAll().size() != 0) return;
            final String emojis[] = getAssets().list("emoji");
            TransactionManager.callInTransaction(getDatabaseHelper()
                            .getConnectionSource(),
                    new Callable<Void>() {
                        public Void call() throws Exception {
                            for (String newEmoji : emojis) {
                                //currently emojis are in png format
                                newEmoji = newEmoji.replace(".png", "");
                                dao.create(new Emoji(newEmoji));
                            }
                            return null;
                        }
                    });
        } catch (SQLException | IOException e) {
            ZLog.logException(e);
        }
    }

    public Boolean isLoggedIn() {
        return this.api_key != null;
    }

    public void clearConnectionState() {
        setEventQueueId(null);
    }

    /**
     * Determines the server URI applicable for the user.
     *
     * @return either the production or staging server's URI
     */
    public String getServerURI() {
        return settings.getString("server_url", DEFAULT_SERVER_URL);
    }

    public String getServerHostUri() {
        String uri = settings.getString("server_url", DEFAULT_SERVER_URL);
        if (uri.contains("/api/")) {
            uri = uri.replace("/api/", "/");
        } else if (uri.contains("/api.")) {
            uri = uri.replace("/api.", "/");
        }
        return uri;
    }

    public String getApiKey() {
        return api_key;
    }

    public String getUserAgent() {
        try {
            return ZulipApp.USER_AGENT
                    + "/"
                    + getPackageManager().getPackageInfo(getPackageName(), 0).versionName;
        } catch (NameNotFoundException e) {
            // This should… never happen, but okay.
            ZLog.logException(e);
            return ZulipApp.USER_AGENT + "/unknown";
        }
    }

    public void setServerURL(String serverURL) {
        Editor ed = this.settings.edit();
        ed.putString("server_url", serverURL);
        ed.apply();
    }

    public void setDontShowAppDialog(boolean value) {
        Editor ed = this.settings.edit();
        ed.putBoolean(Constants.DONT_SHOW_APP_UPDATE_DIALOG, value);
        ed.apply();
    }

    public void useDefaultServerURL() {
        setServerURL(DEFAULT_SERVER_URL);
    }

    public void setLoggedInApiKey(String apiKey, String email) {
        this.api_key = apiKey;
        Editor ed = this.settings.edit();
        ed.putString(EMAIL, email);
        ed.putString(API_KEY, apiKey);
        ed.apply();
        afterLogin();
    }

    public void logOut() {
        Editor ed = this.settings.edit();
        ed.remove(EMAIL);
        ed.remove(API_KEY);
        ed.remove("server_url");
        ed.apply();
        this.api_key = null;
        setEventQueueId(null);

        new GoogleAuthHelper().logOutGoogleAuth();
    }

    public String getEmail() {
        return you == null ? "" : you.getEmail();
    }

    public void setEmail(String email) {
        databaseHelper = new DatabaseHelper(this, email);
        this.you = Person.get(this, email);
    }

    public DatabaseHelper getDatabaseHelper() {
        return databaseHelper;
    }

    @SuppressWarnings("unchecked")
    public <C, T> RuntimeExceptionDao<C, T> getDao(Class<C> cls, boolean useCache) {
        try {
            RuntimeExceptionDao<C, T> ret = new RuntimeExceptionDao<>(
                    (Dao<C, T>) databaseHelper.getDao(cls));
            if (useCache) {
                ret.setObjectCache(getObjectCache());
            }
            return ret;
        } catch (SQLException e) {
            // Well that's sort of awkward.
            throw new RuntimeException(e);
        }
    }

    public <C, T> RuntimeExceptionDao<C, T> getDao(Class<C> cls) {
        return getDao(cls, false);
    }

    public void setContext(Context targetContext) {
        this.attachBaseContext(targetContext);
    }

    public String getEventQueueId() {
        return eventQueueId;
    }

    public void setEventQueueId(String eventQueueId) {
        this.eventQueueId = eventQueueId;
        Editor ed = settings.edit();
        ed.putString("eventQueueId", this.eventQueueId);
        ed.apply();
    }

    public int getLastEventId() {
        return lastEventId;
    }

    public void setLastEventId(int lastEventId) {
        this.lastEventId = lastEventId;
        Editor ed = settings.edit();
        ed.putInt("lastEventId", lastEventId);
        ed.apply();
    }

    public int getMaxMessageId() {
        return max_message_id;
    }

    public void setMaxMessageId(int maxMessageId) {
        this.max_message_id = maxMessageId;
        if (settings != null) {
            Editor ed = settings.edit();
            ed.putInt("max_message_id", maxMessageId);
            ed.apply();
        }
    }

    public int getPointer() {
        return pointer;
    }

    public void setPointer(int pointer) {
        this.pointer = pointer;
        Editor ed = settings.edit();
        ed.putInt("pointer", pointer);
        ed.apply();
    }

    public void resetDatabase() {
        databaseHelper.resetDatabase(databaseHelper.getWritableDatabase());
    }

    public void onResetDatabase() {
        setPointer(-1);
        setMaxMessageId(-1);
        setLastEventId(-1);
        setEventQueueId(null);
    }

    public void markMessageAsRead(Message message) {
        if (unreadMessageHandler == null) {
            Log.e("zulipApp",
                    "markMessageAsRead called before unreadMessageHandler was instantiated");
            return;
        }

        unreadMessageQueue.offer(message.getID());
        if (!unreadMessageHandler.hasMessages(0)) {
            unreadMessageHandler.sendEmptyMessageDelayed(0, 2000);
        }
    }

    public SharedPreferences getSettings() {
        return settings;
    }

    public Person getYou() {
        return you;
    }

    public void setYou(Person person) {
        this.you = person;
    }

    public void syncPointer(final int mID) {
        getZulipServices().updatePointer(Integer.toString(mID))
                .enqueue(new Callback<ResponseBody>() {
                    @Override
                    public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {
                        setPointer(mID);
                    }

                    @Override
                    public void onFailure(Call<ResponseBody> call, Throwable t) {
                        //do nothing.. don't want to mis-update the pointer.
                    }
                });
    }

    /**
     * Sets message content editing parameters
     *
     * @param seconds time limit in seconds for editing message
     * @param param   parameter indicating editing message is allowed or not
     */
    public void setMessageContentEditParams(int seconds, boolean param) {
        SharedPreferences preferences = getSettings();
        SharedPreferences.Editor editor = preferences.edit();

        //Firsts Checks if maximum content edit limit is already saved or not
        if (!preferences.getBoolean(Constants.IS_CONTENT_EDIT_PARAM_SAVED, false)) {
            editor.putBoolean(Constants.IS_EDITING_ALLOWED, param);
            editor.putInt(Constants.MAXIMUM_CONTENT_EDIT_LIMIT, seconds);
            editor.putBoolean(Constants.IS_CONTENT_EDIT_PARAM_SAVED, true);
            editor.apply();
        } else {
            //Check if any value is changed from server
            if (getSettings().getInt(Constants.MAXIMUM_CONTENT_EDIT_LIMIT,
                    Constants.DEFAULT_MAXIMUM_CONTENT_EDIT_LIMIT) != seconds) {
                //time is changed from server and update it locally too
                editor.putInt(Constants.MAXIMUM_CONTENT_EDIT_LIMIT, seconds);
                editor.apply();
            }
            if (getSettings().getBoolean(Constants.IS_EDITING_ALLOWED,
                    Constants.DEFAULT_EDITING_ALLOWED) != param) {
                //isEditingAllowed is changed from server and update it locally too
                editor.putBoolean(Constants.IS_EDITING_ALLOWED, param);
                editor.apply();
            }
        }
    }

    /**
     * Update preferred theme
     *
     * @param b true if night theme is preferred
     */
    public void makeNightThemeDefault(boolean nightTheme) {
        SharedPreferences.Editor editor = getSettings().edit();

        editor.putBoolean(Constants.NIGHT_THEME, nightTheme);
        editor.apply();
    }

    /**
     * update auto night theme preference
     *
     * @param value preference value
     */
    public void setAutoNightTheme(boolean value) {
        SharedPreferences.Editor editor = getSettings().edit();

        editor.putBoolean(Constants.AUTO_NIGHT_THEME, value);
        editor.apply();
    }
} public class NarrowFilterStream implements NarrowFilter {
    public static final Parcelable.Creator<NarrowFilterStream> CREATOR = new Parcelable.Creator<NarrowFilterStream>() {
        public NarrowFilterStream createFromParcel(Parcel in) {
            String[] pair = in.createStringArray();
            return new NarrowFilterStream(Stream.getByName(ZulipApp.get(),
                    pair[0]), pair[1]);
        }

        public NarrowFilterStream[] newArray(int size) {
            return new NarrowFilterStream[size];
        }
    };
    private Stream stream;
    private String subject;

    public NarrowFilterStream(Stream stream, String subject) {
        this.stream = stream;
        this.subject = subject;
    }

    public NarrowFilterStream(String stream, String subject) {
        this.stream = Stream.getByName(ZulipApp.get(), stream);
        this.subject = subject;
    }

    public int describeContents() {
        return 0;
    }

    public void writeToParcel(Parcel dest, int flags) {
        String[] pair = {stream.getName(), subject};
        dest.writeStringArray(pair);
    }

    @Override
    public Where<Message, Object> modWhere(Where<Message, Object> where)
            throws SQLException {
        where.eq(Message.STREAM_FIELD, new SelectArg(stream));
        if (subject != null) {
            where.and().eq(Message.SUBJECT_FIELD, new SelectArg(subject));
        }
        return where;
    }

    @Override
    public String toString() {
        try {
            return getJsonFilter();
        } catch (JSONException e) {
            return null;
        }
    }

    @Override
    public boolean matches(Message msg) {
        if (TextUtils.isEmpty(this.subject)) {
            return msg.getType() == MessageType.STREAM_MESSAGE
                    && msg.getStream().equals(stream);
        } else {
            return msg.getType() == MessageType.STREAM_MESSAGE
                    && msg.getStream().equals(stream)
                    && this.subject.equals(msg.getSubject());
        }
    }

    @Override
    public String getTitle() {
        return stream.getName();
    }

    @Override
    public String getSubtitle() {
        return subject;
    }

    @Override
    public Stream getComposeStream() {
        return stream;
    }

    @Override
    public String getComposePMRecipient() {
        return null;
    }

    @Override
    public String getJsonFilter() throws JSONException {
        JSONArray filter = new JSONArray();
        filter.put(new JSONArray(Arrays.asList("stream", this.stream.getName())));
        if (subject != null) {
            filter.put(new JSONArray(Arrays.asList("topic", this.subject)));
        }
        return filter.toString();
    }

    @Override
    public boolean equals(NarrowFilter filter) {
        if (filter instanceof NarrowFilterStream) {
            NarrowFilterStream filterStream = (NarrowFilterStream) filter;
            if (filterStream.getSubtitle() == null) {
                return this.getSubtitle() == null && filterStream.getTitle().equals(this.getTitle());
            } else {
                return (filterStream.getTitle().equals(this.getTitle()) &&
                        filterStream.getSubtitle().equals(this.getSubtitle()));
            }
        } else {
            return false;
        }
    }
} public interface NarrowListener {
    void onNarrow(NarrowFilter narrowFilter);

    void onNarrow(NarrowFilter narrowFilter, int messageId);

    void onNarrowFillSendBox(Message message, boolean openSoftKeyboard);

    void onNarrowFillSendBoxStream(String stream, String message, boolean openSoftKeyboard);

    void onNarrowFillSendBoxPrivate(Person person[], boolean openSoftKeyboard);
} public enum MessageType {
    STREAM_MESSAGE("stream"), PRIVATE_MESSAGE("private");

    private String jsonMessageType;

    MessageType(String sendMessageType) {
        this.jsonMessageType = sendMessageType;
    }

    public String toString() {
        return this.jsonMessageType;
    }
} @DatabaseTable(tableName = "people")
public class Person {
    public static final String NAME_FIELD = "name";
    public static final String MESSAGESPARTICIPATEDIN_FIELD = "messagesParticipatedIn";
    public static final String EMAIL_FIELD = "email";
    public static final String ISBOT_FIELD = "isBot";
    public static final String ISACTIVE_FIELD = "isActive";
    private static final String ID_FIELD = "id";
    private static final String AVATARURL_FIELD = "avatarUrl";
    @SerializedName("user_id")
    @DatabaseField(columnName = ID_FIELD, id = true)
    protected int id;

    @SerializedName("full_name")
    @DatabaseField(columnName = NAME_FIELD)
    private String name;

    @SerializedName("email")
    @DatabaseField(columnName = EMAIL_FIELD, uniqueIndex = true)
    private String email;

    @DatabaseField(columnName = AVATARURL_FIELD)
    private String avatarURL;

    @SerializedName("is_bot")
    @DatabaseField(columnName = ISBOT_FIELD)
    private boolean isBot;

    @DatabaseField(columnName = ISACTIVE_FIELD)
    private boolean isActive;

    @SerializedName("is_admin")
    private boolean isAdmin;

    @SerializedName("domain")
    private String domain;

    @SerializedName("short_name")
    private String shortName;

    @SerializedName("is_mirror_dummy")
    private boolean isMirrorDummy;

    /**
     * used to get person id in case of {@link Message.ZulipDirectMessage}
     */
    @SerializedName("id")
    private int recipientId;

    public Person(String name, String email) {
        this.setName(name);
        this.setEmail(email);
    }

    public Person(String name, String email, String avatarURL) {
        this(name, email);
        this.setAvatarURL(avatarURL);
        this.isActive = false;
    }

    public Person(int id, String name, String email, String avatarURL, boolean isBot, boolean isActive) {
        this.setId(id);
        this.setName(name);
        this.setEmail(email);
        this.setAvatarURL(avatarURL);
        this.setActive(isActive);
        this.setBot(isBot);
    }

    /**
     * Construct an empty Person object.
     */
    public Person() {

    }

    public static Person getByEmail(ZulipApp app, String email) {
        try {
            return getByEmail(app.getDatabaseHelper().getDao(
                    Person.class), email);
        } catch (SQLException e) {
            ZLog.logException(e);
        }
        return null;
    }

    @SuppressWarnings("WeakerAccess")
    public static Person getByEmail(Dao<Person, ?> dao, String email) {
        try {
            return dao.queryBuilder().where()
                    .eq(Person.EMAIL_FIELD, new SelectArg(email))
                    .queryForFirst();
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }

    }

    @SuppressWarnings("WeakerAccess")
    /**
     * Returns person object {@link Person} from the database if exists or newly created object using
     * the passed parameters.
     * Note: always call this method with valid Person id {@link Person#id} passed as argument.
     *
     * @param app Zulip god object {@link ZulipApp}
     * @param email person email {@link Person#email}
     * @param name person name {@link Person#name}
     * @param avatarURL person avatar url {@link Person#avatarURL}
     * @param personId person id {@link Person#id}
     * @param personCache person cache used ofr faster fetching of results
     * @return {@link Person} object
     */
    public static Person getOrUpdate(ZulipApp app, String email, String name,
                                     String avatarURL, int personId, Map<String, Person> personCache) {

        Person person = null;

        if (personCache != null) {
            person = personCache.get(email);
        }

        if (person == null) {
            person = getByEmail(app, email);
            if (personCache != null) {
                personCache.put(email, person);
            }
        }

        if (person == null) {
            person = new Person(name, email, avatarURL);

            // use the person id passed to generate person skeleton object which is consistent
            // with server id.
            person.setId(personId);
            person.setActive(true);
            app.getDao(Person.class).createOrUpdate(person);
        } else {
            boolean changed = false;
            if (name != null && !name.equals(person.name)) {
                person.name = name;
                changed = true;
            }
            if (avatarURL != null && !avatarURL.equals(person.avatarURL)) {
                person.avatarURL = avatarURL;
                changed = true;
            }
            if (changed) {
                person.setActive(true);
                app.getDao(Person.class).update(person);
            }
        }
        return person;
    }

    /**
     * This function is called from setEmail {@link ZulipApp#setEmail(String)}.
     * It is used to get current user person {@link Person} object if it exists in the database
     * or create a new object using {@param email}.
     *
     * @param app   {@link ZulipApp}
     * @param email Person email id {@link Person#email}
     * @return {@link Person} object
     */
    public static Person get(ZulipApp app, String email) {
        Person foundPerson = getByEmail(app, email);
        if (foundPerson == null) {
            // fake it till you make it
            return new Person(null, email);
        }
        return foundPerson;
    }

    /**
     * This function is used to call {@link Person#getOrUpdate(ZulipApp, String, String, String, int, Map)}
     * with no cache.
     * Note: always call this method with valid Person id {@link Person#id} passed as argument.
     *
     * @param app       Zulip god object {@link ZulipApp}
     * @param email     person email {@link Person#email}
     * @param name      person name {@link Person#name}
     * @param avatarURL person avatar url {@link Person#avatarURL}
     * @param personId  person id {@link Person#id}
     * @return {@link Person} object
     */
    public static Person getOrUpdate(ZulipApp app, String email, String name,
                                     String avatarURL, int personId) {
        return getOrUpdate(app, email, name, avatarURL, personId, null);
    }

    public static Person getById(ZulipApp app, int id) {
        RuntimeExceptionDao<Person, Object> dao = app.getDao(Person.class);
        return dao.queryForId(id);
    }

    public static List<Person> getAllPeople(ZulipApp app) throws SQLException {
        RuntimeExceptionDao<Person, Object> dao = app.getDao(Person.class);
        return dao.queryBuilder().where().eq(Person.ISBOT_FIELD, false).query();
    }

    public static void sortByPresence(ZulipApp app, List<Person> people) {
        final Map<String, Presence> presenceCopy = new HashMap<>(
                app.presences);

        Collections.sort(people, new Comparator<Person>() {
            @Override
            public int compare(Person a, Person b) {
                Presence aPresence = presenceCopy.get(a.getEmail());
                Presence bPresence = presenceCopy.get(b.getEmail());

                final int inactiveTimeout = 2 * 60;

                if (aPresence == null && bPresence == null) {
                    return a.getName().toLowerCase(Locale.US)
                            .compareTo(b.getName().toLowerCase(Locale.US));
                } else if (aPresence == null) {
                    return 1;
                } else if (bPresence == null) {
                    return -1;
                } else if (aPresence.getAge() > inactiveTimeout
                        && bPresence.getAge() > inactiveTimeout) {
                    return a.getName().toLowerCase(Locale.US)
                            .compareTo(b.getName().toLowerCase(Locale.US));
                } else if (aPresence.getAge() > inactiveTimeout) {
                    return 1;
                } else if (bPresence.getAge() > inactiveTimeout) {
                    return -1;
                } else if (aPresence.getStatus() == bPresence.getStatus()) {
                    return a.getName().toLowerCase(Locale.US)
                            .compareTo(b.getName().toLowerCase(Locale.US));
                } else if (aPresence.getStatus() == PresenceType.ACTIVE) {
                    return -1;
                } else {
                    return 1;
                }
            }
        });
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    private void setName(String name) {
        this.name = name;
    }

    public String getEmail() {
        return email;
    }

    private void setEmail(String email) {
        if (email != null) {
            this.email = email.toLowerCase(Locale.US);
        }
    }

    public boolean getIsBot() {
        return isBot;
    }

    public String getAvatarURL() {
        return avatarURL;
    }

    private void setAvatarURL(String avatarURL) {
        this.avatarURL = avatarURL;
    }

    /**
     * Calculate the Humbug realm for the person, currently by splitting the
     * email address.
     * <p/>
     * In the future, realms may be distinct from your email hostname.
     *
     * @return the Person's realm.
     */
    public String getRealm() {
        String[] splitEmail = this.getEmail().split("@");
        return splitEmail[splitEmail.length - 1];
    }

    public boolean equals(Object obj) {
        if (obj == null) {
            return false;
        }
        if (obj == this) {
            return true;
        }
        if (!(obj instanceof Person)) {
            return false;
        }
        Person per = (Person) obj;
        if (this.name == null || this.email == null)
            return false;
        return this.name.equals(per.getName()) && this.email.equals(per
                .getEmail());
    }

    public int hashCode() {
        return new HashCodeBuilder(17, 31).append(name).append(email)
                .toHashCode();
    }

    @SuppressWarnings("WeakerAccess")
    public void setBot(boolean isBot) {
        this.isBot = isBot;
    }

    public void setActive(boolean active) {
        isActive = active;
    }

    public int getRecipientId() {
        return this.recipientId;
    }

    public void setRecipientId(int id) {
        this.recipientId = id;
    }
} @DatabaseTable(tableName = "reactions")
public class Reaction {
    private static final String ID_FIELD = "id";
    private static final String NAME_FIELD = "name";
    private static final String USER_FIELD = "user";
    private static final String MESSAGE_FIELD = "message";


    @SerializedName("emoji_name")
    @DatabaseField(columnName = NAME_FIELD)
    private String emojiName;

    @SerializedName("user")
    @DatabaseField(foreign = true, columnName = USER_FIELD)
    private UserReaction user;

    @DatabaseField(foreign = true, foreignAutoRefresh = true, columnName = MESSAGE_FIELD)
    private Message message;


    /**
     * Construct an empty Reaction object.
     */
    public Reaction() {

    }

    public String getEmoji() {
        return this.emojiName;
    }

    public void setEmoji(String name) {
        this.emojiName = name;
    }

    public UserReaction getUser() {
        return this.user;
    }

    public void setUser(UserReaction user) {
        this.user = user;
    }

    @Override
    public String toString() {
        return ":" + getEmoji() + ":";
    }

    @Override
    public boolean equals(Object obj) {
        if (obj instanceof Reaction) {
            Reaction reaction = (Reaction) obj;
            return this.emojiName.equals(reaction.emojiName) && this.user.equals(reaction.user);
        } else {
            return false;
        }
    }
} public class Constants {
    public final static String IS_CONTENT_EDIT_PARAM_SAVED = "isContentEditParamSaved";
    public final static String IS_EDITING_ALLOWED = "isEditingAllowed";
    public final static String MAXIMUM_CONTENT_EDIT_LIMIT = "maximumContentEditLimit";
    //Default maximum time limit for editing message(Same as server)
    public final static int DEFAULT_MAXIMUM_CONTENT_EDIT_LIMIT = 600;
    public final static boolean DEFAULT_EDITING_ALLOWED = true;
    public final static String SERVER_URL = "SERVER_URL";
    public static final String CANCEL = "CANCEL";
    public static final String DONT_SHOW_APP_UPDATE_DIALOG = "DONT_SHOW_APP_UPDATE_DIALOG";
    public static int MILLISECONDS_IN_A_MINUTE = 1000;
    public static String DATE_FORMAT = "dd/MM/yyyy";
    public static final int REQUEST_PICK_FILE = 3;
    public static final int HIDE_FAB_AFTER_SEC = 5;
    // row number which is used to differentiate the '@-mentions' in people drawer
    public static int MENTIONS = -2;
    public static int REACTION_MARGIN = 64;
    //end points
    public static String END_POINT_TERMS_OF_SERVICE = "terms";
    public static String END_POINT_PRIVACY = "privacy";
    public static String END_POINT_REGISTER = "register";
    //if two continuous messages are from same sender and time difference is less than this then hide it
    public static long HIDE_TIMESTAMP_THRESHOLD = 60 * 1000;// 1 minute
    public static String ACTION_MESSAGE_PUSH_NOTIFICATION = "ACTION_MESSAGE_PUSH_NOTIFICATION";
    public static String NIGHT_THEME = "NIGHT_THEME";
    public static String AUTO_NIGHT_THEME = "AUTO_NIGHT_THEME";
} public class ConvertDpPx {

    /**
     * This method converts dp unit to equivalent pixels, depending on device density.
     *
     * @param dp A value in dp (density independent pixels) unit. Which we need to convert into pixels
     * @return A float value to represent px equivalent to dp depending on device density
     */
    public static int convertDpToPixel(float dp) {
        DisplayMetrics metrics = Resources.getSystem().getDisplayMetrics();
        float px = dp * ((float) metrics.densityDpi / DisplayMetrics.DENSITY_DEFAULT);
        return Math.round(px);
    }

    /**
     * This method converts device specific pixels to density independent pixels.
     *
     * @param px A value in px (pixels) unit. Which we need to convert into db
     * @return A float value to represent dp equivalent to px value
     */
    public static float convertPixelsToDp(float px) {
        DisplayMetrics metrics = Resources.getSystem().getDisplayMetrics();
        float dp = px / ((float) metrics.densityDpi / DisplayMetrics.DENSITY_DEFAULT);
        return dp;
    }
} public class DateMethods {

    /**
     * Check's whether two date are of same day or not
     *
     * @param date1 compare with date2
     * @param date2 compare with date1
     * @return true if date1 and date2 are of same day
     */
    public static boolean isSameDay(Date date1, Date date2) {
        Calendar calendar = Calendar.getInstance();
        Calendar calendar2 = Calendar.getInstance();
        calendar.setTime(date1);
        calendar2.setTime(date2);
        return calendar.get(Calendar.YEAR) == calendar2.get(Calendar.YEAR) &&
                calendar.get(Calendar.DAY_OF_YEAR) == calendar2.get(Calendar.DAY_OF_YEAR);
    }

    /**
     * Check's whether two date are of same year or not
     *
     * @param date1 compare with date2
     * @param date2 compare with date1
     * @return true if date1 and date2 are of same year
     */
    public static boolean isSameYear(Date date1, Date date2) {
        Calendar calendar = Calendar.getInstance();
        Calendar calendar2 = Calendar.getInstance();
        calendar.setTime(date1);
        calendar2.setTime(date2);
        return calendar.get(Calendar.YEAR) == calendar2.get(Calendar.YEAR);
    }

    public static String getStringDate(Date date) {
        if (date == null)
            return "";
        //check for today
        if (isSameDay(date, new Date())) {
            return "Today";
        }
        //check for yesterday
        Calendar calendar = Calendar.getInstance();
        calendar.add(Calendar.DATE, -1);
        if (isSameDay(date, calendar.getTime())) {
            return "Yesterday";
        }
        SimpleDateFormat formatter;
        if (isSameYear(date, new Date())) {
            //return only month and date eg:- MAR 09
            formatter = new SimpleDateFormat("MMM dd", Locale.getDefault());
        } else {
            //return month, date and year eg:- MAR 09, 2017
            formatter = new SimpleDateFormat("MMM dd, yyyy", Locale.getDefault());
        }
        return formatter.format(date);
    }
} public class ZLog {

    private ZLog() {
    }

    public static void logException(Throwable e) {
        if (!BuildHelper.shouldLogToCrashlytics()) {
            Log.e("Error", "oops", e);
        } else {
            Crashlytics.logException(e);
        }
    }

    public static void log(String message) {
        if (!BuildHelper.shouldLogToCrashlytics()) {
            Log.e("Error", message);
        } else {
            Crashlytics.log(message);
        }
    }
} public class LoadingHolder extends RecyclerView.ViewHolder {
    public LoadingHolder(View footerView) {
        super(footerView);
    }
} public class MessageHeaderParent {
    private final Message message;
    private String stream;
    private String subject;
    private String id;
    private boolean isMute;
    private MessageType messageType;
    private String displayRecipent;
    @ColorInt
    private int color;
    //all messages below this header will be of same day as date of this timestamp
    private Date messagesTimestamp;

    //is stream textview max lines is set to 1
    private boolean isStreamExpanded = false;
    private boolean isTopicExpanded = false;


    /**
     * Constructor for the wrapper class.\
     *
     * @param stream  Stores stream name if {@link MessageType#STREAM_MESSAGE} or null if {@link MessageType#PRIVATE_MESSAGE}
     * @param subject Stores the topic/subject name if {@link MessageType#STREAM_MESSAGE} or null if {@link MessageType#PRIVATE_MESSAGE}
     * @param id      Stores the {@link Message#getIdForHolder()} this functions returns a string "subjectnamestreamId" if if {@link MessageType#STREAM_MESSAGE}
     *                or all the recipients ID's of the group conversation or single recipient if {@link MessageType#PRIVATE_MESSAGE}
     */
    public MessageHeaderParent(String stream, String subject, String id, Message message) {
        this.stream = stream;
        this.message = message;
        this.subject = subject;
        this.id = id;
    }

    public int getColor() {
        return color;
    }

    public void setColor(int color) {
        this.color = color;
    }

    public boolean isMute() {
        return isMute;
    }

    public void setMute(boolean mute) {
        isMute = mute;
    }

    public MessageType getMessageType() {
        return messageType;
    }

    public void setMessageType(MessageType messageType) {
        this.messageType = messageType;
    }

    public String getDisplayRecipent() {
        return displayRecipent;
    }

    public void setDisplayRecipent(String displayRecipent) {
        this.displayRecipent = displayRecipent;
    }

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    /**
     * Returns message id of containing message {@link MessageHeaderParent#message}.
     *
     * @return message id
     */
    public int getMessageId() {
        return message.getID();
    }

    public String getStream() {
        return stream;
    }

    public String getSubject() {
        return subject;
    }

    public void setSubject(String subject) {
        this.subject = subject;
    }

    public Message getMessage() {
        return this.message;
    }

    public Person[] getRecipients(ZulipApp app) {
        Person[] recipientsCache;
        String[] ids = TextUtils.split(this.getId(), ",");
        recipientsCache = new Person[ids.length];
        for (int i = 0; i < ids.length; i++) {
            try {
                recipientsCache[i] = Person.getById(app,
                        Integer.parseInt(ids[i]));
            } catch (NumberFormatException e) {
                ZLog.logException(e);
            }
        }
        return recipientsCache;
    }

    public void setMessagesDate(Date timestamp) {
        this.messagesTimestamp = timestamp;
    }

    public Date getMessagesTimestamp() {
        return messagesTimestamp;
    }

    public boolean isStreamExpanded() {
        return isStreamExpanded;
    }

    public void setStreamExpanded(boolean streamExpanded) {
        isStreamExpanded = streamExpanded;
    }

    public boolean isTopicExpanded() {
        return isTopicExpanded;
    }

    public void setTopicExpanded(boolean topicExpanded) {
        isTopicExpanded = topicExpanded;
    }

    public static class MessageHeaderHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
        public TextView streamTextView;
        public TextView topicTextView;
        public ImageView muteMessageImage;
        public TextView arrowHead;
        public TextView timestamp;
        public OnItemClickListener onItemClickListener;

        public MessageHeaderHolder(View itemView) {
            super(itemView);
            streamTextView = (TextView) itemView.findViewById(R.id.displayRecipient);
            topicTextView = (TextView) itemView.findViewById(R.id.instance);
            arrowHead = (TextView) itemView.findViewById(R.id.sep);
            muteMessageImage = (ImageView) itemView.findViewById(R.id.muteMessageImage);
            timestamp = (TextView) itemView.findViewById(R.id.timestamp);
            streamTextView.setOnClickListener(this);
            topicTextView.setOnClickListener(this);
        }


        public void setOnItemClickListener(OnItemClickListener contentHolder) {
            this.onItemClickListener = contentHolder;
        }

        @Override
        public void onClick(View v) {
            onItemClickListener.onItemClick(v.getId(), getAdapterPosition());
        }
    }
} public class MessageHolder extends RecyclerView.ViewHolder implements View.OnClickListener, View.OnCreateContextMenuListener {

    public ImageView gravatar;
    public TextView senderName;
    public TextView timestamp,leftTimestamp;
    public TextView edited, leftEdited;
    public TextView contentView;
    public View leftBar;
    public RelativeLayout messageTile;
    public ImageView contentImage;
    public ImageView starImage, leftStarImage;
    public View contentImageContainer;
    public TableLayout reactionsTable;
    public OnItemClickListener onItemClickListener;

    public MessageHolder(final View itemView) {
        super(itemView);
        gravatar = (ImageView) itemView.findViewById(R.id.gravatar);
        senderName = (TextView) itemView.findViewById(R.id.senderName);
        timestamp = (TextView) itemView.findViewById(R.id.timestamp);
        leftTimestamp = (TextView) itemView.findViewById(R.id.left_timestamp);
        edited = (TextView) itemView.findViewById(R.id.message_edit_tag);
        leftEdited = (TextView) itemView.findViewById(R.id.left_message_edit_tag);
        contentView = (TextView) itemView.findViewById(R.id.contentView);
        contentView.setMovementMethod(LinkMovementMethod.getInstance());
        leftBar = itemView.findViewById(R.id.leftBar);
        messageTile = (RelativeLayout) itemView.findViewById(R.id.messageTile);
        contentImage = (ImageView) itemView.findViewById(R.id.load_image);
        starImage = (ImageView) itemView.findViewById(R.id.star_image);
        leftStarImage = (ImageView) itemView.findViewById(R.id.left_star_image);
        contentImageContainer = itemView.findViewById(R.id.load_image_container);
        reactionsTable = (TableLayout) itemView.findViewById(R.id.reactions_table);
        contentView.setOnClickListener(this);
        contentView.setLongClickable(true);
        itemView.setOnCreateContextMenuListener(this);

        // Add click listener to sender view
        View senderView = itemView.findViewById(R.id.senderTile);
        if (senderView != null) senderView.setOnClickListener(this);
    }

    @Override
    public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
        Message msg = onItemClickListener.getMessageAtPosition(getAdapterPosition());
        onItemClickListener.setContextItemSelectedPosition(getAdapterPosition());
        if (msg == null) {
            return;
        }
        if (msg.getType().equals(MessageType.STREAM_MESSAGE)) {
            MenuInflater inflater = ((Activity) v.getContext()).getMenuInflater();
            inflater.inflate(R.menu.context_stream, menu);
            if (msg.getSender().getId() != ZulipApp.get().getYou().getId()) {
                menu.findItem(R.id.edit_message).setVisible(false);
            }
            if (msg.getMessageStar()) {
                menu.findItem(R.id.star_message).setVisible(false);
            } else {
                menu.findItem(R.id.un_star_message).setVisible(false);
            }

        } else if (msg.getPersonalReplyTo(ZulipApp.get()).length > 1) {
            MenuInflater inflater = ((Activity) v.getContext()).getMenuInflater();
            inflater.inflate(R.menu.context_private, menu);
            if (msg.getSender().getId() != ZulipApp.get().getYou().getId()) {
                menu.findItem(R.id.edit_message).setVisible(false);
            }
            if (msg.getMessageStar()) {
                menu.findItem(R.id.star_message).setVisible(false);
            } else {
                menu.findItem(R.id.un_star_message).setVisible(false);
            }
        } else {
            MenuInflater inflater = ((Activity) v.getContext()).getMenuInflater();
            inflater.inflate(R.menu.context_single_private, menu);
            if (msg.getSender().getId() != ZulipApp.get().getYou().getId()) {
                menu.findItem(R.id.edit_message).setVisible(false);
            }
            if (msg.getMessageStar()) {
                menu.findItem(R.id.star_message).setVisible(false);
            } else {
                menu.findItem(R.id.un_star_message).setVisible(false);
            }
        }
    }


    @Override
    public void onClick(View view) {
        onItemClickListener.onItemClick(view.getId(), getAdapterPosition());
    }

    public void setItemClickListener(OnItemClickListener itemClickListener) {
        this.onItemClickListener = itemClickListener;
    }
} public class RetrieveHeaderView {

    public static int DEFAULT_VIEW_TYPE = -1;

    private final RecyclerView recyclerView;

    private RecyclerView.ViewHolder currentViewHolder;
    private int currentViewType;

    RetrieveHeaderView(RecyclerView recyclerView) {
        this.recyclerView = recyclerView;
        this.currentViewType = DEFAULT_VIEW_TYPE;
    }

    RecyclerView.ViewHolder getViewHolderForPosition(int position) {
        if (currentViewType != recyclerView.getAdapter().getItemViewType(position)) {
            currentViewType = recyclerView.getAdapter().getItemViewType(position);
            currentViewHolder = recyclerView.getAdapter().createViewHolder(
                    (ViewGroup) recyclerView.getParent(), currentViewType);
        }
        //set top margin for sticky header to 0 as in message_header.xml it is present which is only required when header is in between of messages
        CoordinatorLayout.LayoutParams layoutParams = (CoordinatorLayout.LayoutParams) currentViewHolder.itemView.getLayoutParams();
        layoutParams.setMargins(0, ZulipApp.get().getZulipActivity().getFloatingHeaderTopMargin(), 0, 0);

        //set viewHolder in ZulipActivity
        //adjust it when message list get scrolled
        ZulipApp.get().getZulipActivity().setViewHolder(currentViewHolder);
        return currentViewHolder;
    }
} public interface StickyHeaderHandler {

    /**
     * @return The data set supplied to the {@link RecyclerView.Adapter}
     */
    List<?> getAdapterData();

    /**
     * Updated headerView when it is attached or detached
     * @param headerViewAdapterPosition position of view that has to be updated
     */
    void setAttachedHeader(int headerViewAdapterPosition);
} public static int convertDpToPixel(float dp) {
    DisplayMetrics metrics = Resources.getSystem().getDisplayMetrics();
    float px = dp * ((float) metrics.densityDpi / DisplayMetrics.DENSITY_DEFAULT);
    return Math.round(px);
}
import android.content.Context;
import android.content.Intent;
import android.content.res.ColorStateList;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Color;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.GradientDrawable;
import android.support.annotation.ColorInt;
import android.support.v4.content.ContextCompat;
import android.support.v4.view.ViewCompat;
import android.support.v7.app.AppCompatDelegate;
import android.support.v7.widget.RecyclerView;
import android.text.TextUtils;
import android.text.format.DateUtils;
import android.text.method.LinkMovementMethod;
import android.util.Log;
import android.util.TypedValue;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TableRow;
import android.widget.TextView;
import com.j256.ormlite.stmt.UpdateBuilder;
import com.squareup.picasso.Callback;
import com.squareup.picasso.Picasso;
import com.zulip.android.R;
import com.zulip.android.ZulipApp;
import com.zulip.android.filters.NarrowFilterPM;
import com.zulip.android.filters.NarrowFilterStream;
import com.zulip.android.filters.NarrowListener;
import com.zulip.android.models.Message;
import com.zulip.android.models.MessageDateSeparator;
import com.zulip.android.models.MessageType;
import com.zulip.android.models.Person;
import com.zulip.android.models.Reaction;
import com.zulip.android.models.Stream;
import com.zulip.android.util.ActivityTransitionAnim;
import com.zulip.android.util.Constants;
import com.zulip.android.util.ConvertDpPx;
import com.zulip.android.util.DateMethods;
import com.zulip.android.util.MutedTopics;
import com.zulip.android.util.OnItemClickListener;
import com.zulip.android.util.UrlHelper;
import com.zulip.android.util.ZLog;
import com.zulip.android.viewholders.LoadingHolder;
import com.zulip.android.viewholders.MessageHeaderParent;
import com.zulip.android.viewholders.MessageHolder;
import com.zulip.android.viewholders.stickyheaders.RetrieveHeaderView;
import com.zulip.android.viewholders.stickyheaders.interfaces.StickyHeaderHandler;
import java.io.IOException;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Random;
import static com.zulip.android.util.ConvertDpPx.convertDpToPixel;
package com.zulip.android.activities;





/**
 * An adapter to bind the messages to a RecyclerView.
 * This has two main ViewTypes {@link MessageHeaderParent.MessageHeaderHolder} and {@link MessageHolder}
 * Each Message is inserted to its MessageHeader which are distinguished by the {@link Message#getIdForHolder()}
 * saved in {@link MessageHeaderParent#getId()}
 * <p>
 * There are two ways to insert a message in this adapter one {@link RecyclerMessageAdapter#addOldMessage(Message, int, StringBuilder,MessageDateSeparator)}
 * and second one {@link RecyclerMessageAdapter#addNewMessage(Message)}
 * The first one is used to add old messages from the databases with {@link com.zulip.android.util.MessageListener.LoadPosition#BELOW}
 * and {@link com.zulip.android.util.MessageListener.LoadPosition#INITIAL}. Messages are added from 1st index of the adapter and new
 * headerParents are created if it doesn't matches the current header where the adding is being placed, this is done to match the UI as the web.
 * In addNewMessages the messages are loaded in the bottom and new headers are created if it does not matches the last header.
 */
public class RecyclerMessageAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> implements StickyHeaderHandler{

    public static final int VIEWTYPE_MESSAGE_HEADER = 1;
    public static final int VIEWTYPE_HEADER = 3; //At position 0
    private static final int VIEWTYPE_MESSAGE = 2;
    private static final int VIEWTYPE_FOOTER = 4; //At end position
    private static final int VIEWTYPE_DATE_SEPARATOR = 5;
    private static final float HEIGHT_IN_DP = 48;
    private static String privateHuddleText;
    private boolean startedFromFilter;
    private List<Object> items;
    private ZulipApp zulipApp;
    private MutedTopics mMutedTopics;
    private Context context;
    private NarrowListener narrowListener;
    private
    @ColorInt
    int mDefaultStreamHeaderColor;

    @ColorInt
    private int streamMessageBackground;

    @ColorInt
    private int privateMessageBackground;
    private OnItemClickListener onItemClickListener;
    private int contextMenuItemSelectedPosition = RecyclerView.NO_POSITION;
    private View footerView;
    private View headerView;
    private UpdateBuilder<Message, Object> updateBuilder;

    private boolean isCurrentThemeNight;
    private HashMap<Integer, Integer> defaultAvatarColorHMap;

    // position of view (MessageHeaderParent) which float's on top
    private int attachedHeaderAdapterPosition = RetrieveHeaderView.DEFAULT_VIEW_TYPE;

    RecyclerMessageAdapter(List<Message> messageList, final Context context, boolean startedFromFilter) {
        super();
        items = new ArrayList<>();
        zulipApp = ZulipApp.get();
        mMutedTopics = MutedTopics.get();
        this.context = context;
        narrowListener = (NarrowListener) context;
        this.startedFromFilter = startedFromFilter;
        isCurrentThemeNight = (AppCompatDelegate.getDefaultNightMode() == AppCompatDelegate.MODE_NIGHT_YES);
        mDefaultStreamHeaderColor = ContextCompat.getColor(context, R.color.stream_header);
        privateMessageBackground = ContextCompat.getColor(context, R.color.private_background);
        streamMessageBackground = ContextCompat.getColor(context, R.color.stream_background);

        defaultAvatarColorHMap = new HashMap<>();
        privateHuddleText = context.getResources().getString(R.string.huddle_text);
        setupHeaderAndFooterViews();
        onItemClickListener = new OnItemClickListener() {
            @Override
            public void onItemClick(int viewId, int position) {
                switch (viewId) {
                    case R.id.displayRecipient: //StreamTV
                        if (position == RetrieveHeaderView.DEFAULT_VIEW_TYPE) {
                            //clicked on floating header
                            if (attachedHeaderAdapterPosition != RetrieveHeaderView.DEFAULT_VIEW_TYPE) {
                                position = attachedHeaderAdapterPosition;
                            } else {
                                return;
                            }
                        }
                        MessageHeaderParent messageHeaderParent = (MessageHeaderParent) getItem(position);
                        if (messageHeaderParent.getMessageType() == MessageType.PRIVATE_MESSAGE) {
                            Person[] recipientArray = messageHeaderParent.getRecipients(zulipApp);
                            narrowListener.onNarrow(new NarrowFilterPM(Arrays.asList(recipientArray)),
                                    messageHeaderParent.getMessageId());
                            narrowListener.onNarrowFillSendBoxPrivate(recipientArray, false);
                        } else {
                            narrowListener.onNarrow(new NarrowFilterStream(Stream.getByName(zulipApp,