/*******************************************************************************
 * Copyright (c) 2017-2018 Oak Ridge National Laboratory.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *******************************************************************************/
package org.csstudio.display.builder.representation.javafx.widgets;

import static org.csstudio.display.builder.representation.EmbeddedDisplayRepresentationUtil.checkCompletion;
import static org.csstudio.display.builder.representation.EmbeddedDisplayRepresentationUtil.loadDisplayModel;
import static org.csstudio.display.builder.representation.ToolkitRepresentation.logger;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicReference;
import java.util.logging.Level;

import org.csstudio.display.builder.model.DirtyFlag;
import org.csstudio.display.builder.model.DisplayModel;
import org.csstudio.display.builder.model.UntypedWidgetPropertyListener;
import org.csstudio.display.builder.model.WidgetProperty;
import org.csstudio.display.builder.model.WidgetPropertyListener;
import org.csstudio.display.builder.model.widgets.NavigationTabsWidget;
import org.csstudio.display.builder.model.widgets.NavigationTabsWidget.TabProperty;
import org.csstudio.display.builder.representation.EmbeddedDisplayRepresentationUtil.DisplayAndGroup;
import org.csstudio.display.builder.representation.javafx.JFXUtil;
import org.phoebus.framework.jobs.JobManager;
import org.phoebus.framework.jobs.JobMonitor;

import javafx.geometry.Insets;
import javafx.scene.Parent;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundFill;
import javafx.scene.layout.CornerRadii;
import javafx.scene.layout.Pane;

/** Creates JavaFX item for model widget
 *
 *  <p>Different from widget representations in general,
 *  this one implements the loading of the embedded model,
 *  an operation that could be considered a runtime aspect.
 *  This was done to allow viewing the embedded content
 *  in the editor.
 *  The embedded model will be started by the EmbeddedDisplayRuntime.
 *
 *  @author Kay Kasemir
 */
@SuppressWarnings("nls")
public class NavigationTabsRepresentation extends RegionBaseRepresentation<NavigationTabs, NavigationTabsWidget>
{
    private final DirtyFlag dirty_sizes = new DirtyFlag();
    private final DirtyFlag dirty_tabs = new DirtyFlag();
    private final DirtyFlag dirty_tab_look = new DirtyFlag();
    private final DirtyFlag dirty_active_tab = new DirtyFlag();
    private final UntypedWidgetPropertyListener sizesChangedListener = this::sizesChanged;
    private final UntypedWidgetPropertyListener tabLookChangedListener = this::tabLookChanged;
    private final WidgetPropertyListener<Integer> activeTabChangedListener = this::activeTabChanged;
    private final WidgetPropertyListener<List<TabProperty>> tabsChangedListener = this::tabsChanged;

    /** The display file (and optional group inside that display) to load */
    private final AtomicReference<DisplayAndGroup> pending_display_and_group = new AtomicReference<>();

    /** Inner pane that holds child widgets
     *
     *  <p>Set to null when representation is disposed,
     *  which is used as indicator to pending display updates.
     */
    private volatile Pane body;

    /** Track active model in a thread-safe way
     *  to assert that each one is represented and removed
     */
    private final AtomicReference<DisplayModel> active_content_model = new AtomicReference<>();

    private final WidgetPropertyListener<String> tab_name_listener = (property, old_value, new_value) ->
    {
        dirty_tabs.mark();
        toolkit.scheduleUpdate(this);
    };

    /** Handle changed display.
     *
     *  <p>Either the settings for a tab changed,
     *  or the active tab changed.
     *
     *  For details see {@link EmbeddedDisplayRepresentation#fileChanged}
     */
    private final UntypedWidgetPropertyListener tab_display_listener = (property, old_value, new_value) ->
    {
        final List<TabProperty> tabs = model_widget.propTabs().getValue();
        final int active = Math.min(tabs.size()-1, model_widget.propActiveTab().getValue());
        if (active < 0)
            return;

        final TabProperty active_tab = tabs.get(active);
        final DisplayAndGroup file_and_group =
            new DisplayAndGroup(active_tab.file().getValue(), active_tab.group().getValue());

        final DisplayAndGroup skipped = pending_display_and_group.getAndSet(file_and_group);
        if (skipped != null)
            logger.log(Level.FINE, "Skipped: {0}", skipped);

        // Load embedded display in background thread
        toolkit.onRepresentationStarted();
        JobManager.schedule("Load navigation tab", this::updatePendingDisplay);
    };

