public class Utils implements com.leavjenn.hews.misc.UtilsContract {

    private Context mContext;

    public Utils(Context context) {
        mContext = context;
    }

    public static CharSequence formatTime(long timeStamp) {
        timeStamp = timeStamp * 1000;
        CharSequence timeAgo = DateUtils.getRelativeTimeSpanString(timeStamp,
            System.currentTimeMillis(), DateUtils.MINUTE_IN_MILLIS);
        return timeAgo;
    }

    public static float convertPixelsToDp(float px, Context context) {
        DisplayMetrics metrics = context.getResources().getDisplayMetrics();
        return px / (metrics.densityDpi / 160f);
    }

    public static int convertDpToPixels(int dp, Context context) {
        DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics();
        int px = Math.round(dp * (displayMetrics.xdpi / DisplayMetrics.DENSITY_DEFAULT));
        return px;
    }

    public static float convertSpToPixels(float sp, Context context) {
        float scaledDensity = context.getResources().getDisplayMetrics().scaledDensity;
        return sp * scaledDensity;
    }


    public static int getScreenHeight(Context context) {
        WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
        Display display = wm.getDefaultDisplay();
        Point size = new Point();
        display.getSize(size);
        return size.y;
    }

    public static boolean isOnline(Context context) {
        ConnectivityManager connectivityManager = (ConnectivityManager) context
            .getSystemService(Context.CONNECTIVITY_SERVICE);
        return connectivityManager.getActiveNetworkInfo() != null;
    }

    public static void showLongToast(Context context, CharSequence text) {
        Toast.makeText(context, text, Toast.LENGTH_LONG).show();
    }

    public static void showLongToast(Context context, @StringRes int ResId) {
        Toast.makeText(context, ResId, Toast.LENGTH_LONG).show();
    }

    public static void setSnackBarTextColor(Snackbar snackbar, Context context, @ColorRes int color) {
        TextView tvSnackbarText = (TextView) snackbar.getView()
            .findViewById(android.support.design.R.id.snackbar_text);
        tvSnackbarText.setTextColor(context.getResources().getColor(color));
    }

    // uri scheme can only lower case char
    public static Uri validateAndParseUri(String uriString, long postId) {
        if (URLUtil.isValidUrl(uriString)) {
//        if (Patterns.WEB_URL.matcher(uriString).matches()) {
            if (uriString.startsWith("Http")) {
                Log.i("validateAndParseUri", "http" + uriString.substring(4));
                return Uri.parse("http" + uriString.substring(4));
            } else { // TODO other conditions
                return Uri.parse(uriString);
            }
        } else {
            return Uri.parse("https://news.ycombinator.com/item?id=" + postId);
        }
//        }
    }

    // setup url and pretty url
    public static void setupPostUrl(Post post) {
        String url = post.getUrl();
        if (url == null || url.isEmpty()) {
            url = Constants.YCOMBINATOR_ITEM_URL + post.getId();
            post.setUrl(url);
        }
        String[] splitUrl = url.split("/");
        if (splitUrl.length > 2) {
            url = splitUrl[2];
            post.setPrettyUrl(url);
        }
    }

    public static void setupIntentBuilder(CustomTabsIntent.Builder intentBuilder, Context context, SharedPreferences prefs) {
        if (SharedPrefsManager.getTheme(prefs).equals(SharedPrefsManager.THEME_DARK)) {
            intentBuilder.setToolbarColor(context.getResources().getColor(R.color.grey_900));
        } else if (SharedPrefsManager.getTheme(prefs).equals(SharedPrefsManager.THEME_AMOLED_BLACK)) {
            intentBuilder.setToolbarColor(context.getResources().getColor(android.R.color.black));
        } else {
            // use darker orange color here so chrome toolbar will fit dark theme
            intentBuilder.setToolbarColor(context.getResources().getColor(R.color.orange_800));
        }
        intentBuilder.enableUrlBarHiding();
        intentBuilder.setShowTitle(true);
        intentBuilder.setCloseButtonIcon(
            BitmapFactory.decodeResource(context.getResources(), R.drawable.ic_arrow_back));
        // share link option
        String menuItemTitle = context.getString(R.string.share_link_to);
        Intent actionIntent = new Intent(context.getApplicationContext(), ShareBroadcastReceiver.class);
        PendingIntent menuItemPendingIntent =
            PendingIntent.getBroadcast(context.getApplicationContext(), 0, actionIntent, 0);
        intentBuilder.addMenuItem(menuItemTitle, menuItemPendingIntent);
        // TODO Use this way, it will show share to option when long click
//            String shareLabel = getString(R.string.share_link_to);
//            Intent shareIntent = new Intent();
//            shareIntent.setAction(Intent.ACTION_SEND);
//            String postUrl = post.getUrl();
//            shareIntent.putExtra(Intent.EXTRA_TEXT, postUrl);
//            shareIntent.setType("text/plain");
//            shareIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
//            Intent chooserIntent = Intent.createChooser(shareIntent, getString(R.string.share_link_to));
//            startActivity(chooserIntent);
//            PendingIntent pendingIntent = PendingIntent.getActivity(getApplicationContext(), 0,
//                    shareIntent, 0);
//            intentBuilder.addMenuItem(shareLabel, pendingIntent);
    }

    @Override
    public boolean isOnline() {
        ConnectivityManager connectivityManager = (ConnectivityManager) mContext
            .getSystemService(Context.CONNECTIVITY_SERVICE);
        return connectivityManager.getActiveNetworkInfo() != null;
    }

