/*
 * Copyright (c) 2014 Paul Scherrer Institute. All rights reserved.
 */
package ch.psi.plot;

import ch.psi.plot.utils.IO;
import ch.psi.plot.utils.SwingUtils;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.JFileChooser;
import javax.swing.JMenuItem;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import javax.swing.filechooser.FileNameExtensionFilter;

/**
 *
 */
abstract public class PlotBase<T extends PlotSeries> implements Plot<T> {

    final String LINE_SEPARATOR = System.lineSeparator();
    final String FIELD_SEPARATOR = "\t";

    final Class seriesType;

    protected PlotBase(Class<T> seriesType) {
        this(seriesType, null);

    }

    protected PlotBase(Class<T> seriesType, String title) {
        if (title == null) {
            title = "Plot";
        }
        setTitle(title);
        this.seriesType = seriesType;
    }

    String title;

    public void setTitle(String title) {
        this.title = title;
    }

    public String getTitle() {
        return title;
    }

    volatile boolean bUpdating;

    public final void update(boolean deferred) {
        if ((!deferred) && SwingUtilities.isEventDispatchThread()) {
            bUpdating = false;
            doUpdate();
        } else {
            if (!bUpdating) {
                bUpdating = true;
                SwingUtilities.invokeLater(new Runnable() {
                    @Override
                    public void run() {
                        if (bUpdating) {
                            bUpdating = false;
                            try {
                                doUpdate();
                            } catch (Exception ex) {
                                Logger.getLogger(PlotBase.class.getName()).log(Level.SEVERE, null, ex);
                            }
                        }
                    }
                });
            }
        }
    }

    boolean requireUpdateOnAppend = true;

    protected void setRequireUpdateOnAppend(boolean value) {
        requireUpdateOnAppend = value;
    }

    protected boolean getRequireUpdateOnAppend() {
        return requireUpdateOnAppend;
    }

    boolean updatesEnabled = true;

    public void setUpdatesEnabled(boolean value) {
        updatesEnabled = value;
    }

    public boolean isUpdatesEnabled() {
        return updatesEnabled;
    }

    /**
     * Should be improved in implementations to make it independent of the
     * window state;
     */
    public BufferedImage getSnapshot() {
        return SwingUtils.createImage(getChartPanel());
    }

    public void saveSnapshot(String filename, String format) throws IOException {
        IO.writeImageToFile(getSnapshot(), filename, format);
    }

    JPanel panel;

    public JPanel getChartPanel() {
        if (panel == null) {
            panel = createChartPanel();

            addPopupMenuItem(null);//Separator
            JMenuItem saveData = new JMenuItem("Save Data");
            saveData.addActionListener(new ActionListener() {
                public void actionPerformed(ActionEvent e) {
                    try {
                        JFileChooser chooser = new JFileChooser();
                        FileNameExtensionFilter filter = new FileNameExtensionFilter("Text data files", "txt", "dat");
                        chooser.setFileFilter(filter);
                        int returnVal = chooser.showSaveDialog(panel);
                        if (returnVal == JFileChooser.APPROVE_OPTION) {
                            String filename = chooser.getSelectedFile().getAbsolutePath();
                            String type = IO.getExtension(chooser.getSelectedFile());
                            if (type == null) {
                                type = "txt";
                                filename += "." + type;
                            }
                            saveData(filename);
                        }
                    } catch (Exception ex) {
                        SwingUtils.showException(panel, ex);
                    }
                }
            });
            addPopupMenuItem(saveData);
            JMenuItem saveSnapshot = new JMenuItem("Save Snapshot");
            saveSnapshot.addActionListener(new ActionListener() {
                public void actionPerformed(ActionEvent e) {
                    try {
                        JFileChooser chooser = new JFileChooser();
                        FileNameExtensionFilter filter = new FileNameExtensionFilter("Image File", "png", "jpg", "bmp");
                        chooser.setFileFilter(filter);
                        int returnVal = chooser.showSaveDialog(panel);
                        if (returnVal == JFileChooser.APPROVE_OPTION) {
                            String filename = chooser.getSelectedFile().getAbsolutePath();
                            String type = IO.getExtension(chooser.getSelectedFile());
                            if (type == null) {
                                type = "png";
                                filename += "." + type;
                            }
                            saveSnapshot(filename, type);
                        }
                    } catch (Exception ex) {
                        SwingUtils.showException(panel, ex);
                    }
                }
            });
            addPopupMenuItem(saveSnapshot);
            onCreatedPanel();
        }
        return panel;
    }

