/**
 * Copyright 2013-2019 the original author or authors from the Jeddict project (https://jeddict.github.io/).
 *
 * 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 io.github.jeddict.jpa.modeler.initializer;

import static io.github.jeddict.jcode.util.Constants.JAVA_EXT_SUFFIX;
import static io.github.jeddict.jcode.util.JavaIdentifiers.unqualify;
import static io.github.jeddict.jcode.util.ProjectHelper.findSourceGroupForFile;
import static io.github.jeddict.jcode.util.StringHelper.camelCase;
import static io.github.jeddict.jpa.modeler.initializer.JPAModelerUtil.JAVA_CLASS_ICON;
import static io.github.jeddict.jpa.modeler.initializer.JPAModelerUtil.PACKAGE_ICON;
import static io.github.jeddict.jpa.modeler.initializer.JPAModelerUtil.TABLE_ICON;
import io.github.jeddict.jpa.modeler.widget.BeanClassWidget;
import io.github.jeddict.jpa.modeler.widget.JavaClassWidget;
import io.github.jeddict.jpa.modeler.widget.PersistenceClassWidget;
import io.github.jeddict.jpa.spec.Basic;
import io.github.jeddict.jpa.spec.EntityMappings;
import io.github.jeddict.jpa.spec.EnumType;
import io.github.jeddict.jpa.spec.ManagedClass;
import io.github.jeddict.jpa.spec.extend.JavaClass;
import io.github.jeddict.jpa.spec.extend.ReferenceClass;
import io.github.jeddict.reveng.JCREProcessor;
import io.github.jeddict.source.ClassExplorer;
import io.github.jeddict.source.SourceExplorer;
import java.awt.Component;
import java.awt.MouseInfo;
import java.awt.Point;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.Transferable;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.ArrayList;
import static java.util.Collections.singleton;
import java.util.List;
import static java.util.Objects.isNull;
import static java.util.Objects.nonNull;
import java.util.Optional;
import java.util.concurrent.CountDownLatch;
import static java.util.stream.Collectors.toList;
import java.util.stream.Stream;
import javax.swing.JMenuItem;
import javax.swing.JPopupMenu;
import org.netbeans.api.db.explorer.DatabaseMetaDataTransfer.Table;
import org.netbeans.api.project.SourceGroup;
import org.netbeans.api.visual.widget.Widget;
import org.netbeans.modeler.action.WidgetDropListener;
import org.netbeans.modeler.core.ModelerFile;
import static org.netbeans.modeler.core.NBModelerUtil.drawImage;
import static org.netbeans.modeler.core.NBModelerUtil.drawImageOnNodeWidget;
import org.netbeans.modeler.specification.model.document.IModelerScene;
import org.netbeans.api.db.explorer.DatabaseConnection;
import org.netbeans.modules.db.explorer.DatabaseConnectionAccessor;
import org.netbeans.modules.db.explorer.node.SchemaNode;
import org.netbeans.modules.db.explorer.node.TableListNode;
import org.netbeans.modules.db.explorer.node.TableNode;
import org.netbeans.modules.db.metadata.model.api.MetadataElementHandle;
import org.netbeans.modules.db.metadata.model.api.MetadataModel;
import org.netbeans.modules.db.metadata.model.api.MetadataModelException;
import org.openide.ErrorManager;
import org.openide.filesystems.FileObject;
import org.openide.filesystems.FileUtil;
import org.openide.loaders.DataFolder;
import org.openide.nodes.FilterNode;
import org.openide.nodes.Node;
import org.openide.util.Exceptions;
import org.openide.util.Lookup;
import org.openide.util.RequestProcessor;
import org.openide.util.datatransfer.MultiTransferObject;
import org.openide.windows.WindowManager;

/**
 *
 * @author jGauravGupta
 */
public class WidgetDropListenerImpl implements WidgetDropListener {

    private static final String PRIMARY_TYPE = "application";
    private static final String PACKAGE_TYPE = "x-java-org-netbeans-modules-java-project-packagenodednd";
    private static final String DB_TYPE = "x-java-netbeans-dbexplorer-table";

    @Override
    public boolean isDroppable(Widget widget, Point point, Transferable transferable, IModelerScene scene) {
        if (widget == scene) {
            if (isJavaClassDrop(transferable)) {
                drawImage(JAVA_CLASS_ICON, point, scene);
            } else if (isPackageFlavor(transferable)) {
                drawImage(PACKAGE_ICON, point, scene);
            } else if (isDBFlavor(transferable)) {
                drawImage(TABLE_ICON, point, scene);
            }
        } else {
            if (isJavaClassDrop(transferable)) {
                drawImageOnNodeWidget(JAVA_CLASS_ICON, point, scene, widget);
            } else if (isDBFlavor(transferable)) {
                drawImageOnNodeWidget(TABLE_ICON, point, scene, widget);
            }
        }
        return true;
    }

