package io.onedev.server.web.page.project.pullrequests.detail.changes;

import static org.apache.wicket.ajax.attributes.CallbackParameter.explicit;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.List;

import javax.annotation.Nullable;

import org.apache.wicket.Component;
import org.apache.wicket.ajax.AjaxRequestTarget;
import org.apache.wicket.ajax.attributes.AjaxRequestAttributes;
import org.apache.wicket.ajax.markup.html.AjaxLink;
import org.apache.wicket.behavior.AttributeAppender;
import org.apache.wicket.core.request.handler.IPartialPageRequestHandler;
import org.apache.wicket.markup.ComponentTag;
import org.apache.wicket.markup.head.IHeaderResponse;
import org.apache.wicket.markup.head.JavaScriptHeaderItem;
import org.apache.wicket.markup.head.OnDomReadyHeaderItem;
import org.apache.wicket.markup.html.WebMarkupContainer;
import org.apache.wicket.markup.html.basic.Label;
import org.apache.wicket.markup.html.link.Link;
import org.apache.wicket.markup.html.list.ListItem;
import org.apache.wicket.markup.html.list.ListView;
import org.apache.wicket.markup.html.panel.Fragment;
import org.apache.wicket.model.AbstractReadOnlyModel;
import org.apache.wicket.model.IModel;
import org.apache.wicket.model.LoadableDetachableModel;
import org.apache.wicket.request.IRequestParameters;
import org.apache.wicket.request.cycle.RequestCycle;
import org.apache.wicket.request.mapper.parameter.PageParameters;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.revwalk.RevCommit;

import com.google.common.base.Preconditions;
import com.google.common.collect.Sets;

import io.onedev.server.OneDev;
import io.onedev.server.entitymanager.CodeCommentManager;
import io.onedev.server.entitymanager.CodeCommentReplyManager;
import io.onedev.server.git.GitUtils;
import io.onedev.server.model.CodeComment;
import io.onedev.server.model.CodeCommentReply;
import io.onedev.server.model.PullRequest;
import io.onedev.server.model.PullRequestChange;
import io.onedev.server.model.PullRequestComment;
import io.onedev.server.model.PullRequestUpdate;
import io.onedev.server.model.User;
import io.onedev.server.model.support.CompareContext;
import io.onedev.server.model.support.Mark;
import io.onedev.server.model.support.pullrequest.changedata.PullRequestApproveData;
import io.onedev.server.model.support.pullrequest.changedata.PullRequestReopenData;
import io.onedev.server.model.support.pullrequest.changedata.PullRequestRequestedForChangesData;
import io.onedev.server.util.diff.WhitespaceOption;
import io.onedev.server.web.ajaxlistener.ConfirmLeaveListener;
import io.onedev.server.web.behavior.AbstractPostAjaxBehavior;
import io.onedev.server.web.behavior.WebSocketObserver;
import io.onedev.server.web.component.diff.revision.CommentSupport;
import io.onedev.server.web.component.diff.revision.RevisionDiffPanel;
import io.onedev.server.web.component.floating.FloatingPanel;
import io.onedev.server.web.component.link.DropdownLink;
import io.onedev.server.web.page.project.pullrequests.detail.PullRequestDetailPage;
import io.onedev.server.web.util.EditParamsAware;
import io.onedev.server.web.websocket.WebSocketManager;

@SuppressWarnings("serial")
public class PullRequestChangesPage extends PullRequestDetailPage implements CommentSupport, EditParamsAware {

	public static final String PARAM_OLD_COMMIT = "old-commit";
	
	public static final String PARAM_NEW_COMMIT = "new-commit";
	
	private static final String PARAM_WHITESPACE_OPTION = "whitespace-option";
	
	private static final String PARAM_PATH_FILTER = "path-filter";
	
	private static final String PARAM_BLAME_FILE = "blame-file";
	
	public static final String PARAM_COMMENT = "comment";
	
	private static final String PARAM_MARK = "mark";
	
	private State state = new State();
	
	private WebMarkupContainer head;
	