    @Override
    public String convertHtmlToString(String html) {
        return Html.fromHtml(html).toString();
    }
} public class LocalDataManager implements LocalContract {
    private Context mContext;

    public LocalDataManager(Context context) {
        mContext = context;
    }

    @Override
    public Observable<PutResult> putPostToDb(Post post) {
        return StorIOHelper.getStorIOSQLite(mContext)
            .put()
            .object(post)
            .prepare()
            .asRxObservable();
    }

    @Override
    public Observable<List<Post>> getPostFromDb(long postId) {
        return StorIOHelper.getStorIOSQLite(mContext)
            .get()
            .listOfObjects(Post.class)
            .withQuery(Query.builder()
                .table(PostTable.TABLE)
                .where(PostTable.COLUMN_ID + " = " + postId)
                .build())
            .prepare()
            .asRxObservable();
    }

    @Override
    public Observable<List<Post>> getAllPostsFromDb() {
        return StorIOHelper.getStorIOSQLite(mContext)
            .get()
            .listOfObjects(Post.class)
            .withQuery(Query.builder().table(PostTable.TABLE).build())
            .prepare()
            .asRxObservable().onBackpressureBuffer();
    }

    @Override
    public Observable<DeleteResult> deletePostFromDb(Post post) {
        return StorIOHelper.getStorIOSQLite(mContext)
            .delete()
            .object(post)
            .prepare()
            .asRxObservable();
    }

    @Override
    public Observable<PutResults<Comment>> putCommentsToDb(List<Comment> commentList) {
        return StorIOHelper.getStorIOSQLite(mContext)
            .put()
            .objects(commentList)
            .prepare()
            .asRxObservable().onBackpressureBuffer();
    }

    @Override
    public Observable<List<Comment>> getStoryCommentsFromDb(long postId) {
        return StorIOHelper.getStorIOSQLite(mContext)
            .get()
            .listOfObjects(Comment.class)
            .withQuery(Query.builder()
                .table(CommentTable.TABLE)
                .where(CommentTable.COLUMN_PARENT + " = " + postId)
                .orderBy(CommentTable.COLUMN_INDEX + " ASC")
                .build())
            .prepare()
            .asRxObservable().onBackpressureBuffer();
    }

    @Override
    public Observable<DeleteResult> deleteStoryCommentsFromDb(long postId) {
        return StorIOHelper.getStorIOSQLite(mContext)
            .delete()
            .byQuery(DeleteQuery.builder()
                .table(CommentTable.TABLE)
                .where(CommentTable.COLUMN_PARENT + "=" + postId)
                .build())
            .prepare()
            .asRxObservable().onBackpressureBuffer();
    }
} public interface OnRecyclerViewCreatedListener {
    void onRecyclerViewCreated(RecyclerView recyclerView);
} public interface SharedPrefsContract {
    boolean isPostRead(long postId);

    boolean isShowPostSummary();

    boolean isPostBookmarked(long postId);

    boolean areCommentsBookmarked(long postId);

    void setPostBookmarked(long postId);

    void setCommentsBookmarked(long postId);

    void setPostUnbookmarked(long postId);

    void setCommentsUnbookmarked(long postId);
} public class SharedPrefsManager implements SharedPrefsContract {
    public static final String KEY_POST_FONT = "key_post_font";
    public static final String KEY_POST_FONT_SIZE = "key_post_font_size";
    public static final String KEY_POST_LINE_HEIGHT = "key_post_line_height";
    public static final String KEY_COMMENT_FONT = "key_comment_font";
    public static final String KEY_COMMENT_FONT_SIZE = "key_comment_font_size";
    public static final String KEY_COMMENT_LINE_HEIGHT = "key_comment_line_height";
    public static final String KEY_THEME = "themekey";
    public static final String KEY_USERNAME = "user_name";
    public static final String KEY_LOGIN_COOKIE = "login_cookie";
    public static final String KEY_REPLY_TEXT = "replying_text";
    public static final String KEY_POST_IS_READ = "key_post_is_read";
    public static final String KEY_POST_IS_BOOKMARKED = "key_post_is_bookmarked";
    public static final String KEY_COMMENTS_ARE_BOOKMARKED = "key_comments_are_bookmarked";
    public static final String THEME_LIGHT = "0";
    public static final String THEME_SEPIA = "1";
    public static final String THEME_DARK = "2";
    public static final String THEME_AMOLED_BLACK = "3";
    public static final String SCROLL_MODE = "fabkey";
    public static final String SCROLL_MODE_DISABLE = "0";
    public static final String SCROLL_MODE_BUTTON = "1";
    public static final String SCROLL_MODE_FAB_DRAG = "2";
    public static final String SCROLL_MODE_FAB_HOLD = "3";
    public static final String KEY_SHOW_POST_SUMMARY = "key_show_post_summary";

    static String[] fontsForComment = {"PT Sans", "Roboto", "Lato",
        "Open Sans", "Muli", "Slabo 27px", "Crimson Text", "Roboto Slab",
        "Vollkorn", "Merriweather"};
    static String[] fontsForPost = {"Open Sans", "Dosis SemiBold", "Roboto Slab", "Merriweather",
        "RobotoMono"};

    private Context mContext;
    private SharedPreferences mPreferences;

    public SharedPrefsManager(Context context, SharedPreferences preferences) {
        mContext = context;
        mPreferences = preferences;
    }

    public static String[] getPostFontsList() {
        return fontsForPost;
    }

    public static String[] getCommentFontList() {
        return fontsForComment;
    }

    public static String getPostFont(SharedPreferences sp) {
        return sp.getString(KEY_POST_FONT, "Roboto Slab");
    }

    public static void setPostFont(SharedPreferences sp, String fontName) {
        SharedPreferences.Editor editor = sp.edit();
        editor.putString(SharedPrefsManager.KEY_POST_FONT, fontName);
        editor.apply();
    }

    public static float getPostFontSize(SharedPreferences sp) {
        return sp.getFloat(KEY_POST_FONT_SIZE, 16);
    }

    public static void setPostFontSize(SharedPreferences sp, float add) {
        SharedPreferences.Editor editor = sp.edit();
        editor.putFloat(SharedPrefsManager.KEY_POST_FONT_SIZE, getPostFontSize(sp) + add);
        editor.apply();
    }

    public static float getPostLineHeight(SharedPreferences sp) {
        return sp.getFloat(KEY_POST_LINE_HEIGHT, 1.2f);
    }

    public static void setPostLineHeight(SharedPreferences sp, float add) {
        SharedPreferences.Editor editor = sp.edit();
        editor.putFloat(SharedPrefsManager.KEY_POST_LINE_HEIGHT, getPostLineHeight(sp) + add);
        editor.apply();
    }

    public static String getCommentFont(SharedPreferences sp) {
        return sp.getString(KEY_COMMENT_FONT, "Open Sans");
    }

    public static void setCommentFont(SharedPreferences sp, String fontName) {
        SharedPreferences.Editor editor = sp.edit();
        editor.putString(SharedPrefsManager.KEY_COMMENT_FONT, fontName);
        editor.apply();
    }

    public static float getCommentFontSize(SharedPreferences sp) {
        return sp.getFloat(KEY_COMMENT_FONT_SIZE, 15);
    }

    public static void setCommentFontSize(SharedPreferences sp, float add) {
        SharedPreferences.Editor editor = sp.edit();
        editor.putFloat(SharedPrefsManager.KEY_COMMENT_FONT_SIZE, getCommentFontSize(sp) + add);
        editor.apply();
    }

    public static float getCommentLineHeight(SharedPreferences sp) {
        return sp.getFloat(KEY_COMMENT_LINE_HEIGHT, 1.0f);
    }

    public static void setCommentLineHeight(SharedPreferences sp, float add) {
        SharedPreferences.Editor editor = sp.edit();
        editor.putFloat(SharedPrefsManager.KEY_COMMENT_LINE_HEIGHT, getCommentLineHeight(sp) + add);
        editor.apply();
    }

    public static void setLoginCookie(SharedPreferences sp, String cookie) {
        SharedPreferences.Editor editor = sp.edit();
        editor.putString(SharedPrefsManager.KEY_LOGIN_COOKIE, cookie);
        editor.apply();
    }

    public static String getLoginCookie(SharedPreferences sp) {
        return sp.getString(KEY_LOGIN_COOKIE, "");
    }

    public static void setUsername(SharedPreferences sp, String username) {
        SharedPreferences.Editor editor = sp.edit();
        editor.putString(SharedPrefsManager.KEY_USERNAME, username);
        editor.apply();
    }

    public static String getUsername(SharedPreferences sp, Context context) {
        return sp.getString(KEY_USERNAME, context.getResources().getString(R.string.nav_logout));
    }

    public static void setReplyText(SharedPreferences sp, String replyText) {
        SharedPreferences.Editor editor = sp.edit();
        editor.putString(SharedPrefsManager.KEY_REPLY_TEXT, replyText);
        editor.apply();
    }

    public static String getReplyText(SharedPreferences sp) {
        return sp.getString(KEY_REPLY_TEXT, "");
    }

    public static void setPostRead(SharedPreferences sp, long postId) {
        SharedPreferences.Editor editor = sp.edit();
        editor.putBoolean(KEY_POST_IS_READ + postId, true);
        editor.apply();
    }

    public static boolean isPostRead(SharedPreferences sp, long postId) {
        return sp.getBoolean(KEY_POST_IS_READ + postId, false);
    }

    public static void setPostBookmarked(SharedPreferences sp, long postId) {
        SharedPreferences.Editor editor = sp.edit();
        editor.putBoolean(KEY_POST_IS_BOOKMARKED + postId, true);
        editor.apply();
    }

    public static void setPostUnbookmarked(SharedPreferences sp, long postId) {
        SharedPreferences.Editor editor = sp.edit();
        editor.putBoolean(KEY_POST_IS_BOOKMARKED + postId, false);
        editor.apply();
    }

    public static boolean isPostBookmarked(SharedPreferences sp, long postId) {
        return sp.getBoolean(KEY_POST_IS_BOOKMARKED + postId, false);
    }

    public static void setCommentsBookmarked(SharedPreferences sp, long postId) {
        SharedPreferences.Editor editor = sp.edit();
        editor.putBoolean(KEY_COMMENTS_ARE_BOOKMARKED + postId, true);
        editor.apply();
    }

    public static void setCommentsUnbookmarked(SharedPreferences sp, long postId) {
        SharedPreferences.Editor editor = sp.edit();
        editor.putBoolean(KEY_COMMENTS_ARE_BOOKMARKED + postId, false);
        editor.apply();
    }

    public static boolean areCommentsBookmarked(SharedPreferences sp, long postId) {
        return sp.getBoolean(KEY_COMMENTS_ARE_BOOKMARKED + postId, false);
    }

    //Settings
    public static String getTheme(SharedPreferences sp) {
        return sp.getString(KEY_THEME, THEME_LIGHT);
    }

    public static String getScrollMode(SharedPreferences sp) {
        return sp.getString(SCROLL_MODE, SCROLL_MODE_FAB_HOLD);
    }

    public static Boolean getShowPostSummary(SharedPreferences sp, Context context) {
        return sp.getBoolean(context.getResources().getString(R.string.pref_key_show_post_summary),
            false);
    }

    public static Boolean getIsOpenLinkInApp(SharedPreferences sp, Context context) {
        return sp.getBoolean(context.getResources().getString(R.string.pref_key_open_link_option), true);
    }

    @Override
    public boolean isPostRead(long postId) {
        return mPreferences.getBoolean(KEY_POST_IS_READ + postId, false);
    }

    @Override
    public boolean isShowPostSummary() {
        return mPreferences.getBoolean(mContext.getResources()
            .getString(R.string.pref_key_show_post_summary), false);
    }

    @Override
    public boolean isPostBookmarked(long postId) {
        return mPreferences.getBoolean(KEY_POST_IS_BOOKMARKED + postId, false);
    }

    @Override
    public boolean areCommentsBookmarked(long postId) {
        return mPreferences.getBoolean(KEY_COMMENTS_ARE_BOOKMARKED + postId, false);
    }

    @Override
    public void setPostBookmarked(long postId) {
        SharedPreferences.Editor editor = mPreferences.edit();
        editor.putBoolean(KEY_POST_IS_BOOKMARKED + postId, true);
        editor.apply();
    }

    @Override
    public void setCommentsBookmarked(long postId) {
        SharedPreferences.Editor editor = mPreferences.edit();
        editor.putBoolean(KEY_COMMENTS_ARE_BOOKMARKED + postId, true);
        editor.apply();
    }

    @Override
    public void setPostUnbookmarked(long postId) {
        SharedPreferences.Editor editor = mPreferences.edit();
        editor.putBoolean(KEY_POST_IS_BOOKMARKED + postId, false);
        editor.apply();
    }

    @Override
    public void setCommentsUnbookmarked(long postId) {
        SharedPreferences.Editor editor = mPreferences.edit();
        editor.putBoolean(KEY_COMMENTS_ARE_BOOKMARKED + postId, false);
        editor.apply();
    }
} public interface UtilsContract {
    boolean isOnline();

    String convertHtmlToString(String html);
} @Parcel
@StorIOSQLiteType(table = "comment")
public class Comment extends HNItem {

