package com.fastaccess.data.dao;
import android.graphics.Color;
import android.os.Parcel;
import android.os.Parcelable;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import com.annimon.stream.Collectors;
import com.annimon.stream.Stream;
import com.fastaccess.data.dao.model.Comment;
import com.fastaccess.data.dao.model.Issue;
import com.fastaccess.data.dao.model.IssueEvent;
import com.fastaccess.data.dao.model.PullRequest;
import com.fastaccess.data.dao.types.IssueEventType;
import com.fastaccess.data.dao.types.ReviewStateType;
import com.fastaccess.helper.InputHelper;
import com.fastaccess.helper.ParseDateFormat;
import com.fastaccess.ui.widgets.LabelSpan;
import com.fastaccess.ui.widgets.SpannableBuilder;
import java.util.ArrayList;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import static com.annimon.stream.Collectors.toList;
/**
* Created by Kosh on 30 Mar 2017, 9:03 PM
*/
@Getter @Setter @NoArgsConstructor public class TimelineModel implements Parcelable {
public static final int HEADER = 0;
public static final int STATUS = 1;
public static final int REVIEW = 2;
public static final int GROUPED_REVIEW = 3;
public static final int EVENT = 4;
public static final int COMMENT = 5;
private int type;
private Issue issue;
private Comment comment;
private IssueEvent event;
private PullRequest pullRequest;
private PullRequestStatusModel status;
private ReviewModel review;
private GroupedReviewModel groupedReview;
private ReviewCommentModel reviewComment;
private Date sortedDate;
private TimelineModel(Issue issue) {
this.type = HEADER;
this.issue = issue;
this.sortedDate = issue.getCreatedAt();
}
private TimelineModel(PullRequest pullRequest) {
this.type = HEADER;
this.pullRequest = pullRequest;
this.sortedDate = pullRequest.getCreatedAt();
}
private TimelineModel(Comment comment) {
this.type = COMMENT;
this.comment = comment;
this.sortedDate = comment.getCreatedAt() == null ? new Date() : comment.getCreatedAt();
}
private TimelineModel(IssueEvent event) {
this.type = EVENT;
this.event = event;
this.sortedDate = event.getCreatedAt();
}
private TimelineModel(PullRequestStatusModel status) {
this.type = STATUS;
this.status = status;
this.sortedDate = status.getCreatedAt();
}
private TimelineModel(ReviewModel review) {
this.type = REVIEW;
this.review = review;
this.sortedDate = review.getSubmittedAt();
}
private TimelineModel(GroupedReviewModel groupedReview) {
this.type = GROUPED_REVIEW;
this.groupedReview = groupedReview;
this.sortedDate = groupedReview.getDate();
}
@NonNull public static TimelineModel constructHeader(@NonNull Issue issue) {
return new TimelineModel(issue);
}
@NonNull public static TimelineModel constructHeader(@NonNull PullRequest pullRequest) {
return new TimelineModel(pullRequest);
}
@NonNull public static TimelineModel constructComment(@NonNull Comment comment) {
return new TimelineModel(comment);
}
@NonNull public static List<TimelineModel> construct(@Nullable List<Comment> commentList) {
ArrayList<TimelineModel> list = new ArrayList<>();
if (commentList != null && !commentList.isEmpty()) {
list.addAll(Stream.of(commentList)
.map(TimelineModel::new)
.collect(Collectors.toList()));
}
return list;
}
@NonNull public static List<TimelineModel> construct(@NonNull List<Comment> commentList, @NonNull List<IssueEvent> eventList) {
ArrayList<TimelineModel> list = new ArrayList<>();
if (!commentList.isEmpty()) {
list.addAll(Stream.of(commentList)
.map(TimelineModel::new)
.collect(Collectors.toList()));
}
if (!eventList.isEmpty()) {
list.addAll(constructLabels(eventList));
}
return Stream.of(list).sorted((o1, o2) -> {
if (o1.getEvent() != null && o2.getComment() != null) {
return o1.getEvent().getCreatedAt().compareTo(o2.getComment().getCreatedAt());
} else if (o1.getComment() != null && o2.getEvent() != null) {
return o1.getComment().getCreatedAt().compareTo(o2.getEvent().getCreatedAt());
} else {
return Integer.valueOf(o1.getType()).compareTo(o2.getType());
}
}).collect(Collectors.toList());
}
@NonNull public static List<TimelineModel> construct(@NonNull List<Comment> commentList, @NonNull List<IssueEvent> eventList,
@Nullable PullRequestStatusModel status, @Nullable List<ReviewModel> reviews,
@Nullable List<ReviewCommentModel> reviewComments) {
ArrayList<TimelineModel> list = new ArrayList<>();
if (status != null) {
list.add(new TimelineModel(status));
}
if (reviews != null && !reviews.isEmpty()) {
list.addAll(constructReviews(reviews, reviewComments));
}
if (!commentList.isEmpty()) {
list.addAll(Stream.of(commentList)
.map(TimelineModel::new)
.collect(Collectors.toList()));
}
if (!eventList.isEmpty()) {
list.addAll(constructLabels(eventList));
}
return Stream.of(list).sortBy(model -> {
if (model.getSortedDate() != null) {
return model.getSortedDate().getTime();
} else {
return (long) model.getType();
}
}).collect(Collectors.toList());
}
@NonNull private static List<TimelineModel> constructLabels(@NonNull List<IssueEvent> eventList) {
List<TimelineModel> models = new ArrayList<>();
Map<String, List<IssueEvent>> issueEventMap = Stream.of(eventList)
.filter(value -> value.getEvent() != null && value.getEvent() != IssueEventType.subscribed &&
value.getEvent() != IssueEventType.unsubscribed && value.getEvent() != IssueEventType.mentioned)
.collect(Collectors.groupingBy(issueEvent -> {
if (issueEvent.getAssigner() != null && issueEvent.getAssignee() != null) {
return issueEvent.getAssigner().getLogin();
}
return issueEvent.getActor().getLogin();
}));
for (List<IssueEvent> issueEvents : issueEventMap.values()) {
IssueEvent toAdd = null;
SpannableBuilder spannableBuilder = SpannableBuilder.builder();
for (IssueEvent issueEventModel : issueEvents) {
if (issueEventModel != null) {
IssueEventType event = issueEventModel.getEvent();
if (event != null) {
if (toAdd == null) {
toAdd = issueEventModel;
}
long time = toAdd.getCreatedAt().after(issueEventModel.getCreatedAt()) ? (toAdd.getCreatedAt().getTime() - issueEventModel
.getCreatedAt().getTime()) : (issueEventModel.getCreatedAt().getTime() - toAdd.getCreatedAt().getTime());
if (TimeUnit.MINUTES.toMinutes(time) <= 2 && toAdd.getEvent() == event) {
if (event == IssueEventType.labeled || event == IssueEventType.unlabeled) {
LabelModel labelModel = issueEventModel.getLabel();
int color = Color.parseColor("#" + labelModel.getColor());
spannableBuilder.append(" ")
.append(" " + labelModel.getName() + " ", new LabelSpan(color))
.append(" ");
} else if (event == IssueEventType.assigned || event == IssueEventType.unassigned) {
spannableBuilder.append(" ")
.bold(issueEventModel.getAssignee() != null ? issueEventModel.getAssignee().getLogin() : "",
new LabelSpan(Color.TRANSPARENT))
.append(" ");
}
} else {
models.add(new TimelineModel(issueEventModel));
}
} else {
models.add(new TimelineModel(issueEventModel));
}
}
}
if (toAdd != null) {
SpannableBuilder builder = SpannableBuilder.builder();
if (toAdd.getAssignee() != null && toAdd.getAssigner() != null) {
builder.bold(toAdd.getAssigner().getLogin(), new LabelSpan(Color.TRANSPARENT));
} else {
if (toAdd.getActor() != null) {
builder.bold(toAdd.getActor().getLogin(), new LabelSpan(Color.TRANSPARENT));
}
}
builder.append(" ")
.append(toAdd.getEvent().name().replaceAll("_", " "), new LabelSpan(Color.TRANSPARENT));
toAdd.setLabels(SpannableBuilder.builder()
.append(builder)
.append(spannableBuilder)
.append(" ")
.append(ParseDateFormat.getTimeAgo(toAdd.getCreatedAt()), new LabelSpan(Color.TRANSPARENT)));
models.add(new TimelineModel(toAdd));
}
}
return Stream.of(models)
.sortBy(timelineModel -> timelineModel.getEvent().getCreatedAt())
.collect(Collectors.toList());
}
@NonNull private static List<TimelineModel> constructReviews(@NonNull List<ReviewModel> reviews, @Nullable List<ReviewCommentModel> comments) {
List<TimelineModel> models = new ArrayList<>();
if (comments == null || comments.isEmpty()) {
models.addAll(Stream.of(reviews)
.map(TimelineModel::new)
.collect(Collectors.toList()));
} else { // this is how bad github API is.
Map<Integer, List<ReviewCommentModel>> mappedComments = Stream.of(comments)
.collect(Collectors.groupingBy(ReviewCommentModel::getOriginalPosition, LinkedHashMap::new,
Collectors.mapping(o -> o, toList())));
for (Map.Entry<Integer, List<ReviewCommentModel>> entry : mappedComments.entrySet()) {
List<ReviewCommentModel> reviewCommentModels = entry.getValue();
GroupedReviewModel groupedReviewModel = new GroupedReviewModel();
if (!reviewCommentModels.isEmpty()) {
ReviewCommentModel reviewCommentModel = reviewCommentModels.get(0);
groupedReviewModel.setPath(reviewCommentModel.getPath());
groupedReviewModel.setDiffText(reviewCommentModel.getDiffHunk());
groupedReviewModel.setDate(reviewCommentModel.getCreatedAt());
groupedReviewModel.setPosition(reviewCommentModel.getOriginalPosition());
}
groupedReviewModel.setComments(reviewCommentModels);
models.add(new TimelineModel(groupedReviewModel));
}
models.addAll(Stream.of(reviews)
.filter(reviewModel -> !InputHelper.isEmpty(reviewModel.getBody()) || reviewModel.getState() == ReviewStateType.APPROVED)
.map(TimelineModel::new)
.collect(Collectors.toList()));
}
return models;
}
@Override public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
TimelineModel model = (TimelineModel) o;
return (comment != null && model.getComment() != null) && (comment.getId() == model.comment.getId());
}
@Override public int hashCode() {
return comment != null ? (int) comment.getId() : 0;
}
@Override public int describeContents() { return 0; }
@Override public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(this.type);
dest.writeParcelable(this.issue, flags);
dest.writeParcelable(this.comment, flags);
dest.writeParcelable(this.event, flags);
dest.writeParcelable(this.pullRequest, flags);
dest.writeParcelable(this.status, flags);
dest.writeParcelable(this.review, flags);
dest.writeLong(this.sortedDate != null ? this.sortedDate.getTime() : -1);
}
protected TimelineModel(Parcel in) {
this.type = in.readInt();
this.issue = in.readParcelable(Issue.class.getClassLoader());
this.comment = in.readParcelable(Comment.class.getClassLoader());
this.event = in.readParcelable(IssueEvent.class.getClassLoader());
this.pullRequest = in.readParcelable(PullRequest.class.getClassLoader());
this.status = in.readParcelable(PullRequestStatusModel.class.getClassLoader());
this.review = in.readParcelable(ReviewModel.class.getClassLoader());
long tmpSortedDate = in.readLong();
this.sortedDate = tmpSortedDate == -1 ? null : new Date(tmpSortedDate);
}
public static final Creator<TimelineModel> CREATOR = new Creator<TimelineModel>() {
@Override public TimelineModel createFromParcel(Parcel source) {return new TimelineModel(source);}
@Override public TimelineModel[] newArray(int size) {return new TimelineModel[size];}
};
}