@using RobotNet.MapShares.Models
@using RobotNet.WebApp.Maps.Components.Editor.Edge
@using RobotNet.WebApp.Maps.Components.Editor.Node
@using RobotNet.WebApp.Maps.Components.Editor.Zone

@inject IJSRuntime JSRuntime
@inject IHttpClientFactory HttpClientFactory
@inject IDialogService Dialog
@inject ISnackbar Snackbar

<div class="map-viewer-container" @ref="ViewContainerRef" tabindex="1">
    <div @ref=ViewMovementRef>
        <svg @ref=MapContainerRef class="map-editor" letter-spacing="normal">
            <MapSvgDefs />
            <image @ref="MapImageRef" class="map-image" visibility="@(ShowMapSlam ? "visible" : "hidden")" />
            <OriginVector />

            <CascadingValue Value="MapIsActive">
                <MapGrid @ref="@MapGridRef" Show="@ShowGrid" />
                <MapZone Models="@Zones" IsShow="@(EditorState != EditorState.CreateZone && EditorState != EditorState.SettingZone)" />
                <MapEdge @ref="MapEdgesRef" Models="@Edges" EdgeSelectedChanged="OnSelectedEdgeChanged" EditorState="@EditorState" />
                <MapNode Models="@Nodes" Elements="Elements" ShowName="@ShowName" EditorState="@EditorState" />
                <EdgeControlPoint @ref="EdgeControlPointRef" />
                <EdgeStraightCreating @ref="EdgeStraightCreatingRef" />
                <EdgeCurveCreating @ref="EdgeCurveCreatingRef" />
                <MapZone @ref=@MapZoneRef Models="@Zones" IsShow="@(EditorState == EditorState.CreateZone || EditorState == EditorState.SettingZone)" IsSetting="@(EditorState == EditorState.SettingZone)" ActivedZoneChanged="OnActivedZoneChanged" EditorState="@EditorState" />
                <MapScaner @ref=MapScanerRef />
                <ZoneCreating @ref=ZoneCreatingRef />
                <ZoneControlPoint @ref="ZoneControlPointRef"/>
                <MapCopy @ref=MapCopyRef />
            </CascadingValue>
        </svg>
    </div>
    <MapMousePosition @ref="MapInfoRef" />

    <MudOverlay @bind-Visible="OverlayIsVisible" DarkBackground="true" Absolute="true">
        <MudText Typo="Typo.h1" Color="@Color.Error" Style="font-weight: bold;">@OverlayIsStr</MudText>
    </MudOverlay>
</div>