    @StorIOSQLiteColumn(name = CommentTable.COLUMN_COMMENT_ID, key = true)
    @SerializedName("id")
    long commentId;

    @StorIOSQLiteColumn(name = CommentTable.COLUMN_INDEX)
    int _index;

    @StorIOSQLiteColumn(name = CommentTable.COLUMN_BY)
    String by;

    @StorIOSQLiteColumn(name = CommentTable.COLUMN_DELETED)
    boolean deleted;

    @StorIOSQLiteColumn(name = CommentTable.COLUMN_PARENT)
    long parent;

    ArrayList<Long> kids;

    @StorIOSQLiteColumn(name = CommentTable.COLUMN_TIME)
    long time;

    @StorIOSQLiteColumn(name = CommentTable.COLUMN_TEXT)
    String text;

    @StorIOSQLiteColumn(name = CommentTable.COLUMN_LEVEL)
    int level;

    //FIXME necessary?
    public String error;

    public Comment() {
    }

    public Comment(long commentId, String by, long parent, ArrayList<Long> kids, long time, String text) {
        this.commentId = commentId;
        this.by = by;
        this.parent = parent;
        this.kids = kids;
        this.time = time;
        this.text = text;
    }

    public int getIndex() {
        return _index;
    }

    public void setIndex(int _index) {
        this._index = _index;
    }

    public String getText() {
        return text;
    }

    public void setText(String text) {
        this.text = text;
    }

    public String getBy() {
        return by;
    }

    public void setBy(String by) {
        this.by = by;
    }

    public long getCommentId() {
        return commentId;
    }

    public void setCommentId(long commentId) {
        this.commentId = commentId;
    }

    public void setDeleted(boolean deleted) {
        this.deleted = deleted;
    }

    public boolean getDeleted() {
        return deleted;
    }

    public long getParent() {
        return parent;
    }

    public void setParent(long parent) {
        this.parent = parent;
    }

    public ArrayList<Long> getKids() {
        return kids;
    }

    public void setKids(ArrayList<Long> kids) {
        this.kids = kids;
    }

    public long getTime() {
        return time;
    }

    public void setTime(long time) {
        this.time = time;
    }

    public int getLevel() {
        return level;
    }

    public void setLevel(int level) {
        this.level = level;
    }

    public String getError() {
        return error;
    }

    public void setError(String error) {
        this.error = error;
    }
} public class HNItem {

    public static class Footer extends HNItem {

        public Footer() {
        }
    }

    public static class SearchResult extends HNItem {
        SearchHit[] hits;
        int nbHits;
        int page;
        int nbPages;
        int hitsPerPage;
        String query;

        public SearchResult() {
        }

        public void setHits(SearchHit[] hits) {
            this.hits = hits;
        }

        public SearchHit[] getHits(){
            return hits;
        }

        public int getNbPages(){
            return nbPages;
        }
    }

    public static class SearchHit extends HNItem {
        long objectID;

        public SearchHit() {
        }

        public long getObjectID() {
            return objectID;
        }
    }
} @Parcel
@StorIOSQLiteType(table = "post")
public class Post extends HNItem {

    @StorIOSQLiteColumn(name = "id", key = true)
    long id;

    int index;

    @StorIOSQLiteColumn(name = "by")
    String by;

    @StorIOSQLiteColumn(name = "descendants")
    long descendants;

    // StorIO not support List annotation
    List<Long> kids;

    @StorIOSQLiteColumn(name = "score")
    long score;

    @StorIOSQLiteColumn(name = "text")
    String text;

    @StorIOSQLiteColumn(name = "time")
    long time;

    @StorIOSQLiteColumn(name = "title")
    String title;

    @StorIOSQLiteColumn(name = "type")
    String type;

    @StorIOSQLiteColumn(name = "url")
    String url;

    @StorIOSQLiteColumn(name = "prettyUrl")
    String prettyUrl;

    @StorIOSQLiteColumn(name = "summary")
    String summary;

    @StorIOSQLiteColumn(name = "isBookmarked")
    boolean isBookmarked;

    boolean isRead;

    public Post() {
    }

    public Post(Long id) {
        this.id = id;
    }

    public Post(Long id, int index) {
        this.id = id;
        this.index = index;
    }

    public int getIndex() {
        return index;
    }

    public void setIndex(int index) {
        this.index = index;
    }

    public String getBy() {
        return by;
    }

    public void setBy(String by) {
        this.by = by;
    }

    public long getDescendants() {
        return descendants;
    }

    public void setDescendants(long descendants) {
        this.descendants = descendants;
    }

    public long getId() {
        return id;
    }

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

    public List<Long> getKids() {
        return kids;
    }

    public void setKids(List<Long> kids) {
        this.kids = kids;
    }

    public long getScore() {
        return score;
    }

    public void setScore(long score) {
        this.score = score;
    }

    public String getText() {
        return text;
    }

    public void setText(String text) {
        this.text = text;
    }

    public long getTime() {
        return time;
    }

