/*
 * Copyright 2019 junichi11.
 *
 * 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.netbeans.modules.php.cake3.editor;

import java.awt.Component;
import java.awt.Point;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.Transferable;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.awt.dnd.DropTargetDragEvent;
import java.awt.dnd.DropTargetDropEvent;
import java.awt.dnd.InvalidDnDOperationException;
import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.List;
import java.util.StringTokenizer;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.JEditorPane;
import javax.swing.text.BadLocationException;
import javax.swing.text.Document;
import javax.swing.text.StyledDocument;
import org.netbeans.editor.BaseDocument;
import org.netbeans.modules.csl.api.DataLoadersBridge;
import org.netbeans.modules.editor.NbEditorUtilities;
import org.netbeans.modules.php.api.phpmodule.PhpModule;
import org.netbeans.modules.php.cake3.modules.CakePHP3Module;
import org.netbeans.modules.php.cake3.modules.CakePHP3Module.Category;
import org.netbeans.modules.php.cake3.modules.ModuleUtils;
import org.netbeans.modules.php.cake3.options.CakePHP3Options;
import org.openide.ErrorManager;
import org.openide.filesystems.FileObject;
import org.openide.filesystems.FileUtil;
import org.openide.text.NbDocument;
import org.openide.util.lookup.ServiceProvider;
import org.openide.windows.ExternalDropHandler;

/**
 *
 * @author junichi11
 */
@ServiceProvider(service = ExternalDropHandler.class, position = 450)
public class CakePHPExternalDropHandler extends ExternalDropHandler {

    private static final Logger LOGGER = Logger.getLogger(CakePHPExternalDropHandler.class.getName());
    private static DataFlavor uriListDataFlavor;
    private boolean canDrop;

    @Override
    public boolean canDrop(DropTargetDragEvent event) {
        JEditorPane editorPane = findPane(event.getDropTargetContext().getComponent());
        if (editorPane == null || !isInCakePHP(editorPane)) {
            return false;
        }
        Transferable t = event.getTransferable();
        canDrop = canDrop(t);
        if (!canDrop) {
            return false;
        }

        editorPane.setCaretPosition(getOffset(editorPane, event.getLocation()));
        editorPane.requestFocusInWindow(); //pity we need to call this all the time when dragging, but  ExternalDropHandler don't handle dragEnter event
        return canDrop(event.getCurrentDataFlavors());
    }

    @Override
    public boolean canDrop(DropTargetDropEvent event) {
        if (!canDrop) {
            return false;
        }
        JEditorPane editorPane = findPane(event.getDropTargetContext().getComponent());
        if (editorPane == null || !isInCakePHP(editorPane)) {
            return false;
        }
        return canDrop(event.getCurrentDataFlavors());
    }

    private boolean canDrop(Transferable t) {
        if (!CakePHP3Options.getInstance().isExternalDragAndDrop()) {
            return false;
        }
        if (null == t) {
            return false;
        }
        List<File> fileList = getFileList(t);
        if ((fileList == null) || fileList.isEmpty()) {
            return false;
        }
        //handle just the first file
        File file = fileList.get(0);
        FileObject target = FileUtil.toFileObject(file);
        if (file.isDirectory() || !isAvailableMimeType(target) || !isInCakePHP(target)) {
            return false;
        }
        return true;
    }

    private boolean canDrop(DataFlavor[] flavors) {
        for (int i = 0; null != flavors && i < flavors.length; i++) {
            if (DataFlavor.javaFileListFlavor.equals(flavors[i])
                    || getUriListDataFlavor().equals(flavors[i])) {
                return true;
            }
        }
        return false;
    }

    private int getOffset(JEditorPane pane, Point location) {
        return pane.getUI().viewToModel(pane, location);
    }

    @Override
    public boolean handleDrop(DropTargetDropEvent event) {
        Transferable t = event.getTransferable();
        if (null == t) {
            return false;
        }
        List<File> fileList = getFileList(t);
        if ((fileList == null) || fileList.isEmpty()) {
            return false;
        }
        //handle just the first file
        File file = fileList.get(0);
        FileObject target = FileUtil.toFileObject(file);
        if (file.isDirectory()) {
            return true; //as we previously claimed we canDrop() it so we need to say we've handled it even if did nothing.
        }
        JEditorPane pane = findPane(event.getDropTargetContext().getComponent());
        if (pane == null) {
            return false;
        }
        final BaseDocument document = (BaseDocument) pane.getDocument();
        FileObject current = DataLoadersBridge.getDefault().getFileObject(document);
        PhpModule phpModule = PhpModule.Factory.forFileObject(current);
        if (phpModule == null) {
            return true;
        }
        CakePHP3Module cakeModule = CakePHP3Module.forPhpModule(phpModule);

        final StringBuilder sb = new StringBuilder();

        String mimeType = target.getMIMEType();
        if ("content/unknown".equals(mimeType)) { //NOI18N
            return true;
        }

        // plugin
        CakePHP3Module.Base base = cakeModule.getBase(target);
        String pluginName = ""; // NOI18N
        if (base == CakePHP3Module.Base.PLUGIN) {
            pluginName = cakeModule.getPluginName(target);
        }
        String relativePath;
        switch (mimeType) {
            case "text/css": // NOI18N
                List<FileObject> csss = cakeModule.getDirectories(base, Category.CSS, pluginName);
                relativePath = getRelativePath(csss, target);
                relativePath = ModuleUtils.appendPluignName(pluginName, relativePath);
                sb.append("<?= $this->Html->css('").append(relativePath).append("') ?>"); // NOI18N
                break;
            case "text/javascript": // NOI18N
                List<FileObject> jss = cakeModule.getDirectories(base, Category.JS, pluginName);
                relativePath = getRelativePath(jss, target);
                relativePath = ModuleUtils.appendPluignName(pluginName, relativePath);
                sb.append("<?= $this->Html->script('").append(relativePath).append("') ?>"); // NOI18N
                break;
            case "image/png":
            case "image/jpeg":
            case "image/gif":
                List<FileObject> imgs = cakeModule.getDirectories(base, Category.IMG, pluginName);
                relativePath = getRelativePath(imgs, target);
                relativePath = ModuleUtils.appendPluignName(pluginName, relativePath);
                // $this->Html->image('something.png', ['alt' => '']);
                sb.append("<?= $this->Html->image('").append(relativePath).append("', ['alt' => '']) ?>"); // NOI18N
                break;
            case "text/x-php5": // NOI18N
                // TODO add the others?
                Category category = cakeModule.getCategory(target);
                String commonName = ModuleUtils.toCommonName(target.getName(), category);
                String name = ModuleUtils.appendPluignName(pluginName, commonName);
                switch (category) {
                    case COMPONENT:
                        sb.append("$this->loadComponent('").append(name).append("');"); // NOI18N
                        break;
                    case TABLE:
                        // XXX use TableRegistry?
                        sb.append("$this->loadModel('").append(name).append("');"); // NOI18N
                        break;
                    default:
                        return true;
                }
                break;
            default:
                return true;
        }

        final int offset = getOffset(pane, event.getLocation());
        if (!(document instanceof StyledDocument)) {
            return true;
        }
        try {
            NbDocument.runAtomicAsUser((StyledDocument) document, new Runnable() {

                @Override
                public void run() {
                    try {
                        document.insertString(offset, sb.toString(), null);
                    } catch (BadLocationException ex) {
                        // ignore
                    }
                }
            });
        } catch (BadLocationException ex) {
            // ignore
        }

        return true;
    }