    @Override
    public void drop(Widget widget, Point point, Transferable transferable, IModelerScene scene) {
        if (widget == scene) {
            dropOnScene(transferable, scene);
        } else {
            dropOnWidget(widget, transferable, scene);
        }
    }

    private void dropOnScene(Transferable transferable, IModelerScene scene) {
        JCREProcessor processor = Lookup.getDefault().lookup(JCREProcessor.class);
        List<File> files = new ArrayList<>();

        if (isJavaClassDrop(transferable)) {
            try {
                List<File> javaFiles = (List<File>) transferable.getTransferData(DataFlavor.javaFileListFlavor);
                files.addAll(javaFiles.stream()
                        .filter(file -> file.getPath().endsWith(JAVA_EXT_SUFFIX))
                        .collect(toList()));
            } catch (UnsupportedFlavorException | IOException ex) {
                ErrorManager.getDefault().notify(ErrorManager.INFORMATIONAL, ex);
            }

        }

        if (isPackageFlavor(transferable)) {
            files.addAll(
                    getPackageList(transferable)
                            .stream()
                            .map(FileUtil::toFile)
                            .filter(File::isDirectory)
                            .flatMap(dir -> Stream.of(dir.listFiles(file -> file.getPath().endsWith(JAVA_EXT_SUFFIX))))
                            .collect(toList())
            );
        }

        if (!files.isEmpty()) {
            processor.processDropedClasses(scene.getModelerFile(), files);
        }
        files.clear();

        try {
            List<File> docs = (List<File>) transferable.getTransferData(DataFlavor.javaFileListFlavor);
            files.addAll(docs.stream()
                    .filter(file
                            -> file.getPath().toLowerCase().endsWith(".json")
                    || file.getPath().toLowerCase().endsWith(".xml")
                    || file.getPath().toLowerCase().endsWith(".yml")
                    || file.getPath().toLowerCase().endsWith(".yaml")
                    || file.getPath().toLowerCase().endsWith(".jpa")
                    ).collect(toList()));
            if (!files.isEmpty()) {
                processor.processDropedDocument(scene.getModelerFile(), docs);
            }
        } catch (UnsupportedFlavorException | IOException ex) {
            ErrorManager.getDefault().notify(ErrorManager.INFORMATIONAL, ex);
        }

        if (isDBFlavor(transferable)) {
            List<String> tables = getDBTableList(transferable);
            DatabaseConnection databaseConnection = getDatabaseConnection(transferable);
            processor.processDropedTables(
                    scene.getModelerFile(), 
                    tables, 
                    databaseConnection, 
                    Optional.empty()
            );
        }
    }

