import React from "react"
import { NotImplementedError } from "../../utils/errors"

import Tools from "../constants/tools"
import { Layouts, PosType } from "../constants/layouts"
import Cursor from "../constants/cursor"
import { toSnakeCase } from "../utils/utils"
import EditableDiv from "../../components/editableDiv"


import WidgetContainer from "../constants/containers"
import { DragContext } from "../../components/draggable/draggableContext"
import { isNumeric, removeKeyFromObject } from "../../utils/common"
import { info } from "autoprefixer"
import { message } from "antd"


// TODO: make it possible to apply widgetInnerStyle on load

// FIXME: the drag drop indicator is not going invisible if the drop happens on the child

// FIXME: once the width and height is set to fit-content, it can no longer be resized
const ATTRS_KEYS = ['value', 'label', 'tool', 'onChange', 'options', 'toolProps'] // these are attrs keywords, don't use these keywords as keys while defining the attrs property or serializing


/**
 * Base class to be extended
 */
class Widget extends React.Component {

    static widgetType = "widget"

    static requirements = [] // requirements for the widgets (libraries) eg: tkvideoplayer, tktimepicker
    static requiredImports = [] // import statements

    // static contextType = ActiveWidgetContext

    constructor(props) {
        super(props)

        const { id, widgetName, canvasRef } = props
        // console.log("Id: ", id)
        // this id has to be unique inside the canvas, it will be set automatically and should never be changed
        this.__id = id
        this.canvas = canvasRef?.current || null // canvasContainerRef, because some events work properly only if attached to the container

        // this._selected = false
        this._disableResize = false
        this._disableSelection = false

        this.minSize = { width: 10, height: 10 } // disable resizing below this number
        this.maxSize = { width: 2000, height: 2000 } // disable resizing above this number

        this.cursor = Cursor.POINTER

        this.icon = "" // antd icon name representing this widget

        this.elementRef = React.createRef() // this is the outer ref for draggable area
        this.swappableAreaRef = React.createRef() // helps identify if the users intent is to swap or drop inside the widget
        this.innerAreaRef = React.createRef() // this is the inner area where swap is prevented and only drop is accepted

        this.functions = {
            "load": { "args1": "number", "args2": "string" }
        }

        // This indicates if the draggable can be dropped on this widget, set this to null to disable drops
        this.droppableTags = {} 
        

        this.state = {
            zIndex: 0,
            selected: false,
            widgetName: widgetName || 'widget', // this will later be converted to variable name
            enableRename: false, // will open the widgets editable div for renaming

            parentLayout: null, // depending on the parents layout the child will behave

            isDragging: false, //  tells if the widget is currently being dragged
            dragEnabled: true,

            widgetContainer: WidgetContainer.CANVAS, // what is the parent of the widget

            showDroppableStyle: { // shows the droppable indicator
                allow: false,
                show: false,
            },

            pos: { x: 0, y: 0 },
            size: { width: 100, height: 100 },
            fitContent: {width: false, height: false},
            positionType: PosType.ABSOLUTE,

            widgetOuterStyling: {
                // responsible for stuff like position, grid placement etc
            },
            widgetInnerStyling: {
                // use for widget's inner styling
                backgroundColor: "#fff",
                display: "flex",
                flexDirection: "row",
                gap: 10,
                flexWrap: "wrap"
            },

            attrs: {
                styling: {
                    backgroundColor: {
                        label: "Background Color",
                        tool: Tools.COLOR_PICKER, // the tool to display, can be either HTML ELement or a constant string
                        value: "#fff",
                        onChange: (value) => {
                            this.setWidgetInnerStyle("backgroundColor", value)
                            this.setAttrValue("styling.backgroundColor", value)
                        }
                    },
                    
                    label: "Styling"
                },
                layout: {
                    label: "Layout",
                    tool: Tools.LAYOUT_MANAGER, // the tool to display, can be either HTML ELement or a constant string
                    value: {
                        layout: "flex",
                        direction: "row",
                        // grid: {
                        //     rows: 12,
                        //     cols: 12
                        // },
                        gap: 10,
                    },
                    toolProps: {
                        options: [
                            { value: "flex", label: "Flex" },
                            { value: "grid", label: "Grid" },
                            { value: "place", label: "Place" },
                        ],
                    },
                    onChange: (value) => {
                        // this.setAttrValue("layout", value)
                        this.setLayout(value)
                    }
                },
                // events: {
                //     event1: {
                //         tool: Tools.EVENT_HANDLER,
                //         value: ""
                //     }
                // }
            },
        }

        this.getElement = this.getElement.bind(this)

        this.getId = this.getId.bind(this)

        this.getPos = this.getPos.bind(this)
        this.getSize = this.getSize.bind(this)
        this.getWidgetName = this.getWidgetName.bind(this)
        this.getWidgetType = this.getWidgetType.bind(this)
        this.getBoundingRect = this.getBoundingRect.bind(this)

        this.getLayout = this.getLayout.bind(this)
        this.getParentLayout = this.getParentLayout.bind(this)

        this.getAttrValue = this.getAttrValue.bind(this)
        this.getToolbarAttrs = this.getToolbarAttrs.bind(this)

        this.generateCode = this.generateCode.bind(this)

        this.getImports = this.getImports.bind(this)
        this.getRequirements = this.getRequirements.bind(this)

        // this.openRenaming = this.openRenaming.bind(this)

        this.isSelected = this.isSelected.bind(this)

        this.setPos = this.setPos.bind(this)
        this.setAttrValue = this.setAttrValue.bind(this)
        this.setWidgetName = this.setWidgetName.bind(this)
        
        this.setWidgetInnerStyle = this.setWidgetInnerStyle.bind(this)
        this.setWidgetOuterStyle = this.setWidgetOuterStyle.bind(this)

        this.setPosType = this.setPosType.bind(this)
        this.setParentLayout = this.setParentLayout.bind(this)

        this.load = this.load.bind(this)
        this.serialize = this.serialize.bind(this)
        this.serializeAttrsValues = this.serializeAttrsValues.bind(this)

        this.hideDroppableIndicator = this.hideDroppableIndicator.bind(this)

        this.getRenderSize = this.getRenderSize.bind(this)
        this.getInnerRenderStyling = this.getInnerRenderStyling.bind(this)

    }

