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

import ch.psi.plot.utils.IO;
import ch.psi.plot.utils.MonitoredPanel;
import ch.psi.plot.utils.SwingUtils;
import ch.psi.plot.utils.SwingUtils.ExtensionFileFilter;
import ch.psi.plot.utils.SwingUtils.ImageTransferHandler;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Toolkit;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.ClipboardOwner;
import java.awt.datatransfer.Transferable;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.awt.print.PageFormat;
import java.awt.print.Printable;
import java.awt.print.PrinterException;
import java.awt.print.PrinterJob;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
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> extends MonitoredPanel implements Plot<T>, Printable {

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

    protected static final int PREFERRED_WIDTH = 500;
    protected static final int PREFERRED_HEIGHT = 270;

    final Class seriesType;

    protected PlotBase(Class<T> seriesType) {
        this(seriesType, null);
    }
   
    volatile boolean instantiated;
    
    protected PlotBase(Class<T> seriesType, String title) {
        super();
        if (title == null) {
            title = "Plot";
        }
        setTitle(title);
        this.seriesType = seriesType;
//        SwingUtilities.invokeLater(new Runnable() {
//            @Override
//            public void run() {
        try {
            createChart();
            createPopupMenu();
        } catch (Exception ex) {
            Logger.getLogger(PlotBase.class.getName()).log(Level.INFO, null, ex);
        }
//            }
//        });
        instantiated=true;
    }
    String title;

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

    public String getTitle() {
        return title;
    }
    
    
    Font titleFont;

    public void setTitleFont(Font font) {        
        this.titleFont = font;
        if (instantiated)
            onTitleChanged();
    }

    public Font getTitleFont() {
        return titleFont;
    }    

    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;
     */
    @Override
    public BufferedImage getSnapshot() {
        return SwingUtils.createImage(this);
    }

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

    public void copy() {
        BufferedImage img = getSnapshot();
        if (img != null) {
            ImageTransferHandler imageSelection = new ImageTransferHandler(img);
            Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
            clipboard.setContents(imageSelection, new ClipboardOwner() {
                @Override
                public void lostOwnership(Clipboard clipboard, Transferable contents) {
                }
            });
        }
    }
    
    //Printable interface
    public int print(Graphics g, PageFormat pf, int page) throws PrinterException {

        if (page > 0) {
            return NO_SUCH_PAGE;
        }

        BufferedImage img = getSnapshot();
        
        //Schrinks the image if too big but does not expand it
        double scaleX = img.getWidth()>pf.getImageableWidth() ? ((double) pf.getImageableWidth()) / img.getWidth() : 1.0;
        double scaleY = img.getHeight() > pf.getImageableHeight() ? ((double) pf.getImageableHeight()) / img.getHeight()  : 1.0;
        double scale = Math.min(scaleX, scaleY);    //Keep aspect ratio
        AffineTransform transf = new AffineTransform();
        transf.scale(scale, scale);

        Graphics2D g2d = (Graphics2D) g;
        g2d.translate(pf.getImageableX(), pf.getImageableY());
        g2d.drawImage(img, transf, null);
        //g2d.drawImage(img,0,0,null);
        return PAGE_EXISTS;
    }

    protected void createChart() {

    }

    protected void createPopupMenu() {
        addPopupMenuItem(null);//Separator

        JMenuItem saveSnapshot = new JMenuItem("Save Image As...");
        saveSnapshot.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                try {

                    JFileChooser chooser = new JFileChooser(".");
                    chooser.addChoosableFileFilter(new ExtensionFileFilter("PNG files (*.png)", new String[]{"png"}));
                    chooser.addChoosableFileFilter(new ExtensionFileFilter("Bitmap files (*.bmp)", new String[]{"bmp"}));
                    chooser.addChoosableFileFilter(new ExtensionFileFilter("JPEG files (*.jpg)", new String[]{"jpg", "jpeg"}));
                    chooser.setAcceptAllFileFilterUsed(false);
                    if (chooser.showSaveDialog(PlotBase.this) == JFileChooser.APPROVE_OPTION) {

                        String filename = chooser.getSelectedFile().getAbsolutePath();
                        String type = "png";
                        String ext = IO.getExtension(chooser.getSelectedFile());
                        for (String fe : new String[]{"bmp", "jpg"}) {
                            if ((chooser.getFileFilter().getDescription().contains(fe))
                                    || (fe.equals(ext))) {
                                type = fe;
                                break;
                            }
                        }
                        if (ext == null) {
                            filename += "." + type;
                        }
                        if (new File(filename).exists()) {
                            if (SwingUtils.showOption(PlotBase.this, "Overwrite", "File " + filename + " already exists.\nDo you want to overwrite it?", javax.swing.JOptionPane.YES_NO_OPTION) == javax.swing.JOptionPane.NO_OPTION) {
                                return;
                            }
                        }
                        saveSnapshot(filename, type);
                    }
                } catch (Exception ex) {
                    SwingUtils.showException(PlotBase.this, ex);
                }
            }
        });
        addPopupMenuItem(saveSnapshot);

        JMenuItem saveData = new JMenuItem("Save Data As...");
        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(PlotBase.this);
                    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(PlotBase.this, ex);
                }
            }
        });
        addPopupMenuItem(saveData);

        JMenuItem print = new JMenuItem("Print...");
        print.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                try {                    
                    PrinterJob job = PrinterJob.getPrinterJob();
                    job.setPrintable(PlotBase.this);

                    PrinterJob pj = PrinterJob.getPrinterJob();
                    //PageFormat pdef=pj.defaultPage();
                    //PageFormat pf = pj.pageDialog(pdef);
                    //if (pf!=pdef){
                        if (job.printDialog()) {
                            job.print();
                        }
                    //}
                } catch (Exception ex) {
                    SwingUtils.showException(PlotBase.this, ex);
                }
            }
        });
        addPopupMenuItem(print);

        JMenuItem copy = new JMenuItem("Copy");
        copy.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                try {
                    copy();
                } catch (Exception ex) {
                    SwingUtils.showException(PlotBase.this, ex);
                }
            }
        });
        addPopupMenuItem(copy);
    }

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

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

    @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.id = seriesID++;
        series.setToken(onAddedSeries(series));
    }

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

    @Override
    public void removeSeries(T series) {
        if (series != null) {
            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);
        }
    }

    @Override
    public T getSeries(int index) {
        synchronized (seriesList) {
            if ((index < 0) || (index >= seriesList.size())) {
                return null;
            }
            return getAllSeries()[index];
        }
    }

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

    public class SeriesComparator implements Comparator<T> {

        @Override
        public int compare(T o1, T o2) {
            return Integer.valueOf(o1.id).compareTo(Integer.valueOf(o2.id));
        }
    }

    @Override
    public T[] getAllSeries() {
        synchronized (seriesList) {
            ArrayList<T> list = new ArrayList();
            for (T series : seriesList.values()) {
                list.add(series);
            }
            Collections.sort(list, new SeriesComparator());
            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 onTitleChanged() {
        if (isUpdatesEnabled()) {
            doUpdate();
        }
    }

    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;
    }
}