    @Override
    public NavigationTabs createJFXNode() throws Exception
    {
        final NavigationTabs tabs = new NavigationTabs();
        body = tabs.getBodyPane();
        tabs.addListener(index -> model_widget.propActiveTab().setValue(index));

        return tabs;
    }

    @Override
    protected Parent getChildParent(final Parent parent)
    {
        return body;
    }

    @Override
    protected boolean isFilteringEditModeClicks()
    {
        return true;
    }

    @Override
    protected void registerListeners()
    {
        super.registerListeners();
        model_widget.propWidth().addUntypedPropertyListener(sizesChangedListener);
        model_widget.propHeight().addUntypedPropertyListener(sizesChangedListener);

        model_widget.propDirection().addUntypedPropertyListener(tabLookChangedListener);
        model_widget.propTabWidth().addUntypedPropertyListener(tabLookChangedListener);
        model_widget.propTabHeight().addUntypedPropertyListener(tabLookChangedListener);
        model_widget.propTabSpacing().addUntypedPropertyListener(tabLookChangedListener);
        model_widget.propSelectedColor().addUntypedPropertyListener(tabLookChangedListener);
        model_widget.propDeselectedColor().addUntypedPropertyListener(tabLookChangedListener);
        model_widget.propFont().addUntypedPropertyListener(tabLookChangedListener);

        model_widget.propActiveTab().addPropertyListener(activeTabChangedListener);

        model_widget.propTabs().addPropertyListener(tabsChangedListener);

        // Initial update
        tabsChanged(null, null, model_widget.propTabs().getValue());
        activeTabChanged(null, null, model_widget.propActiveTab().getValue());
    }

    @Override
    protected void unregisterListeners()
    {
        tabsChanged(null, model_widget.propTabs().getValue(), null);
        model_widget.propWidth().removePropertyListener(sizesChangedListener);
        model_widget.propHeight().removePropertyListener(sizesChangedListener);
        model_widget.propDirection().removePropertyListener(tabLookChangedListener);
        model_widget.propTabWidth().removePropertyListener(tabLookChangedListener);
        model_widget.propTabHeight().removePropertyListener(tabLookChangedListener);
        model_widget.propTabSpacing().removePropertyListener(tabLookChangedListener);
        model_widget.propSelectedColor().removePropertyListener(tabLookChangedListener);
        model_widget.propDeselectedColor().removePropertyListener(tabLookChangedListener);
        model_widget.propFont().removePropertyListener(tabLookChangedListener);
        model_widget.propActiveTab().removePropertyListener(activeTabChangedListener);
        model_widget.propTabs().removePropertyListener(tabsChangedListener);
        super.unregisterListeners();
    }

    private void activeTabChanged(final WidgetProperty<Integer> property, final Integer old_index, final Integer tab_index)
    {
        dirty_active_tab.mark();
        toolkit.scheduleUpdate(this);
        tab_display_listener.propertyChanged(null, null, null);
    }

    /** Update to the next pending display
     *
     *  <p>Synchronized to serialize the background threads.
     *
     *  <p>Example: Displays A, B, C are requested in quick succession.
     *
     *  <p>pending_display_and_group=A is submitted to executor thread A.
     *
     *  <p>While handling A, pending_display_and_group=B is submitted to executor thread B.
     *  Thread B will be blocked in synchronized method.
     *
     *  <p>Then pending_display_and_group=C is submitted to executor thread C.
     *  As thread A finishes, thread B finds pending_display_and_group==C.
     *  As thread C finally continues, it finds pending_display_and_group empty.
     *  --> Showing A, then C, skipping B.
     */
    private synchronized void updatePendingDisplay(final JobMonitor monitor)
    {
        try
        {
            final DisplayAndGroup handle = pending_display_and_group.getAndSet(null);
            if (handle == null)
                return;
            if (body == null)
            {
                // System.out.println("Aborted: " + handle);
                return;
            }

            monitor.beginTask("Load " + handle.toString());
            try
            {   // Load new model (potentially slow)
                final DisplayModel new_model = loadDisplayModel(model_widget, handle);

                // Atomically update the 'active' model
                final DisplayModel old_model = active_content_model.getAndSet(new_model);
                if (old_model != null)
                {   // Dispose old model
                    final Future<Object> completion = toolkit.submit(() ->
                    {
                        toolkit.disposeRepresentation(old_model);
                        return null;
                    });
                    checkCompletion(model_widget, completion, "timeout disposing old representation");
                }
                // Represent new model on UI thread
                toolkit.onRepresentationStarted();
                final Future<Object> completion = toolkit.submit(() ->
                {
                    representContent(new_model);
                    return null;
                });
                checkCompletion(model_widget, completion, "timeout representing new content");
                model_widget.runtimePropEmbeddedModel().setValue(new_model);
            }
            catch (Exception ex)
            {
                logger.log(Level.WARNING, "Failed to handle embedded display " + handle, ex);
            }
        }
        finally
        {
            toolkit.onRepresentationFinished();
        }
    }