@code {
    [Parameter]
    public bool ShowGrid { get; set; }

    [Parameter]
    public bool ShowName { get; set; }

    [Parameter]
    public bool ShowMapSlam { get; set; }

    [Parameter]
    public bool MapIsActive { get; set; }

    [Parameter]
    public ZoneType ZoneType { get; set; }

    [Parameter]
    public EditorState EditorState { get; set; }

    [Parameter]
    public EventCallback<bool> MultiselectedEdgeChanged { get; set; }

    [Parameter]
    public EventCallback<bool> MultiselectedNodeChanged { get; set; }

    [Parameter]
    public EventCallback<bool> ZoneselectedChanged { get; set; }

    [Parameter]
    public EventCallback<bool> NodesUndoableChanged { get; set; }

    [Parameter]
    public EventCallback<EditorState> KeyPress { get; set; }

    [Parameter]
    public EventCallback<bool> MapIsChecking { get; set; }

    private MapMousePosition MapInfoRef = null!;
    private EdgeStraightCreating EdgeStraightCreatingRef = null!;
    private EdgeCurveCreating EdgeCurveCreatingRef = null!;
    private MapEdge MapEdgesRef = null!;
    private MapGrid MapGridRef = null!;
    private EdgeControlPoint EdgeControlPointRef = null!;
    private MapScaner MapScanerRef = null!;
    private ZoneCreating ZoneCreatingRef = null!;
    private MapZone MapZoneRef = null!;
    private ZoneControlPoint ZoneControlPointRef = null!;
    private MapCopy MapCopyRef = null!;

    private DotNetObjectReference<MapContainer> DotNetObj = null!;
    private ElementReference ViewContainerRef;
    private ElementReference ViewMovementRef;
    private ElementReference MapContainerRef;
    private ElementReference MapImageRef;

    private double ViewContainerRectX;
    private double ViewContainerRectY;
    private double ViewContainerRectWidth;
    private double ViewContainerRectHeight;
    private double ViewContainerRectTop;
    private double ViewContainerRectRight;
    private double ViewContainerRectBottom;
    private double ViewContainerRectLeft;

    private double CursorX;
    private double CursorY;
    private double ClientOriginX;
    private double ClientOriginY;
    private double Scale = 1;
    private double Left;
    private double Top;
    private double FitScale = 1;

    private double Resolution = 1;
    private double OriginX;
    private double OriginY;
    private double ImageWidth;
    private double ImageHeight;

    private MapDataDto MapData = new();
    public MapEdgeModel Edges { get; set; } = [];
    public MapNodeModel Nodes { get; set; } = [];
    public MapZoneModel Zones { get; set; } = [];
    public MapElementModel Elements { get; set; } = [];
    private HttpClient Http = default!;

    private bool OverlayIsVisible = false;
    private string OverlayIsStr = "Loading...";

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        await base.OnAfterRenderAsync(firstRender);
        if (!firstRender) return;

        Http = HttpClientFactory.CreateClient("MapManagerAPI");
        DotNetObj = DotNetObjectReference.Create(this);

        await JSRuntime.InvokeVoidAsync("DOMCssLoaded");

        await JSRuntime.InvokeVoidAsync("UpdateViewContainerRect", DotNetObj, ViewContainerRef, nameof(ViewContainerResize));
        await JSRuntime.InvokeVoidAsync("ResizeObserverRegister", DotNetObj, ViewContainerRef, nameof(ViewContainerResize));
        await JSRuntime.InvokeVoidAsync("AddMouseMoveEventListener", DotNetObj, ViewContainerRef, nameof(MouseMoveOnMapContainer));
        await JSRuntime.InvokeVoidAsync("AddEventListener", DotNetObj, ViewContainerRef, "click", nameof(ViewContainerClick));
        await JSRuntime.InvokeVoidAsync("AddKeyUpEventListener", DotNetObj, ViewContainerRef, nameof(ViewContainerKeyUp));

        await JSRuntime.InvokeVoidAsync("AddMouseWheelEventListener", DotNetObj, MapContainerRef, nameof(MouseWheelOnMapContainer));
        await JSRuntime.InvokeVoidAsync("AddMouseUpEventListener", DotNetObj, MapContainerRef, nameof(MouseUpOnMapContainer));
        await JSRuntime.InvokeVoidAsync("AddMouseDownEventListener", DotNetObj, MapContainerRef, nameof(MouseDownOnMapContainer));
        await JSRuntime.InvokeVoidAsync("AddTouchMoveEventListener", DotNetObj, ViewContainerRef, nameof(TouchMoveOnMapContainer));
    }

    private async Task LoadMap()
    {
        try
        {
            OverlayIsVisible = true;
            StateHasChanged();

            var mapDataResult = await Http.GetFromJsonAsync<MessageResult<MapDataDto>>($"api/MapsData/{MapData.Id}");
            if (mapDataResult is not null && mapDataResult.Data is not null)
            {
                MapIsActive = mapDataResult.Data.Active;
                await LoadMap(mapDataResult.Data);
                OverlayIsVisible = false;
            }
            else OverlayIsStr = "Map Not Existed";
            StateHasChanged();
        }
        catch
        {
            OverlayIsStr = "Map Not Existed";
            StateHasChanged();
            return;
        }
    }

    public async Task LoadMap(MapDataDto? mapData)
    {
        if (mapData == null)
        {
            MapData = new();
            Nodes.ReplaceAll([]);
            Edges.ReplaceAll([]);
            Zones.ReplaceAll([]);
            Elements.ReplaceAll([]);
            Resolution = 1.0;
            OriginY = 0;
            OriginX = 0;
            ImageHeight = 0;
            ImageWidth = 0;
            MapGridRef.Resize(OriginX, OriginY, ImageHeight, ImageWidth);
            await JSRuntime.InvokeVoidAsync("SetMapSvgConfig", MapContainerRef, ImageWidth, ImageHeight, OriginX, 1.0);
            await JSRuntime.InvokeVoidAsync("SetImageAttribute", MapImageRef, ImageWidth, ImageHeight, OriginX, OriginY, "");
        }
        else
        {
            MapData = mapData;
            Nodes.ReplaceAll(MapData.Nodes);
            Edges.ReplaceAll(MapData.Edges.Select(edge => new EdgeModel(edge, Nodes[edge.StartNodeId], Nodes[edge.EndNodeId])));
            Zones.ReplaceAll(MapData.Zones);
            Elements.ReplaceAll(MapData.Elements.Select(element => new ElementModel(element, Nodes[element.NodeId])));
            Resolution = mapData.Resolution;
            OriginY = -mapData.ImageHeight * Resolution - mapData.OriginY;
            OriginX = mapData.OriginX;
            ImageHeight = mapData.ImageHeight * Resolution;
            ImageWidth = mapData.ImageWidth * Resolution;
            MapGridRef.Resize(OriginX, OriginY, ImageHeight, ImageWidth);

            await JSRuntime.InvokeVoidAsync("SetMapSvgConfig", MapContainerRef, ImageWidth, ImageHeight, OriginX, mapData.OriginY);
            await JSRuntime.InvokeVoidAsync("SetImageAttribute", MapImageRef, ImageWidth, ImageHeight, OriginX, OriginY, $"{Http.BaseAddress}api/images/map/{mapData.Id}");
            await JSRuntime.InvokeVoidAsync("UpdateViewContainerRect", DotNetObj, ViewContainerRef, nameof(ViewContainerResize));
            await ScaleFitContentAsync();
        }
    }

    public async Task ScaleFitContentAsync()
    {
        Scale = FitScale;
        await SetViewMovement((ViewContainerRectWidth - ImageWidth * Scale) / 2, (ViewContainerRectHeight - ImageHeight * Scale) / 2);
        await JSRuntime.InvokeVoidAsync("SetMapSvgRect", MapContainerRef, ImageWidth * Scale, ImageHeight * Scale);
    }

    private async Task SetViewMovement(double left, double top)
    {
        Top = top;
        Left = left;

        ClientOriginX = ViewContainerRectLeft + Left - OriginX * Scale;
        ClientOriginY = ViewContainerRectTop + Top - OriginY * Scale;
        await JSRuntime.InvokeVoidAsync("SetMapMovement", ViewMovementRef, Top, Left);
    }

    public async Task ScaleZoom(double deltaY)
    {
        if (deltaY > 0)
        {
            if (Scale >= FitScale * 20) return;
        }
        else
        {
            if (Scale <= FitScale / 2) return;
        }
        double oldScale = Scale;
        Scale += deltaY;

        double centerXBefore = ((ViewContainerRectLeft + ViewContainerRectWidth / 2) - ClientOriginX) / oldScale - OriginX;
        double centerYBefore = (ClientOriginY - (ViewContainerRectHeight / 2 + ViewContainerRectTop)) / oldScale - (MapData is null ? 0 : MapData.OriginY);

        await SetViewMovement(Left - centerXBefore * deltaY, Top - (ImageHeight - centerYBefore) * deltaY);
        await JSRuntime.InvokeVoidAsync("SetMapSvgRect", MapContainerRef, ImageWidth * Scale, ImageHeight * Scale);
    }

    [JSInvokable]
    public async Task ViewContainerClick() => await ViewContainerRef.FocusAsync();

    [JSInvokable]
    public void ViewContainerResize(double x, double y, double width, double height, double top, double right, double bottom, double left)
    {
        ViewContainerRectX = x;
        ViewContainerRectY = y;
        ViewContainerRectWidth = width;
        ViewContainerRectHeight = height;
        ViewContainerRectTop = top;
        ViewContainerRectRight = right;
        ViewContainerRectBottom = bottom;
        ViewContainerRectLeft = left;

        ClientOriginX = ViewContainerRectLeft + Left - OriginX * Scale;
        ClientOriginY = ViewContainerRectTop + Top - OriginY * Scale;

        FitScale = Math.Min(ViewContainerRectWidth / ImageWidth, ViewContainerRectHeight / ImageHeight);
    }

    [JSInvokable]
    public async Task TouchMoveOnMapContainer(double clientX, double clientY, int touchCount, double movementX, double movementY)
    {
        CursorX = (clientX - ClientOriginX) / Scale;
        CursorY = (ClientOriginY - clientY) / Scale;
        MapInfoRef.Update(CursorX, CursorY);

        if (touchCount == 1) await SetViewMovement(Left + movementX, Top + movementY);
    }

    [JSInvokable]
    public async Task MouseWheelOnMapContainer(double deltaY, double offsetX, double offsetY)
    {
        double scaleChange;
        if (deltaY > 0)
        {
            if (Scale <= FitScale / 2) return;
            scaleChange = Scale > FitScale ? -(Scale / FitScale) : -0.1;
        }
        else
        {
            if (Scale >= FitScale * 100) return;
            scaleChange = Scale < FitScale ? 0.5 : (Scale / FitScale);
        }

        double oldScale = Scale;

        Scale += scaleChange;
        await JSRuntime.InvokeVoidAsync("SetMapSvgRect", MapContainerRef, ImageWidth * Scale, ImageHeight * Scale);

        double mouseX = CursorX - OriginX;
        double mouseY = CursorY - (MapData is null ? 0 : MapData.OriginY);
        await SetViewMovement(Left - mouseX * scaleChange, Top - (ImageHeight - mouseY) * scaleChange);
    }

    [JSInvokable]
    public async Task MouseMoveOnMapContainer(double clientX, double clientY, long buttons, bool ctrlKey, double movementX, double movementY)
    {
        CursorX = (clientX - ClientOriginX) / Scale;
        CursorY = (ClientOriginY - clientY) / Scale;
        MapInfoRef.Update(CursorX, CursorY);
        if (EditorState == EditorState.CreateCurveEdge || EditorState == EditorState.CreateDoubleCurveEdge) await EdgeCurveCreatingRef.Update(CursorX, CursorY);
        else if (EditorState == EditorState.CreateZone) await ZoneCreatingRef.Update(CursorX, CursorY);

        switch (buttons)
        {
            case 1:
                if (MapIsActive) break;
                switch (EditorState)
                {
                    case EditorState.CreateStraighEdge:
                        await EdgeStraightCreatingRef.Update(CursorX, CursorY);
                        break;
                    case EditorState.CreateCurveEdge:
                    case EditorState.CreateDoubleCurveEdge:
                        await EdgeCurveCreatingRef.Update(CursorX, CursorY);
                        break;
                    case EditorState.Scaner:
                        await MapScanerRef.Update(CursorX, CursorY);
                        break;
                    case EditorState.NavigationEdit:
                        if (ctrlKey) NodePositionMove(CursorX, CursorY);
                        break;
                    case EditorState.SettingZone:
                        if (ctrlKey) ZoneUpdateShape(CursorX, CursorY);
                        break;
                    case EditorState.Move:
                        if (ctrlKey)
                        {
                            if (Edges.ActivedEdges.Count > 0) EdgesPositionMove(CursorX, CursorY);
                            else NodesPositionMove(CursorX, CursorY);
                        }
                        break;
                    case EditorState.Copy:
                        if (ctrlKey) MapCopyRef.UpdateMove(CursorX, CursorY);
                        break;
                }
                break;
            case 4:
                await SetViewMovement(Left + movementX, Top + movementY);
                break;
        }
    }

    [JSInvokable]
    public async Task ViewContainerKeyUp(string code, string key, bool altKey, bool ctrlKey, bool shiftKey)
    {
        if (MapIsActive) return;
        switch (code)
        {
            case "Escape":
                await EditStateChange();
                break;
            case "Delete":
                _ = InvokeAsync(async Task () =>
                {
                    if (EditorState == EditorState.NavigationEdit || EditorState == EditorState.Scaner) await DeleteEdge();
                    else if (EditorState == EditorState.SettingZone) await DeleteZone();
                });
                break;
            case "KeyZ":
                if (ctrlKey) UndoEditorBackup();
                break;
            case "KeyS":
                if (ctrlKey) await SaveChanged();
                break;
            case "KeyM":
                if (EditorState != EditorState.Scaner) return;
                if (ctrlKey)
                {
                    if (EditorBackup.Count > 0)
                    {
                        var save = await SaveChanged();
                        if (!save) break;
                    }
                    await SetCursor("grab");
                    await KeyPress.InvokeAsync(EditorState.Move);
                }
                break;
            case "KeyC":
                if (EditorState != EditorState.Scaner) return;
                if (ctrlKey) await KeyPress.InvokeAsync(EditorState.Copy);
                break;
        }
        StateHasChanged();
    }

    [JSInvokable]
    public async Task MouseUpOnMapContainer(int button, bool altKey, bool ctrlKey, bool shiftKey)
    {
        if (button == 0 && !MapIsActive)
        {
            switch (EditorState)
            {
                case EditorState.CreateStraighEdge:
                    await CreateEdge(EdgeStraightCreatingRef.X1, EdgeStraightCreatingRef.Y1, EdgeStraightCreatingRef.X2, EdgeStraightCreatingRef.Y2);
                    await EdgeStraightCreatingRef.Hidden();
                    break;
                case EditorState.CreateCurveEdge:
                case EditorState.CreateDoubleCurveEdge:
                    await EdgeCurveCreatingRef.FinishSubEdgeAsync(CursorX, CursorY);
                    if (EdgeCurveCreatingRef.CreateStep == EdgeCurveCreating.CreateCurveEdgeStep.Finish)
                    {
                        var edgeCreateModel = EdgeCurveCreatingRef.GetEdgeCreateModel();
                        await CreateEdge(edgeCreateModel.X1, edgeCreateModel.Y1, edgeCreateModel.X2, edgeCreateModel.Y2, edgeCreateModel.TrajectoryDegree, edgeCreateModel.ControlPoint1X, edgeCreateModel.ControlPoint1Y, edgeCreateModel.ControlPoint2X, edgeCreateModel.ControlPoint2Y);
                        await EdgeCurveCreatingRef.Hidden();
                    }
                    break;
                case EditorState.Scaner:
                    await ScanerActive(MapScanerRef.X1, MapScanerRef.Y1, MapScanerRef.X2, MapScanerRef.Y2);
                    await MapScanerRef.Hidden();
                    break;
                case EditorState.CreateZone:
                    if (ZoneCreatingRef.Step == 4)
                    {
                        await CreateZone(ZoneCreatingRef.X1, ZoneCreatingRef.Y1, ZoneCreatingRef.X2, ZoneCreatingRef.Y2, ZoneCreatingRef.X3, ZoneCreatingRef.Y3, ZoneCreatingRef.X4, ZoneCreatingRef.Y4, ZoneType);
                        await ZoneCreatingRef.Hidden();
                    }
                    break;
                case EditorState.SettingZone:
                    if (Zones.ZoneActived is not null && Zones.ZoneActived.ActiveNode != 5) Zones.ZoneActived.ActiveNode = 5;
                    break;
                case EditorState.NavigationEdit:
                    if (Edges.ActivedEdges.Count == 1)
                    {
                        Edges.ActivedEdges[0].ActivedControlPoint1 = false;
                        Edges.ActivedEdges[0].ActivedControlPoint2 = false;
                    }
                    Nodes.SelectedNode = null;
                    break;
                case EditorState.Move:
                    await SetCursor("grab");
                    break;
            }
            StateHasChanged();
        }
    }

    [JSInvokable]
    public async Task MouseDownOnMapContainer(int button, bool altKey, bool ctrlKey, bool shiftKey)
    {
        if (button == 0 && !MapIsActive)
        {
            var startNode = MapEditorHelper.GetClosesNode(CursorX, CursorY, Nodes.Select(node => new NodeDto() { X = node.X, Y = node.Y }).ToList());
            switch (EditorState)
            {
                case EditorState.CreateStraighEdge:
                    await EdgeStraightCreatingRef.StartCreateAsync(startNode?.X ?? CursorX, startNode?.Y ?? CursorY);
                    break;
                case EditorState.CreateCurveEdge:
                    await EdgeCurveCreatingRef.CreateSubEdgeAsync(startNode?.X ?? CursorX, startNode?.Y ?? CursorY);
                    break;
                case EditorState.CreateDoubleCurveEdge:
                    await EdgeCurveCreatingRef.CreateSubEdgeAsync(startNode?.X ?? CursorX, startNode?.Y ?? CursorY, TrajectoryDegree.Three);
                    break;
                case EditorState.Scaner:
                    await MapScanerRef.CreateAsync(CursorX, CursorY);
                    break;
                case EditorState.CreateZone:
                    await ZoneCreatingRef.CreateAsync(CursorX, CursorY);
                    break;
                case EditorState.SettingZone:
                    if (Zones.ZoneActived is not null) Zones.ZoneActived.SetStartMovePosition(CursorX, CursorY);
                    break;
                case EditorState.Move:
                    await SetCursor("grabbing");
                    Edges.SetStartMovePosition(CursorX, CursorY);
                    Nodes.SetStartMovePosition(CursorX, CursorY);
                    break;
                case EditorState.Copy:
                    MapCopyRef.SetStartMovePosition(CursorX, CursorY);
                    break;
            }
        }
    }

    private async Task OnSelectedEdgeChanged(EdgeModel? model)
    {
        if (model is not null && EditorState == EditorState.NavigationEdit)
        {
            if (model.TrajectoryDegree == TrajectoryDegree.One) EdgeControlPointRef.SetControl(null);
            else EdgeControlPointRef.SetControl(model);
            Edges.ActivedEdge([model]);
            await MultiselectedEdgeChanged.InvokeAsync(Edges.ActivedEdges.Count > 0);
        }
        else EdgeControlPointRef.SetControl(null);
    }

    private void OnActivedZoneChanged((ZoneModel model, bool state) data)
    {
        if (EditorState == EditorState.SettingZone)
        {
            if (data.state)
            {
                ZoneControlPointRef.SetControl(data.model);
                _ = ZoneselectedChanged.InvokeAsync(true);
                return;
            }
        }
        ZoneControlPointRef.SetControl(null);
    }

    public async Task EditStateChange()
    {
        await EdgeCurveCreatingRef.Hidden();
        await EdgeStraightCreatingRef.Hidden();
        await ZoneCreatingRef.Hidden();
        MapCopyRef.OnShow(false, []);
        Zones.UnActivedZone();
        EdgeControlPointRef.SetControl(null);
        if (EditorState == EditorState.Copy) await CreateMapCopy();
        if (EditorState != EditorState.Move)
        {
            Edges.UnActivedEdge();
            Nodes.UnActivedNode();
        }
        if (EditorState != EditorState.Copy) await SaveChanged();
        if (EditorState != EditorState.Copy && EditorState != EditorState.Move) await SetCursor("initial");
        await MultiselectedEdgeChanged.InvokeAsync(Edges.ActivedEdges.Count() > 0);
        await MultiselectedNodeChanged.InvokeAsync(Nodes.ActivedNodes.Count() > 0);
        await ZoneselectedChanged.InvokeAsync(false);
        StateHasChanged();
    }

    public async Task CreateMapCopy()
    {
        if (Edges.ActivedEdges.Count > 0)
        {
            if (EditorBackup.Count > 0)
            {
                var save = await SaveChanged();
                if (!save) return;
            }
            MapCopyRef.OnShow(true, Edges.ActivedEdges);
            EditorBackup.Add(new()
                {
                    Type = MapEditorBackupType.Copy,
                });
            await NodesUndoableChanged.InvokeAsync(true);
            await SetCursor("copy");
        }
    }

    public async Task SetCursor(string data) => await JSRuntime.InvokeVoidAsync("ElementSetAttribute", MapContainerRef, "cursor", data);
}
