/*******************************************************************************
 * Copyright (c) 2012-2017 Codenvy, S.A.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *   Codenvy, S.A. - initial API and implementation
 *******************************************************************************/
package org.eclipse.che.ide.ext.java.client.editor;

import com.google.common.base.Optional;

import org.eclipse.che.ide.api.editor.EditorAgent;
import org.eclipse.che.ide.api.editor.annotation.QueryAnnotationsEvent;
import org.eclipse.che.ide.api.editor.codeassist.CodeAssistCallback;
import org.eclipse.che.ide.api.editor.codeassist.CompletionProposal;
import org.eclipse.che.ide.api.editor.document.Document;
import org.eclipse.che.ide.api.editor.link.HasLinkedMode;
import org.eclipse.che.ide.api.editor.quickfix.QuickAssistInvocationContext;
import org.eclipse.che.ide.api.editor.quickfix.QuickAssistProcessor;
import org.eclipse.che.ide.api.editor.text.LinearRange;
import org.eclipse.che.ide.api.editor.text.Position;
import org.eclipse.che.ide.api.editor.text.annotation.Annotation;
import org.eclipse.che.ide.api.editor.texteditor.TextEditor;
import org.eclipse.che.ide.api.resources.Project;
import org.eclipse.che.ide.api.resources.Resource;
import org.eclipse.che.ide.api.resources.VirtualFile;
import org.eclipse.che.ide.dto.DtoFactory;
import org.eclipse.che.ide.ext.java.client.JavaResources;
import org.eclipse.che.ide.ext.java.client.action.ProposalAction;
import org.eclipse.che.ide.ext.java.client.refactoring.RefactoringUpdater;
import org.eclipse.che.ide.ext.java.shared.dto.Problem;
import org.eclipse.che.ide.ext.java.shared.dto.ProposalPresentation;
import org.eclipse.che.ide.ext.java.shared.dto.Proposals;
import org.eclipse.che.ide.rest.AsyncRequestCallback;
import org.eclipse.che.ide.rest.DtoUnmarshallerFactory;
import org.eclipse.che.ide.rest.Unmarshallable;
import org.eclipse.che.ide.util.loging.Log;

import javax.inject.Inject;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;

import static org.eclipse.che.ide.ext.java.client.editor.JavaCodeAssistProcessor.insertStyle;
import static org.eclipse.che.ide.ext.java.client.util.JavaUtil.resolveFQN;

/**
 * {@link QuickAssistProcessor} for java files.
 */
public class JavaQuickAssistProcessor implements QuickAssistProcessor {

    private final JavaCodeAssistClient        client;
    /** The resources used for java assistants. */
    private final JavaResources               javaResources;
    private final Map<String, ProposalAction> proposalActions;
    private final DtoUnmarshallerFactory      unmarshallerFactory;
    private final DtoFactory                  dtoFactory;
    private final RefactoringUpdater          refactoringUpdater;
    private final EditorAgent                 editorAgent;

    @Inject
    public JavaQuickAssistProcessor(final JavaCodeAssistClient client,
                                    final JavaResources javaResources,
                                    Map<String, ProposalAction> proposalActions,
                                    DtoUnmarshallerFactory unmarshallerFactory,
                                    DtoFactory dtoFactory,
                                    RefactoringUpdater refactoringUpdater,
                                    EditorAgent editorAgent) {
        this.client = client;
        this.javaResources = javaResources;
        this.proposalActions = proposalActions;
        this.unmarshallerFactory = unmarshallerFactory;
        this.dtoFactory = dtoFactory;
        this.refactoringUpdater = refactoringUpdater;
        this.editorAgent = editorAgent;
    }