    public void setTime(long time) {
        this.time = time;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public String getType() {
        return type;
    }

    public void setType(String type) {
        this.type = type;
    }

    public String getUrl() {
        return url;
    }

    public void setUrl(String url) {
        this.url = url;
    }

    public String getPrettyUrl() {
        return prettyUrl;
    }

    public void setPrettyUrl(String prettyUrl) {
        this.prettyUrl = prettyUrl;
    }

    public String getSummary() {
        return summary;
    }

    public void setSummary(String summary) {
        this.summary = summary;
    }

    public boolean isBookmarked() {
        return isBookmarked;
    }

    public void setIsBookmarked(boolean isBookmarked) {
        this.isBookmarked = isBookmarked;
    }

    public boolean isRead() {
        return isRead;
    }

    public void setRead(boolean read) {
        isRead = read;
    }
} public class DataManager {
    public static final String HACKER_NEWS_BASE_URL = "https://news.ycombinator.com/";
    public static final String HACKER_NEWS_ITEM_URL = "https://news.ycombinator.com/item?id=";
    private static final int MINIMUM_STRING = 20;
    HackerNewsService mHackerNewsService, mSearchService;

    public DataManager() {
        mHackerNewsService = new RetrofitHelper().getHackerNewsService();
        mSearchService = new RetrofitHelper().getSearchService();
    }

    public Observable<List<Long>> getPostList(String type) {
        return mHackerNewsService.getStories(type);
    }

    public Observable<Post> getPost(Long postId) {
        return mHackerNewsService.getStory(String.valueOf(postId));
    }

    public Observable<Post> getPostsn(List<Long> postIds, boolean isByOrder) {
        if (isByOrder) {
            return Observable.from(postIds)
                .concatMap(new Func1<Long, Observable<Post>>() {
                    @Override
                    public Observable<Post> call(Long aLong) {
                        return mHackerNewsService.getStory(String.valueOf(aLong));
                    }
                })
                .onErrorReturn(new Func1<Throwable, Post>() {
                    @Override
                    public Post call(Throwable throwable) {
                        Log.e("getPosts", throwable.toString());
                        return null;
                    }
                })
                .filter(new Func1<Post, Boolean>() {
                    @Override
                    public Boolean call(Post post) {
                        return post != null && post.getTitle() != null;
                    }
                });
        } else {
            return Observable.from(postIds)
                .flatMap(new Func1<Long, Observable<Post>>() {
                    @Override
                    public Observable<Post> call(Long aLong) {
                        return mHackerNewsService.getStory(String.valueOf(aLong));
                    }
                })
                .onErrorReturn(new Func1<Throwable, Post>() {
                    @Override
                    public Post call(Throwable throwable) {
                        Log.e("getPosts", throwable.toString());
                        return null;
                    }
                })
                .filter(new Func1<Post, Boolean>() {
                    @Override
                    public Boolean call(Post post) {
                        return post != null && post.getTitle() != null;
                    }
                });
        }
    }

    public Observable<Post> getPosts(List<Long> postIds) {
        if (postIds.size() > Constants.NUM_LOADING_ITEMS_SPLIT * 2) {
            return Observable.concat(
                getSubListPosts(postIds.subList(0, Constants.NUM_LOADING_ITEMS_SPLIT)),
                getSubListPosts(postIds.subList(Constants.NUM_LOADING_ITEMS_SPLIT,
                    Constants.NUM_LOADING_ITEMS_SPLIT * 2)),
                getSubListPosts(postIds.subList(Constants.NUM_LOADING_ITEMS_SPLIT * 2,
                    postIds.size())))
                .flatMap(new Func1<List<Post>, Observable<Post>>() {
                    @Override
                    public Observable<Post> call(List<Post> posts) {
                        return Observable.from(posts);
                    }
                });
        } else {
            return getSubListPosts(postIds)
                .flatMap(new Func1<List<Post>, Observable<Post>>() {
                    @Override
                    public Observable<Post> call(List<Post> posts) {
                        return Observable.from(posts);
                    }
                });
        }
    }

    public Observable<List<Post>> getSubListPosts(final List<Long> postIds) {
        return Observable.from(postIds)
            .flatMap(new Func1<Long, Observable<Post>>() {
                @Override
                public Observable<Post> call(Long aLong) {
                    return mHackerNewsService.getStory(String.valueOf(aLong));
                }
            })
            .onErrorReturn(new Func1<Throwable, Post>() {
                @Override
                public Post call(Throwable throwable) {
                    Log.e("getPosts", throwable.toString());
                    return null;
                }
            })
            .filter(new Func1<Post, Boolean>() {
                @Override
                public Boolean call(Post post) {
                    return post != null && post.getTitle() != null;
                }
            })
            .toList()
            .map(new Func1<List<Post>, List<Post>>() {
                @Override
                public List<Post> call(List<Post> posts) {
                    return sortPosts(postIds, posts);
                }
            });
    }

    private List<Post> sortPosts(List<Long> postIds, List<Post> postList) {
        HashMap<Long, Post> postsMap = new HashMap<>();
        List<Post> orderedPostList = new ArrayList<>();
        for (Post post : postList) {
            postsMap.put(post.getId(), post);
        }
        for (Long id : postIds) {
            orderedPostList.add(postsMap.get(id));
        }
        return orderedPostList;
    }

    public Observable<Comment> getSummary(List<Long> commentIds) {
        return Observable.from(commentIds)
            // if use flatMap(), there would be too many concurrent calls
            .concatMap(new Func1<Long, Observable<Comment>>() {
                @Override
                public Observable<Comment> call(Long aLong) {
                    return mHackerNewsService.getComment(aLong);
                }
            }).filter(new Func1<Comment, Boolean>() {
                @Override
                public Boolean call(Comment comment) {
                    if (comment != null
                        && comment.getBy() != null && !comment.getBy().trim().isEmpty()
                        && comment.getText() != null && !comment.getText().trim().isEmpty()
                        && comment.getText().length() > MINIMUM_STRING * 4) {
                        //TODO length num improve needed.
                        String s = Html.fromHtml(comment.getText()).toString().substring(0,
                            MINIMUM_STRING)
                            .toLowerCase();
                        return s.contains("summary")
                            || s.contains("tldr")
                            || s.contains("tl;dr")
                            || s.contains("tl; dr");
                    } else {
                        return false;
                    }
                }
            }).firstOrDefault(null);
    }

    public Observable<List<Comment>> getComments(Post post, int level) {
        List<Long> commentIds = post.getKids();
        long descendants = post.getDescendants();
        if (commentIds.size() > 3 && descendants > 15) {
            Log.i("---getComments", "kids > 3");
            return Observable.concat(getCommentsByBranches(commentIds.subList(0, 3), level),
                getCommentsAllAtOnce(commentIds.subList(3, commentIds.size()), level));
        } else if (descendants / commentIds.size() > 15) {
            Log.i("---getComments", "few kids, big branch");
            return getCommentsByBranches(commentIds, level);
        } else {
            Log.i("---getComments", "other");
            return getCommentsAllAtOnce(commentIds, level);
        }
    }

    public Observable<List<Comment>> getCommentsByBranches(List<Long> commentIds, final int level) {
        Log.i("---", "getCommentsByBranches");
        return Observable.from(commentIds)
            .flatMap(new Func1<Long, Observable<List<Comment>>>() {
                @Override
                public Observable<List<Comment>> call(Long commentId) {
                    return getOneBranchComments(commentId, level);
                }
            });
    }

    public Observable<List<Comment>> getOneBranchComments(final long commentId, final int level) {
        return mHackerNewsService.getComment(commentId)
            .onErrorReturn(new Func1<Throwable, Comment>() {
                @Override
                public Comment call(Throwable throwable) {
                    Log.e("getOneBranchComments", throwable.toString());
                    return null;
                }
            })
            .filter(new Func1<Comment, Boolean>() {
                @Override
                public Boolean call(Comment comment) {
                    return (comment != null) && !comment.getDeleted() && comment.getText() != null;
                }
            })
            .flatMap(new Func1<Comment, Observable<Comment>>() {
                @Override
                public Observable<Comment> call(Comment comment) {
                    Log.i("---getOneBranchComments", String.valueOf(comment.getCommentId()));
                    return getInnerComments(comment, level);
                }
            })
            .toList()
            .map(new Func1<List<Comment>, List<Comment>>() {
                @Override
                public List<Comment> call(List<Comment> allComments) {
                    List<Long> firstLevelCommentAsList = new ArrayList<>();
                    firstLevelCommentAsList.add(commentId);
                    return sortComments(firstLevelCommentAsList, allComments);
                }
            });
    }

    public Observable<List<Comment>> getCommentsAllAtOnce(final List<Long> firstLevelCommentIds, final int level) {
        Log.i("---", "getCommentsAllAtOnce");
        return Observable.from(firstLevelCommentIds)
            .flatMap(new Func1<Long, Observable<Comment>>() {
                @Override
                public Observable<Comment> call(Long commentId) {
                    return mHackerNewsService.getComment(commentId)
                        .onErrorReturn(new Func1<Throwable, Comment>() {
                            @Override
                            public Comment call(Throwable throwable) {
                                Log.e("getCommentsAllAtOnce", throwable.toString());
                                return null;
                            }
                        });
                }
            })
            .filter(new Func1<Comment, Boolean>() {
                @Override
                public Boolean call(Comment comment) {
                    return (comment != null) && !comment.getDeleted() && comment.getText() != null;
                }
            })
            .flatMap(new Func1<Comment, Observable<Comment>>() {
                @Override
                public Observable<Comment> call(Comment comment) {
                    return getInnerComments(comment, level);
                }
            })
            .toList()
            .map(new Func1<List<Comment>, List<Comment>>() {
                @Override
                public List<Comment> call(List<Comment> allComments) {
                    return sortComments(firstLevelCommentIds, allComments);
                }
            });
    }

    private Observable<Comment> getInnerComments(Comment comment, final int level) {
        if (comment == null || comment.getDeleted() || comment.getText() == null) {
            return null;
        }
        comment.setLevel(level);
        if (comment.getKids() != null && !comment.getKids().isEmpty()) {
            return Observable.just(comment)
                .mergeWith(Observable.from(comment.getKids())
                    .flatMap(new Func1<Long, Observable<Comment>>() {
                        @Override
                        public Observable<Comment> call(Long commentId) {
                            return mHackerNewsService.getComment(commentId)
                                .onErrorReturn(new Func1<Throwable, Comment>() {
                                    @Override
                                    public Comment call(Throwable throwable) {
                                        Log.e("getInnerComments", throwable.toString());
                                        return null;
                                    }
                                });
                        }
                    })
                    .filter(new Func1<Comment, Boolean>() {
                        @Override
                        public Boolean call(Comment comment) {
                            return (comment != null) && !comment.getDeleted()
                                && comment.getText() != null;
                        }
                    })
                    .flatMap(new Func1<Comment, Observable<Comment>>() {
                        @Override
                        public Observable<Comment> call(Comment comment) {
                            return getInnerComments(comment, level + 1);
                        }
                    })
                );
        }
        return Observable.just(comment);
    }

    private List<Comment> sortComments(List<Long> firstLevelCommentIds, List<Comment> allComments) {
        HashMap<Long, Comment> allCommentsMap = new HashMap<>();
        for (Comment childComment : allComments) {
            allCommentsMap.put(childComment.getCommentId(), childComment);
        }
        List<Comment> validFirstLevelCommentList = new ArrayList<>();
        for (Long id : firstLevelCommentIds) {
            Comment firstLevelComment = allCommentsMap.get(id);
            if (firstLevelComment != null && !firstLevelComment.getDeleted()
                && firstLevelComment.getText() != null) {
                validFirstLevelCommentList.add(firstLevelComment);
            }
        }
        return sortAllComments(validFirstLevelCommentList, allCommentsMap);
    }

    private List<Comment> sortAllComments(List<Comment> commentList, HashMap<Long, Comment> allCommentsMap) {
        List<Comment> sortedList = new ArrayList<>();
        for (Comment comment : commentList) {
            sortedList.add(comment);
            if (comment.getKids() != null && comment.getKids().size() > 0) {
                List<Comment> validChildCommentList = new ArrayList<>();
                for (long id : comment.getKids()) {
                    Comment childComment = allCommentsMap.get(id);
                    if (childComment != null && !childComment.getDeleted() && childComment.getText() != null) {
                        validChildCommentList.add(childComment);
                    }
                }
                sortedList.addAll(sortAllComments(validChildCommentList, allCommentsMap));
            }
        }
        return sortedList;
    }

    public Observable<HNItem.SearchResult> getPopularPosts(String startTime, int page) {
        return mSearchService.searchPopularity(startTime, page, Constants.NUM_LOADING_ITEMS);
    }

    public Observable<HNItem.SearchResult> getSearchResult(String keyword, String timeRange,
                                                           int page, boolean isSortByDate) {
        if (isSortByDate) {
            return mSearchService.searchByDate(keyword, timeRange, page, Constants.NUM_LOADING_ITEMS);
        } else {
            return mSearchService.search(keyword, timeRange, page, Constants.NUM_LOADING_ITEMS);
        }
    }

    public Observable<String> login(final String username, final String password) {
        return Observable.create(new Observable.OnSubscribe<String>() {
            @Override
            public void call(Subscriber<? super String> subscriber) {
                try {
                    Connection login = Jsoup.connect(HACKER_NEWS_BASE_URL + "login");
                    login.header("Accept-Encoding", "gzip")
                        .data("go_to", "news")
                        .data("acct", username)
                        .data("pw", password)
                        .header("Origin", "https://news.ycombinator.com")
                        .followRedirects(true)
                        .referrer(HACKER_NEWS_BASE_URL + "login?go_to=news")
                        .method(Connection.Method.POST);
                    Connection.Response response = login.execute();
                    String cookie = response.cookie("user");
                    if (cookie == null) {
                        subscriber.onNext("");
                    } else {
                        subscriber.onNext(cookie);
                    }
                } catch (Exception e) {
                    subscriber.onError(e);
                }
            }
        });
    }

    public Observable<Integer> vote(final long itemId, final int voteState, final SharedPreferences sp) {
        return Observable.create(new Observable.OnSubscribe<Integer>() {
            @Override
            public void call(Subscriber<? super Integer> subscriber) {
                String cookieLogin = SharedPrefsManager.getLoginCookie(sp);
                if (cookieLogin.isEmpty()) {
                    subscriber.onNext(Constants.OPERATE_ERROR_NO_COOKIE);
                    subscriber.onCompleted();
                }
                try {
                    Connection vote = Jsoup.connect(HACKER_NEWS_ITEM_URL + itemId)
                        .header("Accept-Encoding", "gzip")
                        .cookie("user", cookieLogin);
                    Document commentsDocument = vote.get();
                    /*
                    votable element:
                    <a id="up_10276091"
                     href="vote?for=10276091&dir=up&auth=3ecc4be748d7cc412223ea906b559ce72ccdc262&goto=news"
                     onclick="return vote(this)">

                     url:
                    https://news.ycombinator.com/vote?for=10276091&dir=up
                     &auth=3ecc4be748d7cc412223ea906b559ce72ccdc262&goto=news

                    logout element:
                    <a id="up_10276091" href="vote?for=10276091&dir=up&goto=item%3Fid%3D10276091">

                    url:
                    https://news.ycombinator.com/vote?for=10276091&dir=up&goto=item%3Fid%3D10276091
                    */
                    Elements links;
                    if (voteState == Constants.VOTE_DOWN) {
                        links = commentsDocument.select("a[id=down_" + itemId + "]");
                    } else {
                        links = commentsDocument.select("a[id=up_" + itemId + "]");
                    }
                    if (links.size() == 0) {
                        if (voteState == Constants.VOTE_DOWN) {
                            subscriber.onNext(Constants.OPERATE_ERROR_NOT_ENOUGH_KARMA);
                        } else {
                            subscriber.onNext(Constants.OPERATE_ERROR_HAVE_VOTED);
                        }
                        subscriber.onCompleted();
                    } else {
                        Element voteElement = links.get(0).select("a[href^=vote]").first();
                        if (!voteElement.attr("href").contains("auth=")) {
                            subscriber.onNext(Constants.OPERATE_ERROR_COOKIE_EXPIRED);
                            subscriber.onCompleted();
                        }
                        if (voteElement.attr("href").contains("auth=")) {
                            String url = (voteElement.attr("href"));
                            Request voteRequest = new Request.Builder()
                                .addHeader("cookie", "user=" + cookieLogin)
                                .url(HACKER_NEWS_BASE_URL + url)
                                .build();
                            Response response = new OkHttpClient().newCall(voteRequest).execute();
                            if (response.code() == 200) {
                                if (response.body() == null) {
                                    subscriber.onNext(Constants.OPERATE_ERROR_UNKNOWN);
                                } else {
                                    subscriber.onNext(Constants.OPERATE_SUCCESS);
                                }
                            } else {
                                subscriber.onNext(Constants.OPERATE_ERROR_UNKNOWN);
                            }
                        }
                    }
                } catch (Exception e) {
                    subscriber.onError(e);
                }
            }
        });
    }

    public Observable<Integer> reply(final long itemId, final String replyText, final String cookieLogin) {
        return Observable.create(new Observable.OnSubscribe<Integer>() {
            @Override
            public void call(Subscriber<? super Integer> subscriber) {
                if (cookieLogin.isEmpty()) {
                    subscriber.onNext(Constants.OPERATE_ERROR_NO_COOKIE);
                    subscriber.onCompleted();
                }
                try {
                    Connection reply = Jsoup.connect(HACKER_NEWS_ITEM_URL + itemId)
                        .header("Accept-Encoding", "gzip")
                        .cookie("user", cookieLogin);
                    Document replyDocument = reply.get();
                    Element element = replyDocument.select("input[name=hmac]").first();
                    if (element != null) {
                        String replyHmac = element.attr("value");
                        RequestBody requestBody = (new FormEncodingBuilder())
                            .add("parent", String.valueOf(itemId))
                            .add("goto", (new StringBuilder()).append("item?id=").append(itemId).toString())
                            .add("hmac", replyHmac)
                            .add("text", replyText)
                            .build();
                        Request request = new Request.Builder()
                            .addHeader("cookie", "user=" + cookieLogin)
                            .url(HACKER_NEWS_BASE_URL + "comment")
                            .post(requestBody)
                            .build();

                        Response response = new OkHttpClient().newCall(request).execute();
                        if (response.code() == 200) {
                            subscriber.onNext(Constants.OPERATE_SUCCESS);
                        } else {
                            subscriber.onNext(Constants.OPERATE_ERROR_UNKNOWN);
                        }
                    } else {
                        subscriber.onNext(Constants.OPERATE_ERROR_COOKIE_EXPIRED);
                    }
                } catch (Exception e) {
                    subscriber.onError(e);
                }
            }
        });
    }
} public class CommentAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
    private static final int VIEW_TYPE_HEADER = 0;
    private static final int VIEW_TYPE_COMMENT = 1;
    private static final int VIEW_TYPE_FOOTER = 2;

