/*
 * Copyright 2000-2012 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 com.intellij.designer.componentTree;

import com.intellij.designer.clipboard.SimpleTransferable;
import com.intellij.designer.designSurface.ComponentTargetFilter;
import com.intellij.designer.designSurface.EditOperation;
import com.intellij.designer.designSurface.EditableArea;
import com.intellij.designer.designSurface.OperationContext;
import com.intellij.designer.designSurface.tools.CreationTool;
import com.intellij.designer.designSurface.tools.ToolProvider;
import com.intellij.designer.model.RadComponent;
import com.intellij.designer.model.RadComponentVisitor;
import com.intellij.designer.model.RadLayout;
import com.intellij.designer.palette.PaletteItem;
import com.intellij.designer.utils.Cursors;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.util.ArrayUtil;

import javax.swing.*;
import java.awt.*;
import java.awt.dnd.*;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

/**
 * @author Alexander Lobas
 */
public class TreeDropListener extends DropTargetAdapter {
  private final EditableArea myArea;
  private final ToolProvider myToolProvider;
  private final Class[] myDragTargets;
  private final OperationContext myContext = new OperationContext();
  private DropTargetDragEvent myEvent;
  private EditOperation myTargetOperation;
  private RadComponent myTarget;
  private boolean myExecuteEnabled;
  private boolean myShowFeedback;

  private TreeDropListener(EditableArea area, ToolProvider provider) {
    this(area, provider, TreeDropListener.class, PaletteItem.class);
  }

  private TreeDropListener(EditableArea area, ToolProvider provider, Class... dragTargets) {
    myArea = area;
    myContext.setArea(area);
    myToolProvider = provider;
    myDragTargets = dragTargets;
  }

  public static void installOn(ComponentTree tree, EditableArea area, ToolProvider provider) {
    TreeDropListener listener = new TreeDropListener(area, provider);
    if (!ApplicationManager.getApplication().isHeadlessEnvironment()) {
      tree.setDragEnabled(true);
      tree.setTransferHandler(new TreeTransfer(TreeDropListener.class));
      tree.setDropTarget(new DropTarget(tree, listener));
    }
  }

  public static void installOn(JComponent component, EditableArea area, ToolProvider provider, Class... dragTargets) {
    TreeDropListener listener = new TreeDropListener(area, provider, dragTargets);
    if (!ApplicationManager.getApplication().isHeadlessEnvironment()) {
      component.setDropTarget(new DropTarget(component, listener));
    }
  }

  @Override
  public void dragExit(DropTargetEvent event) {
    eraseFeedback();
    clearState(false);
  }

  @Override
  public void dragOver(DropTargetDragEvent event) {
    myEvent = event;
    updateContext();
    updateTargetUnderMouse();
    updateCommand();
    showFeedback();
  }

  @Override
  public void drop(DropTargetDropEvent event) {
    event.acceptDrop(event.getDropAction());
    eraseFeedback();
    executeCommand();
    clearState(true);
    event.dropComplete(true);
  }

  private void showFeedback() {
    if (myTargetOperation != null) {
      myTargetOperation.showFeedback();
    }
    myShowFeedback = true;
  }

  private void eraseFeedback() {
    if (myShowFeedback) {
      myShowFeedback = false;
      if (myTargetOperation != null) {
        myTargetOperation.eraseFeedback();
      }
    }
  }

  private void updateContext() {
    myContext.setLocation(getLocation());

    if (myContext.getComponents() == null) {
      if (!ArrayUtil.contains(SimpleTransferable.getData(myEvent.getTransferable(), Class.class), myDragTargets)) {
        myContext.setComponents(Collections.emptyList());
        return;
      }

      if (myToolProvider.getActiveTool() instanceof CreationTool tool) {
        myContext.setType(OperationContext.CREATE);
        try {
          myContext.setComponents(Collections.singletonList(tool.getFactory().create()));
        }
        catch (Throwable e) {
          myContext.setComponents(Collections.emptyList());
          myToolProvider.loadDefaultTool();
        }
        return;
      }

      List<RadComponent> components = RadComponent.getPureSelection(myArea.getSelection());

      RadComponent parent = null;
      for (RadComponent component : components) {
        if (parent == null) {
          parent = component.getParent();
        }
        else if (parent != component.getParent()) {
          components = Collections.emptyList();
          break;
        }
      }

      myContext.setComponents(components);

      myContext.resetMoveAddEnabled();
      for (RadComponent component : components) {
        component.processDropOperation(myContext);
      }
    }
  }

