using Pilz.Drawing;
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Linq;
using System.Windows.Forms;

namespace Pilz.UI;


public class PaintingControl : UserControl, IPaintingObjectContainer
{

    private PaintingObject curObjMouseDown = null;
    private Color bgColor = Color.White;
    private Point startMousePos = default;
    private Point lastMousePos = default;
    private int lastHashCode = 0;
    private Point calcOffset_MouseOnTab = Point.Empty;
    private bool calcOffset_IsActive = false;
    private PointF calcOffset_LastOffset = PointF.Empty;

    private new Color ForeColor { get; set; }
    private new Font Font { get; set; }
    private new string Text { get; set; }

    public PointF Offset { get; set; } = PointF.Empty;
    public PaintingObjectList PaintingObjects { get; private set; }
    public bool VisibleForMouseEvents { get; set; } = true;
    public bool AutoAreaSelection { get; set; } = true;
    public bool AutoSingleSelection { get; set; } = true;
    public bool AutoMultiselection { get; set; } = true;
    public bool AutoRemoveSelection { get; set; } = true;
    public DashStyle AreaSelectionDashStyle { get; set; } = DashStyle.DashDot;
    public Color AreaSelectionColor { get; set; } = Color.CornflowerBlue;
    public bool AutoMoveObjects { get; set; } = true;
    private bool _IsAreaSelecting = false;
    public bool IsMovingObjects { get; private set; } = false;
    public bool GridEnabled { get; set; } = true;
    public bool GridVisible { get; set; } = false;
    public Size GridChunkSize { get; set; } = new Size(20, 20);
    public Color GridColor { get; set; } = Color.LightGray;
    public DelegateDrawPaintingControlGridMethode DrawGridMethode { get; set; } = DefaultDrawMethodes.DrawGrid;
    public DelegateDrawPaintingControlAreaSelectionMethode DrawAreaSelectionMethode { get; set; } = DefaultDrawMethodes.DrawAreaSelection;
    private SizeF _ZoomFactor = new SizeF(1f, 1f);

    private int _stopDrawing = -1;
    private Image bufferedImg = null;

    private bool pressedShift = false;
    private bool pressedControl = false;
    private bool pressedAlt = false;

    private Dictionary<PaintingObject, PointF> savedPos = new Dictionary<PaintingObject, PointF>();

    public event SelectionChangedEventHandler SelectionChanged;

    public delegate void SelectionChangedEventHandler(object sender, PaintingObjectEventArgs e);
    public event PaintingObjectAddedEventHandler PaintingObjectAdded;

    public delegate void PaintingObjectAddedEventHandler(object sender, PaintingObjectEventArgs e);
    public event PaintingObjectRemovedEventHandler PaintingObjectRemoved;

    public delegate void PaintingObjectRemovedEventHandler(object sender, PaintingObjectEventArgs e);
    public event AfterScrollingDoneEventHandler AfterScrollingDone;

    public delegate void AfterScrollingDoneEventHandler(object sender, EventArgs e);
    public event ZoomFactorChangedEventHandler ZoomFactorChanged;

    public delegate void ZoomFactorChangedEventHandler(object sender, EventArgs e);

    public PaintingObject[] SelectedObjects
    {
        get
        {
            var objs = new List<PaintingObject>();

            foreach (PaintingObject obj in PaintingObjects)
            {
                if (obj.Selected)
                    objs.Add(obj);
            }

            return objs.ToArray();
        }
    }

    public bool IsLayoutSuspended
    {
        get
        {
            return Convert.ToInt32(GetType().GetField("layoutSuspendCount", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic).GetValue(this)) != 0;
        }
    }

    public bool StopDrawing
    {
        get
        {
            return _stopDrawing > -1;
        }
    }

    public override Color BackColor
    {
        get
        {
            return bgColor;
        }
        set
        {
            bgColor = value;
            base.BackColor = value;
            // If value <> Color.Transparent Then
            // MyBase.BackColor = value
            // End If
        }
    }
    public bool IsAreaSelecting
    {
        get
        {
            return _IsAreaSelecting && startMousePos != lastMousePos;
        }
    }
    public SizeF ZoomFactor
    {
        get
        {
            return _ZoomFactor;
        }
        set
        {
            if (_ZoomFactor != value)
            {
                _ZoomFactor = value;
                ResetAllBufferedImages();
                ZoomFactorChanged?.Invoke(this, new EventArgs());
            }
        }
    }