    @Override
    public void computeQuickAssistProposals(final QuickAssistInvocationContext quickAssistContext, final CodeAssistCallback callback) {
        final TextEditor textEditor = quickAssistContext.getTextEditor();
        final Document document = textEditor.getDocument();

        LinearRange tempRange;

        tempRange = textEditor.getSelectedLinearRange();

        final LinearRange range = tempRange;

        final boolean goToClosest = (range.getLength() == 0);

        final QueryAnnotationsEvent.AnnotationFilter filter = new QueryAnnotationsEvent.AnnotationFilter() {
            @Override
            public boolean accept(final Annotation annotation) {
                if (!(annotation instanceof JavaAnnotation)) {
                    return false;
                } else {
                    JavaAnnotation javaAnnotation = (JavaAnnotation)annotation;
                    return (!javaAnnotation.isMarkedDeleted()) /*&& JavaAnnotationUtil.hasCorrections(annotation)*/;
                }
            }
        };
        final QueryAnnotationsEvent.QueryCallback queryCallback = new QueryAnnotationsEvent.QueryCallback() {
            @Override
            public void respond(final Map<Annotation, Position> annotations) {
                List<Problem> problems = new ArrayList<>();
                /*final Map<Annotation, Position> problems =*/
                int offset = collectQuickFixableAnnotations(range, document, annotations, goToClosest, problems);
                if (offset != range.getStartOffset()) {
                    TextEditor presenter = ((TextEditor)textEditor);
                    presenter.getCursorModel().setCursorPosition(offset);
                }

                setupProposals(callback, textEditor, offset, problems);
            }
        };
        final QueryAnnotationsEvent event = new QueryAnnotationsEvent.Builder().withFilter(filter).withCallback(queryCallback).build();
        document.getDocumentHandle().getDocEventBus().fireEvent(event);

    }

    private void showProposals(final CodeAssistCallback callback, final Proposals responds, TextEditor editor) {
        List<ProposalPresentation> presentations = responds.getProposals();
        final List<CompletionProposal> proposals = new ArrayList<>(presentations.size());
        HasLinkedMode linkedEditor = editor instanceof HasLinkedMode ? (HasLinkedMode)editor : null;
        for (ProposalPresentation proposal : presentations) {
            CompletionProposal completionProposal;
            String actionId = proposal.getActionId();
            if (actionId != null) {
                ProposalAction action = proposalActions.get(actionId);
                completionProposal = new ActionCompletionProposal(insertStyle(javaResources, proposal.getDisplayString()),
                                                                  actionId,
                                                                  action,
                                                                  JavaCodeAssistProcessor.getIcon(proposal.getImage())
                );
            } else {
                completionProposal = new JavaCompletionProposal(proposal.getIndex(),
                                                                insertStyle(javaResources, proposal.getDisplayString()),
                                                                JavaCodeAssistProcessor.getIcon(proposal.getImage()),
                                                                client,
                                                                responds.getSessionId(),
                                                                linkedEditor,
                                                                refactoringUpdater,
                                                                editorAgent);
            }
            proposals.add(completionProposal);
        }

        callback.proposalComputed(proposals);
    }

    private void setupProposals(final CodeAssistCallback callback,
                                final TextEditor textEditor,
                                final int offset,
                                final List<Problem> annotations) {
        final VirtualFile file = textEditor.getEditorInput().getFile();

        if (file instanceof Resource) {
            final Optional<Project> project = ((Resource)file).getRelatedProject();

            Unmarshallable<Proposals> unmarshaller = unmarshallerFactory.newUnmarshaller(Proposals.class);
            client.computeAssistProposals(project.get().getLocation().toString(), resolveFQN(file), offset, annotations,
                                          new AsyncRequestCallback<Proposals>(unmarshaller) {
                                              @Override
                                              protected void onSuccess(Proposals proposals) {
                                                  showProposals(callback, proposals, textEditor);
                                              }

                                              @Override
                                              protected void onFailure(Throwable throwable) {
                                                  Log.error(JavaCodeAssistProcessor.class, throwable);
                                              }
                                          });
        }
    }