	private final IModel<List<RevCommit>> commitsModel = new LoadableDetachableModel<List<RevCommit>>() {

		@Override
		protected List<RevCommit> load() {
			List<RevCommit> commits = new ArrayList<>();
			for (PullRequestUpdate update: getPullRequest().getSortedUpdates()) {
				commits.addAll(update.getCommits());
			}
			return commits;
		}
		
	};
	
	private final IModel<ObjectId> comparisonBaseModel = new LoadableDetachableModel<ObjectId>() {
		
		@Override
		protected ObjectId load() {
			ObjectId oldCommitId = ObjectId.fromString(state.oldCommitHash);
			ObjectId newCommitId = ObjectId.fromString(state.newCommitHash);
			return getPullRequest().getComparisonBase(oldCommitId, newCommitId);
		}
		
	};
	
	private final IModel<Collection<CodeComment>> commentsModel = 
			new LoadableDetachableModel<Collection<CodeComment>>() {

		@Override
		protected Collection<CodeComment> load() {
			Collection<CodeComment> comments = new ArrayList<>();
			for (CodeComment comment: getPullRequest().getCodeComments()) {
				Mark diffMark = getDiffMark(comment.getMark());
				if (diffMark != null) {
					comment.setMark(diffMark);
					comments.add(comment);
				}
			}
			return comments;
		}
		
	};
	
	private final IModel<CodeComment> openCommentModel = new LoadableDetachableModel<CodeComment>() {

		@Override
		protected CodeComment load() {
			if (state.commentId != null) {
				CodeComment comment = OneDev.getInstance(CodeCommentManager.class).load(state.commentId);
				Mark diffMark = getDiffMark(comment.getMark());
				if (diffMark != null) {
					comment.setMark(diffMark);
					return comment;
				}
			} 
			return null;
		}
		
	};
	
	public PullRequestChangesPage(PageParameters params) {
		super(params);

		state.commentId = params.get(PARAM_COMMENT).toOptionalLong();
		state.mark = Mark.fromString(params.get(PARAM_MARK).toString());
		
		state.oldCommitHash = params.get(PARAM_OLD_COMMIT).toString();
		state.newCommitHash = params.get(PARAM_NEW_COMMIT).toString();
		state.pathFilter = params.get(PARAM_PATH_FILTER).toString();
		state.blameFile = params.get(PARAM_BLAME_FILE).toString();
		state.whitespaceOption = WhitespaceOption.ofNullableName(params.get(PARAM_WHITESPACE_OPTION).toString());
		     
		PullRequest request = getPullRequest();
		if (state.commentId != null) {
			CodeComment comment = OneDev.getInstance(CodeCommentManager.class).load(state.commentId);
			Preconditions.checkState(comment.getRequest().equals(request));
			
			CodeComment.ComparingInfo commentComparingInfo = comment.getComparingInfo();
			PullRequest.ComparingInfo requestComparingInfo = 
					getPullRequest().getRequestComparingInfo(commentComparingInfo);
			if (requestComparingInfo != null && state.oldCommitHash == null && state.newCommitHash == null) {
				if (comment.isContextChanged(request)) {
					state.oldCommitHash = comment.getMark().getCommitHash();
					state.newCommitHash = request.getLatestUpdate().getHeadCommitHash();
				} else {
					state.oldCommitHash = requestComparingInfo.getOldCommitHash();
					state.newCommitHash = requestComparingInfo.getNewCommitHash();
				}
			} 
		} 
		if (state.oldCommitHash == null) 
			state.oldCommitHash = request.getBaseCommitHash();
		if (state.newCommitHash == null)
			state.newCommitHash = request.getLatestUpdate().getHeadCommitHash();
	}
	
	private int getCommitIndex(String commitHash) {
		int index = -1;
		for (int i=0; i<commitsModel.getObject().size(); i++) {
			RevCommit commit = commitsModel.getObject().get(i);
			if (commit.name().equals(commitHash)) {
				index = i;
				break;
			}
		}
		return index;
	}
	