    public PaintingControl()
    {
        PaintingObjects = new(this);
        PaintingObjectResizing.CheckEnabled += PaintingObjectResizing_CheckEnabled;
        DoubleBuffered = true;
        KeyDown += CheckKeyDown;
        KeyUp += CheckKeyDown;
        MouseClick += CheckMouseClick;
        MouseDown += CheckMouseDown;
        MouseUp += CheckMouseUp;
        MouseMove += CheckMouseMove;
        PaintingObjectAdded += PaintingControl_PaintingObjectAdded;
        PaintingObjectRemoved += PaintingControl_PaintingObjectAdded;
        MouseWheel += CheckMouseWheel;
    }

    ~PaintingControl()
    {
        PaintingObjectResizing.CheckEnabled -= PaintingObjectResizing_CheckEnabled;
    }

    private void PaintingObjectResizing_CheckEnabled(PaintingObjectResizing sender, ref bool enabled)
    {
        if (PaintingObjects.Contains(sender.PaintingObject) && pressedAlt)
            enabled = false;
    }

    private void ResetAllBufferedImages()
    {
        foreach (PaintingObject ob in PaintingObjects)
            ob.ResetImageBuffer();
        Refresh();
    }

    protected void CheckKeyDown(object sender, KeyEventArgs e)
    {
        pressedShift = e.Shift;
        pressedControl = e.Control;
        pressedAlt = e.Alt;
    }

    internal RectangleF AreaSelectionRectangle
    {
        get
        {
            return HelpfulDrawingFunctions.GetRectangle(startMousePos, lastMousePos);
        }
    }

    protected void CheckMouseClick(object sender, MouseEventArgs e)
    {
        foreach (PaintingObject obj in GetObjects(new Point((int)Math.Round(e.X + Offset.X), (int)Math.Round(e.Y + Offset.Y))))
        {
            if (!obj.MouseTransparency)
                obj.RaiseMouseClick(GetMouseEventArgs(e, obj));
        }
    }