    componentDidMount() {

        if (this.state.attrs.layout){
            this.setLayout(this.state.attrs.layout.value)
            // console.log("prior layout: ", this.state.attrs.layout.value)
        }
        
        if (this.state.attrs.styling.backgroundColor)
            this.setWidgetInnerStyle('backgroundColor', this.state.attrs.styling?.backgroundColor.value || "#fff")

        this.load(this.props.initialData || {}) // load the initial data

    }

    componentWillUnmount() {
    }

    updateState = (newState, callback) => {

        // FIXME: maximum recursion error when updating size, color etc
        this.setState(newState, () => {

            const { onWidgetUpdate } = this.props
            if (onWidgetUpdate) {
                onWidgetUpdate(this.__id)
            }

            // const { activeWidgetId, updateToolAttrs } = this.context

            // if (activeWidgetId === this.__id)
            //     updateToolAttrs(this.getToolbarAttrs())

            if (callback) callback()

        })
    }

    _getWidgetMethods = () => {
        return {
            rename: this.setWidgetName,
            resize: this.setWidgetSize,
            setWidgetAttrs: this.setAttrValue,
        }
    }

    getToolbarAttrs() {

        return ({
            id: this.__id,
            widgetName: {
                label: "Widget Name",
                tool: Tools.INPUT, // the tool to display, can be either HTML ELement or a constant string
                toolProps: { placeholder: "Widget name", maxLength: 40 },
                value: this.state.widgetName,
                onChange: (value) => this.setWidgetName(value)
            },
            size: {
                label: "Size",
                display: "horizontal",
                width: {
                    label: "Width",
                    tool: Tools.NUMBER_INPUT, // the tool to display, can be either HTML ELement or a constant string
                    toolProps: { placeholder: "width", max: this.maxSize.width, min: this.minSize.width },
                    value: this.state.size.width || 100,
                    onChange: (value) => this.setWidgetSize(value, null)
                },
                height: {
                    label: "Height",
                    tool: Tools.NUMBER_INPUT,
                    toolProps: { placeholder: "height", max: this.maxSize.height, min: this.minSize.height },
                    value: this.state.size.height || 100,
                    onChange: (value) => this.setWidgetSize(null, value)
                },
                fitWidth: {
                    label: "Fit width",
                    tool: Tools.CHECK_BUTTON,
                    value: this.state.fitContent.width,
                    onChange: (value) => {  
                        this.updateState((prev) => ({
                            fitContent: {...prev.fitContent, width: value}
                        }))
                    }
                },
                fitHeight: {
                    label: "Fit height",
                    tool: Tools.CHECK_BUTTON,
                    value: this.state.fitContent.height,
                    onChange: (value) => {  
                        this.updateState((prev) => ({
                            fitContent: {...prev.fitContent, height: value}
                        }))
                    }
                },
            },

            ...this.state.attrs,

        })
    }

    // TODO: add context menu items such as delete, add etc
    contextMenu() {

    }

    getVariableName() {
        return toSnakeCase(this.state.widgetName)
    }

    getWidgetName() {
        return this.state.widgetName
    }