    private void dropOnWidget(Widget widget, Transferable transferable, IModelerScene scene) {
        JCREProcessor processor = Lookup.getDefault().lookup(JCREProcessor.class);
        List<File> files = new ArrayList<>();
        if (isJavaClassDrop(transferable)) {
            try {
                List<File> javaFiles = (List<File>) transferable.getTransferData(DataFlavor.javaFileListFlavor);
                files.addAll(javaFiles.stream()
                        .filter(file -> file.getPath().endsWith(JAVA_EXT_SUFFIX))
                        .collect(toList()));
            } catch (UnsupportedFlavorException | IOException ex) {
                ErrorManager.getDefault().notify(ErrorManager.INFORMATIONAL, ex);
            }
        }
        if (!files.isEmpty()) {
            File file = files.get(0);
            FileObject fileObject = FileUtil.toFileObject(file);
            SourceGroup sourceGroup = findSourceGroupForFile(fileObject);
            if (isNull(sourceGroup)) {
                return;
            }
            String clazzFQN = fileObject.getPath()
                    .substring(sourceGroup.getRootFolder().getPath().length() + 1, fileObject.getPath().lastIndexOf(JAVA_EXT_SUFFIX))
                    .replace('/', '.');
            String clazz = unqualify(clazzFQN);
            EntityMappings entityMappings = (EntityMappings) scene.getBaseElementSpec();
            SourceExplorer source = new SourceExplorer(sourceGroup.getRootFolder(), entityMappings, singleton(clazzFQN), false);
            Optional<ClassExplorer> classExplorerOpt = Optional.empty();
            try {
                classExplorerOpt = source.createClass(clazzFQN);
            } catch (FileNotFoundException ex) {
                Exceptions.printStackTrace(ex);
            }
            if (classExplorerOpt.isPresent() && widget instanceof JavaClassWidget) {
                ClassExplorer classExplorer = classExplorerOpt.get();
                JavaClassWidget<JavaClass> javaClassWidget = (JavaClassWidget) widget;
                JavaClass javaClass = javaClassWidget.getBaseElementSpec();

                if (javaClass.getClazz().equals(clazz)) {
                    processor.processDropedClasses(scene.getModelerFile(), files);
                } else if (classExplorer.isInterface()) {
                    interfaceDropOption(widget, scene, classExplorer, clazzFQN);
                } else if (classExplorer.isClass()) {
                    classDropOption(widget, scene, classExplorer, clazzFQN);
                } else if (classExplorer.isEnum()) {
                    enumDropOption(widget, clazzFQN);
                }
            }
        }

        if (isDBFlavor(transferable)) {
            List<String> tables = getDBTableList(transferable);
            DatabaseConnection databaseConnection = getDatabaseConnection(transferable);
            if (widget instanceof JavaClassWidget) {
                JavaClassWidget<JavaClass> javaClassWidget = (JavaClassWidget) widget;
                JavaClass javaClass = javaClassWidget.getBaseElementSpec();
                if (javaClass == null || javaClass instanceof ManagedClass) {
                    processor.processDropedTables(
                            scene.getModelerFile(),
                            tables,
                            databaseConnection,
                            Optional.ofNullable(javaClass)
                    );
                }
            }
        }
    }

    private void classDropOption(Widget widget, IModelerScene scene, ClassExplorer classExplorer, String clazzFQN) {
        ModelerFile modelerFile = scene.getModelerFile();
        String clazz = unqualify(clazzFQN);
        JavaClassWidget<JavaClass> javaClassWidget = (JavaClassWidget) widget;
        JavaClass javaClass = javaClassWidget.getBaseElementSpec();
        Runnable addAttributesAction = () -> {
            javaClass.getAttributes().load(classExplorer);
            modelerFile.save(true);
            modelerFile.close();
            JPAFileActionListener.open(modelerFile);
        };
        final JPopupMenu popup = new JPopupMenu();
        JMenuItem extendClass = new JMenuItem("extend class " + clazz);
        extendClass.addActionListener(e -> javaClass.setSuperclassRef(new ReferenceClass(clazzFQN)));
        popup.add(extendClass);

        JMenuItem addAttributes = new JMenuItem("copy attributes from " + clazz);
        addAttributes.addActionListener(e -> addAttributesAction.run());
        popup.add(addAttributes);

        Component comp = WindowManager.getDefault().getMainWindow();
        Point location = MouseInfo.getPointerInfo().getLocation();
        popup.show(
                comp,
                (int) location.x - comp.getLocationOnScreen().x,
                (int) location.y - comp.getLocationOnScreen().y
        );
    }

    private void interfaceDropOption(Widget widget, IModelerScene scene, ClassExplorer classExplorer, String clazzFQN) {
        ModelerFile modelerFile = scene.getModelerFile();
        String clazz = unqualify(clazzFQN);
        JavaClassWidget<JavaClass> javaClassWidget = (JavaClassWidget) widget;
        JavaClass javaClass = javaClassWidget.getBaseElementSpec();
        Runnable addAttributesAction = () -> {
            javaClass.getAttributes().load(classExplorer);
            modelerFile.save(true);
            modelerFile.close();
            JPAFileActionListener.open(modelerFile);
        };
        final JPopupMenu popup = new JPopupMenu();
        JMenuItem implementInterface = new JMenuItem("implement interface " + clazz);
        implementInterface.addActionListener(e -> javaClass.addInterface(new ReferenceClass(clazzFQN)));
        popup.add(implementInterface);

        JMenuItem implementInterfaceWithAttributes = new JMenuItem("implement interface " + clazz + " with attributes");
        implementInterfaceWithAttributes.addActionListener(e -> {
            javaClass.addInterface(new ReferenceClass(clazzFQN));
            addAttributesAction.run();
        });
        popup.add(implementInterfaceWithAttributes);

        JMenuItem addAttributes = new JMenuItem("add attributes from " + clazz);
        addAttributes.addActionListener(e -> addAttributesAction.run());
        popup.add(addAttributes);

        Component comp = WindowManager.getDefault().getMainWindow();
        Point location = MouseInfo.getPointerInfo().getLocation();
        popup.show(
                comp,
                (int) location.x - comp.getLocationOnScreen().x,
                (int) location.y - comp.getLocationOnScreen().y
        );
    }