    protected void CheckMouseDown(object sender, MouseEventArgs e)
    {
        lastMousePos = new Point((int)Math.Round(e.X + Offset.X), (int)Math.Round(e.Y + Offset.Y));

        curObjMouseDown = GetObjects(lastMousePos).Where(n => !n.MouseTransparency).LastOrDefault();
        curObjMouseDown?.RaiseMouseDown(GetMouseEventArgs(e, curObjMouseDown));

        if (curObjMouseDown is null || !curObjMouseDown.Selected || pressedControl)
        {
            bool hasMovedObjects = false;
            if (IsMovingObjects)
            {
                foreach (PaintingObject obj in GetSelectedObjects())
                {
                    if (HelpfulDrawingFunctions.IsPointInRectangle(lastMousePos, obj.Rectangle))
                    {
                        hasMovedObjects = true;
                        break;
                    }
                }
            }

            if (!hasMovedObjects && !_IsAreaSelecting)
            {
                var selChanged = new List<PaintingObject>();

                if (AutoRemoveSelection && !pressedControl)
                {
                    foreach (PaintingObject obj in PaintingObjects)
                    {
                        if (obj.Selected)
                        {
                            obj.SelectedDirect = false;
                            if (!selChanged.Contains(obj))
                                selChanged.Add(obj);
                        }
                    }
                }

                if (AutoSingleSelection && curObjMouseDown is not null)
                {
                    var objtosel = curObjMouseDown;
                    if (objtosel.EnableSelection)
                    {
                        objtosel.SelectedDirect = !objtosel.Selected;
                        if (!selChanged.Contains(objtosel))
                            selChanged.Add(objtosel);
                        else
                        {
                            selChanged.Remove(objtosel);
                        }
                    }
                }

                SelectionChanged?.Invoke(this, new PaintingObjectEventArgs(selChanged.ToArray()));
            }
        }

        if (pressedAlt)
        {

            calcOffset_MouseOnTab = new Point(e.X, e.Y);
            calcOffset_LastOffset = Offset;
            calcOffset_IsActive = true;
            Cursor = Cursors.Arrow;
        }

        else
        {

            switch (e.Button)
            {
                case MouseButtons.Left:
                    {
                        savedPos.Clear();
                        if (AutoMoveObjects)
                            SaveObjectPositions(e, GetSelectedObjects());
                        if (savedPos.Count > 0)
                            IsMovingObjects = true;
                        else if (AutoAreaSelection)
                        {
                            startMousePos = new Point((int)Math.Round(e.X + Offset.X), (int)Math.Round(e.Y + Offset.Y));
                            lastMousePos = startMousePos; // New Point(e.X - Offset.X, e.Y - Offset.Y)
                            _IsAreaSelecting = true;
                        }

                        break;
                    }
            }

        }
    }
    public void RaiseSelectionChanged()
    {
        SelectionChanged?.Invoke(this, new PaintingObjectEventArgs(SelectedObjects));
    }
    protected void CheckMouseUp(object sender, MouseEventArgs e)
    {
        if (_IsAreaSelecting)
            _IsAreaSelecting = false;

        if (IsMovingObjects)
        {
            IsMovingObjects = false;
            foreach (PaintingObject obj in GetSelectedObjects())
                obj.RaiseMoved(new EventArgs());
            AutoArrangeToGrid();
        }

        if (curObjMouseDown is not null)
        {
            if (!curObjMouseDown.MouseTransparency)
                curObjMouseDown.RaiseMouseUp(GetMouseEventArgs(e, curObjMouseDown));
            curObjMouseDown = null;
        }

        if (calcOffset_IsActive)
        {
            calcOffset_IsActive = false;
            Cursor = Cursors.Default;
            CalcNewOffset(e.Location);
            AfterScrollingDone?.Invoke(this, new EventArgs());
        }
    }
    protected void CheckMouseMove(object sender, MouseEventArgs e)
    {
        if (_IsAreaSelecting || IsMovingObjects)
            lastMousePos = new Point((int)Math.Round(e.X + Offset.X), (int)Math.Round(e.Y + Offset.Y));

        if (_IsAreaSelecting)
            SelectControlsInArea();

        if (IsMovingObjects)
            UpdateObjectPositions(e);

        foreach (PaintingObject obj in GetObjects(new Point((int)Math.Round(e.X + Offset.X), (int)Math.Round(e.Y + Offset.Y))))
        {
            if (!obj.MouseTransparency)
                obj.RaiseMouseMove(GetMouseEventArgs(e, obj));
        }

        var topObj = GetObject(new Point((int)Math.Round(e.X + Offset.X), (int)Math.Round(e.Y + Offset.Y)), true);
        if (topObj is not null)
            Cursor = topObj.Cursor;
        else if (calcOffset_IsActive)
        {
            Cursor = Cursors.Arrow;
        }
        else
        {
            Cursor = Cursors.Default;
        }

        if (calcOffset_IsActive)
        {
            if (pressedAlt)
                CalcNewOffset(e.Location);
            else
            {
                calcOffset_IsActive = false;
            }
        }

        Refresh();
    }

    private void CalcNewOffset(Point newMousePos)
    {
        Offset = new PointF(calcOffset_LastOffset.X - (newMousePos.X - calcOffset_MouseOnTab.X), calcOffset_LastOffset.Y - (newMousePos.Y - calcOffset_MouseOnTab.Y));
        if (Offset.X < 0f)
            Offset = new PointF(0f, Offset.Y);
        if (Offset.Y < 0f)
            Offset = new PointF(Offset.X, 0f);
    }

    private PaintingObject[] GetSelectedObjects()
    {
        var objs = new List<PaintingObject>();

        foreach (PaintingObject obj in PaintingObjects)
        {
            if (obj.Selected)
                objs.Add(obj);
        }

        return objs.ToArray();
    }