    private int mLoadingState;
    private int mCommentIndentColorOrange, mCommentIndentColorBg, mCommentIndentWidth;
    private Typeface mFont;
    private float mTextSize, mLineHeight;
    private HTMLCodeTagHandler mHTMLCodeTagHandler;
    private Map<Long, List<Comment>> mCollapsedChildrenCommentsIndex;
    private Map<Long, List<Comment>> mCollapsedOlderCommentsIndex;
    private Context mContext;
    private RecyclerView mRecyclerView;
    private ArrayList<HNItem> mItemList;
    private ArrayList<Comment> mCommentList;
    private SharedPreferences prefs;

    public CommentAdapter(Context context, RecyclerView recyclerView) {
        mContext = context;
        mRecyclerView = recyclerView;
        mItemList = new ArrayList<>();
        mCommentList = new ArrayList<>();
        mHTMLCodeTagHandler = new HTMLCodeTagHandler();
        mCollapsedChildrenCommentsIndex = new HashMap<>();
        mCollapsedOlderCommentsIndex = new HashMap<>();
        prefs = ((CommentsActivity) mContext).getSharedPreferences();
        updateCommentPrefs();
        setCommentIndentStyle();
    }

    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup viewGroup, int viewType) {
        View v;
        RecyclerView.ViewHolder viewHolder = null;
        switch (viewType) {
            case VIEW_TYPE_COMMENT:
                v = LayoutInflater.from(viewGroup.getContext()).inflate(
                    R.layout.list_item_comment, viewGroup, false);
                CommentViewHolder vh = new CommentViewHolder(v, setupViewHolderClickListener());
                vh.tvComment.setTypeface(mFont);
                vh.tvComment.setPaintFlags(vh.tvComment.getPaintFlags() | Paint.SUBPIXEL_TEXT_FLAG);
                vh.tvComment.setTextSize(mTextSize);
                vh.tvComment.setLineSpacing(0, mLineHeight);
                vh.setCommentIndentStripeStyle(mCommentIndentColorOrange,
                    mCommentIndentColorBg, mCommentIndentWidth);
                viewHolder = vh;
                break;
            case VIEW_TYPE_HEADER:
                v = LayoutInflater.from(viewGroup.getContext())
                    .inflate(R.layout.list_item_comment_header, viewGroup, false);
                viewHolder = new CommentHeaderViewHolder(v);
                break;
            case VIEW_TYPE_FOOTER:
                v = LayoutInflater.from(viewGroup.getContext())
                    .inflate(R.layout.list_item_footer, viewGroup, false);
                viewHolder = new FooterViewHolder(v);
        }
        return viewHolder;
    }

    private CommentViewHolder.ViewHolderClickListener setupViewHolderClickListener() {
        return new CommentViewHolder.ViewHolderClickListener() {
            @Override
            public void onClick(int position) {
                if (isInBetweenCommentsCollapsed(position)) {
                    expandInBetweenComments(position);
                } else {
                    collapseInBetweenComments(position);
                }
            }

            @Override
            public void onClickComment(int position) {
                if (isChildrenCommentsCollapsed(position)) {
                    expandChildrenComments(position);
                } else {
                    collapseChildrenComment(position);
                }
            }

            @Override
            public void onLongClick(int position) {
                showCommentOptionDialog(position);
            }
        };
    }

    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, int position) {
        if (viewHolder instanceof CommentViewHolder) {
            // which one is better?
//        if (getItemViewType(position) == VIEW_TYPE_COMMENT) {
            bindCommentViewHolder(viewHolder, position);
        } else if (viewHolder instanceof CommentHeaderViewHolder) {
            bindHeaderViewHolder(viewHolder);
        } else if (viewHolder instanceof FooterViewHolder) {
            bindFooterViewHolder(viewHolder);
        }
    }

    private void bindCommentViewHolder(RecyclerView.ViewHolder viewHolder, int position) {
        CommentViewHolder commentViewHolder = (CommentViewHolder) viewHolder;
        Comment comment = (Comment) mItemList.get(position);
        commentViewHolder.setCommentIndent(comment.getLevel());
        commentViewHolder.tvTime.setText(Utils.formatTime(comment.getTime()));
        if (!comment.getDeleted()) {
            commentViewHolder.tvAuthor.setText(comment.getBy());
        }

        if (mCollapsedChildrenCommentsIndex.containsKey(comment.getCommentId())) {
            commentViewHolder.tvComment.setText(mContext.getString(R.string.comments_collapsed_prompt,
                mCollapsedChildrenCommentsIndex.get(comment.getCommentId()).size() + 1));
            commentViewHolder.tvComment.setMinLines(2);
            commentViewHolder.tvComment.setGravity(Gravity.CENTER);
        } else {
            setTextViewHTML(commentViewHolder.tvComment, comment.getText());
            commentViewHolder.tvComment.setMinLines(Integer.MIN_VALUE);
            commentViewHolder.tvComment.setGravity(Gravity.LEFT);
        }

        if (mCollapsedOlderCommentsIndex.containsKey(comment.getCommentId())) {
            commentViewHolder.tvCollapseOlderComments.setVisibility(View.VISIBLE);
            commentViewHolder.tvCollapseOlderComments.setText(
                mContext.getString(R.string.comments_collapsed_prompt,
                    mCollapsedOlderCommentsIndex.get(comment.getCommentId()).size()));
        } else {
            commentViewHolder.tvCollapseOlderComments.setVisibility(View.GONE);
        }
    }

    private void bindHeaderViewHolder(RecyclerView.ViewHolder viewHolder) {
        CommentHeaderViewHolder commentHeaderViewHolder = (CommentHeaderViewHolder) viewHolder;
        Post post = (Post) mItemList.get(0);
        commentHeaderViewHolder.tvTitle.setText(post.getTitle());
        Typeface postFont = Typeface.createFromAsset(mContext.getAssets(),
            SharedPrefsManager.getPostFont(prefs) + ".ttf");
        commentHeaderViewHolder.tvTitle.setTypeface(postFont);
        commentHeaderViewHolder.tvTitle.setTextSize(SharedPrefsManager.getCommentFontSize(prefs) * 1.5f);
        commentHeaderViewHolder.tvTitle.setPaintFlags(
            commentHeaderViewHolder.tvTitle.getPaintFlags() | Paint.SUBPIXEL_TEXT_FLAG);
        commentHeaderViewHolder.tvUrl.setText(post.getUrl());
        commentHeaderViewHolder.tvPoints.setText("+" + String.valueOf(post.getScore()));
        commentHeaderViewHolder.tvComments.setText(String.valueOf(post.getDescendants())
            + (post.getDescendants() > 1 ? " comments" : " comment"));
        commentHeaderViewHolder.tvTime.setText(Utils.formatTime(post.getTime()));
        commentHeaderViewHolder.tvPoster.setText("by: " + post.getBy());
        if (post.getText() != null && !post.getText().isEmpty()) {
            commentHeaderViewHolder.tvContent.setVisibility(View.VISIBLE);
            commentHeaderViewHolder.tvContent.setText(Html.fromHtml(post.getText()));
            commentHeaderViewHolder.tvContent.setTextSize(SharedPrefsManager.getCommentFontSize(prefs));
            Typeface commentFont = Typeface.createFromAsset(mContext.getAssets(),
                SharedPrefsManager.getCommentFont(prefs) + ".ttf");
            commentHeaderViewHolder.tvContent.setTypeface(commentFont);
            commentHeaderViewHolder.tvContent.
                setLineSpacing(0, SharedPrefsManager.getCommentLineHeight(prefs));
        }
    }

    private void bindFooterViewHolder(RecyclerView.ViewHolder viewHolder) {
        FooterViewHolder footerViewHolder = (FooterViewHolder) viewHolder;
        if (mLoadingState == Constants.LOADING_FINISH) {
            footerViewHolder.progressBar.setVisibility(View.GONE);
            footerViewHolder.tvPrompt.setVisibility(View.GONE);
        } else if (mLoadingState == Constants.LOADING_PROMPT_NO_CONTENT) {
            footerViewHolder.progressBar.setVisibility(View.GONE);
            footerViewHolder.tvPrompt.setText(
                mContext.getResources().getString(R.string.prompt_no_comments));
        } else if (mLoadingState == Constants.LOADING_IN_PROGRESS) {
            footerViewHolder.progressBar.setVisibility(View.VISIBLE);
            footerViewHolder.tvPrompt.setVisibility(View.GONE);
        } else if (mLoadingState == Constants.LOADING_ERROR) {
            footerViewHolder.progressBar.setVisibility(View.GONE);
            footerViewHolder.tvPrompt.setText(
                mContext.getResources().getString(R.string.prompt_comments_loading_error));
        }
    }

    private void setTextViewHTML(TextView textView, String string) {
        // trim two trailing blank lines
        CharSequence sequence =
            Html.fromHtml(string.replace("<p>", "<br /><br />").replace("\n", "<br />"), null, mHTMLCodeTagHandler);
        // use Chrome custom tab if it is available
        if (mContext instanceof CommentsActivity
            && ((CommentsActivity) mContext).getChromeCustomTabsHelper() != null) {
            SpannableStringBuilder strBuilder = new SpannableStringBuilder(sequence);
            URLSpan[] urls = strBuilder.getSpans(0, sequence.length(), URLSpan.class);
            for (URLSpan span : urls) {
                makeLinkClickable(strBuilder, span);
            }
            textView.setText(strBuilder);
        } else {
            textView.setText(sequence);
        }
    }

    private void makeLinkClickable(SpannableStringBuilder strBuilder, final URLSpan span) {
        int start = strBuilder.getSpanStart(span);
        int end = strBuilder.getSpanEnd(span);
        int flags = strBuilder.getSpanFlags(span);
        ClickableSpan clickable = new ClickableSpan() {
            public void onClick(View view) {
                Uri uri = Uri.parse(span.getURL());
                CustomTabsIntent.Builder intentBuilder = new CustomTabsIntent.Builder();
                Utils.setupIntentBuilder(intentBuilder, mContext,
                    ((CommentsActivity) mContext).getSharedPreferences());
                ChromeCustomTabsHelper.openCustomTab((Activity) mContext, intentBuilder.build(),
                    uri, null);
            }
        };
        strBuilder.setSpan(clickable, start, end, flags);
        strBuilder.removeSpan(span);
    }


    @Override
    public int getItemCount() {
        return mItemList.size();
    }

    @Override
    public int getItemViewType(int position) {
        if (mItemList.get(position) instanceof Post) {
            return VIEW_TYPE_HEADER;
        } else if (mItemList.get(position) instanceof HNItem.Footer) {
            return VIEW_TYPE_FOOTER;
        } else {
            return VIEW_TYPE_COMMENT;
        }
    }

    public void add(HNItem item) {
        mItemList.add(item);
    }

    public void addHeader(Post post) {
        mItemList.add(0, post);
        notifyItemInserted(0);
    }

    public void addComment(Comment comment) {
        mItemList.add(mItemList.size() - 1, comment);
        notifyItemInserted(mItemList.indexOf(comment));
        mCommentList.add(comment);
    }

    public void addFooter(HNItem.Footer footer) {
        mItemList.add(footer);
        notifyItemInserted(mItemList.size() - 1);
    }

    public void addAllComments(List<Comment> commentList) {
        mItemList.addAll(mItemList.size() - 1, commentList);
        notifyDataSetChanged();
        mCommentList.addAll(commentList);
    }

    public void updateFooter(int loadingState) {
        mLoadingState = loadingState;
    }

    public ArrayList<Comment> getCommentList() {
        return mCommentList;
    }

    public void clear() {
        mItemList.clear();
        mCommentList.clear();
    }

    public void collapseChildrenComment(int position) {
        ArrayList<Comment> childrenComments = new ArrayList<>();
        Comment parentComment = (Comment) mItemList.get(position);
        LinearLayoutManager linearLayoutManager = (LinearLayoutManager) mRecyclerView.getLayoutManager();
        // if a comment header is not visible when collapsing, scroll to it's header
        if (position != linearLayoutManager.findFirstCompletelyVisibleItemPosition()) {
            linearLayoutManager.scrollToPosition(position);
        }

        for (int curPosition = position + 1;
             (mItemList.get(curPosition) instanceof Comment
                 && ((Comment) mItemList.get(curPosition)).getLevel() > parentComment.getLevel());
             curPosition++) {
            childrenComments.add((Comment) mItemList.get(curPosition));
        }

        if (!childrenComments.isEmpty()) {
            mCollapsedChildrenCommentsIndex.put(parentComment.getCommentId(), childrenComments);
            for (Comment comment : childrenComments) {
                mItemList.remove(comment);
            }
            notifyItemChanged(position);
            notifyItemRangeRemoved(position + 1, childrenComments.size());
        }
    }

    public void expandChildrenComments(int position) {
        Comment parentComment = (Comment) mItemList.get(position);
        List<Comment> collapsedComments =
            mCollapsedChildrenCommentsIndex.get(parentComment.getCommentId());
        int insertPosition = mItemList.indexOf(parentComment) + 1;
        for (Comment comment : collapsedComments) {
            mItemList.add(insertPosition, comment);
            insertPosition++;
        }
        notifyItemRangeInserted(mItemList.indexOf(parentComment) + 1,
            collapsedComments.size());
        notifyItemChanged(position);
        mCollapsedChildrenCommentsIndex.remove(parentComment.getCommentId());
    }

    public boolean isChildrenCommentsCollapsed(int position) {
        Comment Comment = (Comment) mItemList.get(position);
        return mCollapsedChildrenCommentsIndex.containsKey(Comment.getCommentId());
    }

    public void collapseInBetweenComments(int position) {
        ArrayList<Comment> olderComments = new ArrayList<>();
        Comment curComment = (Comment) mItemList.get(position);
        for (int curPosition = position - 1;
             curComment.getLevel() > 0
                 && ((Comment) mItemList.get(curPosition)).getLevel() >= curComment.getLevel()
                 && curPosition > 0;
             curPosition--) {
            olderComments.add(0, (Comment) mItemList.get(curPosition));
        }

        if (!olderComments.isEmpty()) {
            mCollapsedOlderCommentsIndex.put(curComment.getCommentId(), olderComments);
            for (Comment comment : olderComments) {
                mItemList.remove(comment);
            }
            notifyItemChanged(position);
            notifyItemRangeRemoved(position - olderComments.size(), olderComments.size());
        }
    }

    public void expandInBetweenComments(int position) {
        Comment clingedComment = (Comment) mItemList.get(position);
        List<Comment> olderComments = mCollapsedOlderCommentsIndex.get(clingedComment.getCommentId());
        int insertPosition = position;
        for (Comment comment : olderComments) {
            mItemList.add(insertPosition, comment);
            insertPosition++;
        }

        notifyItemChanged(position);
        notifyItemRangeInserted(position, olderComments.size());
        mCollapsedOlderCommentsIndex.remove(clingedComment.getCommentId());

    }

    public boolean isInBetweenCommentsCollapsed(int position) {
        Comment Comment = (Comment) mItemList.get(position);
        return mCollapsedOlderCommentsIndex.containsKey(Comment.getCommentId());
    }

    private void showCommentOptionDialog(int position) {
        final Comment comment = (Comment) mItemList.get(position);
        ((CommentsActivity) mContext).showCommentOptionDialog(comment);
        // scrollToPosition() not working
        ((LinearLayoutManager) mRecyclerView.getLayoutManager()).scrollToPositionWithOffset(position, 0);
    }

    public void updateCommentPrefs() {
        mFont = Typeface.createFromAsset(mContext.getAssets(),
            SharedPrefsManager.getCommentFont(prefs) + ".ttf");
        mTextSize = SharedPrefsManager.getCommentFontSize(prefs);
        mLineHeight = SharedPrefsManager.getCommentLineHeight(prefs);
    }

    private void setCommentIndentStyle() {
        mCommentIndentWidth = Utils.convertDpToPixels(CommentViewHolder.UNIT_COMMENT_INDENT_DP, mContext);
        if (SharedPrefsManager.getTheme(prefs).equals(SharedPrefsManager.THEME_LIGHT)) {
            mCommentIndentColorOrange = 0xFFffa726; //orange_400
            mCommentIndentColorBg = 0xFFEEEEEE; //grey_200
        } else if (SharedPrefsManager.getTheme(prefs).equals(SharedPrefsManager.THEME_DARK)) {
            mCommentIndentColorOrange = 0xFFe65100; //orange_900
            mCommentIndentColorBg = 0xFF212121; //grey_900
        } else if (SharedPrefsManager.getTheme(prefs).equals(SharedPrefsManager.THEME_AMOLED_BLACK)) {
            mCommentIndentColorOrange = 0xFFe65100; //orange_900
            mCommentIndentColorBg = 0xFF000000; //black
        } else {
            mCommentIndentColorOrange = 0xFFffa726; //orange_400
            mCommentIndentColorBg = 0xFFF4ECD8; //sepia
        }
    }
}
import android.app.Activity;
import android.app.Fragment;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.os.Parcelable;
import android.preference.PreferenceManager;
import android.support.annotation.StringRes;
import android.support.design.widget.Snackbar;
import android.support.v4.widget.SwipeRefreshLayout;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.RelativeLayout;
import android.widget.TextView;
import android.widget.Toast;
import com.leavjenn.hews.R;
import com.leavjenn.hews.misc.Utils;
import com.leavjenn.hews.data.local.LocalDataManager;
import com.leavjenn.hews.listener.OnRecyclerViewCreatedListener;
import com.leavjenn.hews.misc.SharedPrefsContract;
import com.leavjenn.hews.misc.SharedPrefsManager;
import com.leavjenn.hews.misc.UtilsContract;
import com.leavjenn.hews.model.Comment;
import com.leavjenn.hews.model.HNItem;
import com.leavjenn.hews.model.Post;
import com.leavjenn.hews.data.remote.DataManager;
import com.leavjenn.hews.ui.adapter.CommentAdapter;
import org.parceler.Parcels;
import java.util.List;
package com.leavjenn.hews.ui.comment;





