// License: GPL. For details, see LICENSE file.
package org.openstreetmap.josm.gui.layer;

import static org.openstreetmap.josm.tools.I18n.tr;

import java.awt.Color;
import java.awt.Component;
import java.awt.Graphics2D;
import java.awt.event.ActionEvent;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.io.File;
import java.util.List;

import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.Icon;
import javax.swing.JOptionPane;
import javax.swing.JSeparator;

import org.openstreetmap.josm.Main;
import org.openstreetmap.josm.actions.GpxExportAction;
import org.openstreetmap.josm.actions.SaveAction;
import org.openstreetmap.josm.actions.SaveActionBase;
import org.openstreetmap.josm.actions.SaveAsAction;
import org.openstreetmap.josm.data.Bounds;
import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor;
import org.openstreetmap.josm.data.projection.Projection;
import org.openstreetmap.josm.data.projection.ProjectionChangeListener;
import org.openstreetmap.josm.gui.MapView;
import org.openstreetmap.josm.tools.Destroyable;
import org.openstreetmap.josm.tools.ImageProvider;
import org.openstreetmap.josm.tools.Utils;

/**
 * A layer encapsulates the gui component of one dataset and its representation.
 *
 * Some layers may display data directly imported from OSM server. Other only
 * display background images. Some can be edited, some not. Some are static and
 * other changes dynamically (auto-updated).
 *
 * Layers can be visible or not. Most actions the user can do applies only on
 * selected layers. The available actions depend on the selected layers too.
 *
 * All layers are managed by the MapView. They are displayed in a list to the
 * right of the screen.
 *
 * @author imi
 */
public abstract class Layer implements Destroyable, MapViewPaintable, ProjectionChangeListener {

    public interface LayerAction {
        boolean supportLayers(List<Layer> layers);

        Component createMenuComponent();
    }

    public interface MultiLayerAction {
        Action getMultiLayerAction(List<Layer> layers);
    }

    /**
     * Special class that can be returned by getMenuEntries when JSeparator needs to be created
     *
     */
    public static class SeparatorLayerAction extends AbstractAction implements LayerAction {
        public static final SeparatorLayerAction INSTANCE = new SeparatorLayerAction();

        @Override
        public void actionPerformed(ActionEvent e) {
            throw new UnsupportedOperationException();
        }

        @Override
        public Component createMenuComponent() {
            return new JSeparator();
        }

        @Override
        public boolean supportLayers(List<Layer> layers) {
            return false;
        }
    }

    public static final String VISIBLE_PROP = Layer.class.getName() + ".visible";
    public static final String OPACITY_PROP = Layer.class.getName() + ".opacity";
    public static final String NAME_PROP = Layer.class.getName() + ".name";

    public static final int ICON_SIZE = 16;

    /** keeps track of property change listeners */
    protected PropertyChangeSupport propertyChangeSupport;

    /**
     * The visibility state of the layer.
     *
     */
    private boolean visible = true;

    /**
     * The opacity of the layer.
     *
     */
    private double opacity = 1;

    /**
     * The layer should be handled as a background layer in automatic handling
     *
     */
    private boolean background = false;

    /**
     * The name of this layer.
     *
     */
    private  String name;

    /**
     * If a file is associated with this layer, this variable should be set to it.
     */
    private File associatedFile;

    /**
     * Create the layer and fill in the necessary components.
     * @param name Layer name
     */
    public Layer(String name) {
        this.propertyChangeSupport = new PropertyChangeSupport(this);
        setName(name);
    }

    /**
     * Initialization code, that depends on Main.map.mapView.
     *
     * It is always called in the event dispatching thread.
     * Note that Main.map is null as long as no layer has been added, so do
     * not execute code in the constructor, that assumes Main.map.mapView is
     * not null. Instead override this method.
     */
    public void hookUpMapView() {
    }

    /**
     * Paint the dataset using the engine set.
     * @param mv The object that can translate GeoPoints to screen coordinates.
     */
    @Override
    public abstract void paint(Graphics2D g, MapView mv, Bounds box);

    /**
     * Return a representative small image for this layer. The image must not
     * be larger than 64 pixel in any dimension.
     */
    public abstract Icon getIcon();

    /**
     * Return a Color for this layer. Return null when no color specified.
     * @param ignoreCustom Custom color should return null, as no default color
     *      is used. When this is true, then even for custom coloring the base
     *      color is returned - mainly for layer internal use.
     */
    public Color getColor(boolean ignoreCustom) {
        return null;
    }