    private void SaveObjectPositions(MouseEventArgs e, IList objs)
    {
        foreach (PaintingObject obj in objs)
        {
            if (!obj.HardcodedLocation && !savedPos.ContainsKey(obj))
            {
                savedPos.Add(obj, new PointF(e.X - obj.Location.X + Offset.X, e.Y - obj.Location.Y + Offset.Y));
                SaveObjectPositions(e, obj.PinnedObjects);
            }
        }
    }

    private void UpdateObjectPositions(MouseEventArgs e)
    {
        UpdateObjectPositions(e, GetSelectedObjects());
    }

    private void UpdateObjectPositions(MouseEventArgs e, IList<PaintingObject> objs, List<PaintingObject> movedObjs = null)
    {
        if (IsResizingObjs(objs))
            return;
        if (movedObjs is null)
            movedObjs = new List<PaintingObject>();

        SuspendDrawing();

        foreach (PaintingObject obj in objs)
        {
            var sp = savedPos[obj];

            if (!movedObjs.Contains(obj))
            {
                if (UpdateObjectPosition(e, obj, sp))
                    movedObjs.Add(obj);
            }

            if (obj.PinnedObjects.Count > 0)
            {
                UpdateObjectPositions(e, obj.PinnedObjects, movedObjs);
                movedObjs.AddRange(obj.PinnedObjects.ToArray());
            }
        }

        ResumeDrawing(false);
    }

    private bool UpdateObjectPosition(MouseEventArgs e, PaintingObject obj, PointF sp)
    {
        bool moved = false;
        var cancel = new CancelEventArgs(false);
        obj.RaiseMovingBeforePositionUpdated(cancel);

        if (!cancel.Cancel)
        {
            obj.Location = new Point((int)Math.Round(e.X - sp.X + Offset.X), (int)Math.Round(e.Y - sp.Y + Offset.Y));
            obj.RaiseMoving(new EventArgs());
            moved = true;
        }

        return moved;
    }

    private bool IsResizingObjs(IList<PaintingObject> objs)
    {
        foreach (PaintingObject obj in objs)
        {
            if (obj.IsResizing)
                return true;
        }
        return false;
    }

    private MouseEventArgs GetMouseEventArgs(MouseEventArgs e, PaintingObject obj)
    {
        return new MouseEventArgs(e.Button, e.Clicks, (int)Math.Round(e.X - obj.X + Offset.X), (int)Math.Round(e.Y - obj.Y + Offset.Y), e.Delta);
    }

    public PaintingObject GetObject(PointF p, bool UseExtRect = false)
    {
        PaintingObject val = null;

        for (int i = PaintingObjects.Count - 1; i >= 0; i -= 1)
        {
            var obj = PaintingObjects[i];

            if (val is null)
            {
                if (UseExtRect)
                {
                    if (HelpfulDrawingFunctions.IsPointInRectangle(p, obj.RectangleExtended))
                        val = obj;
                }
                else if (HelpfulDrawingFunctions.IsPointInRectangle(p, obj.Rectangle))
                {
                    val = obj;
                }
            }
        }

        return val;
    }

    public PaintingObject[] GetObjects(Point p)
    {
        var objs = new List<PaintingObject>();

        foreach (PaintingObject obj in PaintingObjects)
        {
            if (HelpfulDrawingFunctions.IsPointInRectangle(p, obj.RectangleExtended))
                objs.Add(obj);
        }

        return objs.ToArray();
    }

    public PaintingObject[] GetObjects(Point startPoint, Point endPoint)
    {
        return GetObjects(new Rectangle(startPoint, (Size)(endPoint - (Size)startPoint)));
    }

    public PaintingObject[] GetObjects(Rectangle rect)
    {
        var objs = new List<PaintingObject>();

        foreach (PaintingObject obj in PaintingObjects)
        {
            var objRect = obj.Rectangle;
            if (HelpfulDrawingFunctions.IsPointInRectangle(objRect.Location, rect) || HelpfulDrawingFunctions.IsPointInRectangle(objRect.Location + objRect.Size, rect) || HelpfulDrawingFunctions.IsPointInRectangle(new PointF(objRect.Left, objRect.Bottom), rect) || HelpfulDrawingFunctions.IsPointInRectangle(new PointF(objRect.Right, objRect.Top), rect))
                objs.Add(obj);
        }

        return objs.ToArray();
    }