    getWidgetType() {
        return this.constructor.widgetType
    }

    getRequirements(){
        return this.constructor.requirements
    }

    getImports(){
        return this.constructor.requiredImports
    }

    generateCode(){
        throw new NotImplementedError("generateCode() must be implemented by the subclass")
    }

    getAttributes() {
        return this.state.attrs
    }

    getId() {
        return this.__id
    }

    select() {
        this.setState({
            selected: true
        })

    }

    deSelect() {
        this.setState({
            selected: false
        })
    }

    isSelected() {
        return this.state.selected
    }

    setPosType(positionType) {

        if (!Object.values(PosType).includes(positionType)) {
            throw Error(`The Position type can only be among: ${Object.values(PosType).join(", ")}`)
        }

        this.setState({
            positionType: positionType
        })

    }

    setPos(x, y) {

        this.setState({
            pos: { x, y }
        })

        // this.updateState({
        //     pos: { x, y }
        // })
    }


    getPos() {
        return this.state.pos
    }

    getProps() {
        return this.attrs
    }

    getBoundingRect() {
        return this.elementRef.current?.getBoundingClientRect()
    }

    getSize() {
        return this.state.size
    }

    getWidgetFunctions() {
        return this.functions
    }

    getId() {
        return this.__id
    }

    getElement() {
        return this.elementRef.current
    }

    hideDroppableIndicator(){
        console.log("hide drop indicator")
        this.setState({
            showDroppableStyle: {
                allow: false, 
                show: false
            }
        }, () => {
            console.log("hidden the drop indicator")
        })
    }

    /**
     * 
     * @param {string} path - eg: styling.backgroundColor
     * @returns 
     */
    removeAttr = (path) =>{

        const newAttrs = removeKeyFromObject(path, this.state.attrs)

        this.setState({
            attrs: newAttrs
        })

        return newAttrs

    }

    /**
     * Given the key as a path, sets the value for the widget attribute
     * @param {string} path - path to the key, eg: styling.backgroundColor
     * @param {any} value 
     */
    setAttrValue(path, value) {
        const keys = path.split('.')
        const lastKey = keys.pop()

        
        // Traverse the state and update the nested value immutably
        let newAttrs = { ...this.state.attrs }
        let nestedObject = newAttrs
        
        keys.forEach(key => {
            nestedObject[key] = { ...nestedObject[key] } // Ensure immutability
            nestedObject = nestedObject[key]
        })
        
        nestedObject[lastKey].value = value

        this.updateState({ attrs: newAttrs })
    }

    /**
     * Given the key as a path, retrieves the value for the widget attribute
     * @param {string} path - path to the key, eg: styling.backgroundColor
     * @returns {any} - the value at the given path
     */
    getAttrValue(path) {
        const keys = path.split('.')

        // Traverse the state and get the nested value
        let nestedObject = this.state.attrs

        for (const key of keys) {
            if (nestedObject[key] !== undefined) {
                nestedObject = nestedObject[key]
            } else {
                return undefined  // Return undefined if the key doesn't exist
            }
        }

        return nestedObject?.value  // Return the value (assuming it has a 'value' field)
    }

    /**
     * returns the path from the serialized attrs values, 
     * this is a helper function to remove any non-serializable data associated with attrs
     * eg: {"styling.backgroundColor": "#ffff", "layout": {layout: "flex", direction: "", grid: }}
     */
    serializeAttrsValues(){

        const serializeValues = (obj, currentPath = "") => {
            const result = {}

            for (let key in obj) {

                if (ATTRS_KEYS.includes(key)) continue // don't serialize these as separate keys

                if (typeof obj[key] === 'object' && obj[key] !== null) {
                    // If the key contains a value property
                    if (obj[key].hasOwnProperty('value')) {
                        const path = currentPath ? `${currentPath}.${key}` : key;

                        // If the value is an object, retain the entire value object
                        if (typeof obj[key].value === 'object' && obj[key].value !== null) {
                            result[path] = obj[key].value
                        } else {
                            result[`${path}`] = obj[key].value
                        }
                    }
                    // Continue recursion for nested objects
                    Object.assign(result, serializeValues(obj[key], currentPath ? `${currentPath}.${key}` : key))
                }
            }

            return result
        }

        return serializeValues(this.state.attrs)
    }

    setZIndex(zIndex) {
        this.setState({
            zIndex: zIndex
        })
    }

    setWidgetName(name) {
        this.updateState({
            widgetName: name.length > 0 ? name : this.state.widgetName
        })
    }