	@Override
	protected String getRobotsMeta() {
		return "noindex,nofollow";
	}
	
	@Nullable
	private Mark getDiffMark(Mark mark) {
		if (mark.getCommitHash().equals(state.oldCommitHash)) {
			return mark.mapTo(getProject(), getComparisonBase());
		} else if (mark.getCommitHash().equals(state.newCommitHash) 
				|| mark.getCommitHash().equals(getComparisonBase().name())) {
			return mark;
		} else {
			return null;
		}
	}
	
	@Nullable
	private Mark getPermanentMark(Mark mark) {
		ObjectId oldCommitId = ObjectId.fromString(state.oldCommitHash);
		if (mark.getCommitHash().equals(getComparisonBase().name())) {
			return mark.mapTo(getProject(), oldCommitId);
		} else if (mark.getCommitHash().equals(state.oldCommitHash) 
					|| mark.getCommitHash().equals(state.newCommitHash)) {
			return mark;
		} else {
			return null;
		}
	}
	
	@Override
	protected void onInitialize() {
		super.onInitialize();
		
		add(head = new WebMarkupContainer("changesHead"));
		head.add(new WebSocketObserver() {

			@Override
			public Collection<String> getObservables() {
				return Sets.newHashSet(PullRequest.getWebSocketObservable(getPullRequest().getId()));
			}

			@Override
			public void onObservableChanged(IPartialPageRequestHandler handler) {
				handler.add(component);
			}
			
		});
		head.setOutputMarkupId(true);
		
		head.add(new AjaxLink<Void>("prevCommitLink") {

			@Override
			protected void onComponentTag(ComponentTag tag) {
				super.onComponentTag(tag);

				configure();
				if (!isEnabled()) {
					tag.put("disabled", "disabled");
				}
			}
			
			@Override
			protected void onConfigure() {
				super.onConfigure();
				setEnabled(getCommitIndex(state.oldCommitHash) != -1 && getCommitIndex(state.newCommitHash) != -1);
			}

			@Override
			public void onClick(AjaxRequestTarget target) {
				int index = getCommitIndex(state.oldCommitHash);
				if (index != -1) {
					state.newCommitHash = commitsModel.getObject().get(index).name();
					index--;
					if (index == -1) {
						state.oldCommitHash = getPullRequest().getBaseCommitHash();
					} else {
						state.oldCommitHash = commitsModel.getObject().get(index).name();
					}
					newRevisionDiff(target);
					OneDev.getInstance(WebSocketManager.class).observe(PullRequestChangesPage.this);
				}
				target.add(head);
				pushState(target);
			}
			
		});
		head.add(new AjaxLink<Void>("nextCommitLink") {

			@Override
			protected void onConfigure() {
				super.onConfigure();

				int oldIndex = getCommitIndex(state.oldCommitHash);
				int newIndex = getCommitIndex(state.newCommitHash);
				if (!state.oldCommitHash.equals(getPullRequest().getBaseCommitHash()) && oldIndex == -1 
						|| newIndex == -1 || newIndex == commitsModel.getObject().size()-1) {
					setEnabled(false);
				} else {
					setEnabled(true);
				}
				
			}

			@Override
			protected void onComponentTag(ComponentTag tag) {
				super.onComponentTag(tag);
				
				configure();
				if (!isEnabled()) {
					tag.put("disabled", "disabled");
				}
			}
			
			@Override
			public void onClick(AjaxRequestTarget target) {
				int index = getCommitIndex(state.newCommitHash);
				if (index != -1 && index != commitsModel.getObject().size()-1) {
					CodeComment comment = getOpenComment();
					
					// we will not move old commit if an opened comment points to it
					if (comment == null || comment.getMark().getCommitHash().equals(state.newCommitHash)) {
						state.oldCommitHash = state.newCommitHash;
					}
					index++;
					state.newCommitHash = commitsModel.getObject().get(index).name();
					newRevisionDiff(target);
					OneDev.getInstance(WebSocketManager.class).observe(PullRequestChangesPage.this);
				} 
				target.add(head);
				pushState(target);
			}
			
		});
		
		DropdownLink selectedCommitsLink = new DropdownLink("comparingCommits") {
			
			@Override
			protected Component newContent(String id, FloatingPanel dropdown) {
				AbstractPostAjaxBehavior callbackBehavior = new AbstractPostAjaxBehavior() {
					
					@Override
					protected void updateAjaxAttributes(AjaxRequestAttributes attributes) {
						super.updateAjaxAttributes(attributes);
						attributes.getAjaxCallListeners().add(new ConfirmLeaveListener());
					}

					@Override
					protected void respond(AjaxRequestTarget target) {
						IRequestParameters params = RequestCycle.get().getRequest().getPostParameters();
						state.oldCommitHash = params.getParameterValue("oldCommit").toString();
						state.newCommitHash = params.getParameterValue("newCommit").toString();
						target.add(head);
						newRevisionDiff(target);
						OneDev.getInstance(WebSocketManager.class).observe(PullRequestChangesPage.this);
						pushState(target);
						dropdown.close();
					}
					
				};
				Fragment fragment = new Fragment(id, "commitsFrag", PullRequestChangesPage.this) {

					@Override
					public void renderHead(IHeaderResponse response) {
						super.renderHead(response);
						
						String callback = callbackBehavior.getCallbackFunction(explicit("oldCommit"), explicit("newCommit")).toString();
						String script;
						int oldIndex = getCommitIndex(state.oldCommitHash);
						int newIndex = getCommitIndex(state.newCommitHash);
						if ((state.oldCommitHash.equals(getPullRequest().getBaseCommitHash()) || oldIndex != -1) 
								&& newIndex != -1) {
							script = String.format("onedev.server.requestChanges.initCommitSelector(%s, '%s', %d, %d);", 
									callback, getPullRequest().getBaseCommitHash(), oldIndex+1, newIndex);
						} else {
							script = String.format("onedev.server.requestChanges.initCommitSelector(%s, '%s');", 
									callback, getPullRequest().getBaseCommitHash());
						}
						response.render(OnDomReadyHeaderItem.forScript(script));
					}
					
				};
				fragment.add(callbackBehavior);
				fragment.add(new Link<Void>("allChanges") {

					@Override
					public void onClick() {
						PullRequestChangesPage.State state = new PullRequestChangesPage.State();
						
						PullRequest request = getPullRequest();
						state.oldCommitHash = request.getBaseCommitHash();
						state.newCommitHash = request.getLatestUpdate().getHeadCommitHash();
						setResponsePage(PullRequestChangesPage.class, 
								PullRequestChangesPage.paramsOf(request, state));
					}
					
				});
				
				fragment.add(new Link<Date>("changesSinceLastReview", new LoadableDetachableModel<Date>() {

					@Override
					protected Date load() {
						Date lastReviewDate = null;
						User user = getLoginUser();
						if (user != null) {
							PullRequest request = getPullRequest();
							for (PullRequestComment comment: request.getComments()) { 
								if (comment.getUser().equals(user) && 
										(lastReviewDate == null || lastReviewDate.before(comment.getDate()))) {
									lastReviewDate = comment.getDate();
								}
							}
							
							for (PullRequestChange change: request.getChanges()) {
								if (change.getUser().equals(user) 
										&& (lastReviewDate == null || lastReviewDate.before(change.getDate()))
										&& (change.getData() instanceof PullRequestApproveData 
												|| change.getData() instanceof PullRequestReopenData
												|| change.getData() instanceof PullRequestRequestedForChangesData)) {
									lastReviewDate = change.getDate();
								}
							}
							
							for (CodeComment comment: request.getCodeComments()) {
								if (comment.getUser().equals(user) && 
										(lastReviewDate == null || lastReviewDate.before(comment.getCreateDate()))) {
									lastReviewDate = comment.getCreateDate();
								}
								for (CodeCommentReply reply: comment.getReplies()) {
									if (reply.getUser().equals(user) && 
											(lastReviewDate == null || lastReviewDate.before(reply.getDate()))) {
										lastReviewDate = reply.getDate();
									}
								}
							}
						}
						return lastReviewDate;
					}
					
				}) {

					@Override
					public void onClick() {
						PullRequestChangesPage.State state = new PullRequestChangesPage.State();
						
						PullRequest request = getPullRequest();
						state.oldCommitHash = request.getBaseCommitHash();
						for (PullRequestUpdate update: request.getSortedUpdates()) {
							if (update.getDate().before(getModelObject()))
								state.oldCommitHash = update.getHeadCommitHash();
						}
						
						state.newCommitHash = request.getLatestUpdate().getHeadCommitHash();
						setResponsePage(PullRequestChangesPage.class, 
								PullRequestChangesPage.paramsOf(request, state));
					}

					@Override
					protected void onConfigure() {
						super.onConfigure();
						Date lastReviewDate = getModelObject();
						setVisible(lastReviewDate != null 
								&& lastReviewDate.before(getPullRequest().getLatestUpdate().getDate()));
					}
					
				});
				fragment.add(new ListView<RevCommit>("commits", commitsModel) {

					@Override
					protected void populateItem(ListItem<RevCommit> item) {
						RevCommit commit = item.getModelObject();
						if (!getPullRequest().getPendingCommits().contains(commit)) {
							item.add(AttributeAppender.append("class", "rebased"));
							item.add(AttributeAppender.append("title", "This commit is rebased"));
						}
						item.add(AttributeAppender.append("data-hash", commit.name()));
						item.add(new Label("hash", GitUtils.abbreviateSHA(commit.name())));
						item.add(new Label("subject", commit.getShortMessage()));
					}
					
				});
				return fragment;
			}
		};
		selectedCommitsLink.add(new Label("label", new AbstractReadOnlyModel<String>() {

			@Override
			public String getObject() {
				String oldName;
				if (state.oldCommitHash.equals(getPullRequest().getBaseCommitHash()))
					oldName = "base";
				else
					oldName = GitUtils.abbreviateSHA(state.oldCommitHash);

				String newName;
				if (state.newCommitHash.equals(getPullRequest().getLatestUpdate().getHeadCommitHash())) {
					newName = "head";
				} else {
					newName = GitUtils.abbreviateSHA(state.newCommitHash);
				}
				
				return oldName + " ... " + newName;
			}
			
		}));
		
		head.add(selectedCommitsLink);
		
		head.add(new AjaxLink<Void>("fullChanges") {

			@Override
			public void onClick(AjaxRequestTarget target) {
				state.oldCommitHash = getPullRequest().getBaseCommitHash();
				state.newCommitHash = getPullRequest().getLatestUpdate().getHeadCommitHash();
				target.add(head);
				newRevisionDiff(target);
				OneDev.getInstance(WebSocketManager.class).observe(PullRequestChangesPage.this);
				pushState(target);
			}

			@Override
			protected void onConfigure() {
				super.onConfigure();
				setVisible(!state.oldCommitHash.equals(getPullRequest().getBaseCommitHash()) 
						|| !state.newCommitHash.equals(getPullRequest().getLatestUpdate().getHeadCommitHash()));
			}
			
		});
		
		newRevisionDiff(null);
	}