    protected override CreateParams CreateParams
    {
        get
        {
            var cp = base.CreateParams;

            // If EnableRealTransparency Then
            // cp.ExStyle = cp.ExStyle Or &H20 'WS_EX_TRANSPARENT
            // End If

            return cp;
        }
    }

    /// <summary>
    /// Sorg dafür, dass Events durch dieses Control durchdringen zum Parnet-Control.
    /// </summary>
    /// <param name="m"></param>
    protected override void WndProc(ref Message m)
    {
        const nint WM_NCHITTEST = 0x84;
        const nint HTTRANSPARENT = -1;

        if (!VisibleForMouseEvents && m.Msg == WM_NCHITTEST)
            m.Result = HTTRANSPARENT;
        else
        {
            base.WndProc(ref m);
        }
    }

    protected override void OnPaintBackground(PaintEventArgs e)
    {
        // Stop Drawing directly to the parent
        SuspendLayout();

        // Draw Background
        // If Not EnableRealTransparency Then
        base.OnPaintBackground(e);
        // End If
    }

    protected override void OnPaint(PaintEventArgs e)
    {
        // Draw PaintingObjects stuff
        if (StopDrawing)
            e.Graphics.DrawImage(bufferedImg, Point.Empty);
        else
        {
            {
                var withBlock = e.Graphics;
                withBlock.SmoothingMode = SmoothingMode.HighQuality;
                withBlock.PixelOffsetMode = PixelOffsetMode.HighQuality;
                withBlock.PageUnit = GraphicsUnit.Pixel;
                withBlock.InterpolationMode = InterpolationMode.HighQualityBicubic;
            }

            if (GridVisible)
                DrawGridMethode?.Invoke(e, this, Offset);

            var baserect = new RectangleF(Offset, Size);

            foreach (PaintingObject obj in PaintingObjects)
            {
                if (obj.Visible && HelpfulDrawingFunctions.OverlapsTwoRectangles(obj.Rectangle, baserect))
                    obj.Draw(e, Offset);
            }

            if (_IsAreaSelecting)
                DrawAreaSelectionMethode?.Invoke(e, this, new PointF(startMousePos.X - Offset.X, startMousePos.Y - Offset.Y), new PointF(lastMousePos.X - Offset.X, lastMousePos.Y - Offset.Y));
        }

        // Do default Drawing Methode
        base.OnPaint(e);

        // Start Drawing directly to the Form
        ResumeLayout(false);
    }

    public new Graphics CreateGraphics()
    {
        return base.CreateGraphics();
    }

    public void PaintFullView(Graphics g)
    {
        foreach (PaintingObject obj in PaintingObjects)
        {
            if (obj.Visible)
                obj.Draw(g, PointF.Empty);
        }
    }

    private SizeF CalcTextSize(PaintingObject obj)
    {
        return CalcTextSize(obj, Parent.CreateGraphics());
    }

    private SizeF CalcTextSize(PaintingObject obj, Graphics g)
    {
        return g.MeasureString(obj.Text, obj.TextFont, (int)Math.Round(obj.Width));
    }

    private void SelectControlsInArea()
    {
        var rect = HelpfulDrawingFunctions.GetRectangle(startMousePos, lastMousePos);
        foreach (PaintingObject obj in PaintingObjects)
            obj.Selected = startMousePos.X >= lastMousePos.X ? HelpfulDrawingFunctions.OverlapsTwoRectangles(obj.Rectangle, rect) : HelpfulDrawingFunctions.RectangleContainsRectangle(rect, obj.Rectangle);
    }