    /** @param content_model Model to represent */
    private void representContent(final DisplayModel content_model)
    {
        try
        {
            toolkit.representModel(body, content_model);
            // Set 'body' of navtabs to color of the embedded model
            body.setBackground(new Background(new BackgroundFill(JFXUtil.convert(content_model.propBackgroundColor().getValue()), CornerRadii.EMPTY, Insets.EMPTY)));
        }
        catch (final Exception ex)
        {
            logger.log(Level.WARNING, "Failed to represent embedded display", ex);
        }
        finally
        {
            toolkit.onRepresentationFinished();
        }
    }

    private void sizesChanged(final WidgetProperty<?> property, final Object old_value, final Object new_value)
    {
        dirty_sizes.mark();
        toolkit.scheduleUpdate(this);
    }

    private void tabLookChanged(final WidgetProperty<?> property, final Object old_value, final Object new_value)
    {
        dirty_tab_look.mark();
        toolkit.scheduleUpdate(this);
    }

    private void tabsChanged(final WidgetProperty<List<TabProperty>> property, final List<TabProperty> removed, final List<TabProperty> added)
    {
        if (removed != null)
            removeTabs(removed);
        if (added != null)
            addTabs(added);

        dirty_tabs.mark();
        toolkit.scheduleUpdate(this);
    }

    private void removeTabs(final List<TabProperty> removed)
    {
        for (TabProperty tab : removed)
        {
            tab.name().removePropertyListener(tab_name_listener);
            tab.file().removePropertyListener(tab_display_listener);
            tab.macros().removePropertyListener(tab_display_listener);
            tab.group().removePropertyListener(tab_display_listener);
        }
    }

    private void addTabs(final List<TabProperty> added)
    {
        for (TabProperty tab : added)
        {
            tab.group().addUntypedPropertyListener(tab_display_listener);
            tab.macros().addUntypedPropertyListener(tab_display_listener);
            tab.file().addUntypedPropertyListener(tab_display_listener);
            tab.name().addPropertyListener(tab_name_listener);
        }
    }

    @Override
    public void updateChanges()
    {
        super.updateChanges();
        if (dirty_sizes.checkAndClear())
        {
            final Integer width = model_widget.propWidth().getValue();
            final Integer height = model_widget.propHeight().getValue();
            jfx_node.setPrefSize(width, height);
        }
        if (dirty_tab_look.checkAndClear())
        {
            jfx_node.setDirection(model_widget.propDirection().getValue());
            jfx_node.setTabSize(model_widget.propTabWidth().getValue(),
                                model_widget.propTabHeight().getValue());
            jfx_node.setTabSpacing(model_widget.propTabSpacing().getValue());
            jfx_node.setSelectedColor(JFXUtil.convert(model_widget.propSelectedColor().getValue()));
            jfx_node.setDeselectedColor(JFXUtil.convert(model_widget.propDeselectedColor().getValue()));
            jfx_node.setFont(JFXUtil.convert(model_widget.propFont().getValue()));
        }
        if (dirty_tabs.checkAndClear())
        {
            final List<String> tabs = new ArrayList<>();
            model_widget.propTabs().getValue().forEach(tab -> tabs.add(tab.name().getValue()));
            jfx_node.setTabs(tabs);
        }
        if (dirty_active_tab.checkAndClear())
            jfx_node.selectTab(model_widget.propActiveTab().getValue());
    }

    @Override
    public void dispose()
    {
        body = null;
        super.dispose();
    }
}