	@Override
	public void onDetach() {
		commitsModel.detach();
		comparisonBaseModel.detach();
		commentsModel.detach();
		openCommentModel.detach();
		super.onDetach();
	}
	
	public static PageParameters paramsOf(PullRequest request, String oldCommit, String newCommit) {
		State state = new State();
		state.oldCommitHash = oldCommit;
		state.newCommitHash = newCommit;
		return paramsOf(request, state);
	}

	public static State getState(CodeComment comment) {
		PullRequestChangesPage.State state = new PullRequestChangesPage.State();
		state.commentId = comment.getId();
		state.mark = comment.getMark();
		state.pathFilter = comment.getCompareContext().getPathFilter();
		state.whitespaceOption = comment.getCompareContext().getWhitespaceOption();
		return state;
	}
	
	public static PageParameters paramsOf(PullRequest request, CodeComment comment) {
		return paramsOf(request, getState(comment));
	}
	
	public static PageParameters paramsOf(PullRequest request, State state) {
		PageParameters params = PullRequestDetailPage.paramsOf(request);
		fillParams(params, state);
		return params;
	}
	
	public static void fillParams(PageParameters params, State state) {
		if (state.oldCommitHash != null)
			params.add(PARAM_OLD_COMMIT, state.oldCommitHash);
		if (state.newCommitHash != null)
			params.add(PARAM_NEW_COMMIT, state.newCommitHash);
		if (state.whitespaceOption != WhitespaceOption.DEFAULT)
			params.add(PARAM_WHITESPACE_OPTION, state.whitespaceOption.name());
		if (state.pathFilter != null)
			params.add(PARAM_PATH_FILTER, state.pathFilter);
		if (state.blameFile != null)
			params.add(PARAM_BLAME_FILE, state.blameFile);
		if (state.commentId != null)
			params.add(PARAM_COMMENT, state.commentId);
		if (state.mark != null)
			params.add(PARAM_MARK, state.mark.toString());
	}
	
