# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2004-2020 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
# ###########################################################################*/
"""
:mod:`silx.gui.plot.actions.io` provides a set of QAction relative of inputs
and outputs for a :class:`.PlotWidget`.

The following QAction are available:

- :class:`CopyAction`
- :class:`PrintAction`
- :class:`SaveAction`
"""

from __future__ import division

__authors__ = ["V.A. Sole", "T. Vincent", "P. Knobel"]
__license__ = "MIT"
__date__ = "25/09/2020"

from . import PlotAction
from silx.io.utils import save1D, savespec, NEXUS_HDF5_EXT
from silx.io.nxdata import save_NXdata
import logging
import sys
import os.path
from collections import OrderedDict
import traceback
import numpy
from silx.utils.deprecation import deprecated
from silx.gui import qt, printer
from silx.gui.dialog.GroupDialog import GroupDialog
from silx.third_party.EdfFile import EdfFile
from silx.third_party.TiffIO import TiffIO
from ...utils.image import convertArrayToQImage
if sys.version_info[0] == 3:
    from io import BytesIO
else:
    import cStringIO as _StringIO
    BytesIO = _StringIO.StringIO

_logger = logging.getLogger(__name__)

_NEXUS_HDF5_EXT_STR = ' '.join(['*' + ext for ext in NEXUS_HDF5_EXT])


def selectOutputGroup(h5filename):
    """Open a dialog to prompt the user to select a group in
    which to output data.

    :param str h5filename: name of an existing HDF5 file
    :rtype: str
    :return: Name of output group, or None if the dialog was cancelled
    """
    dialog = GroupDialog()
    dialog.addFile(h5filename)
    dialog.setWindowTitle("Select an output group")
    if not dialog.exec_():
        return None
    return dialog.getSelectedDataUrl().data_path()