    private String getRelativePath(List<FileObject> directories, FileObject target) {
        String relativePath = ""; // NOI18N
        for (FileObject directory : directories) {
            String targetPath = target.getPath();
            String directoryPath = directory.getPath();
            if (targetPath.startsWith(directoryPath)) {
                relativePath = targetPath.replace(directoryPath + "/", ""); // NOI18N
                break;
            }
        }
        return relativePath;
    }

    private JEditorPane findPane(Component component) {
        while (component != null) {
            if (component instanceof JEditorPane) {
                return (JEditorPane) component;
            }
            component = component.getParent();
        }
        return null;
    }

    private boolean isAvailableMimeType(FileObject fileObject) {
        String mimeType = fileObject.getMIMEType();
        switch (mimeType) {
            case "text/css": // NOI18N
            case "text/javascript": // NOI18N
            case "image/png": // NOI18N
            case "image/jpeg": // NOI18N
            case "image/gif": // NOI18N
            case "text/x-php5": // NOI18N
                return true;
            default:
                return false;
        }
    }

    private boolean isInCakePHP(JEditorPane pane) {
        Document document = pane.getDocument();
        if (document == null) {
            return false;
        }
        FileObject fileObject = NbEditorUtilities.getFileObject(document);
        return isInCakePHP(fileObject);
    }

    private boolean isInCakePHP(FileObject fileObject) {
        if (fileObject == null) {
            return false;
        }
        PhpModule phpModule = PhpModule.Factory.forFileObject(fileObject);
        if (phpModule == null) {
            return false;
        }
        return CakePHP3Module.isCakePHP(phpModule);
    }

    //copied from org.netbeans.modules.openfile.DefaultExternalDropHandler
    private List<File> getFileList(Transferable t) {
        try {
            if (t.isDataFlavorSupported(DataFlavor.javaFileListFlavor)) {
                //windows & mac
                try {
                    return (List<File>) t.getTransferData(DataFlavor.javaFileListFlavor);
                } catch (InvalidDnDOperationException ex) { // #212390
                    LOGGER.log(Level.FINE, null, ex);
                }
            }
            if (t.isDataFlavorSupported(getUriListDataFlavor())) {
                //linux
                String uriList = (String) t.getTransferData(getUriListDataFlavor());
                return textURIListToFileList(uriList);
            }
        } catch (UnsupportedFlavorException ex) {
            ErrorManager.getDefault().notify(ErrorManager.INFORMATIONAL, ex);
        } catch (IOException ex) {
            // Ignore. Can be just "Owner timed out" from sun.awt.X11.XSelection.getData.
            LOGGER.log(Level.FINE, null, ex);
        }
        return null;
    }

    //copied from org.netbeans.modules.openfile.DefaultExternalDropHandler
    private DataFlavor getUriListDataFlavor() {
        if (null == uriListDataFlavor) {
            try {
                uriListDataFlavor = new DataFlavor("text/uri-list;class=java.lang.String");
            } catch (ClassNotFoundException cnfE) {
                //cannot happen
                throw new AssertionError(cnfE);
            }
        }
        return uriListDataFlavor;
    }

    //copied from org.netbeans.modules.openfile.DefaultExternalDropHandler
    private List<File> textURIListToFileList(String data) {
        List<File> list = new ArrayList<>(1);
        for (StringTokenizer st = new StringTokenizer(data, "\r\n\u0000");
                st.hasMoreTokens();) {
            String s = st.nextToken();
            if (s.startsWith("#")) {
                // the line is a comment (as per the RFC 2483)
                continue;
            }
            try {
                URI uri = new URI(s);
                File file = org.openide.util.Utilities.toFile(uri);
                list.add(file);
            } catch (URISyntaxException | IllegalArgumentException e) {
                // malformed URI
            }
        }
        return list;
    }

}