public class CommentsFragment extends Fragment
    implements CommentView, SharedPreferences.OnSharedPreferenceChangeListener {
    private static final String KEY_LIST_STATE = "key_list_state";

    private RelativeLayout layoutRoot;
    private SwipeRefreshLayout swipeRefreshLayout;
    private RecyclerView rvCommentList;
    private Snackbar snackbarNoConnection;
    private LinearLayoutManager mLinearLayoutManager;
    private CommentAdapter mCommentAdapter;

    private float mFontSize, mLineHeight;

    private CommentPresenter mCommentPresenter;
    private SharedPreferences prefs;
    private DataManager mDataManager;
    private LocalDataManager mLocalDataManager;
    private SharedPrefsContract mSharedPrefs;
    private UtilsContract mUtils;

    private OnRecyclerViewCreatedListener mOnRecyclerViewCreatedListener;

    public CommentsFragment() {
    }

    public static CommentsFragment newInstance(Parcelable postParcel, boolean isBookmarked) {
        CommentsFragment fragment = new CommentsFragment();
        Bundle args = new Bundle();
        args.putParcelable(CommentPresenter.KEY_POST_PARCEL, postParcel);
        args.putBoolean(CommentPresenter.KEY_IS_BOOKMARKED, isBookmarked);
        fragment.setArguments(args);
        return fragment;
    }

    public static CommentsFragment newInstance(Long postId) {
        CommentsFragment fragment = new CommentsFragment();
        Bundle args = new Bundle();
        args.putLong(CommentPresenter.KEY_POST_ID, postId);
        fragment.setArguments(args);
        return fragment;
    }

    @Override
    public void onAttach(Activity activity) {
        super.onAttach(activity);
        try {
            mOnRecyclerViewCreatedListener = (OnRecyclerViewCreatedListener) activity;
        } catch (ClassCastException e) {
            throw new ClassCastException(activity.toString()
                + " must implement (MainActivity.OnRecyclerViewCreatedListener)");
        }
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setRetainInstance(true);
        mDataManager = new DataManager();
        prefs = PreferenceManager.getDefaultSharedPreferences(getActivity());
        prefs.registerOnSharedPreferenceChangeListener(this);
        mLocalDataManager = new LocalDataManager(getActivity());
        mSharedPrefs = new SharedPrefsManager(getActivity(), prefs);
        mUtils = new Utils(getActivity());