    /**
     * inform the child about the parent layout changes
     * @param {Layouts} parentLayout 
     */
    setParentLayout(parentLayout){

        if (!parentLayout){
            // if parent layout is null (i,e the widget is on the canvas)
            return {}
        } 

        const {layout, direction, gap} = parentLayout


        let updates = {
            parentLayout: parentLayout,
        }

        if (layout === Layouts.FLEX || layout === Layouts.GRID){

            updates = {
                ...updates,
                positionType: PosType.NONE
            }

        }else if (layout === Layouts.PLACE){
            updates = {
                ...updates,
                positionType: PosType.ABSOLUTE
            }
        }

        this.setState(updates)
    }

    getParentLayout(){
        return this.state.parentLayout
    }

    getLayout(){

        return this.state?.attrs?.layout?.value || Layouts.FLEX
    }

    setLayout(value) {
        const { layout, direction, grid = { rows: 1, cols: 1 }, gap = 10, align } = value

        // console.log("layout value: ", value)
        // FIXME: In grid layout the layout doesn't adapt to the size of the child if resized
        let widgetStyle = {
            ...this.state.widgetInnerStyling,
            display: layout !== Layouts.PLACE ? layout : "block",
            flexDirection: direction,
            gap: `${gap}px`,
            flexWrap: "wrap",
            gridTemplateColumns: "repeat(auto-fill, minmax(100px, 1fr))",
            gridTemplateRows: "repeat(auto-fill, minmax(100px, 1fr))",  
            // gridAutoRows: 'minmax(100px, auto)',  // Rows with minimum height of 100px, and grow to fit content
            // gridAutoCols: 'minmax(100px, auto)',  // Cols with minimum height of 100px, and grow to fit content
        }

        if (align === "start"){
            widgetStyle["alignItems"] = "flex-start"
        }else if (align === "center"){
            widgetStyle["alignItems"] = "center"
        }else if (align === "end"){
            widgetStyle["alignItems"] = "flex-end"
        }else{
            widgetStyle["alignItems"] = "unset"
        }

        this.updateState({
            widgetInnerStyling: widgetStyle
        })

        this.setAttrValue("layout", value)
        this.props.onLayoutUpdate({parentId: this.__id, parentLayout: value})// inform children about the layout update

    }

    getWidgetInnerStyle = (key) => {
        return this.state.widgetInnerStyling[key]
    }

    getWidgetOuterStyle = (key) => {
        return this.state.widgetOuterStyling[key]
    }

    /**
     * sets outer styling like grid placement etc, don't use this for background color, foreground color etc
     * @param {string} key - The string in react Style format
     * @param {string} value - Value of the style
     */
    setWidgetOuterStyle(key, value){
        const widgetStyle = {
            ...this.state.widgetOuterStyling,
            [key]: value
        }

        this.setState({
            widgetOuterStyling: widgetStyle
        })

    }

    /**
     * 
     * @param {string} key - The string in react Style format
     * @param {string} value - Value of the style
     */
    setWidgetInnerStyle(key, value) {
        
        const widgetStyle = {
            ...this.state.widgetInnerStyling,
            [key]: value
        }

        this.setState({
            widgetInnerStyling: widgetStyle
        })

    }

    /**
     * 
     * @param {number|null} width 
     * @param {number|null} height 
     */
    setWidgetSize(width, height) {

        const fitWidth = this.state.fitContent?.width
        const fitHeight = this.state.fitContent?.height

        if (fitWidth && fitHeight){
            message.warning("both width and height are set to fit-content, unset it to start resizing")
            return
        }

        const newSize = {
            width: Math.max(this.minSize.width, Math.min(width || this.state.size.width, this.maxSize.width)),
            height: Math.max(this.minSize.height, Math.min(height || this.state.size.height, this.maxSize.height)),
        }
        this.updateState({
            size: newSize
        })
    }

    setResize(pos, size) {
        // useful when resizing the widget relative to the canvas, sets all pos, and size
        this.updateState({
            size: size,
            pos: pos
        })
    }

    openRenaming() {
        this.setState({
            selected: true,
            enableRename: true
        })
    }

    closeRenaming() {
        this.setState({
            enableRename: false
        })
    }

    enableDrag = () => {
        this.setState({
            dragEnabled: true
        })
    }

    disableDrag = () => {
        this.setState({
            dragEnabled: false
        })
    }


    /**
     * 
     * serialize data for saving
     */
    serialize(){
        // NOTE: when serializing make sure, you are only passing serializable objects not functions or other
        return ({
            zIndex: this.state.zIndex,
            widgetName: this.state.widgetName,
            pos: this.state.pos,
            size: this.state.size,
            widgetContainer: this.state.widgetContainer,
            widgetInnerStyling: this.state.widgetInnerStyling,
            widgetOuterStyling: this.state.widgetOuterStyling,
            parentLayout: this.state.parentLayout,
            positionType: this.state.positionType,
            attrs: this.serializeAttrsValues() // makes sure that functions are not serialized
        })

    }

