import java.lang.reflect.Field;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.Optional;
import java.util.function.BooleanSupplier;
import java.util.function.Consumer;
import java.util.stream.Stream;

import javafx.application.Application;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.value.ObservableValue;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.geometry.Insets;
import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.MenuItem;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.TitledPane;
import javafx.scene.control.skin.NestedTableColumnHeader;
import javafx.scene.control.skin.TableColumnHeader;
import javafx.scene.control.skin.TableHeaderRow;
import javafx.scene.control.skin.TableViewSkin;
import javafx.scene.control.skin.TableViewSkinBase;
import javafx.scene.input.ContextMenuEvent;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

public class PopupTest extends Application
{
    public static void main( String[] args )
    {
        System.err.println( System.getProperty( "javafx.runtime.version" ) );
        Application.launch( args );
    }

    /**
     * @see com.sun.javafx.scene.control.skin.Utils#executeOnceWhenPropertyIsNonNull(ObservableValue, Consumer)
     */
    public static < T > void executeOnceWhenPropertyIsNonNull( ObservableValue< T > p,
        Consumer< T > consumer )
    {
        if( p == null )
        {
            return;
        }

        T value = p.getValue();
        if( value != null )
        {
            consumer.accept( value );
        }
        else
        {
            final InvalidationListener listener = new InvalidationListener()
            {
                @Override
                public void invalidated( Observable observable )
                {
                    T value = p.getValue();

                    if( value != null )
                    {
                        p.removeListener( this );
                        consumer.accept( value );
                    }
                }
            };
            p.addListener( listener );
        }
    }

    @Override
    public void start( Stage stage )
    {
        System.err.println( System.getProperty( "javafx.runtime.version" ) );
        Parent content = createContent();
        Scene scene = new Scene( content, 800, 600 );
        stage.setScene( scene );
        stage.show();
    }

    TableView< ? > createTable()
    {
        final TableView< String > table = new TableView<>();
        final TableColumn< String, String > firstNameCol = new TableColumn<>( "Name" );
        firstNameCol.setMinWidth( 100 );
        firstNameCol.setCellValueFactory( param -> new SimpleStringProperty( param.getValue() ) );
        table.getColumns().add( firstNameCol );
        table.getItems().addAll( "Test1", "Test2", "Test3" );

        executeOnceWhenPropertyIsNonNull( table.skinProperty(), skin -> {
            if( skin instanceof TableViewSkin )
            {
                installContextMenu( (TableViewSkin< ? >)skin );
            }
        } );
        return table;
    }

    Parent createContent()
    {
        final VBox pane = new VBox();
        pane.setPadding( new Insets( 3 ) );

        final TableView< ? > noFixTable = createTable();
        final TableView< ? > fixedTable = createTable();
        installWorkaround( fixedTable );

        final TitledPane t1 = new TitledPane( "Standard table", noFixTable );
        t1.setCollapsible( false );
        final TitledPane t2 = new TitledPane( "Table with workaround", fixedTable );
        t2.setCollapsible( false );
        pane.getChildren().addAll( t1, t2 );

        return pane;
    }

    @SuppressWarnings( "unchecked" )
    public static < T > Optional< T > getFieldValueSafely( final Object aInstance, final Class< ? > aClass,
        final String fieldName, final Class< T > aReturnType )
    {
        final Field aField =
            Stream.of( aClass.getDeclaredFields() ).filter( f -> fieldName.equals( f.getName() ) ).findAny()
                .orElseThrow( IllegalStateException::new );
        final boolean aAccessible = aField.isAccessible();
        try
        {
            aField.setAccessible( true );
            final Object fieldValue = aField.get( aInstance );
            if( fieldValue != null && aReturnType.isInstance( fieldValue ) )
            {
                return Optional.of( (T)fieldValue );
            }
        }
        catch( IllegalAccessException aE )
        {
            return Optional.empty();
        }
        finally
        {
            aField.setAccessible( aAccessible );
        }
        return Optional.empty();
    }

