/*
 * BSD-style license; for more info see http://pmd.sourceforge.net/license.html
 */

package net.sourceforge.pmd.util.fxdesigner.util.controls;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Objects;
import java.util.Optional;

import org.apache.commons.lang3.reflect.FieldUtils;
import org.apache.commons.lang3.reflect.MethodUtils;

import javafx.application.Platform;
import javafx.scene.control.Skin;
import javafx.scene.control.TreeCell;
import javafx.scene.control.TreeView;


/**
 * Reflective solution to know if a cell in a TreeView is
 * visible or not, to prevent confusing scrolling. Works
 * under Java 8, 9, 10. Under Java 9+, requires the
 * "--add-opens javafx.controls/javafx.scene.control.skin=ALL-UNNAMED"
 * VM option.
 *
 * @param <T> Element type of the treeview
 *
 * @author Clément Fournier
 * @since 6.4.0
 */
class TreeViewWrapper<T> {


    private final TreeView<T> wrapped;
    private Method treeViewFirstVisibleMethod;
    private Method treeViewLastVisibleMethod;
    // We can't use strong typing
    // because the class has moved packages over different java versions
    private Object virtualFlow = null;

    private boolean reflectionImpossibleWarning = false;


    TreeViewWrapper(TreeView<T> wrapped) {
        Objects.requireNonNull(wrapped);
        this.wrapped = wrapped;
        Platform.runLater(this::initialiseTreeViewReflection);
    }


    private void initialiseTreeViewReflection() {

        // we can't use wrapped.getSkin() because it may be null.
        // we don't care about the specific instance, we just want the class
        @SuppressWarnings("PMD.UselessOverridingMethod")
        Skin<?> dftSkin = new TreeView<Object>() {
            @Override
            protected Skin<?> createDefaultSkin() {
                return super.createDefaultSkin();
            }
        }.createDefaultSkin();

        Object flow = getVirtualFlow(dftSkin);

        if (flow == null) {
            return;
        }

        treeViewFirstVisibleMethod = MethodUtils.getMatchingMethod(flow.getClass(), "getFirstVisibleCell");
        treeViewLastVisibleMethod = MethodUtils.getMatchingMethod(flow.getClass(), "getLastVisibleCell");
    }


    /**
     * Returns true if the item at the given index
     * is visible in the TreeView.
     */
    boolean isIndexVisible(int index) {
        if (reflectionImpossibleWarning) {
            return false;
        }

        if (virtualFlow == null && wrapped.getSkin() == null) {
            return false;
        } else if (virtualFlow == null && wrapped.getSkin() != null) {
            // the flow is cached, so the skin must not be changed
            virtualFlow = getVirtualFlow(wrapped.getSkin());
        }

        if (virtualFlow == null) {
            return false;
        }

        Optional<TreeCell<T>> first = getFirstVisibleCell();
        Optional<TreeCell<T>> last = getLastVisibleCell();

        return first.isPresent()
                && last.isPresent()
                && first.get().getIndex() < index
                && last.get().getIndex() > index;
    }


    private Optional<TreeCell<T>> getCellFromAccessor(Method accessor) {
        return Optional.ofNullable(accessor).map(m -> {
            try {
                @SuppressWarnings("unchecked")
                TreeCell<T> cell = (TreeCell<T>) m.invoke(virtualFlow);
                return cell;
            } catch (IllegalAccessException | InvocationTargetException e) {
                e.printStackTrace();
            }
            return null;
        });
    }


    private Optional<TreeCell<T>> getFirstVisibleCell() {
        return getCellFromAccessor(treeViewFirstVisibleMethod);
    }


    private Optional<TreeCell<T>> getLastVisibleCell() {
        return getCellFromAccessor(treeViewLastVisibleMethod);
    }


    private Object getVirtualFlow(Skin<?> skin) {
        try {
            // On JRE 9 and 10, the field is declared in TreeViewSkin
            // http://hg.openjdk.java.net/openjfx/9/rt/file/c734b008e3e8/modules/javafx.controls/src/main/java/javafx/scene/control/skin/TreeViewSkin.java#l85
            // http://hg.openjdk.java.net/openjfx/10/rt/file/d14b61c6be12/modules/javafx.controls/src/main/java/javafx/scene/control/skin/TreeViewSkin.java#l85
            // On JRE 8, the field is declared in the VirtualContainerBase superclass
            // http://hg.openjdk.java.net/openjfx/8/master/rt/file/f89b7dc932af/modules/controls/src/main/java/com/sun/javafx/scene/control/skin/VirtualContainerBase.java#l68

            return FieldUtils.readField(skin, "flow", true);
        } catch (IllegalAccessException ignored) {

        } catch (RuntimeException re) {
            if (!reflectionImpossibleWarning && "java.lang.reflect.InaccessibleObjectException".equals(re.getClass().getName())) {
                // that exception was introduced for Jigsaw (JRE 9)
                // so we can't refer to it without breaking compat with Java 8

                // TODO find a way to report errors in the app directly, System.out is too shitty

                System.out.println();
                System.out.println("On JRE 9+, the following VM argument makes the controls smarter:");
                System.out.println("--add-opens javafx.controls/javafx.scene.control.skin=ALL-UNNAMED");
                System.out.println("Please consider adding it to your command-line or using the launch script bundled with PMD's binary distribution.");

                reflectionImpossibleWarning = true;
            } else {
                throw re;
            }
        }
        return null;
    }
}