    /**
     * loads the data 
     * @param {object} data 
     * @param {() => void | undefined} callback - optional callback that will be called after load 
     */
    load(data, callback){

        if (Object.keys(data).length === 0) return // no data to load

        data = {...data} // create a shallow copy

        const {attrs, parentLayout, ...restData} = data


        let layoutUpdates = {
            parentLayout: parentLayout.layout || null
        }

        if (parentLayout?.layout === Layouts.FLEX || parentLayout?.layout === Layouts.GRID){

            layoutUpdates = {
                ...layoutUpdates,
                positionType: PosType.NONE
            }

        }else if (parentLayout?.layout === Layouts.PLACE){
            layoutUpdates = {
                ...layoutUpdates,
                positionType: PosType.ABSOLUTE
            }
        }

        const newData = {
            ...restData,
            ...layoutUpdates
        }

        this.setState(newData,  () => {
            // Updates attrs
            let newAttrs = { ...this.state.attrs }

            // Iterate over each path in the updates object
            Object.entries(attrs).forEach(([path, value]) => {
                const keys = path.split('.')
                const lastKey = keys.pop()

                // Traverse the nested object within attrs
                let nestedObject = newAttrs

                keys.forEach(key => {
                    nestedObject[key] = { ...nestedObject[key] } // Ensure immutability for each nested level
                    nestedObject = nestedObject[key]
                })

                // Set the value at the last key
                if (nestedObject[lastKey]) // TODO: remove this check, else won't be able to catch buggy data
                    nestedObject[lastKey].value = value
            })

            if (newAttrs?.styling?.backgroundColor){
                // some widgets don't have background color
                this.setWidgetInnerStyle("backgroundColor", newAttrs.styling.backgroundColor)
            }

            this.updateState({ attrs: newAttrs }, callback)

        })  

    }


    handleDragStart = (e, callback) => {
        e.stopPropagation()

        callback(this.elementRef?.current || null)

        // this.props.onWidgetDragStart(this.elementRef?.current)

        // Create custom drag image with full opacity, this will ensure the image isn't taken from part of the canvas
        const dragImage = this.elementRef?.current.cloneNode(true)
        dragImage.style.opacity = '1' // Ensure full opacity
        dragImage.style.position = 'absolute'
        dragImage.style.top = '-9999px' // Move it out of view

        document.body.appendChild(dragImage)
        const rect = this.elementRef?.current.getBoundingClientRect()
        // snap to mouse pos
        // const offsetX = e.clientX - rect.left
        // const offsetY = e.clientY - rect.top

        // snap to middle
        const offsetX = rect.width / 2
        const offsetY = rect.height / 2

        // Set the custom drag image with correct offset to avoid snapping to the top-left corner
        e.dataTransfer.setDragImage(dragImage, offsetX, offsetY)


        // Remove the custom drag image after some time to avoid leaving it in the DOM
        setTimeout(() => {
            document.body.removeChild(dragImage)
        }, 0)

        // NOTE: this line will prevent problem's such as self-drop or dropping inside its own children
        setTimeout(this.disablePointerEvents, 1)

        this.setState({ isDragging: true })

    }

    handleDragEnter = (e, draggedElement, setOverElement) => {

        if (!draggedElement || !draggedElement.getAttribute("data-drag-start-within")){
            // if the drag is starting from outside (eg: file drop) or if drag doesn't exist
            return
        }

        const dragEleType = draggedElement.getAttribute("data-draggable-type")

        // console.log("Drag entering...", dragEleType, draggedElement, this.droppableTags)
        // FIXME:  the outer widget shouldn't be swallowed by inner widget
        if (draggedElement === this.elementRef.current) {
            // prevent drop on itself, since the widget is invisible when dragging, if dropped on itself, it may consume itself
            return
        }

        setOverElement(this.elementRef.current) // provide context to the provider

        let showDrop = {
            allow: true,
            show: true
        }

        const allowDrop = (this.droppableTags && this.droppableTags !== null && (Object.keys(this.droppableTags).length === 0 ||
            (this.droppableTags.include?.length > 0 && this.droppableTags.include?.includes(dragEleType)) ||
            (this.droppableTags.exclude?.length > 0 && !this.droppableTags.exclude?.includes(dragEleType))
        ))

        if (allowDrop) {
            showDrop = {
                allow: true,
                show: true
            }

        } else {
            showDrop = {
                allow: false,
                show: true
            }
        }

        this.setState({
            showDroppableStyle: showDrop
        })

    }