    private void enumDropOption(Widget widget, String clazzFQN) {
        JavaClassWidget<? extends JavaClass> javaClassWidget = (JavaClassWidget) widget;
        String clazz = unqualify(clazzFQN);
        String name = camelCase(clazz);
        String type = clazzFQN;
        if (javaClassWidget instanceof BeanClassWidget) {
            ((BeanClassWidget) javaClassWidget)
                    .addBeanAttribute(name)
                    .getBaseElementSpec()
                    .setAttributeType(type);
        } else if (javaClassWidget instanceof PersistenceClassWidget) {
            Basic basic = ((PersistenceClassWidget) javaClassWidget)
                    .addBasicAttribute(name)
                    .getBaseElementSpec();
            basic.setAttributeType(type);
            basic.setEnumerated(EnumType.DEFAULT);
        }
    }

    protected boolean isJavaClassDrop(Transferable transferable) {
        return transferable.isDataFlavorSupported(DataFlavor.javaFileListFlavor);
    }

    private boolean isPackageFlavor(Transferable transferable) {
        DataFlavor[] flavors = transferable.getTransferDataFlavors();
        for (int i = 0; i < flavors.length; i++) {
            if (PACKAGE_TYPE.equals(flavors[i].getSubType())
                    && PRIMARY_TYPE.equals(flavors[i].getPrimaryType())) {
                //Disable pasting into package, only paste into root is allowed
                return true;
            }
        }
        return false;
    }

    private List<FileObject> getPackageList(Transferable transferable) {
        List<FileObject> packages = new ArrayList<>();
        DataFlavor[] flavors = transferable.getTransferDataFlavors();
        for (int i = 0; i < flavors.length; i++) {
            if (PACKAGE_TYPE.equals(flavors[i].getSubType())
                    && PRIMARY_TYPE.equals(flavors[i].getPrimaryType())) {
                FilterNode node;
                try {
                    node = (FilterNode) transferable.getTransferData(flavors[i]);
                    packages.add(node.getCookie(DataFolder.class).getPrimaryFile());
                } catch (UnsupportedFlavorException | IOException ex) {
                    Exceptions.printStackTrace(ex);
                }
            }
        }
        return packages;
    }

    private boolean isDBFlavor(Transferable transferable) {
        DataFlavor[] flavors = transferable.getTransferDataFlavors();
        for (DataFlavor flavor : flavors) {
            Object transferData = null;
            try {
                transferData = transferable.getTransferData(flavor);
            } catch (UnsupportedFlavorException | IOException ex) {
            }
            if (DB_TYPE.equals(flavor.getSubType()) && PRIMARY_TYPE.equals(flavor.getPrimaryType())) {
                return true;
            } else if (transferData instanceof MultiTransferObject) {
                MultiTransferObject multiTransfer = (MultiTransferObject) transferData;
                for (int i = 0; i < multiTransfer.getCount(); i++) {
                    if (isDBFlavor(multiTransfer.getTransferableAt(i))) {
                        return true;
                    }
                }
            } else if ("org.netbeans.modules.db.explorer.node.TableListNode"
                    .equals(transferData.getClass().getName())
                    || "org.netbeans.modules.db.explorer.node.SchemaNode"
                            .equals(transferData.getClass().getName())) {
                return true;
            } else if ("org.netbeans.modules.db.explorer.node.ConnectionNode"
                            .equals(transferData.getClass().getName())) {
               return false;
            }
        }
        return false;
    }

    private static final RequestProcessor RP = new RequestProcessor(WidgetDropListenerImpl.class);

    private static List<org.netbeans.modules.db.metadata.model.api.Table> getTableListFromTablesNode(TableListNode tableListNode) {
        org.netbeans.modules.db.explorer.DatabaseConnection connection = tableListNode.getLookup().lookup(org.netbeans.modules.db.explorer.DatabaseConnection.class);
        MetadataModel metaDataModel = connection.getMetadataModel();
        List<org.netbeans.modules.db.metadata.model.api.Table> tables = new ArrayList<>();
        tableListNode.getChildren(); // load eagerly
        for (Node node : tableListNode.getChildNodes()) {
            TableNode tableNode = (TableNode) node;
            MetadataElementHandle<org.netbeans.modules.db.metadata.model.api.Table> tableHandle
                    = tableNode.getTableHandle();
            try {
                CountDownLatch latch = new CountDownLatch(1);
                RP.post(() -> {
                    try {
                        metaDataModel.runReadAction(metaData -> {
                            tables.add(tableHandle.resolve(metaData));
                            latch.countDown();
                        });
                    } catch (MetadataModelException ex) {
                        Exceptions.printStackTrace(ex);
                    }
                });
                latch.await();
            } catch (InterruptedException ex) {
                Exceptions.printStackTrace(ex);
            }
        }
        return tables;
    }