class SaveAction(PlotAction):
    """QAction for saving Plot content.

    It opens a Save as... dialog.

    :param plot: :class:`.PlotWidget` instance on which to operate.
    :param parent: See :class:`QAction`.
    """

    SNAPSHOT_FILTER_SVG = 'Plot Snapshot as SVG (*.svg)'
    SNAPSHOT_FILTER_PNG = 'Plot Snapshot as PNG (*.png)'

    DEFAULT_ALL_FILTERS = (SNAPSHOT_FILTER_PNG, SNAPSHOT_FILTER_SVG)

    # Dict of curve filters with CSV-like format
    # Using ordered dict to guarantee filters order
    # Note: '%.18e' is numpy.savetxt default format
    CURVE_FILTERS_TXT = OrderedDict((
        ('Curve as Raw ASCII (*.txt)',
         {'fmt': '%.18e', 'delimiter': ' ', 'header': False}),
        ('Curve as ";"-separated CSV (*.csv)',
         {'fmt': '%.18e', 'delimiter': ';', 'header': True}),
        ('Curve as ","-separated CSV (*.csv)',
         {'fmt': '%.18e', 'delimiter': ',', 'header': True}),
        ('Curve as tab-separated CSV (*.csv)',
         {'fmt': '%.18e', 'delimiter': '\t', 'header': True}),
        ('Curve as OMNIC CSV (*.csv)',
         {'fmt': '%.7E', 'delimiter': ',', 'header': False}),
        ('Curve as SpecFile (*.dat)',
         {'fmt': '%.10g', 'delimiter': '', 'header': False})
    ))

    CURVE_FILTER_NPY = 'Curve as NumPy binary file (*.npy)'

    CURVE_FILTER_NXDATA = 'Curve as NXdata (%s)' % _NEXUS_HDF5_EXT_STR

    DEFAULT_CURVE_FILTERS = list(CURVE_FILTERS_TXT.keys()) + [
        CURVE_FILTER_NPY, CURVE_FILTER_NXDATA]

    DEFAULT_ALL_CURVES_FILTERS = ("All curves as SpecFile (*.dat)",)

    IMAGE_FILTER_EDF = 'Image data as EDF (*.edf)'
    IMAGE_FILTER_TIFF = 'Image data as TIFF (*.tif)'
    IMAGE_FILTER_NUMPY = 'Image data as NumPy binary file (*.npy)'
    IMAGE_FILTER_ASCII = 'Image data as ASCII (*.dat)'
    IMAGE_FILTER_CSV_COMMA = 'Image data as ,-separated CSV (*.csv)'
    IMAGE_FILTER_CSV_SEMICOLON = 'Image data as ;-separated CSV (*.csv)'
    IMAGE_FILTER_CSV_TAB = 'Image data as tab-separated CSV (*.csv)'
    IMAGE_FILTER_RGB_PNG = 'Image as PNG (*.png)'
    IMAGE_FILTER_NXDATA = 'Image as NXdata (%s)' % _NEXUS_HDF5_EXT_STR

    DEFAULT_IMAGE_FILTERS = (IMAGE_FILTER_EDF,
                             IMAGE_FILTER_TIFF,
                             IMAGE_FILTER_NUMPY,
                             IMAGE_FILTER_ASCII,
                             IMAGE_FILTER_CSV_COMMA,
                             IMAGE_FILTER_CSV_SEMICOLON,
                             IMAGE_FILTER_CSV_TAB,
                             IMAGE_FILTER_RGB_PNG,
                             IMAGE_FILTER_NXDATA)

    SCATTER_FILTER_NXDATA = 'Scatter as NXdata (%s)' % _NEXUS_HDF5_EXT_STR
    DEFAULT_SCATTER_FILTERS = (SCATTER_FILTER_NXDATA,)

    # filters for which we don't want an "overwrite existing file" warning
    DEFAULT_APPEND_FILTERS = (CURVE_FILTER_NXDATA, IMAGE_FILTER_NXDATA,
                              SCATTER_FILTER_NXDATA)

    def __init__(self, plot, parent=None):
        self._filters = {
            'all': OrderedDict(),
            'curve': OrderedDict(),
            'curves': OrderedDict(),
            'image': OrderedDict(),
            'scatter': OrderedDict()}

        self._appendFilters = list(self.DEFAULT_APPEND_FILTERS)

        # Initialize filters
        for nameFilter in self.DEFAULT_ALL_FILTERS:
            self.setFileFilter(
                dataKind='all', nameFilter=nameFilter, func=self._saveSnapshot)

        for nameFilter in self.DEFAULT_CURVE_FILTERS:
            self.setFileFilter(
                dataKind='curve', nameFilter=nameFilter, func=self._saveCurve)

        for nameFilter in self.DEFAULT_ALL_CURVES_FILTERS:
            self.setFileFilter(
                dataKind='curves', nameFilter=nameFilter, func=self._saveCurves)

        for nameFilter in self.DEFAULT_IMAGE_FILTERS:
            self.setFileFilter(
                dataKind='image', nameFilter=nameFilter, func=self._saveImage)

        for nameFilter in self.DEFAULT_SCATTER_FILTERS:
            self.setFileFilter(
                dataKind='scatter', nameFilter=nameFilter, func=self._saveScatter)

        super(SaveAction, self).__init__(
            plot, icon='document-save', text='Save as...',
            tooltip='Save curve/image/plot snapshot dialog',
            triggered=self._actionTriggered,
            checkable=False, parent=parent)
        self.setShortcut(qt.QKeySequence.Save)
        self.setShortcutContext(qt.Qt.WidgetShortcut)

    @staticmethod
    def _errorMessage(informativeText='', parent=None):
        """Display an error message."""
        # TODO issue with QMessageBox size fixed and too small
        msg = qt.QMessageBox(parent)
        msg.setIcon(qt.QMessageBox.Critical)
        msg.setInformativeText(informativeText + ' ' + str(sys.exc_info()[1]))
        msg.setDetailedText(traceback.format_exc())
        msg.exec_()

    def _saveSnapshot(self, plot, filename, nameFilter):
        """Save a snapshot of the :class:`PlotWindow` widget.

        :param str filename: The name of the file to write
        :param str nameFilter: The selected name filter
        :return: False if format is not supported or save failed,
                 True otherwise.
        """
        if nameFilter == self.SNAPSHOT_FILTER_PNG:
            fileFormat = 'png'
        elif nameFilter == self.SNAPSHOT_FILTER_SVG:
            fileFormat = 'svg'
        else:  # Format not supported
            _logger.error(
                'Saving plot snapshot failed: format not supported')
            return False

        plot.saveGraph(filename, fileFormat=fileFormat)
        return True

    def _getAxesLabels(self, item):
        # If curve has no associated label, get the default from the plot
        xlabel = item.getXLabel() or self.plot.getXAxis().getLabel()
        ylabel = item.getYLabel() or self.plot.getYAxis().getLabel()
        return xlabel, ylabel

    def _get1dData(self, item):
        "provide xdata, [ydata], xlabel, [ylabel] and manages error bars"
        xlabel, ylabel = self._getAxesLabels(item)
        x_data = item.getXData(copy=False)
        y_data = item.getYData(copy=False)
        x_err = item.getXErrorData(copy=False)
        y_err = item.getYErrorData(copy=False)
        labels = [ylabel]
        data = [y_data]

        if x_err is not None:
            if numpy.isscalar(x_err):
                data.append(numpy.zeros_like(y_data) + x_err)
                labels.append(xlabel + "_errors")
            elif x_err.ndim == 1:
                data.append(x_err)
                labels.append(xlabel + "_errors")
            elif x_err.ndim == 2:
                data.append(x_err[0])
                labels.append(xlabel + "_errors_below")
                data.append(x_err[1])
                labels.append(xlabel + "_errors_above")

        if y_err is not None:
            if numpy.isscalar(y_err):
                data.append(numpy.zeros_like(y_data) + y_err)
                labels.append(ylabel + "_errors")
            elif y_err.ndim == 1:
                data.append(y_err)
                labels.append(ylabel + "_errors")
            elif y_err.ndim == 2:
                data.append(y_err[0])
                labels.append(ylabel + "_errors_below")
                data.append(y_err[1])
                labels.append(ylabel + "_errors_above")
        return x_data, data, xlabel, labels

    @staticmethod
    def _selectWriteableOutputGroup(filename, parent):
        if os.path.exists(filename) and os.path.isfile(filename) \
                and os.access(filename, os.W_OK):
            entryPath = selectOutputGroup(filename)
            if entryPath is None:
                _logger.info("Save operation cancelled")
                return None
            return entryPath
        elif not os.path.exists(filename):
            # create new entry in new file
            return "/entry"
        else:
            SaveAction._errorMessage('Save failed (file access issue)\n', parent=parent)
            return None

    def _saveCurveAsNXdata(self, curve, filename):
        entryPath = self._selectWriteableOutputGroup(filename, parent=self.plot)
        if entryPath is None:
            return False

        xlabel, ylabel = self._getAxesLabels(curve)

        return save_NXdata(
            filename,
            nxentry_name=entryPath,
            signal=curve.getYData(copy=False),
            axes=[curve.getXData(copy=False)],
            signal_name="y",
            axes_names=["x"],
            signal_long_name=ylabel,
            axes_long_names=[xlabel],
            signal_errors=curve.getYErrorData(copy=False),
            axes_errors=[curve.getXErrorData(copy=True)],
            title=self.plot.getGraphTitle())

    def _saveCurve(self, plot, filename, nameFilter):
        """Save a curve from the plot.

        :param str filename: The name of the file to write
        :param str nameFilter: The selected name filter
        :return: False if format is not supported or save failed,
                 True otherwise.
        """
        if nameFilter not in self.DEFAULT_CURVE_FILTERS:
            return False

        # Check if a curve is to be saved
        curve = plot.getActiveCurve()
        # before calling _saveCurve, if there is no selected curve, we
        # make sure there is only one curve on the graph
        if curve is None:
            curves = plot.getAllCurves()
            if not curves:
                self._errorMessage("No curve to be saved", parent=self.plot)
                return False
            curve = curves[0]

        if nameFilter in self.CURVE_FILTERS_TXT:
            filter_ = self.CURVE_FILTERS_TXT[nameFilter]
            fmt = filter_['fmt']
            csvdelim = filter_['delimiter']
            autoheader = filter_['header']
        else:
            # .npy or nxdata
            fmt, csvdelim, autoheader = ("", "", False)

        if nameFilter == self.CURVE_FILTER_NXDATA:
            return self._saveCurveAsNXdata(curve, filename)

        xdata, data, xlabel, labels = self._get1dData(curve)

        try:
            save1D(filename,
                   xdata, data,
                   xlabel, labels,
                   fmt=fmt, csvdelim=csvdelim,
                   autoheader=autoheader)
        except IOError:
            self._errorMessage('Save failed\n', parent=self.plot)
            return False

        return True

    def _saveCurves(self, plot, filename, nameFilter):
        """Save all curves from the plot.

        :param str filename: The name of the file to write
        :param str nameFilter: The selected name filter
        :return: False if format is not supported or save failed,
                 True otherwise.
        """
        if nameFilter not in self.DEFAULT_ALL_CURVES_FILTERS:
            return False

        curves = plot.getAllCurves()
        if not curves:
            self._errorMessage("No curves to be saved", parent=self.plot)
            return False

        curve = curves[0]
        scanno = 1
        try:
            xdata, data, xlabel, labels = self._get1dData(curve)

            specfile = savespec(filename,
                                xdata, data,
                                xlabel, labels,
                                fmt="%.7g", scan_number=1, mode="w",
                                write_file_header=True,
                                close_file=False)
        except IOError:
            self._errorMessage('Save failed\n', parent=self.plot)
            return False

        for curve in curves[1:]:
            try:
                scanno += 1
                xdata, data, xlabel, labels = self._get1dData(curve)
                specfile = savespec(specfile,
                                    xdata, data,
                                    xlabel, labels,
                                    fmt="%.7g", scan_number=scanno,
                                    write_file_header=False,
                                    close_file=False)
            except IOError:
                self._errorMessage('Save failed\n', parent=self.plot)
                return False
        specfile.close()

        return True

    def _saveImage(self, plot, filename, nameFilter):
        """Save an image from the plot.

        :param str filename: The name of the file to write
        :param str nameFilter: The selected name filter
        :return: False if format is not supported or save failed,
                 True otherwise.
        """
        if nameFilter not in self.DEFAULT_IMAGE_FILTERS:
            return False

        image = plot.getActiveImage()
        if image is None:
            qt.QMessageBox.warning(
                plot, "No Data", "No image to be saved")
            return False

        data = image.getData(copy=False)

        # TODO Use silx.io for writing files
        if nameFilter == self.IMAGE_FILTER_EDF:
            edfFile = EdfFile(filename, access="w+")
            edfFile.WriteImage({}, data, Append=0)
            return True

        elif nameFilter == self.IMAGE_FILTER_TIFF:
            tiffFile = TiffIO(filename, mode='w')
            tiffFile.writeImage(data, software='silx')
            return True

        elif nameFilter == self.IMAGE_FILTER_NUMPY:
            try:
                numpy.save(filename, data)
            except IOError:
                self._errorMessage('Save failed\n', parent=self.plot)
                return False
            return True

        elif nameFilter == self.IMAGE_FILTER_NXDATA:
            entryPath = self._selectWriteableOutputGroup(filename, parent=self.plot)
            if entryPath is None:
                return False
            xorigin, yorigin = image.getOrigin()
            xscale, yscale = image.getScale()
            xaxis = xorigin + xscale * numpy.arange(data.shape[1])
            yaxis = yorigin + yscale * numpy.arange(data.shape[0])
            xlabel, ylabel = self._getAxesLabels(image)
            interpretation = "image" if len(data.shape) == 2 else "rgba-image"

            return save_NXdata(filename,
                               nxentry_name=entryPath,
                               signal=data,
                               axes=[yaxis, xaxis],
                               signal_name="image",
                               axes_names=["y", "x"],
                               axes_long_names=[ylabel, xlabel],
                               title=plot.getGraphTitle(),
                               interpretation=interpretation)

        elif nameFilter in (self.IMAGE_FILTER_ASCII,
                            self.IMAGE_FILTER_CSV_COMMA,
                            self.IMAGE_FILTER_CSV_SEMICOLON,
                            self.IMAGE_FILTER_CSV_TAB):
            csvdelim, filetype = {
                self.IMAGE_FILTER_ASCII: (' ', 'txt'),
                self.IMAGE_FILTER_CSV_COMMA: (',', 'csv'),
                self.IMAGE_FILTER_CSV_SEMICOLON: (';', 'csv'),
                self.IMAGE_FILTER_CSV_TAB: ('\t', 'csv'),
                }[nameFilter]

            height, width = data.shape
            rows, cols = numpy.mgrid[0:height, 0:width]
            try:
                save1D(filename, rows.ravel(), (cols.ravel(), data.ravel()),
                       filetype=filetype,
                       xlabel='row',
                       ylabels=['column', 'value'],
                       csvdelim=csvdelim,
                       autoheader=True)

            except IOError:
                self._errorMessage('Save failed\n', parent=self.plot)
                return False
            return True

        elif nameFilter == self.IMAGE_FILTER_RGB_PNG:
            # Get displayed image
            rgbaImage = image.getRgbaImageData(copy=False)
            # Convert RGB QImage
            qimage = convertArrayToQImage(rgbaImage[:, :, :3])

            if qimage.save(filename, 'PNG'):
                return True
            else:
                _logger.error('Failed to save image as %s', filename)
                qt.QMessageBox.critical(
                    self.parent(),
                    'Save image as',
                    'Failed to save image')

        return False

    def _saveScatter(self, plot, filename, nameFilter):
        """Save an image from the plot.

        :param str filename: The name of the file to write
        :param str nameFilter: The selected name filter
        :return: False if format is not supported or save failed,
                 True otherwise.
        """
        if nameFilter not in self.DEFAULT_SCATTER_FILTERS:
            return False

        if nameFilter == self.SCATTER_FILTER_NXDATA:
            entryPath = self._selectWriteableOutputGroup(filename, parent=self.plot)
            if entryPath is None:
                return False
            scatter = plot.getScatter()

            x = scatter.getXData(copy=False)
            y = scatter.getYData(copy=False)
            z = scatter.getValueData(copy=False)

            xerror = scatter.getXErrorData(copy=False)
            if isinstance(xerror, float):
                xerror = xerror * numpy.ones(x.shape, dtype=numpy.float32)

            yerror = scatter.getYErrorData(copy=False)
            if isinstance(yerror, float):
                yerror = yerror * numpy.ones(x.shape, dtype=numpy.float32)

            xlabel = plot.getGraphXLabel()
            ylabel = plot.getGraphYLabel()

            return save_NXdata(
                filename,
                nxentry_name=entryPath,
                signal=z,
                axes=[x, y],
                signal_name="values",
                axes_names=["x", "y"],
                axes_long_names=[xlabel, ylabel],
                axes_errors=[xerror, yerror],
                title=plot.getGraphTitle())

    def setFileFilter(self, dataKind, nameFilter, func, index=None, appendToFile=False):
        """Set a name filter to add/replace a file format support

        :param str dataKind:
            The kind of data for which the provided filter is valid.
            One of: 'all', 'curve', 'curves', 'image', 'scatter'
        :param str nameFilter: The name filter in the QFileDialog.
            See :meth:`QFileDialog.setNameFilters`.
        :param callable func: The function to call to perform saving.
            Expected signature is:
            bool func(PlotWidget plot, str filename, str nameFilter)
        :param bool appendToFile: True to append the data into the selected
            file.
        :param integer index: Index of the filter in the final list (or None)
        """
        assert dataKind in ('all', 'curve', 'curves', 'image', 'scatter')

        if appendToFile:
            self._appendFilters.append(nameFilter)

        # first append or replace the new filter to prevent colissions
        self._filters[dataKind][nameFilter] = func
        if index is None:
            # we are already done
            return

        # get the current ordered list of keys
        keyList = list(self._filters[dataKind].keys())

        # deal with negative indices
        if index < 0:
            index = len(keyList) + index
            if index < 0:
                index = 0

        if index >= len(keyList):
            # nothing to be done, already at the end
            txt = 'Requested index %d impossible, already at the end' % index
            _logger.info(txt)
            return

        # get the new ordered list
        oldIndex = keyList.index(nameFilter)
        del keyList[oldIndex]
        keyList.insert(index, nameFilter)

        # build the new filters
        newFilters = OrderedDict()
        for key in keyList:
            newFilters[key] = self._filters[dataKind][key]

        # and update the filters
        self._filters[dataKind] = newFilters
        return

    def getFileFilters(self, dataKind):
        """Returns the nameFilter and associated function for a kind of data.

        :param str dataKind:
            The kind of data for which the provided filter is valid.
            On of: 'all', 'curve', 'curves', 'image', 'scatter'
        :return: {nameFilter: function} associations.
        :rtype: collections.OrderedDict
        """
        assert dataKind in ('all', 'curve', 'curves', 'image', 'scatter')

        return self._filters[dataKind].copy()

    def _actionTriggered(self, checked=False):
        """Handle save action."""
        # Set-up filters
        filters = OrderedDict()

        # Add image filters if there is an active image
        if self.plot.getActiveImage() is not None:
            filters.update(self._filters['image'].items())

        # Add curve filters if there is a curve to save
        if (self.plot.getActiveCurve() is not None or
                len(self.plot.getAllCurves()) == 1):
            filters.update(self._filters['curve'].items())
        if len(self.plot.getAllCurves()) >= 1:
            filters.update(self._filters['curves'].items())

        # Add scatter filters if there is a scatter
        # todo: CSV
        if self.plot.getScatter() is not None:
            filters.update(self._filters['scatter'].items())

        filters.update(self._filters['all'].items())

        # Create and run File dialog
        dialog = qt.QFileDialog(self.plot)
        dialog.setOption(dialog.DontUseNativeDialog)
        dialog.setWindowTitle("Output File Selection")
        dialog.setModal(1)
        dialog.setNameFilters(list(filters.keys()))

        dialog.setFileMode(dialog.AnyFile)
        dialog.setAcceptMode(dialog.AcceptSave)

        def onFilterSelection(filt_):
            # disable overwrite confirmation for NXdata types,
            # because we append the data to existing files
            if filt_ in self._appendFilters:
                dialog.setOption(dialog.DontConfirmOverwrite)
            else:
                dialog.setOption(dialog.DontConfirmOverwrite, False)

        dialog.filterSelected.connect(onFilterSelection)

        if not dialog.exec_():
            return False

        nameFilter = dialog.selectedNameFilter()
        filename = dialog.selectedFiles()[0]
        dialog.close()

        if '(' in nameFilter and ')' == nameFilter.strip()[-1]:
            # Check for correct file extension
            # Extract file extensions as .something
            extensions = [ext[ext.find('.'):] for ext in
                          nameFilter[nameFilter.find('(') + 1:-1].split()]
            for ext in extensions:
                if (len(filename) > len(ext) and
                        filename[-len(ext):].lower() == ext.lower()):
                    break
            else:  # filename has no extension supported in nameFilter, add one
                if len(extensions) >= 1:
                    filename += extensions[0]

        # Handle save
        func = filters.get(nameFilter, None)
        if func is not None:
            return func(self.plot, filename, nameFilter)
        else:
            _logger.error('Unsupported file filter: %s', nameFilter)
            return False


