/*******************************************************************************
 * Copyright (c) 2012, 2020 Original authors and others.
 *
 * This program and the accompanying materials are made
 * available under the terms of the Eclipse Public License 2.0
 * which is available at https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     Original authors and others - initial API and implementation
 *     Dirk Fauth <dirk.fauth@googlemail.com> - made commandHandlers and eventHandlers
 *                                              visible to subclasses for testing
 *     Dirk Fauth <dirk.fauth@googlemail.com> - Bug 447145
 ******************************************************************************/
package org.eclipse.nebula.widgets.nattable.layer;

import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

import org.eclipse.nebula.widgets.nattable.command.ILayerCommand;
import org.eclipse.nebula.widgets.nattable.command.ILayerCommandHandler;
import org.eclipse.nebula.widgets.nattable.config.CellConfigAttributes;
import org.eclipse.nebula.widgets.nattable.config.IConfigRegistry;
import org.eclipse.nebula.widgets.nattable.config.IConfiguration;
import org.eclipse.nebula.widgets.nattable.layer.cell.IConfigLabelAccumulator;
import org.eclipse.nebula.widgets.nattable.layer.cell.IConfigLabelProvider;
import org.eclipse.nebula.widgets.nattable.layer.cell.ILayerCell;
import org.eclipse.nebula.widgets.nattable.layer.cell.LayerCell;
import org.eclipse.nebula.widgets.nattable.layer.event.ILayerEvent;
import org.eclipse.nebula.widgets.nattable.layer.event.ILayerEventHandler;
import org.eclipse.nebula.widgets.nattable.painter.cell.ICellPainter;
import org.eclipse.nebula.widgets.nattable.painter.layer.GridLineCellLayerPainter;
import org.eclipse.nebula.widgets.nattable.painter.layer.ILayerPainter;
import org.eclipse.nebula.widgets.nattable.persistence.IPersistable;
import org.eclipse.nebula.widgets.nattable.style.DisplayMode;
import org.eclipse.nebula.widgets.nattable.ui.binding.UiBindingRegistry;
import org.eclipse.nebula.widgets.nattable.util.IClientAreaProvider;
import org.eclipse.swt.graphics.Rectangle;

/**
 * Base layer implementation with common methods for managing listeners and
 * caching, etc.
 */
public abstract class AbstractLayer implements ILayer {

    private String regionName;
    protected ILayerPainter layerPainter;
    private IClientAreaProvider clientAreaProvider = IClientAreaProvider.DEFAULT;
    private IConfigLabelAccumulator configLabelAccumulator;

    protected final Map<Class<? extends ILayerCommand>, ILayerCommandHandler<? extends ILayerCommand>> commandHandlers = new LinkedHashMap<>();
    protected Map<Class<? extends ILayerEvent>, ILayerEventHandler<? extends ILayerEvent>> eventHandlers = new HashMap<>();

    private final List<IPersistable> persistables = new LinkedList<>();
    private final Set<ILayerListener> listeners = new LinkedHashSet<>();
    private final Collection<IConfiguration> configurations = new LinkedList<>();

    private boolean configurationApplied = false;

    /**
     * {@link ReadWriteLock} that is used to ensure that no concurrent
     * modifications happen on event handling
     *
     * @since 1.4
     */
    protected ReadWriteLock eventHelperLock = new ReentrantReadWriteLock();

    // Dispose

    @Override
    public void dispose() {
    }

    // Regions

    @Override
    public LabelStack getRegionLabelsByXY(int x, int y) {
        LabelStack regionLabels = new LabelStack();
        if (this.regionName != null) {
            regionLabels.addLabel(this.regionName);
        }
        return regionLabels;
    }

    public String getRegionName() {
        return this.regionName;
    }

    public void setRegionName(String regionName) {
        this.regionName = regionName;
    }

    // Config lables

    @Override
    public LabelStack getConfigLabelsByPosition(int columnPosition, int rowPosition) {
        LabelStack configLabels = new LabelStack();
        if (this.configLabelAccumulator != null) {
            this.configLabelAccumulator.accumulateConfigLabels(configLabels, columnPosition, rowPosition);
        }
        if (this.regionName != null) {
            configLabels.addLabel(this.regionName);
        }
        return configLabels;
    }

    public IConfigLabelAccumulator getConfigLabelAccumulator() {
        return this.configLabelAccumulator;
    }

    public void setConfigLabelAccumulator(IConfigLabelAccumulator cellLabelAccumulator) {
        this.configLabelAccumulator = cellLabelAccumulator;
    }

    // Persistence

