import numpy as np

from .. import functions as fn
from .. import getConfigOption
from .. import Qt
from ..Qt import QtCore, QtGui
from .GraphicsObject import GraphicsObject

__all__ = ['BarGraphItem']

class BarGraphItem(GraphicsObject):
    def __init__(self, **opts):
        """
        Valid keyword options are:
        x, x0, x1, y, y0, y1, width, height, pen, brush
        
        x specifies the x-position of the center of the bar.
        x0, x1 specify left and right edges of the bar, respectively.
        width specifies distance from x0 to x1.
        You may specify any combination:
            
            x, width
            x0, width
            x1, width
            x0, x1
            
        Likewise y, y0, y1, and height. 
        If only height is specified, then y0 will be set to 0
        
        Example uses:
        
            BarGraphItem(x=range(5), height=[1,5,2,4,3], width=0.5)
            
        
        """
        GraphicsObject.__init__(self)
        self.opts = dict(
            x=None,
            y=None,
            x0=None,
            y0=None,
            x1=None,
            y1=None,
            name=None,
            height=None,
            width=None,
            pen=None,
            brush=None,
            pens=None,
            brushes=None,
        )

        if 'pen' not in opts:
            opts['pen'] = getConfigOption('foreground')
        if 'brush' not in opts:
            opts['brush'] = (128, 128, 128)
        # the first call to _updateColors() will thus always be an update

        self._rectarray = Qt.internals.PrimitiveArray(QtCore.QRectF, 4)
        self._shape = None
        self.picture = None
        self.setOpts(**opts)
        
    def setOpts(self, **opts):
        self.opts.update(opts)
        self.picture = None
        self._shape = None
        self._prepareData()
        self._updateColors(opts)
        self.prepareGeometryChange()
        self.update()
        self.informViewBoundsChanged()

    def _updatePenWidth(self, pen):
        no_pen = pen is None or pen.style() == QtCore.Qt.PenStyle.NoPen
        if no_pen:
            return

        idx = pen.isCosmetic()
        self._penWidth[idx] = max(self._penWidth[idx], pen.widthF())

    def _updateColors(self, opts):
        # the logic here is to permit the user to update only data
        # without updating pens/brushes

        # update only if fresh pen/pens supplied
        if 'pen' in opts or 'pens' in opts:
            self._penWidth = [0, 0]

            if self.opts['pens'] is None:
                # pens not configured, use single pen
                pen = self.opts['pen']
                pen = fn.mkPen(pen)
                self._updatePenWidth(pen)
                self._sharedPen = pen
                self._pens = None
            else:
                # pens configured, ignore single pen (if any)
                pens = []
                for pen in self.opts['pens']:
                    if not isinstance(pen, QtGui.QPen):
                        pen = fn.mkPen(pen)
                    pens.append(pen)
                self._updatePenWidth(pen)
                self._sharedPen = None
                self._pens = pens

        # update only if fresh brush/brushes supplied
        if 'brush' in opts or 'brushes' in opts:
            if self.opts['brushes'] is None:
                # brushes not configured, use single brush
                brush = self.opts['brush']
                self._sharedBrush = fn.mkBrush(brush)
                self._brushes = None
            else:
                # brushes configured, ignore single brush (if any)
                brushes = []
                for brush in self.opts['brushes']:
                    if not isinstance(brush, QtGui.QBrush):
                        brush = fn.mkBrush(brush)
                    brushes.append(brush)
                self._sharedBrush = None
                self._brushes = brushes

        self._singleColor = (
            self._sharedPen is not None and
            self._sharedBrush is not None
        )

    def _getNormalizedCoords(self):
        def asarray(x):
            if x is None or np.isscalar(x) or isinstance(x, np.ndarray):
                return x
            return np.array(x)

        x = asarray(self.opts.get('x'))
        x0 = asarray(self.opts.get('x0'))
        x1 = asarray(self.opts.get('x1'))
        width = asarray(self.opts.get('width'))
        
        if x0 is None:
            if width is None:
                raise Exception('must specify either x0 or width')
            if x1 is not None:
                x0 = x1 - width
            elif x is not None:
                x0 = x - width/2.
            else:
                raise Exception('must specify at least one of x, x0, or x1')
        if width is None:
            if x1 is None:
                raise Exception('must specify either x1 or width')
            width = x1 - x0
            
        y = asarray(self.opts.get('y'))
        y0 = asarray(self.opts.get('y0'))
        y1 = asarray(self.opts.get('y1'))
        height = asarray(self.opts.get('height'))

        if y0 is None:
            if height is None:
                y0 = 0
            elif y1 is not None:
                y0 = y1 - height
            elif y is not None:
                y0 = y - height/2.
            else:
                y0 = 0
        if height is None:
            if y1 is None:
                raise Exception('must specify either y1 or height')
            height = y1 - y0

        # ensure x0 < x1 and y0 < y1
        t0, t1 = x0, x0 + width
        x0 = np.minimum(t0, t1, dtype=np.float64)
        x1 = np.maximum(t0, t1, dtype=np.float64)
        t0, t1 = y0, y0 + height
        y0 = np.minimum(t0, t1, dtype=np.float64)
        y1 = np.maximum(t0, t1, dtype=np.float64)

        # here, all of x0, y0, x1, y1 are numpy objects,
        # BUT could possibly be numpy scalars
        return x0, y0, x1, y1

    def _prepareData(self):
        x0, y0, x1, y1 = self._getNormalizedCoords()
        xmn, xmx = np.min(x0), np.max(x1)
        ymn, ymx = np.min(y0), np.max(y1)
        self._dataBounds = (xmn, xmx), (ymn, ymx)

        self._rectarray.resize(max(x0.size, y0.size))
        memory = self._rectarray.ndarray()
        memory[:, 0] = x0
        memory[:, 1] = y0
        memory[:, 2] = x1 - x0
        memory[:, 3] = y1 - y0

    def _render(self, painter):
        multi_pen = self._pens is not None
        multi_brush = self._brushes is not None
        no_pen = (
            not multi_pen
            and self._sharedPen.style() == QtCore.Qt.PenStyle.NoPen
        )

        rects = self._rectarray.instances()

        if no_pen and multi_brush:
            for idx, rect in enumerate(rects):
                painter.fillRect(rect, self._brushes[idx])
        else:
            if not multi_pen:
                painter.setPen(self._sharedPen)
            if not multi_brush:
                painter.setBrush(self._sharedBrush)

            for idx, rect in enumerate(rects):
                if multi_pen:
                    painter.setPen(self._pens[idx])
                if multi_brush:
                    painter.setBrush(self._brushes[idx])

                painter.drawRect(rect)

    def drawPicture(self):
        self.picture = QtGui.QPicture()
        painter = QtGui.QPainter(self.picture)
        self._render(painter)
        painter.end()

    def paint(self, p, *args):
        if self._singleColor:
            p.setPen(self._sharedPen)
            p.setBrush(self._sharedBrush)
            inst = self._rectarray.instances()
            p.drawRects(inst)
        else:
            if self.picture is None:
                self.drawPicture()
            self.picture.play(p)
            
    def shape(self):
        if self._shape is None:
            shape = QtGui.QPainterPath()
            rects = self._rectarray.instances()
            for rect in rects:
                shape.addRect(rect)
            self._shape = shape
        return self._shape

    def implements(self, interface=None):
        ints = ['plotData']
        if interface is None:
            return ints
        return interface in ints

    def name(self):
        return self.opts.get('name', None)

    def getData(self):
        return self.opts.get('x'),  self.opts.get('height')

    def dataBounds(self, ax, frac=1.0, orthoRange=None):
        # _penWidth is available after _updateColors()
        pw = self._penWidth[0] * 0.5
        # _dataBounds is available after _prepareData()
        bounds = self._dataBounds[ax]
        return (bounds[0] - pw, bounds[1] + pw)

    def pixelPadding(self):
        # _penWidth is available after _updateColors()
        pw = (self._penWidth[1] or 1) * 0.5
        return pw

    def boundingRect(self):
        xmn, xmx = self.dataBounds(ax=0)
        if xmn is None or xmx is None:
            return QtCore.QRectF()
        ymn, ymx = self.dataBounds(ax=1)
        if ymn is None or ymx is None:
            return QtCore.QRectF()

        px = py = 0
        pxPad = self.pixelPadding()
        if pxPad > 0:
            # determine length of pixel in local x, y directions
            px, py = self.pixelVectors()
            px = 0 if px is None else px.length()
            py = 0 if py is None else py.length()
            # return bounds expanded by pixel size
            px *= pxPad
            py *= pxPad

        return QtCore.QRectF(xmn-px, ymn-py, (2*px)+xmx-xmn, (2*py)+ymx-ymn)