def _plotAsPNG(plot):
    """Save a :class:`Plot` as PNG and return the payload.

    :param plot: The :class:`Plot` to save
    """
    pngFile = BytesIO()
    plot.saveGraph(pngFile, fileFormat='png')
    pngFile.flush()
    pngFile.seek(0)
    data = pngFile.read()
    pngFile.close()
    return data


class PrintAction(PlotAction):
    """QAction for printing the plot.

    It opens a Print dialog.

    Current implementation print a bitmap of the plot area and not vector
    graphics, so printing quality is not great.

    :param plot: :class:`.PlotWidget` instance on which to operate.
    :param parent: See :class:`QAction`.
    """

    def __init__(self, plot, parent=None):
        super(PrintAction, self).__init__(
            plot, icon='document-print', text='Print...',
            tooltip='Open print dialog',
            triggered=self.printPlot,
            checkable=False, parent=parent)
        self.setShortcut(qt.QKeySequence.Print)
        self.setShortcutContext(qt.Qt.WidgetShortcut)

    def getPrinter(self):
        """The QPrinter instance used by the PrintAction.

        :rtype: QPrinter
        """
        return printer.getDefaultPrinter()

    @property
    @deprecated(replacement="getPrinter()", since_version="0.8.0")
    def printer(self):
        return self.getPrinter()

    def printPlotAsWidget(self):
        """Open the print dialog and print the plot.

        Use :meth:`QWidget.render` to print the plot

        :return: True if successful
        """
        dialog = qt.QPrintDialog(self.getPrinter(), self.plot)
        dialog.setWindowTitle('Print Plot')
        if not dialog.exec_():
            return False

        # Print a snapshot of the plot widget at the top of the page
        widget = self.plot.centralWidget()

        painter = qt.QPainter()
        if not painter.begin(self.getPrinter()):
            return False

        pageRect = self.getPrinter().pageRect()
        xScale = pageRect.width() / widget.width()
        yScale = pageRect.height() / widget.height()
        scale = min(xScale, yScale)

        painter.translate(pageRect.width() / 2., 0.)
        painter.scale(scale, scale)
        painter.translate(-widget.width() / 2., 0.)
        widget.render(painter)
        painter.end()

        return True

    def printPlot(self):
        """Open the print dialog and print the plot.

        Use :meth:`Plot.saveGraph` to print the plot.

        :return: True if successful
        """
        # Init printer and start printer dialog
        dialog = qt.QPrintDialog(self.getPrinter(), self.plot)
        dialog.setWindowTitle('Print Plot')
        if not dialog.exec_():
            return False

        # Save Plot as PNG and make a pixmap from it with default dpi
        pngData = _plotAsPNG(self.plot)

        pixmap = qt.QPixmap()
        pixmap.loadFromData(pngData, 'png')

        xScale = self.getPrinter().pageRect().width() / pixmap.width()
        yScale = self.getPrinter().pageRect().height() / pixmap.height()
        scale = min(xScale, yScale)

        # Draw pixmap with painter
        painter = qt.QPainter()
        if not painter.begin(self.getPrinter()):
            return False

        painter.drawPixmap(0, 0,
                           pixmap.width() * scale,
                           pixmap.height() * scale,
                           pixmap)
        painter.end()

        return True


class CopyAction(PlotAction):
    """QAction to copy :class:`.PlotWidget` content to clipboard.

    :param plot: :class:`.PlotWidget` instance on which to operate
    :param parent: See :class:`QAction`
    """

    def __init__(self, plot, parent=None):
        super(CopyAction, self).__init__(
            plot, icon='edit-copy', text='Copy plot',
            tooltip='Copy a snapshot of the plot into the clipboard',
            triggered=self.copyPlot,
            checkable=False, parent=parent)
        self.setShortcut(qt.QKeySequence.Copy)
        self.setShortcutContext(qt.Qt.WidgetShortcut)

    def copyPlot(self):
        """Copy plot content to the clipboard as a bitmap."""
        # Save Plot as PNG and make a QImage from it with default dpi
        pngData = _plotAsPNG(self.plot)
        image = qt.QImage.fromData(pngData, 'png')
        qt.QApplication.clipboard().setImage(image)
