#!/usr/bin/env python
# -*- coding: utf-8 -*-
# py-indent-offset:4 -*-

#-------------------------------------------------------------------------------
# Name:             gradient_button.py
# Purpose:          Gradient button
# Author:           Ecco
# Created:          2023
# Copyright:        ...
# License:          wxWindows license
# Version:          1.0.0
# Tags:             phoenix-port, py3-port
# ....
# Tested:           - Windows 10/11 | Python 3.11.9 |
# ....              wxPython 4.2.3 | wxWidgets 3.2.6
# ....
# ....              - Linux Mint 21 | Python 3.10.12 |
# ....              wxPython 4.2.1 gtk3 | wxWidgets 3.2.2.1
# ....
# ....              - MacOS Sequoia 15 | Python 3.12.4 |
# ....              wxPython 4.2.2 | wxWidgets 3.2.6
# ....
# Thanks to:        Cody Precord 
# (A-Z)             ...
#-------------------------------------------------------------------------------

"""

0.0.1   First release

"""

#-------------------------------------------------------------------------------
# Import wxPython packages
#-------------------------------------------------------------------------------
import wx
import wx.lib.colourdb

#-------------------------------------------------------------------------------
# Constants
#-------------------------------------------------------------------------------
STYLE_NOBG        = 90   # Useful on Windows to get a transparent appearance
                         # when the control is shown on a non solid background
                       
#-------------------------------------------------------------------------------

# class CustomGradientButton

#-------------------------------------------------------------------------------