    private int collectQuickFixableAnnotations(final LinearRange lineRange,
                                               Document document, final Map<Annotation, Position> annotations,
                                               final boolean goToClosest, List<Problem> resultingProblems) {
        int invocationLocation = lineRange.getStartOffset();
        if (goToClosest) {


            LinearRange line =
                    document.getLinearRangeForLine(document.getPositionFromIndex(lineRange.getStartOffset()).getLine());
            int rangeStart = line.getStartOffset();
            int rangeEnd = rangeStart + line.getLength();

            ArrayList<Position> allPositions = new ArrayList<>();
            List<JavaAnnotation> allAnnotations = new ArrayList<>();
            int bestOffset = Integer.MAX_VALUE;
            for (Annotation problem : annotations.keySet()) {
                if (problem instanceof JavaAnnotation) {
                    JavaAnnotation ann = ((JavaAnnotation)problem);

                    Position pos = annotations.get(problem);
                    if (pos != null && isInside(pos.offset, rangeStart, rangeEnd)) { // inside our range?

                        allAnnotations.add(ann);
                        allPositions.add(pos);
                        bestOffset = processAnnotation(problem, pos, invocationLocation, bestOffset);
                    }
                }
            }
            if (bestOffset == Integer.MAX_VALUE) {
                return invocationLocation;
            }
            for (int i = 0; i < allPositions.size(); i++) {
                Position pos = allPositions.get(i);
                if (isInside(bestOffset, pos.offset, pos.offset + pos.length)) {
                    resultingProblems.add(createProblem(allAnnotations.get(i), pos));
                }
            }
            return bestOffset;
        } else {
            for (Annotation problem : annotations.keySet()) {
                Position pos = annotations.get(problem);
                if (pos != null && isInside(invocationLocation, pos.offset, pos.offset + pos.length)) {
                    resultingProblems.add(createProblem((JavaAnnotation)problem, pos));
                }
            }
            return invocationLocation;
        }
    }

    private Problem createProblem(JavaAnnotation javaAnnotation, Position pos) {
        Problem problem = dtoFactory.createDto(Problem.class);
        //server use only this fields
        problem.setID(javaAnnotation.getId());
        problem.setError(javaAnnotation.isError());
        problem.setArguments(Arrays.asList(javaAnnotation.getArguments()));
        problem.setSourceStart(pos.getOffset());
        //TODO I don't know why but in that place source end is bugger on 1 char
        problem.setSourceEnd(pos.getOffset() + pos.getLength() - 1);

        return problem;
    }

    private static int processAnnotation(Annotation annot, Position pos, int invocationLocation, int bestOffset) {
        final int posBegin = pos.offset;
        final int posEnd = posBegin + pos.length;
        if (isInside(invocationLocation, posBegin, posEnd)) { // covers invocation location?
            return invocationLocation;
        } else if (bestOffset != invocationLocation) {
            final int newClosestPosition = computeBestOffset(posBegin, invocationLocation, bestOffset);
            if (newClosestPosition != -1) {
                if (newClosestPosition != bestOffset) { // new best
                    if (JavaAnnotationUtil.hasCorrections(annot)) { // only jump to it if there are proposals
                        return newClosestPosition;
                    }
                }
            }
        }
        return bestOffset;
    }

    /**
     * Computes and returns the invocation offset given a new position, the initial offset and the best invocation offset found so far.
     * <p>
     * The closest offset to the left of the initial offset is the best. If there is no offset on the left, the closest on the right is the
     * best.
     * </p>
     *
     * @param newOffset the offset to llok at
     * @param invocationLocation the invocation location
     * @param bestOffset the current best offset
     * @return -1 is returned if the given offset is not closer or the new best offset
     */
    private static int computeBestOffset(int newOffset, int invocationLocation, int bestOffset) {
        if (newOffset <= invocationLocation) {
            if (bestOffset > invocationLocation) {
                return newOffset; // closest was on the right, prefer on the left
            } else if (bestOffset <= newOffset) {
                return newOffset; // we are closer or equal
            }
            return -1; // further away
        }

        if (newOffset <= bestOffset) {
            return newOffset; // we are closer or equal
        }

        return -1; // further away
    }

    /**
     * Tells is the offset is inside the (inclusive) range defined by start-end.
     *
     * @param offset the offset
     * @param start the start of the range
     * @param end the end of the range
     * @return true iff offset is inside
     */
    private static boolean isInside(int offset, int start, int end) {
        return offset == start || offset == end || (offset > start && offset < end); // make sure to handle 0-length ranges
    }
}