    private List<String> getDBTableList(Transferable transferable) {
        List<String> tables = new ArrayList<>();
        DataFlavor[] flavors = transferable.getTransferDataFlavors();
        for (DataFlavor flavor : flavors) {
            Object transferData = null;
            try {
                transferData = transferable.getTransferData(flavor);
            } catch (UnsupportedFlavorException | IOException ex) {
                Exceptions.printStackTrace(ex);
            }
            if (DB_TYPE.equals(flavor.getSubType()) && PRIMARY_TYPE.equals(flavor.getPrimaryType())) {
                tables.add(((Table) transferData).getTableName());
            } else if (nonNull(transferData) && transferData instanceof MultiTransferObject) {
                MultiTransferObject multiTransfer = (MultiTransferObject) transferData;
                for (int i = 0; i < multiTransfer.getCount(); i++) {
                    if (isDBFlavor(multiTransfer.getTransferableAt(i))) {
                        tables.addAll(getDBTableList(multiTransfer.getTransferableAt(i)));
                    }
                }
            } else if (nonNull(transferData)
                    && "org.netbeans.modules.db.explorer.node.TableListNode"
                            .equals(transferData.getClass().getName())) {
                TableListNode tableListNode = (TableListNode)transferData;
                tables.addAll(
                        getTableListFromTablesNode(tableListNode)
                                .stream()
                                .map(org.netbeans.modules.db.metadata.model.api.Table::getName)
                                .collect(toList())
                );
            } else if (nonNull(transferData)
                    && "org.netbeans.modules.db.explorer.node.SchemaNode"
                            .equals(transferData.getClass().getName())) {
                TableListNode tableListNode = (TableListNode)((SchemaNode)transferData).getChildNodes().toArray()[0];
                tables.addAll(
                        getTableListFromTablesNode(tableListNode)
                                .stream()
                                .map(org.netbeans.modules.db.metadata.model.api.Table::getName)
                                .collect(toList())
                );
                 
            }
        }
        return tables;
    }
    
    private DatabaseConnection getDatabaseConnection(Transferable transferable) {
        DataFlavor[] flavors = transferable.getTransferDataFlavors();
        for (DataFlavor flavor : flavors) {
            Object transferData = null;
            try {
                transferData = transferable.getTransferData(flavor);
            } catch (UnsupportedFlavorException | IOException ex) {
                Exceptions.printStackTrace(ex);
            }
            if (DB_TYPE.equals(flavor.getSubType()) && PRIMARY_TYPE.equals(flavor.getPrimaryType())) {
                return ((Table) transferData).getDatabaseConnection();
            } else if (transferData instanceof MultiTransferObject) {
                MultiTransferObject multiTransfer = (MultiTransferObject) transferData;
                for (int i = 0; i < multiTransfer.getCount(); i++) {
                    if (isDBFlavor(multiTransfer.getTransferableAt(i))) {
                        return getDatabaseConnection(multiTransfer.getTransferableAt(i));
                    }
                }
            } else if (nonNull(transferData)
                    && "org.netbeans.modules.db.explorer.node.TableListNode"
                            .equals(transferData.getClass().getName())) {
                TableListNode tableListNode = (TableListNode) transferData;
                return DatabaseConnectionAccessor.DEFAULT.createDatabaseConnection(
                        tableListNode
                                .getLookup()
                                .lookup(org.netbeans.modules.db.explorer.DatabaseConnection.class)
                );
            } else if (nonNull(transferData)
                    && "org.netbeans.modules.db.explorer.node.SchemaNode"
                            .equals(transferData.getClass().getName())) {
                TableListNode tableListNode = (TableListNode) ((SchemaNode) transferData).getChildNodes().toArray()[0];
                return DatabaseConnectionAccessor.DEFAULT.createDatabaseConnection(
                        tableListNode
                                .getLookup()
                                .lookup(org.netbeans.modules.db.explorer.DatabaseConnection.class)
                );
            }
        }
        return null;
    }
}