    //Overidables
    protected void onCreatedPanel() {
    }

    abstract protected JPanel createChartPanel();

    /**
     * null for separator
     */
    abstract protected void addPopupMenuItem(JMenuItem item);

    //Series list support
    final HashMap<String, T> seriesList = new HashMap<>();

    @Override
    public void addSeries(T series) {
        if (series.name == null) {
            return;
        }
        T existingSeries = getSeries(series.name);
        if (existingSeries == series) {
            return;
        }
        if (existingSeries != null) {
            removeSeries(existingSeries);
        }
        synchronized (seriesList) {
            seriesList.put(series.name, series);
        }
        series.setPlot(this);
        series.setToken(onAddedSeries(series));
    }

    @Override
    public void addSeries(T[] series) {
        for (T s : series) {
            addSeries(s);
        }
    }

    @Override
    public void removeSeries(T series) {
        if (series.getPlot() == this) {
            series.setPlot(null);
        }
        onRemovedSeries(series);
        synchronized (seriesList) {
            seriesList.remove(series.name);
        }
    }

    @Override
    public T getSeries(String name) {
        synchronized (seriesList) {
            return seriesList.get(name);
        }
    }

    //TODO: Not respecting order 
    @Override
    public T getSeries(int index) {
        synchronized (seriesList) {
            if ((index < 0) || (index >= seriesList.size())) {
                return null;
            }
            return seriesList.get(getAllSeries()[index]);
        }
    }

    @Override
    public int getNumberOfSeries() {
        return seriesList.size();
    }

    @Override
    public T[] getAllSeries() {
        synchronized (seriesList) {
            ArrayList<T> list = new ArrayList();
            for (T series : seriesList.values()) {
                list.add(series);
            }
            return list.toArray((T[]) Array.newInstance(seriesType, 0));
        }
    }

    @Override
    public void requestSeriesUpdate(final T series) {
        if (!series.updating) {
            series.updating = true;
            SwingUtilities.invokeLater(new Runnable() {
                @Override
                public void run() {
                    series.updating = false;
                    try {
                        updateSeries(series);
                    } catch (Exception ex) {
                        Logger.getLogger(LinePlotBase.class.getName()).log(Level.SEVERE, null, ex);
                    }
                }
            });
        }
    }

    abstract protected Object onAddedSeries(T series);

    abstract protected void onRemovedSeries(T series);

    /**
     * These overridables can be optimised in implementations
     */
    protected void onRemovedAllSeries() {
        for (T s : getAllSeries()) {
            removeSeries(s);
        }
    }

    protected void doUpdate() {
        for (T s : getAllSeries()) {
            updateSeries(s);
        }
    }

    @Override
    public void clear() {
        onRemovedAllSeries();
        synchronized (seriesList) {
            seriesList.clear();
        }
        update(true);
    }

    //Axis
    final HashMap<AxisId, Axis> axisList = new HashMap<>();

    protected void createAxis(AxisId axis) {
        createAxis(axis, null);
    }

    protected void createAxis(AxisId axis, String label) {
        axisList.put(axis, new Axis(label, this, axis));
    }

    public Axis getAxis(AxisId id) {
        return axisList.get(id);
    }

    //Axis Callbacks
    protected void onAxisLabelChanged(AxisId axis) {
        if (isUpdatesEnabled()) {
            doUpdate();
        }
    }

    protected void onAxisRangeChanged(AxisId axis) {
        if (isUpdatesEnabled()) {
            doUpdate();
        }
    }   
    
    //Static Configuration
    
    //Hardware Acceleration
    static boolean hardwareAccelerated=true;
    public static void setHardwareAccelerated(boolean value){
        hardwareAccelerated=value;
    }   
    
    public static boolean getHardwareAccelerated(){
        return  hardwareAccelerated;
    }
    
    static boolean lighweightPopups=true;
    public static void setLighweightPopups(boolean value){
        lighweightPopups=value;
        if (javax.swing.JPopupMenu.getDefaultLightWeightPopupEnabled()!=value){
            javax.swing.JPopupMenu.setDefaultLightWeightPopupEnabled(lighweightPopups);
        }
    }   
    
    public static boolean getLighweightPopups(){
        return  lighweightPopups;
    }    
}