    /**
     * @return A small tooltip hint about some statistics for this layer.
     */
    public abstract String getToolTipText();

    /**
     * Merges the given layer into this layer. Throws if the layer types are
     * incompatible.
     * @param from The layer that get merged into this one. After the merge,
     *      the other layer is not usable anymore and passing to one others
     *      mergeFrom should be one of the last things to do with a layer.
     */
    public abstract void mergeFrom(Layer from);

    /**
     * @param other The other layer that is tested to be mergable with this.
     * @return Whether the other layer can be merged into this layer.
     */
    public abstract boolean isMergable(Layer other);

    public abstract void visitBoundingBox(BoundingXYVisitor v);

    public abstract Object getInfoComponent();

    /**
     * Determines if info dialog can be resized (false by default).
     * @return {@code true} if the info dialog can be resized, {@code false} otherwise
     * @since 6708
     */
    public boolean isInfoResizable() {
        return false;
    }

    /**
     * Returns list of actions. Action can implement LayerAction interface when it needs to be represented by other
     * menu component than JMenuItem or when it supports multiple layers. Actions that support multiple layers should also
     * have correct equals implementation.
     *
     * Use SeparatorLayerAction.INSTANCE instead of new JSeparator
     *
     */
    public abstract Action[] getMenuEntries();

    /**
     * Called, when the layer is removed from the mapview and is going to be destroyed.
     *
     * This is because the Layer constructor can not add itself safely as listener
     * to the layerlist dialog, because there may be no such dialog yet (loaded
     * via command line parameter).
     */
    @Override
    public void destroy() {
        // Override in subclasses if needed
    }

    public File getAssociatedFile() {
        return associatedFile;
    }

    public void setAssociatedFile(File file) {
        associatedFile = file;
    }

    /**
     * Replies the name of the layer
     *
     * @return the name of the layer
     */
    public String getName() {
        return name;
    }

    /**
     * Sets the name of the layer
     *
     *@param name the name. If null, the name is set to the empty string.
     *
     */
    public final void setName(String name) {
        if (name == null) {
            name = "";
        }
        String oldValue = this.name;
        this.name = name;
        if (!this.name.equals(oldValue)) {
            propertyChangeSupport.firePropertyChange(NAME_PROP, oldValue, this.name);
        }
    }

    /**
     * Replies true if this layer is a background layer
     *
     * @return true if this layer is a background layer
     */
    public boolean isBackgroundLayer() {
        return background;
    }

    /**
     * Sets whether this layer is a background layer
     *
     * @param background true, if this layer is a background layer
     */
    public void setBackgroundLayer(boolean background) {
        this.background = background;
    }

    /**
     * Sets the visibility of this layer. Emits property change event for
     * property {@link #VISIBLE_PROP}.
     *
     * @param visible true, if the layer is visible; false, otherwise.
     */
    public void setVisible(boolean visible) {
        boolean oldValue = isVisible();
        this.visible  = visible;
        if (visible && opacity == 0) {
            setOpacity(1);
        } else if (oldValue != isVisible()) {
            fireVisibleChanged(oldValue, isVisible());
        }
    }

    /**
     * Replies true if this layer is visible. False, otherwise.
     * @return  true if this layer is visible. False, otherwise.
     */
    public boolean isVisible() {
        return visible && opacity != 0;
    }

    public double getOpacity() {
        return opacity;
    }

    public void setOpacity(double opacity) {
        if (!(opacity >= 0 && opacity <= 1))
            throw new IllegalArgumentException("Opacity value must be between 0 and 1");
        double oldOpacity = getOpacity();
        boolean oldVisible = isVisible();
        this.opacity = opacity;
        if (!Utils.equalsEpsilon(oldOpacity, getOpacity())) {
            fireOpacityChanged(oldOpacity, getOpacity());
        }
        if (oldVisible != isVisible()) {
            fireVisibleChanged(oldVisible, isVisible());
        }
    }

    /**
     * Toggles the visibility state of this layer.
     */
    public void toggleVisible() {
        setVisible(!isVisible());
    }

    /**
     * Adds a {@link PropertyChangeListener}
     *
     * @param listener the listener
     */
    public void addPropertyChangeListener(PropertyChangeListener listener) {
        propertyChangeSupport.addPropertyChangeListener(listener);
    }

    /**
     * Removes a {@link PropertyChangeListener}
     *
     * @param listener the listener
     */
    public void removePropertyChangeListener(PropertyChangeListener listener) {
        propertyChangeSupport.removePropertyChangeListener(listener);
    }