    @Override
    public void saveState(String prefix, Properties properties) {
        for (IPersistable persistable : this.persistables) {
            persistable.saveState(prefix, properties);
        }
    }

    @Override
    public void loadState(String prefix, Properties properties) {
        for (IPersistable persistable : this.persistables) {
            persistable.loadState(prefix, properties);
        }
    }

    @Override
    public void registerPersistable(IPersistable persistable) {
        this.persistables.add(persistable);
    }

    @Override
    public void unregisterPersistable(IPersistable persistable) {
        this.persistables.remove(persistable);
    }

    // Configuration

    public void addConfiguration(IConfiguration configuration) {
        this.configurations.add(configuration);
    }

    public void clearConfiguration() {
        this.configurations.clear();
    }

    @Override
    public void configure(IConfigRegistry configRegistry, UiBindingRegistry uiBindingRegistry) {
        if (!this.configurationApplied) {
            for (IConfiguration configuration : this.configurations) {
                configuration.configureLayer(this);
                configuration.configureRegistry(configRegistry);
                configuration.configureUiBindings(uiBindingRegistry);
            }

            // Bug 447145
            // to avoid registering configuration values multiple times, we
            // remember that this layer has already been configured
            this.configurationApplied = true;
        }
    }

    // Commands

    @Override
    @SuppressWarnings({ "unchecked", "rawtypes" })
    public boolean doCommand(ILayerCommand command) {
        for (Class<? extends ILayerCommand> commandClass : this.commandHandlers.keySet()) {
            if (commandClass.isInstance(command)) {
                ILayerCommandHandler commandHandler = this.commandHandlers.get(commandClass);
                if (commandHandler.doCommand(this, command)) {
                    return true;
                }
            }
        }

        return false;
    }

    // Command handlers

    /**
     * Layers should use this method to register their command handlers and call
     * it from their constructor. This allows easy overriding if required of
     * command handlers
     */
    protected void registerCommandHandlers() {
        // No op
    }

    @Override
    public void registerCommandHandler(ILayerCommandHandler<?> commandHandler) {
        this.commandHandlers.put(commandHandler.getCommandClass(), commandHandler);
    }

    @Override
    public void unregisterCommandHandler(Class<? extends ILayerCommand> commandClass) {
        this.commandHandlers.remove(commandClass);
    }

    // Events

    @Override
    public void addLayerListener(ILayerListener listener) {
        this.listeners.add(listener);
    }

    @Override
    public void removeLayerListener(ILayerListener listener) {
        this.listeners.remove(listener);
    }

    @Override
    public boolean hasLayerListener(Class<? extends ILayerListener> layerListenerClass) {
        for (ILayerListener listener : this.listeners) {
            if (listener.getClass().equals(layerListenerClass)) {
                return true;
            }
        }
        return false;
    }

    /**
     * Handle layer event notification. Convert it to your context and propagate
     * <i>UP</i>.
     *
     * If you override this method you <strong>MUST NOT FORGET</strong> to raise
     * the event up the layer stack by calling
     * <code>super.fireLayerEvent(event)</code> - unless you plan to eat the
     * event yourself.
     **/
    @Override
    @SuppressWarnings({ "unchecked", "rawtypes" })
    public void handleLayerEvent(ILayerEvent event) {
        Map<Class<? extends ILayerEvent>, ILayerEventHandler<? extends ILayerEvent>> currentEventHandlers;
        this.eventHelperLock.readLock().lock();
        try {
            currentEventHandlers = this.eventHandlers;
        } finally {
            this.eventHelperLock.readLock().unlock();
        }

        for (Class<? extends ILayerEvent> eventClass : currentEventHandlers.keySet()) {
            if (eventClass.isInstance(event)) {
                ILayerEventHandler eventHandler = currentEventHandlers.get(eventClass);
                eventHandler.handleLayerEvent(event);
            }
        }

        // Pass on the event to our parent
        if (event.convertToLocal(this)) {
            fireLayerEvent(event);
        }
    }

    public void registerEventHandler(ILayerEventHandler<?> eventHandler) {
        this.eventHelperLock.writeLock().lock();
        try {
            this.eventHandlers = new HashMap<>(this.eventHandlers);
            this.eventHandlers.put(eventHandler.getLayerEventClass(), eventHandler);
        } finally {
            this.eventHelperLock.writeLock().unlock();
        }
    }

    public void unregisterEventHandler(ILayerEventHandler<?> eventHandler) {
        this.eventHelperLock.writeLock().lock();
        try {
            this.eventHandlers = new HashMap<>(this.eventHandlers);
            this.eventHandlers.remove(eventHandler.getLayerEventClass());
        } finally {
            this.eventHelperLock.writeLock().unlock();
        }
    }

