/*
 * Copyright 2010-2015 JetBrains s.r.o.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.jetbrains.kotlin.idea.codeInsight.upDownMover;

import com.intellij.codeInsight.editorActions.moveUpDown.LineRange;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.TextRange;
import com.intellij.psi.*;
import com.intellij.psi.util.PsiTreeUtil;
import kotlin.jvm.functions.Function1;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.kotlin.idea.refactoring.KotlinRefactoringUtilKt;
import org.jetbrains.kotlin.lexer.KtTokens;
import org.jetbrains.kotlin.psi.*;
import org.jetbrains.kotlin.psi.psiUtil.PsiUtilsKt;

import java.util.List;
import java.util.function.Predicate;

public class KotlinExpressionMover extends AbstractKotlinUpDownMover {

    private static final Predicate<KtElement> IS_CALL_EXPRESSION = input -> input instanceof KtCallExpression;

    public KotlinExpressionMover() {
    }

    private final static Class[] MOVABLE_ELEMENT_CLASSES = {
            KtExpression.class,
            KtWhenEntry.class,
            KtValueArgument.class,
            PsiComment.class
    };

    private static final Function1<PsiElement, Boolean> MOVABLE_ELEMENT_CONSTRAINT = new Function1<PsiElement, Boolean>() {
        @NotNull
        @Override
        public Boolean invoke(PsiElement element) {
            return (!(element instanceof KtExpression)
                    || element instanceof KtDeclaration
                    || element instanceof KtBlockExpression
                    || element.getParent() instanceof KtBlockExpression);
        }
    };

    private final static Class[] BLOCKLIKE_ELEMENT_CLASSES =
            {KtBlockExpression.class, KtWhenExpression.class, KtClassBody.class, KtFile.class};

    private final static Class[] FUNCTIONLIKE_ELEMENT_CLASSES =
            {KtFunction.class, KtPropertyAccessor.class, KtAnonymousInitializer.class};

    private static final Predicate<KtElement> CHECK_BLOCK_LIKE_ELEMENT =
            input -> (input instanceof KtBlockExpression || input instanceof KtClassBody)
                     && !PsiTreeUtil.instanceOf(input.getParent(), FUNCTIONLIKE_ELEMENT_CLASSES);

    private static final Predicate<KtElement> CHECK_BLOCK =
            input -> input instanceof KtBlockExpression && !PsiTreeUtil.instanceOf(input.getParent(), FUNCTIONLIKE_ELEMENT_CLASSES);

    @Nullable
    private static PsiElement getStandaloneClosingBrace(@NotNull PsiFile file, @NotNull Editor editor) {
        LineRange range = getLineRangeFromSelection(editor);
        if (range.endLine - range.startLine != 1) return null;
        int offset = editor.getCaretModel().getOffset();
        Document document = editor.getDocument();
        int line = document.getLineNumber(offset);
        int lineStartOffset = document.getLineStartOffset(line);
        String lineText = document.getText().substring(lineStartOffset, document.getLineEndOffset(line));
        if (!lineText.trim().equals("}")) return null;

        return file.findElementAt(lineStartOffset + lineText.indexOf('}'));
    }

    private static BraceStatus checkForMovableDownClosingBrace(
            @NotNull PsiElement closingBrace,
            @NotNull PsiElement block,
            @NotNull Editor editor,
            @NotNull MoveInfo info
    ) {
        PsiElement current = block;
        PsiElement nextElement = null;
        PsiElement nextExpression = null;
        do {
            PsiElement sibling = firstNonWhiteElement(current.getNextSibling(), true);
            if (sibling != null && nextElement == null) {
                nextElement = sibling;
            }

            if (sibling instanceof KtExpression) {
                nextExpression = sibling;
                break;
            }

            current = current.getParent();
        }
        while (current != null && !(PsiTreeUtil.instanceOf(current, BLOCKLIKE_ELEMENT_CLASSES)));

        if (nextExpression == null) return BraceStatus.NOT_MOVABLE;

        Document doc = editor.getDocument();

        info.toMove = new LineRange(closingBrace, closingBrace, doc);
        info.toMove2 = new LineRange(nextElement, nextExpression);
        info.indentSource = true;

        return BraceStatus.MOVABLE;
    }

    private static BraceStatus checkForMovableUpClosingBrace(
            @NotNull PsiElement closingBrace,
            PsiElement block,
            @NotNull Editor editor,
            @NotNull MoveInfo info
    ) {
        //noinspection unchecked
        PsiElement prev = KtPsiUtil.getLastChildByType(block, KtExpression.class);
        if (prev == null) return BraceStatus.NOT_MOVABLE;

        Document doc = editor.getDocument();

        info.toMove = new LineRange(closingBrace, closingBrace, doc);
        info.toMove2 = new LineRange(prev, prev, doc);
        info.indentSource = true;

        return BraceStatus.MOVABLE;
    }

    private static enum BraceStatus {
        NOT_FOUND,
        MOVABLE,
        NOT_MOVABLE
    }

    // Returns null if standalone closing brace is not found
    private static BraceStatus checkForMovableClosingBrace(
            @NotNull Editor editor,
            @NotNull PsiFile file,
            @NotNull MoveInfo info,
            boolean down
    ) {
        PsiElement closingBrace = getStandaloneClosingBrace(file, editor);
        if (closingBrace == null) return BraceStatus.NOT_FOUND;

        PsiElement blockLikeElement = closingBrace.getParent();
        if (!(blockLikeElement instanceof KtBlockExpression)) return BraceStatus.NOT_MOVABLE;

        PsiElement blockParent = blockLikeElement.getParent();
        if (blockParent instanceof KtWhenEntry) return BraceStatus.NOT_FOUND;
        if (PsiTreeUtil.instanceOf(blockParent, FUNCTIONLIKE_ELEMENT_CLASSES)) return BraceStatus.NOT_FOUND;

        PsiElement enclosingExpression = PsiTreeUtil.getParentOfType(blockLikeElement, KtExpression.class);

        if (enclosingExpression instanceof KtDoWhileExpression) return BraceStatus.NOT_MOVABLE;

        if (enclosingExpression instanceof KtIfExpression) {
            KtIfExpression ifExpression = (KtIfExpression) enclosingExpression;

            if (blockLikeElement == ifExpression.getThen() && ifExpression.getElse() != null) return BraceStatus.NOT_MOVABLE;
        }

        return down
               ? checkForMovableDownClosingBrace(closingBrace, blockLikeElement, editor, info)
               : checkForMovableUpClosingBrace(closingBrace, blockLikeElement, editor, info);
    }

    @Nullable
    private static KtBlockExpression findClosestBlock(@NotNull PsiElement anchor, boolean down, boolean strict) {
        PsiElement current = PsiTreeUtil.getParentOfType(anchor, KtBlockExpression.class, strict);
        while (current != null) {
            PsiElement parent = current.getParent();
            if (parent instanceof KtClassBody ||
                (parent instanceof KtAnonymousInitializer && !(parent instanceof KtScriptInitializer)) ||
                parent instanceof KtNamedFunction ||
                (parent instanceof KtProperty && !((KtProperty) parent).isLocal())) {
                return null;
            }

            if (parent instanceof KtBlockExpression) return (KtBlockExpression) parent;

            PsiElement sibling = down ? current.getNextSibling() : current.getPrevSibling();
            if (sibling != null) {
                //noinspection unchecked
                KtBlockExpression block = (KtBlockExpression) KtPsiUtil.getOutermostDescendantElement(sibling, down, CHECK_BLOCK);
                if (block != null) return block;

                current = sibling;
            }
            else {
                current = parent;
            }
        }

        return null;
    }

    @Nullable
    private static KtBlockExpression getDSLLambdaBlock(@NotNull Editor editor, @NotNull PsiElement element, boolean down) {
        KtCallExpression callExpression =
                (KtCallExpression) KtPsiUtil.getOutermostDescendantElement(element, down, IS_CALL_EXPRESSION);
        if (callExpression == null) return null;

        List<KtLambdaArgument> functionLiterals = callExpression.getLambdaArguments();
        if (functionLiterals.isEmpty()) return null;

        KtLambdaExpression lambdaExpression = functionLiterals.get(0).getLambdaExpression();
        if (lambdaExpression == null) return null;

        Document document = editor.getDocument();
        TextRange range = lambdaExpression.getTextRange();
        if (document.getLineNumber(range.getStartOffset()) == document.getLineNumber(range.getEndOffset())) return null;

        return lambdaExpression.getBodyExpression();
    }

    @Nullable
    private static LineRange getExpressionTargetRange(@NotNull Editor editor, @NotNull PsiElement sibling, boolean down) {

        PsiElement start = sibling;
        PsiElement end = sibling;

        if (!down) {
            if (sibling instanceof KtIfExpression) {
                KtIfExpression ifExpression = (KtIfExpression) sibling;
                KtExpression elseExpression = ifExpression.getElse();
                while (elseExpression instanceof KtIfExpression) {
                    KtIfExpression elseIfExpression = (KtIfExpression) elseExpression;
                    KtExpression next = elseIfExpression.getElse();
                    if (next == null) {
                        elseExpression = elseIfExpression.getThen();
                        break;
                    }
                    elseExpression = next;
                }
                if (elseExpression instanceof KtBlockExpression) {
                    sibling = elseExpression;
                    start = sibling;
                }

            } else if (sibling instanceof KtWhenExpression) {
                List<KtWhenEntry> entries = ((KtWhenExpression) sibling).getEntries();
                if (!entries.isEmpty()) {
                    KtWhenEntry lastEntry = null;
                    for (KtWhenEntry entry : entries) {
                        if (entry.getExpression() instanceof KtBlockExpression) lastEntry = entry;
                    }
                    if (lastEntry != null) {
                        sibling = lastEntry;
                        start = sibling;
                    }
                }

            } else if (sibling instanceof KtTryExpression) {
                KtTryExpression tryExpression = (KtTryExpression) sibling;
                KtFinallySection finallyBlock = tryExpression.getFinallyBlock();
                if (finallyBlock != null) {
                    sibling = finallyBlock;
                    start = sibling;
                } else {
                    List<KtCatchClause> clauses = tryExpression.getCatchClauses();
                    if (!clauses.isEmpty()) {
                        sibling = clauses.get(clauses.size() - 1);
                        start = sibling;
                    }
                }
            }
        }

        // moving out of code block
        if (sibling.getNode().getElementType() == (down ? KtTokens.RBRACE : KtTokens.LBRACE)) {
            PsiElement parent = sibling.getParent();
            if (!(parent instanceof KtBlockExpression || parent instanceof KtFunctionLiteral)) return null;

            KtBlockExpression newBlock;
            if (parent instanceof KtFunctionLiteral) {
                //noinspection ConstantConditions
                newBlock = findClosestBlock(((KtFunctionLiteral) parent).getBodyExpression(), down, false);

                if (!down) {
                    PsiElement arrow = ((KtFunctionLiteral) parent).getArrow();
                    if (arrow != null) {
                        end = arrow;
                    }
                }
            } else {
                newBlock = findClosestBlock(sibling, down, true);
            }

            if (newBlock == null) return null;

            if (PsiTreeUtil.isAncestor(newBlock, parent, true)) {
                PsiElement outermostParent = KtPsiUtil.getOutermostParent(parent, newBlock, true);

                if (down) {
                    end = outermostParent;
                }
                else {
                    start = outermostParent;
                }
            }
            else {
                if (down) {
                    end = newBlock.getLBrace();
                }
                else {
                    start = newBlock.getRBrace();
                }
            }
        }
        // moving into code block
        else {
            PsiElement blockLikeElement;

            KtBlockExpression dslBlock = getDSLLambdaBlock(editor, sibling, down);
            if (dslBlock != null) {
                // Use JetFunctionLiteral (since it contains braces)
                blockLikeElement = dslBlock.getParent();
            } else {
                // JetBlockExpression and other block-like elements
                blockLikeElement = KtPsiUtil.getOutermostDescendantElement(sibling, down, CHECK_BLOCK_LIKE_ELEMENT);
            }

            if (blockLikeElement != null) {
                if (down) {
                    end = KtPsiUtil.findChildByType(blockLikeElement, KtTokens.LBRACE);
                    if (blockLikeElement instanceof KtFunctionLiteral) {
                        PsiElement arrow = ((KtFunctionLiteral) blockLikeElement).getArrow();
                        if (arrow != null) {
                            end = arrow;
                        }
                    }
                }
                else {
                    start = KtPsiUtil.findChildByType(blockLikeElement, KtTokens.RBRACE);
                }
            }
        }

        return start != null && end != null ? new LineRange(start, end, editor.getDocument()) : null;
    }

    @Nullable
    private static LineRange getWhenEntryTargetRange(@NotNull Editor editor, @NotNull PsiElement sibling, boolean down) {
        if (sibling.getNode().getElementType() == (down ? KtTokens.RBRACE : KtTokens.LBRACE) &&
            PsiTreeUtil.getParentOfType(sibling, KtWhenEntry.class) == null) {
            return null;
        }

        return new LineRange(sibling, sibling, editor.getDocument());
    }

    @Nullable
    private LineRange getValueParamOrArgTargetRange(
            @NotNull Editor editor,
            @NotNull PsiElement elementToCheck,
            @NotNull PsiElement sibling,
            boolean down
    ) {
        PsiElement next = sibling;

        if (next.getNode().getElementType() == KtTokens.COMMA) {
            next = firstNonWhiteSibling(next, down);
        }

        LineRange range = (next instanceof KtParameter || next instanceof KtValueArgument)
                          ? new LineRange(next, next, editor.getDocument())
                          : null;

        if (range != null) {
            parametersOrArgsToMove = new Pair<PsiElement, PsiElement>(elementToCheck, next);
        }

        return range;
    }

    @Nullable
    private LineRange getTargetRange(
            @NotNull Editor editor,
            @Nullable PsiElement elementToCheck,
            @NotNull PsiElement sibling,
            boolean down
    ) {
        if (elementToCheck instanceof KtParameter || elementToCheck instanceof KtValueArgument) {
            return getValueParamOrArgTargetRange(editor, elementToCheck, sibling, down);
        }

        if (elementToCheck instanceof KtExpression || elementToCheck instanceof PsiComment) {
            return getExpressionTargetRange(editor, sibling, down);
        }

        if (elementToCheck instanceof KtWhenEntry) {
            return getWhenEntryTargetRange(editor, sibling, down);
        }

        return null;
    }

    @Override
    protected boolean checkSourceElement(@NotNull PsiElement element) {
        return PsiTreeUtil.instanceOf(element, MOVABLE_ELEMENT_CLASSES);
    }

    @Override
    protected LineRange getElementSourceLineRange(@NotNull PsiElement element, @NotNull Editor editor, @NotNull LineRange oldRange) {
        TextRange textRange = element.getTextRange();
        if (editor.getDocument().getTextLength() < textRange.getEndOffset()) return null;

        int startLine = editor.offsetToLogicalPosition(textRange.getStartOffset()).line;
        int endLine = editor.offsetToLogicalPosition(textRange.getEndOffset()).line + 1;

        return new LineRange(startLine, endLine);
    }

    @Nullable
    private static PsiElement getMovableElement(@NotNull PsiElement element, boolean lookRight) {
        //noinspection unchecked
        PsiElement movableElement = PsiUtilsKt.getParentOfTypesAndPredicate(
                element,
                false,
                MOVABLE_ELEMENT_CLASSES,
                MOVABLE_ELEMENT_CONSTRAINT
        );
        if (movableElement == null) return null;

        if (isBracelessBlock(movableElement)) {
            movableElement = firstNonWhiteElement(lookRight ? movableElement.getLastChild() : movableElement.getFirstChild(), !lookRight);
        }

        return movableElement;
    }

    private static boolean isLastOfItsKind(@NotNull PsiElement element, boolean down) {
        return getSiblingOfType(element, down, element.getClass()) == null;
    }

    private static boolean isForbiddenMove(@NotNull PsiElement element, boolean down) {
        if (element instanceof KtParameter || element instanceof KtValueArgument) {
            return isLastOfItsKind(element, down);
        }

        return false;
    }

    private static boolean isBracelessBlock(@NotNull PsiElement element) {
        if (!(element instanceof KtBlockExpression)) return false;

        KtBlockExpression block = (KtBlockExpression) element;

        return block.getLBrace() == null && block.getRBrace() == null;
    }

    private static PsiElement adjustSibling(
            @NotNull Editor editor,
            @NotNull LineRange sourceRange,
            @NotNull MoveInfo info,
            boolean down
    ) {
        PsiElement element = down ? sourceRange.lastElement : sourceRange.firstElement;
        PsiElement sibling = down ? element.getNextSibling() : element.getPrevSibling();

        PsiElement whiteSpaceTestSubject = sibling;
        if (sibling == null) {
            PsiElement parent = element.getParent();
            if (parent != null && isBracelessBlock(parent)) {
                whiteSpaceTestSubject = down ? parent.getNextSibling() : parent.getPrevSibling();
            }
        }

        if (whiteSpaceTestSubject instanceof PsiWhiteSpace) {
            if (KotlinRefactoringUtilKt.isMultiLine(whiteSpaceTestSubject)) {
                int nearLine = down ? sourceRange.endLine : sourceRange.startLine - 1;

                info.toMove = sourceRange;
                info.toMove2 = new LineRange(nearLine, nearLine + 1);
                info.indentTarget = false;

                return null;
            }

            if (sibling != null) {
                sibling = firstNonWhiteElement(sibling, down);
            }
        }

        if (sibling == null) {
            KtCallExpression callExpression = PsiTreeUtil.getParentOfType(element, KtCallExpression.class);
            if (callExpression != null) {
                KtBlockExpression dslBlock = getDSLLambdaBlock(editor, callExpression, down);
                if (PsiTreeUtil.isAncestor(dslBlock, element, false)) {
                    //noinspection ConstantConditions
                    PsiElement blockParent = dslBlock.getParent();
                    return down
                           ? KtPsiUtil.findChildByType(blockParent, KtTokens.RBRACE)
                           : KtPsiUtil.findChildByType(blockParent, KtTokens.LBRACE);
                }
            }

            info.toMove2 = null;
            return null;
        }

        return sibling;
    }

    @Override
    public boolean checkAvailable(@NotNull Editor editor, @NotNull PsiFile file, @NotNull MoveInfo info, boolean down) {
        parametersOrArgsToMove = null;

        if (!super.checkAvailable(editor, file, info, down)) return false;

        switch (checkForMovableClosingBrace(editor, file, info, down)) {
            case NOT_MOVABLE: {
                info.toMove2 = null;
                return true;
            }
            case MOVABLE:
                return true;
            default:
                break;
        }

        LineRange oldRange = info.toMove;

        Pair<PsiElement, PsiElement> psiRange = getElementRange(editor, file, oldRange);
        if (psiRange == null) return false;

        //noinspection unchecked
        PsiElement firstElement = getMovableElement(psiRange.getFirst(), false);
        PsiElement lastElement = getMovableElement(psiRange.getSecond(), true);

        if (firstElement == null || lastElement == null) return false;

        if (isForbiddenMove(firstElement, down) || isForbiddenMove(lastElement, down)) {
            info.toMove2 = null;
            return true;
        }

        if ((firstElement instanceof KtParameter || firstElement instanceof KtValueArgument) &&
            PsiTreeUtil.isAncestor(lastElement, firstElement, false)) {
            lastElement = firstElement;
        }

        LineRange sourceRange = getSourceRange(firstElement, lastElement, editor, oldRange);
        if (sourceRange == null) return false;

        PsiElement sibling = getLastNonWhiteSiblingInLine(adjustSibling(editor, sourceRange, info, down), editor, down);

        // Either reached last sibling, or jumped over multi-line whitespace
        if (sibling == null) return true;

        info.toMove = sourceRange;
        info.toMove2 = getTargetRange(editor, sourceRange.firstElement, sibling, down);
        return true;
    }

    @Nullable
    private Pair<PsiElement, PsiElement> parametersOrArgsToMove;

    private static PsiElement getComma(@NotNull PsiElement element) {
        PsiElement sibling = firstNonWhiteSibling(element, true);
        return sibling != null && (sibling.getNode().getElementType() == KtTokens.COMMA) ? sibling : null;
    }

    private static void fixCommaIfNeeded(@NotNull PsiElement element, boolean willBeLast) {
        PsiElement comma = getComma(element);
        if (willBeLast && comma != null) {
            comma.delete();
        }
        else if (!willBeLast && comma == null) {
            PsiElement parent = element.getParent();
            assert parent != null;

            parent.addAfter(KtPsiFactoryKt.KtPsiFactory(parent.getProject()).createComma(), element);
        }
    }

    @Override
    public void beforeMove(@NotNull Editor editor, @NotNull MoveInfo info, boolean down) {
        if (parametersOrArgsToMove != null) {
            PsiElement element1 = parametersOrArgsToMove.first;
            PsiElement element2 = parametersOrArgsToMove.second;

            fixCommaIfNeeded(element1, down && isLastOfItsKind(element2, true));
            fixCommaIfNeeded(element2, !down && isLastOfItsKind(element1, true));

            //noinspection ConstantConditions
            PsiDocumentManager.getInstance(editor.getProject()).doPostponedOperationsAndUnblockDocument(editor.getDocument());
        }
    }
}