	@Override
	protected void onPopState(AjaxRequestTarget target, Serializable data) {
		super.onPopState(target, data);

		state = (State) data;
		newRevisionDiff(target);
		OneDev.getInstance(WebSocketManager.class).observe(this);
	}
	
	private void pushState(IPartialPageRequestHandler partialPageRequestHandler) {
		PageParameters params = paramsOf(getPullRequest(), state);
		CharSequence url = RequestCycle.get().urlFor(PullRequestChangesPage.class, params);
		pushState(partialPageRequestHandler, url.toString(), state);
	}
		
	private void newRevisionDiff(@Nullable AjaxRequestTarget target) {
		IModel<String> blameModel = new IModel<String>() {

			@Override
			public void detach() {
			}

			@Override
			public String getObject() {
				return state.blameFile;
			}

			@Override
			public void setObject(String object) {
				state.blameFile = object;
				pushState(RequestCycle.get().find(AjaxRequestTarget.class));
			}
			
		};
		IModel<String> pathFilterModel = new IModel<String>() {

			@Override
			public void detach() {
			}

			@Override
			public String getObject() {
				return state.pathFilter;
			}

			@Override
			public void setObject(String object) {
				state.pathFilter = object;
				pushState(RequestCycle.get().find(AjaxRequestTarget.class));
			}
			
		};
		IModel<WhitespaceOption> whitespaceOptionModel = new IModel<WhitespaceOption>() {

			@Override
			public void detach() {
			}

			@Override
			public WhitespaceOption getObject() {
				return state.whitespaceOption;
			}

			@Override
			public void setObject(WhitespaceOption object) {
				state.whitespaceOption = object;
				pushState(RequestCycle.get().find(AjaxRequestTarget.class));
			}

		};
		
		Component revisionDiff = new RevisionDiffPanel("revisionDiff", projectModel,  
				requestModel, getComparisonBase().name(), state.newCommitHash, 
				pathFilterModel, whitespaceOptionModel, blameModel, this);
		revisionDiff.setOutputMarkupId(true);
		if (target != null) {
			replace(revisionDiff);
			target.add(revisionDiff);
		} else {
			add(revisionDiff);
		}
	}
	