    private void installWorkaround( final TableView< ? > fixedTable )
    {
        final PopupTriggerWorkaround aPopupTriggerWorkaround = new PopupTriggerWorkaround( fixedTable );
        executeOnceWhenPropertyIsNonNull( fixedTable.skinProperty(), skin -> {
            final TableHeaderRow tableHeaderRow =
                getFieldValueSafely( skin, TableViewSkinBase.class, "tableHeaderRow", TableHeaderRow.class )
                    .orElseThrow( IllegalStateException::new );
            final ObservableList< TableColumnHeader > columnHeaders =
                ( (NestedTableColumnHeader)getFieldValueSafely( tableHeaderRow, TableHeaderRow.class,
                    "rootHeader", ReadOnlyObjectWrapper.class ).orElseThrow( IllegalStateException::new )
                    .get() ).getColumnHeaders();

            columnHeaders
                .addListener( ( ListChangeListener.Change< ? extends TableColumnHeader > aChange ) -> {
                    while( aChange.next() )
                    {
                        aChange.getRemoved().forEach( this::uninstallWorkaround );
                        aChange.getAddedSubList()
                            .forEach( n -> installWorkaround( n, aPopupTriggerWorkaround ) );
                    }
                } );
        } );
    }

    private void uninstallWorkaround( final Node node )
    {
        throw new UnsupportedOperationException();
    }

    private void installWorkaround( final Node node, final PopupTriggerWorkaround aPopupTriggerWorkaround )
    {
        executeOnceWhenPropertyIsNonNull( node.onMouseReleasedProperty(), aEventHandler -> {
            node.onMouseReleasedProperty().set( event -> {
                if( !aPopupTriggerWorkaround.getAsBoolean() )
                {
                    aEventHandler.handle( event );
                }
            } );
        } );
    }

    private void installContextMenu( final TableViewSkin< ? > skin )
    {
        final TableHeaderRow tableHeaderRow =
            getFieldValueSafely( skin, TableViewSkinBase.class, "tableHeaderRow", TableHeaderRow.class )
                .orElseThrow( IllegalStateException::new );
        tableHeaderRow.addEventHandler( ContextMenuEvent.CONTEXT_MENU_REQUESTED, ( aEvent ) -> {
            final ContextMenu aContextMenu = new ContextMenu( new MenuItem( "a" ), new MenuItem( "b" ) );
            aContextMenu
                .show( skin.getSkinnable().getScene().getWindow(), aEvent.getScreenX(), aEvent.getScreenY() );
            aEvent.consume();
        } );
    }

    static class PopupTriggerWorkaround implements BooleanSupplier
    {
        boolean shouldSuppresSorting = false;
        private static final String os = System.getProperty( "os.name" );
        private static String javafxPlatform = AccessController
            .doPrivileged( (PrivilegedAction< String >)() -> System.getProperty( "javafx.platform" ) );
        private static final boolean ANDROID =
            "android".equals( javafxPlatform ) || "Dalvik".equals( System.getProperty( "java.vm.name" ) );
        private static final boolean MAC = os.startsWith( "Mac" );
        private static final boolean LINUX = os.startsWith( "Linux" ) && !ANDROID;

        public PopupTriggerWorkaround( final Node node )
        {
            if( MAC || LINUX )
            {
                node.addEventFilter( MouseEvent.ANY, evt -> {
                    if( evt.getEventType() == MouseEvent.MOUSE_MOVED
                        || evt.getEventType() == MouseEvent.MOUSE_DRAGGED )
                    {
                        return;
                    }
                    if( evt.getEventType() == MouseEvent.MOUSE_PRESSED )
                    {
                        shouldSuppresSorting = evt.isPopupTrigger();
                    }
                    else if( evt.getEventType() != MouseEvent.MOUSE_RELEASED )
                    {
                        shouldSuppresSorting = false;
                    }
                } );
            }
        }

        @Override
        public boolean getAsBoolean()
        {
            final boolean oldValue = shouldSuppresSorting;
            shouldSuppresSorting = false;
            return oldValue;
        }
    }

}