    public void ArrangeToGrid(PaintingObject obj, bool snapPinnedObjects)
    {
        if (snapPinnedObjects || !IsPinnedObject(obj))
        {
            var zoomedGridChunkSize = new SizeF(GridChunkSize.Width * ZoomFactor.Width, GridChunkSize.Height * ZoomFactor.Height);

            int modTop = (int)Math.Round(obj.Y % zoomedGridChunkSize.Height);
            int modLeft = (int)Math.Round(obj.X % zoomedGridChunkSize.Width);

            int halfHeight = (int)Math.Round(zoomedGridChunkSize.Height / 2f);
            int halfWidth = (int)Math.Round(zoomedGridChunkSize.Width / 2f);

            void zoomLocation(PaintingObject obj2)
            {
                if (modTop > halfHeight)
                    obj2.Y += zoomedGridChunkSize.Height - modTop;
                else
                {
                    obj2.Y -= modTop;
                }

                if (modLeft > halfWidth)
                    obj2.X += zoomedGridChunkSize.Width - modLeft;
                else
                {
                    obj2.X -= modLeft;
                }
            };

            zoomLocation(obj);

            foreach (PaintingObject pinned in obj.PinnedObjects)
                zoomLocation(pinned);

            int modH = (int)Math.Round(obj.Height % zoomedGridChunkSize.Height);
            int modW = (int)Math.Round(obj.Width % zoomedGridChunkSize.Width);


            void zoomSize(PaintingObject obj2) { if (obj2.EnableResize && !obj2.HardcodedSize) { if (modH > halfHeight) obj2.Height += zoomedGridChunkSize.Height - modH; else { obj2.Height -= modH; } if (modW > halfWidth) obj2.Width += zoomedGridChunkSize.Width - modW; else { obj2.Width -= modW; } } };

            zoomSize(obj);

            foreach (PaintingObject pinned in obj.PinnedObjects)
                zoomSize(pinned);
        }
    }

    public bool IsPinnedObject(PaintingObject o)
    {
        foreach (PaintingObject obj in PaintingObjects)
        {
            if (obj.PinnedObjects.Contains(o))
                return true;
        }
        return false;
    }

    public void AutoArrangeToGrid()
    {
        if (GridEnabled)
        {
            foreach (PaintingObject obj in GetSelectedObjects())
            {
                if (obj.AutoAlignToGrid)
                    ArrangeToGrid(obj, false);
            }
            if (!StopDrawing)
                Refresh();
        }
    }

    public SizeF GetFullSize()
    {
        return GetFullSize(PaintingObjects);
    }

    public static SizeF GetFullSize(IEnumerable<PaintingObject> objects)
    {
        float curX = 0f;
        float curY = 0f;

        foreach (PaintingObject po in objects)
        {
            float myX = po.X + po.Width;
            if (curX < myX)
                curX = myX;
            float myY = po.Y + po.Height;
            if (curY < myY)
                curY = myY;
        }

        return new SizeF(curX + 20f, curY + 20f);
    }

    internal void RaisePaintingObjectAdded(PaintingObjectEventArgs args)
    {
        PaintingObjectAdded?.Invoke(this, args);
    }
    internal void RaisePaintingObjectRemoved(PaintingObjectEventArgs args)
    {
        PaintingObjectRemoved?.Invoke(this, args);
    }

    private void PaintingControl_PaintingObjectAdded(object sender, PaintingObjectEventArgs e)
    {
        // CalculateScrollValues()
    }

    private void CheckMouseWheel(object sender, MouseEventArgs e)
    {
        if (pressedAlt)
        {
            float val = (float)(e.Delta / 120d / 10d);
            ZoomFactor = new SizeF((float)Math.Max((double)(ZoomFactor.Width + val), 0.25d), (float)Math.Max((double)(ZoomFactor.Height + val), 0.25d));
            Refresh();
        }
        else
        {
            // ...
        }
    }

    public void SuspendDrawing()
    {
        if (_stopDrawing < 0)
            // bufferedImg = New Bitmap(Width, Height)
            // DrawToBitmap(bufferedImg, New Rectangle(0, 0, bufferedImg.Width, bufferedImg.Height))
            Utils.DrawingControl.SuspendDrawing(this);
        _stopDrawing += 1;
    }

    public void ResumeDrawing()
    {
        ResumeDrawing(true);
    }

    public void ResumeDrawing(bool executeRefresh)
    {
        if (_stopDrawing >= 0)
            _stopDrawing -= 1;

        if (_stopDrawing == -1)
            // bufferedImg.Dispose()
            // bufferedImg = Nothing
            // If executeRefresh Then Refresh()
            Utils.DrawingControl.ResumeDrawing(this, executeRefresh);
    }

}