    handleDragOver = (e, draggedElement) => {

        if (!draggedElement || !draggedElement.getAttribute("data-drag-start-within")){
            // if the drag is starting from outside (eg: file drop) or if drag doesn't exist
            return
        }

        if (draggedElement === this.elementRef.current) {
            // prevent drop on itself, since the widget is invisible when dragging, if dropped on itself, it may consume itself
            return
        }

        // console.log("Drag over: ", e.dataTransfer.getData("text/plain"), e.dataTransfer)
        const dragEleType = draggedElement.getAttribute("data-draggable-type")

        const allowDrop = (this.droppableTags && this.droppableTags !== null && (Object.keys(this.droppableTags).length === 0 ||
            (this.droppableTags.include?.length > 0 && this.droppableTags.include?.includes(dragEleType)) ||
            (this.droppableTags.exclude?.length > 0 && !this.droppableTags.exclude?.includes(dragEleType))
        ))

        if (allowDrop) {
            e.preventDefault() // NOTE: this is necessary to allow drop to take place
        }

    }

    handleDropEvent = (e, draggedElement, widgetClass = null) => {

        if (!draggedElement || !draggedElement.getAttribute("data-drag-start-within")){
            // if the drag is starting from outside (eg: file drop) or if drag doesn't exist
            return
        }

        e.preventDefault()
        e.stopPropagation()
    
        // FIXME: sometimes the elements showDroppableStyle is not gone, when dropping on the same widget
        this.setState({
            showDroppableStyle: {
                allow: false,
                show: false
            }
        })


        if (draggedElement === this.elementRef.current){
            // prevent drop on itself, since the widget is invisible when dragging, if dropped on itself, it may consume itself
            return 
        }

        let currentElement = e.currentTarget
        while (currentElement) {
            if (currentElement === draggedElement) {
                // if the parent is dropped accidentally into the child don't allow drop
                // console.log("Dropped into a descendant element, ignoring drop")
                return // Exit early to prevent the drop
            }
            currentElement = currentElement.parentElement // Traverse up to check ancestors
        }

        const container = draggedElement.getAttribute("data-container")

        const thisContainer = this.elementRef.current.getAttribute("data-container")
        // console.log("Dropped as swappable: ", e.target, this.swappableAreaRef.current.contains(e.target))
        // If swaparea is true, then it swaps instead of adding it as a child, also make sure that the parent widget(this widget) is on the widget and not on the canvas
        const swapArea = (this.swappableAreaRef.current.contains(e.target) && !this.innerAreaRef.current.contains(e.target) && thisContainer === WidgetContainer.WIDGET)

        const dragEleType = draggedElement.getAttribute("data-draggable-type")

        const allowDrop = (this.droppableTags && this.droppableTags !== null && (Object.keys(this.droppableTags).length === 0 ||
            (this.droppableTags.include?.length > 0 && this.droppableTags.include?.includes(dragEleType)) ||
            (this.droppableTags.exclude?.length > 0 && !this.droppableTags.exclude?.includes(dragEleType))
        ))

        if (!allowDrop && !swapArea) {
            // only if both swap and drop is not allowed return, if swap is allowed continue
            return  
        }
        // TODO: check if the drop is allowed
        if ([WidgetContainer.CANVAS, WidgetContainer.WIDGET].includes(container)) {
            // console.log("Dropped on meee: ", swapArea, this.swappableAreaRef.current.contains(e.target), thisContainer)

            this.props.onAddChildWidget({
                event: e,
                parentWidgetId: this.__id,
                dragElementID: draggedElement.getAttribute("data-widget-id"),
                swap: swapArea || false
            })

        } else if (container === WidgetContainer.SIDEBAR) {

            // console.log("Dropped on Sidebar: ", this.__id)
            this.props.onCreateWidgetRequest(widgetClass, ({ id, widgetRef }) => {
                this.props.onAddChildWidget({
                                            event: e, 
                                            parentWidgetId: this.__id, 
                                            dragElementID: id 
                                        }) //  if dragged from the sidebar create the widget first
            })

        }

    }


    handleDragLeave = (e, draggedElement, overElement) => {

        e.preventDefault()
        e.stopPropagation()

        const rect = this.getBoundingRect()
        
        const {clientX, clientY} = e
        
        const isInBoundingBox = (clientX >= rect.left && clientX <= rect.right &&
            clientY >= rect.top && clientY <= rect.bottom)
            
        // if (!e.currentTarget.contains(draggedElement)) {
        if (!isInBoundingBox) {
            // FIXME: if the mouse pointer is over this widget's child, then droppable style should be invisible
            // only if the dragging element is not within the rect of this element remove the dragging rect
            this.setState({
                showDroppableStyle: {
                    allow: false,
                    show: false
                }
            }, () => {
                // console.log("Drag left", this.state.showDroppableStyle)
            })

        }
    }