	@Override
	public void renderHead(IHeaderResponse response) {
		super.renderHead(response);
		response.render(JavaScriptHeaderItem.forReference(new PullRequestChangesResourceReference()));
	}

	@Override
	public Mark getMark() {
		if (state.mark != null)
			return getDiffMark(state.mark);
		else
			return null;
	}

	@Override
	public String getMarkUrl(Mark mark) {
		State markState = new State();
		markState.mark = getPermanentMark(mark);
		if (markState.mark != null) {
			markState.oldCommitHash = state.oldCommitHash;
			markState.newCommitHash = state.newCommitHash;
			markState.pathFilter = state.pathFilter;
			markState.whitespaceOption = state.whitespaceOption;
			return urlFor(PullRequestChangesPage.class, paramsOf(getPullRequest(), markState)).toString();
		} else {
			return null;
		}
	}

	@Override
	public CodeComment getOpenComment() {
		return openCommentModel.getObject();
	}

	private ObjectId getComparisonBase() {
		return comparisonBaseModel.getObject();
	}
	
	@Override
	public Collection<CodeComment> getComments() {
		return commentsModel.getObject();
	}
	
	@Override
	public void onCommentOpened(AjaxRequestTarget target, CodeComment comment) {
		state.commentId = comment.getId();
		state.mark = getPermanentMark(comment.getMark());
		OneDev.getInstance(WebSocketManager.class).observe(this);
		pushState(target);
	}