    /**
     * Pass the event to all the {@link ILayerListener} registered on this
     * layer. A cloned copy is passed to each listener.
     */
    @Override
    public void fireLayerEvent(ILayerEvent event) {
        if (!this.listeners.isEmpty()) {
            Iterator<ILayerListener> it = this.listeners.iterator();
            boolean isLastListener = false;
            do {
                ILayerListener l = it.next();
                isLastListener = !it.hasNext(); // Lookahead

                // Fire cloned event to first n-1 listeners; fire original event
                // to last listener
                ILayerEvent eventToFire = isLastListener ? event : event.cloneEvent();
                l.handleLayerEvent(eventToFire);
            } while (!isLastListener);
        }
    }

    /**
     * @return {@link ILayerPainter}. Defaults to
     *         {@link GridLineCellLayerPainter}
     */
    @Override
    public ILayerPainter getLayerPainter() {
        if (this.layerPainter == null) {
            this.layerPainter = new GridLineCellLayerPainter();
        }
        return this.layerPainter;
    }

    public void setLayerPainter(ILayerPainter layerPainter) {
        this.layerPainter = layerPainter;
    }

    // Client area

    @Override
    public IClientAreaProvider getClientAreaProvider() {
        return this.clientAreaProvider;
    }

    @Override
    public void setClientAreaProvider(IClientAreaProvider clientAreaProvider) {
        this.clientAreaProvider = clientAreaProvider;
    }

    @Override
    public String toString() {
        return getClass().getSimpleName();
    }

    @Override
    public ILayerCell getCellByPosition(int columnPosition, int rowPosition) {
        if (columnPosition < 0 || columnPosition >= getColumnCount()
                || rowPosition < 0 || rowPosition >= getRowCount()) {
            return null;
        }

        return new LayerCell(this, columnPosition, rowPosition);
    }

    @Override
    public Rectangle getBoundsByPosition(int columnPosition, int rowPosition) {
        ILayerCell cell = getCellByPosition(columnPosition, rowPosition);
        ILayer cellLayer = cell.getLayer();

        int xOffset = -1;
        int yOffset = -1;
        int width = 0;
        int height = 0;
        {
            int column = cell.getOriginColumnPosition();
            int end = column + cell.getColumnSpan();
            for (; column < end; column++) {
                int columnOffset = cellLayer.getStartXOfColumnPosition(column);
                if (column < cellLayer.getColumnCount()) {
                    xOffset = columnOffset;
                    break;
                }
            }
            for (; column < end; column++) {
                width += cellLayer.getColumnWidthByPosition(column);
            }
        }
        {
            int row = cell.getOriginRowPosition();
            int end = row + cell.getRowSpan();
            for (; row < end; row++) {
                int rowOffset = cellLayer.getStartYOfRowPosition(row);
                if (row < cellLayer.getRowCount()) {
                    yOffset = rowOffset;
                    break;
                }
            }
            for (; row < end; row++) {
                height += cellLayer.getRowHeightByPosition(row);
            }
        }

        return new Rectangle(xOffset, yOffset, width, height);
    }

    @Override
    public DisplayMode getDisplayModeByPosition(int columnPosition, int rowPosition) {
        return DisplayMode.NORMAL;
    }

    @Override
    public ICellPainter getCellPainter(
            int columnPosition, int rowPosition,
            ILayerCell cell, IConfigRegistry configRegistry) {
        return configRegistry.getConfigAttribute(
                CellConfigAttributes.CELL_PAINTER,
                cell.getDisplayMode(),
                cell.getConfigLabels());
    }

    /**
     * @return <code>true</code> if the layer has a dynamic size (e.g. viewport)
     *         or a fixed size.
     * @since 1.4
     */
    @Override
    public boolean isDynamicSizeLayer() {
        return false;
    }

    /**
     * @return The collection of labels that are provided by this layer.
     * @since 1.4
     */
    @Override
    public Collection<String> getProvidedLabels() {
        Collection<String> labels = null;

        if (getUnderlyingLayerByPosition(0, 0) != null) {
            labels = getUnderlyingLayerByPosition(0, 0).getProvidedLabels();
        } else {
            labels = new LinkedHashSet<>();
        }

        // add the region
        if (this.regionName != null) {
            labels.add(this.regionName);
        }

        // add the labels configured via IConfigLabelAccumulator
        if (this.configLabelAccumulator instanceof IConfigLabelProvider) {
            labels.addAll(((IConfigLabelProvider) this.configLabelAccumulator).getProvidedLabels());
        }

        return labels;
    }
}