class GradientButton(wx.Control):
    def __init__(self, parent, id=wx.ID_ANY, label="", normalLabelColour="#ffffff",
                 hoverLabelColour="#ffffff", pressedLabelColour="#ffffff",
                 font_size=10, font_family=wx.FONTFAMILY_DEFAULT, 
                 font_style=wx.FONTSTYLE_NORMAL, font_weight=wx.FONTWEIGHT_NORMAL,
                 normalBtnColour=(wx.Colour(90, 173, 255, 255), wx.Colour(5, 114, 255, 255)),  
                 hoverBtnColour="#46a0f5", pressedBtnColour="#1473e6",
                 disabledBtnColour="#838383", borderRadius=4, # borderColour=None,
                 focusColour="#89c0f7", focusSize=3, indicatorColour=wx.Colour(0, 0, 0, 0),
                 # shadowColour=wx.Colour(0, 0, 0, 0),
                 pos=wx.DefaultPosition, size=wx.DefaultSize,
                 style=STYLE_NOBG, 
                 image=None, 
                 image_position='left',  # ('top', 'bottom', 'left', 'right')
                 text_alignment='center'  # (''left', 'right', 'center')
                 ):    # appearance=None,
        super(GradientButton, self).__init__(parent, id, pos, size,
                                           style=wx.BORDER_NONE| wx.TRANSPARENT_WINDOW)

        # Colourdb
        wx.lib.colourdb.updateColourDB()

        # Attributes        
        self._style = style 
        self._label = label
        self.state = "normal"
        self.normalLabelColour = normalLabelColour
        self.hoverLabelColour = hoverLabelColour
        self.pressedLabelColour = pressedLabelColour
        self.font_size = font_size  
        self.font_family = font_family  
        self.font_style = font_style  
        self.font_weight = font_weight
        self.normalBtnColour = normalBtnColour
        self.hoverBtnColour = hoverBtnColour
        self.pressedBtnColour = pressedBtnColour
        self.disabledBtnColour = disabledBtnColour
        # self.borderColour = borderColour
        self.borderRadius = borderRadius
        self.focusColour = focusColour
        self.focusSize = focusSize
        self.indicatorColour = indicatorColour
        # self.shadowColour = shadowColour
        # self.appearance = appearance              
        # self.font = wx.SystemSettings.GetFont(wx.SYS_DEFAULT_GUI_FONT)  # (wx.SYS_SYSTEM_FONT)
        self.font = wx.Font(self.font_size, self.font_family, self.font_style, self.font_weight)
        self.SetLabel(label)

        # Store the alignment parameter
        self.text_alignment = text_alignment
        
        # New attributes for the image
        self.image = wx.Image(image) if image else None 
        self.image_position = image_position  
        self.image_bitmap = wx.Bitmap(self.image) if self.image else None 

        if self.image_bitmap:
            self.grayscale_image_bitmap = self.GrayscaleBitmap(self.image_bitmap)
        else:
            self.grayscale_image_bitmap = None
    
        self.InheritAttributes()
        # Setup Initial Size
        self.SetInitialSize(size)

        # Simplified init method
        self.BindEvents()

    def BindEvents(self):        
        self.Bind(wx.EVT_MOUSE_CAPTURE_LOST, self.OnMouseCaptureLost)
        self.Bind(wx.EVT_PAINT, lambda event: self.OnPaint())
        self.Bind(wx.EVT_ERASE_BACKGROUND, self.OnErase)
        self.Bind(wx.EVT_ENTER_WINDOW, self.OnMouseEnter)
        self.Bind(wx.EVT_LEAVE_WINDOW, self.OnMouseLeave)       
        self.Bind(wx.EVT_LEFT_DOWN, self.OnMouseDown)
        self.Bind(wx.EVT_LEFT_UP, self.OnMouseUp)
        self.Bind(wx.EVT_SET_FOCUS, self.OnSetFocus)
        self.Bind(wx.EVT_KILL_FOCUS, self.OnKillFocus)
        self.Bind(wx.EVT_KEY_DOWN, self.OnKeyDown)
        self.Bind(wx.EVT_KEY_UP, self.OnKeyUp)

    def DoGetBestSize(self):
        """
        Determines the best size of the control based on the label size and the current font.
        """

        label = self.GetLabel()
        font = self.GetFont()
        if not font:
            font = wx.SystemSettings.GetFont(wx.SYS_DEFAULT_GUI_FONT)
        dc = wx.ClientDC(self)
        dc.SetFont(font)

        maxWidth = 0
        totalHeight = 0
        for line in label.split('\n'):
            if line == '':
                w, h = dc.GetTextExtent(self._label)
            else:
                w, h = dc.GetTextExtent(line)
            totalHeight += 20
            maxWidth = max(maxWidth, w + 20)
        best = wx.Size(maxWidth, totalHeight)
        self.CacheBestSize(best)
        return best
    
    def OnPaint(self):
        # TODO using a buffered paintdc on windows with the nobg style
        #      causes lots of weird drawing. So currently the use of a
        #      buffered dc is disabled for this style
        if STYLE_NOBG & self._style:
            dc = wx.PaintDC(self)
        else:
            dc = wx.AutoBufferedPaintDCFactory(self)

        gc = wx.GCDC(dc)

        # Setup
        dc.SetBrush(wx.TRANSPARENT_BRUSH)
        gc.SetBrush(wx.TRANSPARENT_BRUSH)
        gc.SetBackgroundMode(wx.TRANSPARENT)

        # The background needs some help to look transparent on
        # on Gtk and Windows
        if wx.Platform in ['__WXGTK__', '__WXMSW__']:
            gc.SetBackground(self.GetBackgroundBrush(gc))
            gc.Clear()

        # Get button size
        w, h = self.GetSize()
        
        # Draw button
        self.DrawButton(dc, w, h)

        # Draw text
        self.DrawContent(gc, w, h)
        
    def DrawButton(self, dc, w, h):
        gc = wx.GraphicsContext.Create(dc)

        # Choose background colour based on button state  
        if self.IsEnabled():
            if self.state == "normal":
                start_colour, end_colour = self.normalBtnColour  
                gradient = gc.CreateLinearGradientBrush(1, 1, 1, 1 + h, start_colour, end_colour)
                gc.SetBrush(gradient)
            elif self.state == "hover":
                gc.SetBrush(wx.Brush(self.hoverBtnColour))
            elif self.state == "pressed":
                gc.SetBrush(wx.Brush(self.pressedBtnColour))
        else:  # If the button is disabled 
            gc.SetBrush(wx.Brush(self.disabledBtnColour))

        # Draw the rounded button  
        gc.DrawRoundedRectangle(1, 1, w - 2, h - 2, self.borderRadius)

        # Draw the focus indicator if the button has focus  
        if self.HasFocus():
            self.DrawFocusIndicator(gc, w, h)

    def DrawContent(self, gc, w, h):
        """
        Draws the image and multi-line text, centered according to the position.
        """

        gc.SetFont(self.font) 
        
        # Image dimensions
        img_width, img_height = 0, 0
        if self.image_bitmap:
            img_width, img_height = self.image_bitmap.GetSize()

        lines = self._label.splitlines()
        line_extents = [gc.GetTextExtent(line) for line in lines]
        text_width = max((extent[0] for extent in line_extents), default=0)
        line_height = gc.GetTextExtent('A')[1]
        text_height = line_height * len(lines)

        padding = 5
        # Total width/height calculation based on the presence of an image
        if self.image:
            if self.image_position in ['left', 'right']:
                total_width = img_width + padding + text_width
                total_height = max(img_height, text_height)
            elif self.image_position in ['top', 'bottom']:
                total_width = max(img_width, text_width)
                total_height = img_height + padding + text_height
            else:
                total_width, total_height = text_width + 6, text_height
        else:
            total_width, total_height = text_width + 6, text_height

        start_x = (w - total_width) // 2
        start_y = (h - total_height) // 2

        # Image positioning
        if self.image:
            if self.image_position == 'left':
                img_x = start_x
                img_y = start_y + (total_height - img_height) // 2
                text_x = img_x + img_width + padding
                text_y = start_y + (total_height - text_height) // 2
            elif self.image_position == 'right':
                text_x = start_x
                text_y = start_y + (total_height - text_height) // 2
                img_x = text_x + text_width + padding
                img_y = start_y + (total_height - img_height) // 2
            elif self.image_position == 'top':
                img_x = start_x + (total_width - img_width) // 2
                img_y = start_y
                text_x = start_x + (total_width - text_width) // 2
                text_y = img_y + img_height + padding
            elif self.image_position == 'bottom':
                text_x = start_x + (total_width - text_width) // 2
                text_y = start_y
                img_x = start_x + (total_width - img_width) // 2
                img_y = text_y + text_height + padding
            else:
                # Default
                img_x = start_x
                img_y = start_y
                text_x = start_x
                text_y = start_y
            # Draw the image
            if self.IsEnabled():
                gc.DrawBitmap(self.image_bitmap, img_x, img_y)
            else:
                gc.DrawBitmap(self.grayscale_image_bitmap, img_x, img_y)
        else:
            # No image
            text_x = start_x + 4
            text_y = start_y
        
        # Set text color
        if not self.IsEnabled():
            gc.SetTextForeground(wx.SystemSettings.GetColour(wx.SYS_COLOUR_GRAYTEXT))
        else:
            gc.SetTextForeground(self.normalLabelColour if self.state == "normal" else
                                 self.hoverLabelColour if self.state == "hover" else
                                 self.pressedLabelColour)
        
        # Positioning text according to alignment
        for i, line in enumerate(lines):
            line_width, _ = line_extents[i]
            # Calculating x based on alignment
            if self.text_alignment == 'left':
                line_x = text_x
            elif self.text_alignment == 'right':
                line_x = text_x + (text_width - line_width)
            else:  # 'center'
                line_x = text_x + (text_width - line_width) // 2
            line_y = text_y + i * line_height
            gc.DrawText(line, line_x, line_y)

    def SetImage(self, image_path, position='left'):
        """
        Method to define a new image and its position.
        """
        
        self.image = wx.Image(image_path)
        self.image_bitmap = wx.Bitmap(self.image)
        self.image_position = position
        self.Refresh()

    def GrayscaleBitmap(self, bitmap):
        """
        Convert the given bitmap to a grayscale bitmap.
        """
        
        # Convert bitmap to image  
        image = bitmap.ConvertToImage()
    
        # Set a lightening factor  
        brighten_factor = 10  # You can adjust this value according to your needs
    
        # Convert image to grayscale 
        for x in range(image.GetWidth()):
            for y in range(image.GetHeight()):
                r = image.GetRed(x, y)
                g = image.GetGreen(x, y)
                b = image.GetBlue(x, y)
                gray = int(0.299 * r + 0.587 * g + 0.114 * b)  # Brightness method
            
                # Add the lightening factor while making sure not to exceed 255  
                gray = min(255, gray + brighten_factor)
            
                image.SetRGB(x, y, gray, gray, gray)

        # Create a bitmap from the edited image  
        return wx.Bitmap(image)

    def DrawFocusIndicator(self, gc, w, h):
        # Draw the focus indicator rectangle 
        if wx.Platform in ['__WXMAC__']:
            pen = wx.Pen(colour=self.indicatorColour, width=0, style=wx.PENSTYLE_USER_DASH)
            pen.SetDashes([1, 1])
            gc.SetBrush(wx.TRANSPARENT_BRUSH)
            gc.SetPen(pen)
            gc.DrawRoundedRectangle(5, 6, w - 11, h - 11, 1)
        else:
            pen = wx.Pen(colour=self.indicatorColour, width=0, style=wx.PENSTYLE_USER_DASH)
            pen.SetDashes([1, 1])
            gc.SetBrush(wx.TRANSPARENT_BRUSH)
            gc.SetPen(pen)
            gc.DrawRoundedRectangle(5, 5, w - 11, h - 11, 1)                
                        
        # Draw the second focus rectangle  
        pen = wx.Pen(colour=self.focusColour, width=self.focusSize, style=wx.PENSTYLE_SOLID)
        gc.SetBrush(wx.TRANSPARENT_BRUSH)
        gc.SetPen(pen)
        gc.DrawRoundedRectangle(1, 1, w - 3, h - 3, self.borderRadius)
            
    def GetBackgroundBrush(self, dc):
        """
        Get the brush for drawing the background of the button

        :return: :class:`Brush`

        ..note::
            used internally when on gtk
        """
        
        if wx.Platform == '__WXMAC__' or self._style & STYLE_NOBG:
            return wx.TRANSPARENT_BRUSH

        bkgrd = self.GetBackgroundColour()
        brush = wx.Brush(bkgrd, wx.SOLID)
        my_attr = self.GetDefaultAttributes()
        p_attr = self.Parent.GetDefaultAttributes()
        my_def = bkgrd == my_attr.colBg
        p_def = self.Parent.GetBackgroundColour() == p_attr.colBg
        if my_def and not p_def:
            bkgrd = self.Parent.GetBackgroundColour()
            brush = wx.Brush(bkgrd, wx.SOLID)
        return brush

    def HasTransparentBackground(self):
        """
        Override setting of background fill
        """
        
        return True

    def OnErase(self, event):
        """
        Trap the erase event to keep the background transparent on windows.
        """
        
        pass

    def SetWindowStyle(self, style):
        """
        Sets the window style bytes, the updates take place
        immediately no need to call refresh afterwards.

        :param `style`: bitmask of _STYLE_* values
        """
        
        self._style = style
        self.Refresh()   

    def GetLabel(self):
        return self._label

    def SetLabel(self, label=None):
        if label is None:
            label = ''
        self._label = label
        
    def OnSetFocus(self, event):
        self.Refresh()

    def OnKillFocus(self, event):
        self.Refresh()

    def AcceptsFocus(self):
        """
        Can this window be given focus by mouse click?
        """
        
        return self.IsShown() and self.IsEnabled()

    def Enable(self, enable=True):
        """
        Enables/disables the button.
        """
        
        wx.Control.Enable(self, enable)
        self.Refresh()

    def Disable(self):
        wx.Control.Disable(self)
        self.Refresh()

    def OnMouseCaptureLost(self, event):
        print("Mouse capture lost!")
        if self.HasCapture():
            self.ReleaseMouse()

    def ReleaseMouseIfCaptured(self):
        if self.HasCapture():
            self.ReleaseMouse()
            
    def OnKeyDown(self, event):
        hasFocus = self.HasFocus()
        if hasFocus and event.GetKeyCode() == ord(" "):
            self.ReleaseMouseIfCaptured()   
            self.state = "pressed"
            self.Refresh()
        elif hasFocus and self.HasFlag(wx.WANTS_CHARS) and wx.GetKeyState(wx.WXK_RETURN):
            self.ReleaseMouseIfCaptured()   
            self.state = "pressed"
            self.Refresh()
        event.Skip()
        print('OnKeyDown')

    def OnKeyUp(self, event):
        self.ReleaseMouseIfCaptured() 
        hasFocus = self.HasFocus()
        if hasFocus and event.GetKeyCode() == ord(" "):
            self.ReleaseMouseIfCaptured()   
            self.state = "normal"
            self.PostEvent()
        elif hasFocus and self.HasCapture() and not wx.GetKeyState(wx.WXK_RETURN):
            self.ReleaseMouseIfCaptured()   
            self.state = "normal"
        self.SetFocus() 
        self.Refresh()
        event.Skip()
        print('OnKeyUp')

    def OnMouseEnter(self, event):
        if self.state != "disabled":
            self.state = "hover"
            self.Refresh()

    def OnMouseLeave(self, event):
        if self.state != "disabled":
            self.state = "normal"
            self.Refresh()

    def OnMouseDown(self, event):
        if self.state != "disabled":
            self.state = "pressed"
            self.Refresh()

    def OnMouseUp(self, event):
        self.ReleaseMouseIfCaptured()   
        if self.state == "pressed":
            self.state = "hover"
            self.Refresh()
            self.PostEvent()
        self.SetFocus()

    def PostEvent(self):
        print("Id: ", self.GetId())
        event = wx.CommandEvent(wx.EVT_BUTTON.typeId, self.GetId())
        event.SetInt(0)
        event.SetEventObject(self)
        wx.PostEvent(self.GetEventHandler(), event)