	@Override
	public void onCommentClosed(AjaxRequestTarget target) {
		state.commentId = null;
		state.mark = null;
		OneDev.getInstance(WebSocketManager.class).observe(this);
		pushState(target);
	}
	
	@Override
	public void onMark(AjaxRequestTarget target, Mark mark) {
		state.mark = getPermanentMark(mark);
		pushState(target);
	}
	
	@Override
	public void onUnmark(AjaxRequestTarget target) {
		state.mark = null;
		pushState(target);
	}
	
	public State getState() {
		return state;
	}

	@Override
	public void onAddComment(AjaxRequestTarget target, Mark mark) {
		state.commentId = null;
		state.mark = getPermanentMark(mark);
		pushState(target);
		OneDev.getInstance(WebSocketManager.class).observe(this);
	}

	private void saveCommentOrReply(CodeComment comment, @Nullable CodeCommentReply reply) {
		Mark prevMark = comment.getMark();
		CompareContext prevCompareContext = comment.getCompareContext();
		try {
			comment.setMark(Preconditions.checkNotNull(getPermanentMark(prevMark)));
			if (prevCompareContext.getCompareCommitHash().equals(getComparisonBase().name())) {
				CompareContext compareContext = new CompareContext();
				compareContext.setLeftSide(prevCompareContext.isLeftSide());
				compareContext.setPathFilter(prevCompareContext.getPathFilter());
				compareContext.setWhitespaceOption(prevCompareContext.getWhitespaceOption());
				compareContext.setCompareCommitHash(state.oldCommitHash);
				comment.setCompareContext(compareContext);
			}
			if (reply != null)
				OneDev.getInstance(CodeCommentReplyManager.class).save(reply);
			else
				OneDev.getInstance(CodeCommentManager.class).save(comment);
		} finally {
			comment.setMark(prevMark);
			comment.setCompareContext(prevCompareContext);
		}
	}
	
	@Override
	public void onSaveComment(CodeComment comment) {
		saveCommentOrReply(comment, null);
	}
	
	@Override
	public void onSaveCommentReply(CodeCommentReply reply) {
		saveCommentOrReply(reply.getComment(), reply);
	}
	
	@Override
	public PageParameters getParamsBeforeEdit() {
		return paramsOf(getPullRequest(), state);
	}

	@Override
	public PageParameters getParamsAfterEdit() {
		PageParameters params = getParamsBeforeEdit();
		params.remove(PARAM_NEW_COMMIT);
		if (getOpenComment() != null)
			params.set(PARAM_OLD_COMMIT, getOpenComment().getMark().getCommitHash());
		else
			params.remove(PARAM_OLD_COMMIT);
		return params;
	}
	
	public static class State implements Serializable {

		private static final long serialVersionUID = 1L;
		
		public String oldCommitHash;
		
		public String newCommitHash;
		
		public WhitespaceOption whitespaceOption = WhitespaceOption.DEFAULT;
		
		@Nullable
		public String pathFilter;
		
		@Nullable
		public String blameFile;
		
		@Nullable
		public Long commentId;
		
		@Nullable
		public Mark mark;
		
	}

}