  private void updateTargetUnderMouse() {
    if (myContext.getComponents().isEmpty()) {
      return;
    }

    final List<RadComponent> excludeComponents = new ArrayList<>(myContext.getComponents());
    if (!myContext.isCreate()) {
      for (RadComponent component : myContext.getComponents()) {
        component.accept(new RadComponentVisitor() {
          @Override
          public void endVisit(RadComponent component) {
            excludeComponents.add(component);
          }
        }, true);
      }
    }

    final EditOperation[] operation = new EditOperation[1];
    ComponentTargetFilter filter = new ComponentTargetFilter() {
      @Override
      public boolean preFilter(RadComponent component) {
        return myContext.isCreate() || !excludeComponents.contains(component);
      }

      @Override
      public boolean resultFilter(RadComponent target) {
        if (!myContext.isCreate()) {
          if (myContext.getComponents().get(0).getParent() == target) {
            myContext.setType(OperationContext.MOVE);
          }
          else {
            myContext.setType(OperationContext.ADD);
          }
        }

        if (myTarget == target) {
          return true;
        }

        RadLayout layout = target.getLayout();
        if (layout != null) {
          operation[0] = layout.processChildOperation(myContext);
        }

        return operation[0] != null;
      }
    };
    Point location = getLocation();
    RadComponent target = myArea.findTarget(location.x, location.y, filter);

    if (target != myTarget) {
      if (myTargetOperation != null) {
        eraseFeedback();
      }

      myTarget = target;
      myTargetOperation = operation[0];
    }

    if (target == null) {
      if (!myContext.isCreate()) {
        myContext.setType(null);
      }
    }
    else {
      myTargetOperation.setComponents(myContext.getComponents());
    }
  }

  private Point getLocation() {
    return myEvent.getLocation();
  }

  private void updateCommand() {
    if (myTargetOperation != null) {
      if (myContext.isMove()) {
        setExecuteEnabled(myContext.isMoveEnabled() && myTargetOperation.canExecute());
      }
      else if (myContext.isAdd()) {
        setExecuteEnabled(myContext.isAddEnabled() && myTargetOperation.canExecute());
      }
      else if (myContext.isCreate()) {
        setExecuteEnabled(myTargetOperation.canExecute());
      }
      else {
        setExecuteEnabled(false);
      }
    }
    else {
      setExecuteEnabled(false);
    }
  }

  private static final Cursor myDragCursor = Cursors.getMoveCursor();

  private void setExecuteEnabled(boolean enabled) {
    myExecuteEnabled = enabled;
    if (enabled) {
      myEvent.acceptDrag(myEvent.getDropAction());
      myArea.setCursor(myContext.isCreate() ? Cursors.getCopyCursor() : myDragCursor);
    }
    else {
      myEvent.rejectDrag();
      myArea.setCursor(Cursors.getSystemNoCursor());
    }
  }

  private void executeCommand() {
    if (myExecuteEnabled) {
      myToolProvider.execute(Collections.singletonList(myTargetOperation), myContext.getMessage());
      if (myContext.isCreate()) {
        myArea.setSelection(myContext.getComponents());
      }
    }
  }

  private void clearState(boolean full) {
    myContext.setComponents(null);
    myEvent = null;
    myTargetOperation = null;
    myTarget = null;
    myExecuteEnabled = false;
    if (full || !myContext.isCreate()) {
      myContext.setType(null);
      myToolProvider.loadDefaultTool();
    }
    myArea.setCursor(null);
  }
}