    handleDragEnd = (callback) => {
        callback()
        this.setState({ isDragging: false })
        this.enablePointerEvents()

        // this.props.onWidgetDragEnd(this.elementRef?.current)
    }

    disablePointerEvents = () => {

        if (this.elementRef.current)
            this.elementRef.current.style.pointerEvents = "none"
    }

    enablePointerEvents = () => {
        if (this.elementRef.current)
            this.elementRef.current.style.pointerEvents = "auto"
    }

    /**
     * Note: you must implement this method in subclass, if you want children make sure to pass
     * {this.props.children}, to modify the style add this.state.widgetInnerStyling
    */
    renderContent() {
        // throw new NotImplementedError("render method has to be implemented")
        return (
            <div className="tw-w-full tw-h-full tw-p-2 tw-content-start tw-rounded-md tw-overflow-hidden" style={this.state.widgetInnerStyling}>
                {this.props.children}
            </div>
        )
    }

    getInnerRenderStyling(){
        const {width, height, minWidth, minHeight} = this.getRenderSize()

        const styling = {
            ...this.state.widgetInnerStyling,
            width, 
            height,
            minWidth, 
            minHeight
        }
        return styling
    }

    getRenderSize(){

        let width = isNumeric(this.state.size.width) ? `${this.state.size.width}px` : this.state.size.width
        let height = isNumeric(this.state.size.height) ? `${this.state.size.height}px` : this.state.size.height

        let fitWidth = this.state.fitContent.width
        let fitHeight = this.state.fitContent.height
        
        if (fitWidth){
            width = "max-content"
        }

        if (fitHeight){
            height = "max-content"
        }

        // if fit width is enabled then the minsize is the resizable size
        let minWidth = fitWidth ? this.state.size.width : this.minSize.width
        let minHeight = fitHeight ? this.state.size.height : this.minSize.height
        
        return {width, height, minWidth, minHeight}

    }