    /**
     * fires a property change for the property {@link #VISIBLE_PROP}
     *
     * @param oldValue the old value
     * @param newValue the new value
     */
    protected void fireVisibleChanged(boolean oldValue, boolean newValue) {
        propertyChangeSupport.firePropertyChange(VISIBLE_PROP, oldValue, newValue);
    }

    /**
     * fires a property change for the property {@link #OPACITY_PROP}
     *
     * @param oldValue the old value
     * @param newValue the new value
     */
    protected void fireOpacityChanged(double oldValue, double newValue) {
        propertyChangeSupport.firePropertyChange(OPACITY_PROP, oldValue, newValue);
    }

    /**
     * Check changed status of layer
     *
     * @return True if layer was changed since last paint
     */
    public boolean isChanged() {
        return true;
    }

    /**
     * allows to check whether a projection is supported or not
     *
     * @return True if projection is supported for this layer
     */
    public boolean isProjectionSupported(Projection proj) {
        return true;
    }

    /**
     * Specify user information about projections
     *
     * @return User readable text telling about supported projections
     */
    public String nameSupportedProjections() {
        return tr("All projections are supported");
    }

    /**
     * The action to save a layer
     *
     */
    public static class LayerSaveAction extends AbstractAction {
        private final transient Layer layer;

        public LayerSaveAction(Layer layer) {
            putValue(SMALL_ICON, ImageProvider.get("save"));
            putValue(SHORT_DESCRIPTION, tr("Save the current data."));
            putValue(NAME, tr("Save"));
            setEnabled(true);
            this.layer = layer;
        }

        @Override
        public void actionPerformed(ActionEvent e) {
            SaveAction.getInstance().doSave(layer);
        }
    }

    public static class LayerSaveAsAction extends AbstractAction {
        private final transient Layer layer;

        public LayerSaveAsAction(Layer layer) {
            putValue(SMALL_ICON, ImageProvider.get("save_as"));
            putValue(SHORT_DESCRIPTION, tr("Save the current data to a new file."));
            putValue(NAME, tr("Save As..."));
            setEnabled(true);
            this.layer = layer;
        }

        @Override
        public void actionPerformed(ActionEvent e) {
            SaveAsAction.getInstance().doSave(layer);
        }
    }

    public static class LayerGpxExportAction extends AbstractAction {
        private final transient Layer layer;

        public LayerGpxExportAction(Layer layer) {
            putValue(SMALL_ICON, ImageProvider.get("exportgpx"));
            putValue(SHORT_DESCRIPTION, tr("Export the data to GPX file."));
            putValue(NAME, tr("Export to GPX..."));
            setEnabled(true);
            this.layer = layer;
        }

        @Override
        public void actionPerformed(ActionEvent e) {
            new GpxExportAction().export(layer);
        }
    }

    /* --------------------------------------------------------------------------------- */
    /* interface ProjectionChangeListener                                                */
    /* --------------------------------------------------------------------------------- */
    @Override
    public void projectionChanged(Projection oldValue, Projection newValue) {
        if (!isProjectionSupported(newValue)) {
            JOptionPane.showMessageDialog(Main.parent,
                    tr("The layer {0} does not support the new projection {1}.\n{2}\n"
                            + "Change the projection again or remove the layer.",
                            getName(), newValue.toCode(), nameSupportedProjections()),
                            tr("Warning"),
                            JOptionPane.WARNING_MESSAGE);
        }
    }

    /**
     * Initializes the layer after a successful load of data from a file
     * @since 5459
     */
    public void onPostLoadFromFile() {
        // To be overriden if needed
    }

    /**
     * Replies the savable state of this layer (i.e if it can be saved through a "File-&gt;Save" dialog).
     * @return true if this layer can be saved to a file
     * @since 5459
     */
    public boolean isSavable() {
        return false;
    }

    /**
     * Checks whether it is ok to launch a save (whether we have data, there is no conflict etc.)
     * @return <code>true</code>, if it is safe to save.
     * @since 5459
     */
    public boolean checkSaveConditions() {
        return true;
    }

    /**
     * Creates a new "Save" dialog for this layer and makes it visible.<br>
     * When the user has chosen a file, checks the file extension, and confirms overwrite if needed.
     * @return The output {@code File}
     * @see SaveActionBase#createAndOpenSaveFileChooser
     * @since 5459
     */
    public File createAndOpenSaveFileChooser() {
        return SaveActionBase.createAndOpenSaveFileChooser(tr("Save Layer"), "lay");
    }
}