    /**
     * This is an internal methods don't override
     * @returns {HTMLElement}
     */
    render() {  

        const {width, height, minWidth, minHeight} = this.getRenderSize()
        
        // NOTE: first check tkinter behaviour with the width and height

        let outerStyle = {
            ...this.state.widgetOuterStyling,
            cursor: this.cursor,
            zIndex: this.state.zIndex,
            position: this.state.positionType, //  don't change this if it has to be movable on the canvas
            top: `${this.state.pos.y}px`,
            left: `${this.state.pos.x}px`,
            width: width,
            height: height,
            minWidth: minWidth, 
            minHeight: minHeight,
            opacity: this.state.isDragging ? 0.3 : 1,
        }

        // const boundingRect = this.getBoundingRect

        // FIXME: if the parent container has tw-overflow-none, then the resizable indicator are also hidden
        return (

            <DragContext.Consumer>
                {
                    ({ draggedElement, widgetClass, onDragStart, onDragEnd, overElement, setOverElement }) => {

                        
                        return ( 
                            <div data-widget-id={this.__id}
                                ref={this.elementRef}
                                className="tw-shadow-xl tw-w-fit tw-h-fit"
                                style={outerStyle}
                                data-draggable-type={this.getWidgetType()} // helps with droppable 
                                data-container={this.state.widgetContainer} // indicates how the canvas should handle dragging, one is sidebar other is canvas

                                data-drag-start-within // this attribute indicates that the drag is occurring from within the project and not a outside file drop

                                draggable={this.state.dragEnabled}

                                onDragOver={(e) => this.handleDragOver(e, draggedElement)}
                                onDrop={(e) => {this.handleDropEvent(e, draggedElement, widgetClass); onDragEnd()}}

                                onDragEnter={(e) => this.handleDragEnter(e, draggedElement, setOverElement)}
                                onDragLeave={(e) => this.handleDragLeave(e, draggedElement, overElement)}

                                onDragStart={(e) => this.handleDragStart(e, onDragStart)}
                                onDragEnd={(e) => this.handleDragEnd(onDragEnd)}
                            >
                                {/* FIXME: Swappable when the parent layout is flex/grid and gap is more, this trick won't work, add bg color to check */}
                                {/* FIXME: Swappable, when the parent layout is gap is 0, it doesn't work well */}
                                <div className="tw-relative tw-w-full  tw-h-full tw-top-0 tw-left-0"
                                        
                                    >
                                    
                                    <div className={`tw-absolute tw-top-[-5px] tw-left-[-5px] 
                                                        tw-border-1 tw-opacity-0 tw-border-solid tw-border-black
                                                        tw-w-full tw-h-full tw-bg-red-400
                                                        tw-scale-[1.1] tw-opacity-1 tw-z-[-1] `}
                                        style={{
                                            width: "calc(100% + 10px)",
                                            height: "calc(100% + 10px)",
                                        }}
                                        ref={this.swappableAreaRef}
                                        // swapable area
                                    >
                                        {/* helps with swappable: if the mouse is in this area while hovering/dropping, then swap */}
                                    </div>

                                    <div className="tw-relative tw-top-0 tw-left-0 tw-w-full tw-h-full" ref={this.innerAreaRef}
                                        >
                                        {this.renderContent()}
                                    </div>
                                    {
                                        // show drop style on drag hover
                                        draggedElement && this.state.showDroppableStyle.show &&
                                        <div className={`${this.state.showDroppableStyle.allow ? "tw-border-blue-600" : "tw-border-red-600"} 
                                                                tw-absolute tw-top-[-5px] tw-left-[-5px] tw-w-full tw-h-full tw-z-[2]
                                                                tw-border-2 tw-border-dashed  tw-rounded-lg tw-pointer-events-none

                                                                `}
                                            style={{
                                                width: "calc(100% + 10px)",
                                                height: "calc(100% + 10px)",
                                            }}
                                        >
                                        </div>
                                    }
                                    {/* FIXME: the resize handles get clipped in parent container */}
                                    <div className={`tw-absolute tw-z-[-1] tw-bg-transparent tw-top-[-10px] tw-left-[-10px] tw-opacity-100 
                                                    tw-w-full tw-h-full 
                                                    ${this.state.selected ? 'tw-border-2 tw-border-solid tw-border-blue-500' : 'tw-hidden'}`}
                                        style={{
                                            width: "calc(100% + 20px)",
                                            height: "calc(100% + 20px)",
                                            zIndex: -1,
                                        }}
                                    >

                                        <div className={`"tw-relative tw-w-full  tw-h-full"`}> {/* ${this.state.isDragging ? "tw-pointer-events-none" : "tw-pointer-events-auto"} */}
                                            <EditableDiv value={this.state.widgetName} onChange={this.setWidgetName}
                                                maxLength={40}
                                                openEdit={this.state.enableRename}
                                                className="tw-text-sm tw-w-fit tw-max-w-[160px] tw-text-clip tw-min-w-[150px] 
                                                                        tw-overflow-hidden tw-absolute tw--top-6 tw-h-6"
                                            />

                                            <div
                                                className="tw-w-2 tw-h-2 tw-absolute  tw--left-1 tw--top-1 tw-bg-blue-500"
                                                style={{ cursor: Cursor.NW_RESIZE }}
                                                onMouseDown={(e) => {
                                                    e.stopPropagation()
                                                    e.preventDefault()
                                                    this.props.onWidgetResizing("nw")
                                                    this.setState({ dragEnabled: false })
                                                }}
                                                onMouseUp={() => this.setState({ dragEnabled: true })}
                                            />
                                            <div
                                                className="tw-w-2 tw-h-2 tw-absolute tw--right-1 tw--top-1 tw-bg-blue-500"
                                                style={{ cursor: Cursor.SW_RESIZE }}
                                                onMouseDown={(e) => {
                                                    e.stopPropagation()
                                                    e.preventDefault()
                                                    this.props.onWidgetResizing("ne")
                                                    this.setState({ dragEnabled: false })
                                                }}
                                                onMouseUp={() => this.setState({ dragEnabled: true })}
                                            />
                                            <div
                                                className="tw-w-2 tw-h-2 tw-absolute tw--left-1 tw--bottom-1 tw-bg-blue-500"
                                                style={{ cursor: Cursor.SW_RESIZE }}
                                                onMouseDown={(e) => {
                                                    e.stopPropagation()
                                                    e.preventDefault()
                                                    this.props.onWidgetResizing("sw")
                                                    this.setState({ dragEnabled: false })
                                                }}
                                                onMouseUp={() => this.setState({ dragEnabled: true })}
                                            />
                                            <div
                                                className="tw-w-2 tw-h-2 tw-absolute tw--right-1 tw--bottom-1 tw-bg-blue-500"
                                                style={{ cursor: Cursor.SE_RESIZE }}
                                                onMouseDown={(e) => {
                                                    e.stopPropagation()
                                                    e.preventDefault()
                                                    this.props.onWidgetResizing("se")
                                                    this.setState({ dragEnabled: false })
                                                }}
                                                onMouseUp={() => this.setState({ dragEnabled: true })}
                                            />

                                        </div>

                                    </div>


                                </div>
                            </div>
                            )
                    }
                }

            </DragContext.Consumer>
        )

    }

}


export default Widget