@using RobotNet.WebApp.Robots.Components.Monitoring.Element
@using System.Text.Json

@inject IJSRuntime JSRuntime
@inject ISnackbar Snackbar
@inject IDialogService DialogService
@inject IHttpClientFactory HttpFactory

<div class="monitor-map" @ref="ViewContainerRef">
    <div @ref="ViewMovementRef">
        <svg @ref="MapContainerRef" class="monitor-map-view" letter-spacing="normal">
            <MapSvgDefs />
            <image @ref="MapImageRef" class="monitor-map-image" />
            <OriginVector />
            <MapGrid @ref="@MapGridRef" Show="@ShowGrid" />

            @foreach (var zone in zones)
            {
                <Zone Model="zone" IsShow="true" />
            }
            @foreach (var edge in edges)
            {
                <Edge Model="edge" />
            }
            @foreach (var edge in edges)
            {
                <EdgeDirection Model="edge" />
            }
            @foreach (var node in nodes)
            {
                <Node Model="node" ShowName="@ShowName" />
            }
            @foreach (var element in elements)
            {
                <Element Model="element" ShowName="@ShowName" Show="@ShowElement" DoubleClick="ElementDoubleClick" />
            }
            <RobotCurrentPath @ref="@RobotCurrentPathRef" Show="@ShowPath" />
            <RobotPath @ref="@RobotPathRef" Show="@ShowPath" />
            <MapRobot Models="robots" ShowName="@ShowName" />
            <RobotLaserScaner @ref="RobotLaserScanerRef" ShowLaser="ShowLaser" />
        </svg>
    </div>
    <MapMousePosition @ref="MapInfoRef" />
</div>

<MudDialog @bind-Visible="updateElementVisble">
    <TitleContent>
        <MudText Typo="Typo.h6">
            Update Element @ElementUpdateModel.Name
        </MudText>
    </TitleContent>
    <DialogContent>
        <div class="d-flex flex-row" style="max-height: 700px; overflow-y: auto; width: 600px">
            <div class="d-flex flex-column h-100 me-2" style="width: 47%">
                <MudTextField T="string" Label="Name" @bind-value="ElementUpdateModel.Name" Variant="Variant.Outlined"
                              Required="true" RequiredError="Name is required!" ReadOnly
                              Validation="@(new Func<string, IEnumerable<string>>(MapEditorHelper.NameValidation))" />
                <MudNumericField T="double" Label="Offset X" @bind-value="ElementUpdateModel.OffsetX" Variant="Variant.Outlined" Step="0.1" ReadOnly />
                <MudNumericField T="double" Label="Offset Y" @bind-value="ElementUpdateModel.OffsetY" Variant="Variant.Outlined" Step="0.1" ReadOnly />
                <MudCheckBox Size="Size.Small" T="bool" LabelPlacement="Placement.Start" Label="IsOpen" @bind-value="ElementUpdateModel.IsOpen" Color="Color.Success"
                             UncheckedColor="Color.Default" />
            </div>
            <div class="paper-property-container">
                <div class="paper-property ms-2">
                    @foreach (var property in ElementProperties)
                    {
                        @if (property.Type == typeof(int).ToString())
                        {
                            if (int.TryParse(property.DefaultValue, out int value))
                            {
                                <MudNumericField T="int" Margin="Margin.Dense" Label="@property.Name" Value="value" Variant="Variant.Outlined" ReadOnly="@(property.ReadOnly)"
                                                 ValueChanged="((data) => DefaultIntValueChanged(property.Id, data))" />
                            }
                        }
                        else if (property.Type == typeof(double).ToString())
                        {
                            if (double.TryParse(property.DefaultValue, out double value))
                            {
                                <MudNumericField T="double" Margin="Margin.Dense" Label="@property.Name" Value="value" Variant="Variant.Outlined" Step="0.1" ReadOnly="@(property.ReadOnly)"
                                                 ValueChanged="((data) => DefaultDoubleValueChanged(property.Id, data))" />
                            }
                        }
                        else if (property.Type == typeof(bool).ToString())
                        {
                            if (bool.TryParse(property.DefaultValue, out bool value))
                            {
                                <MudCheckBox Class="m-0" Size="Size.Small" T="bool" LabelPlacement="Placement.Start" Label="@property.Name" Value="value" Color="Color.Success"
                                             UncheckedColor="Color.Default" ReadOnly="@(property.ReadOnly)" ValueChanged="((data) => DefaultBooleanValueChanged(property.Id, data))" />
                            }
                        }
                        else if (property.Type == typeof(string).ToString())
                        {
                            <MudTextField T="string" Label="@property.Name" Value="@property.DefaultValue" Variant="Variant.Outlined" Margin="Margin.Dense" ReadOnly="@(property.ReadOnly)"
                                          ValueChanged="((data) => property.DefaultValue = data)" />
                        }
                    }
                </div>
                <span class="paper-title">Properties</span>
            </div>
        </div>
    </DialogContent>
    <DialogActions>
        <MudButton OnClick="(() => updateElementVisble = false)" Class="px-10" Variant="Variant.Filled">Cancel</MudButton>
        <MudButton OnClick="UpdateElement" Variant="Variant.Filled" Color="Color.Primary" Class="px-10 me-4">Update</MudButton>
    </DialogActions>
</MudDialog>

@code {

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

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

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

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

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

    private MapMousePosition MapInfoRef = null!;
    private RobotPath RobotPathRef = null!;
    private RobotCurrentPath RobotCurrentPathRef = null!;
    private MapGrid MapGridRef = null!;
    private RobotLaserScaner RobotLaserScanerRef = null!;

    private DotNetObjectReference<MonitorMap> 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 Resolution = 1;
    private double OriginX;
    private double OriginY;
    private double ImageOriginY;
    private double ImageWidth;
    private double ImageHeight;
    private double Scale = 1;
    private double Left;
    private double Top;
    private double FitScale = 1;
    private double OldOriginY;

    private List<EdgeModel> edges = new();
    private List<NodeModel> nodes = new();
    private List<ZoneModel> zones = new();
    private List<ElementModel> elements = new();
    private MapRobotModel robots = new();

    private bool IsFocusRobot = false;
    private string RobotSelectedId = "";

    private bool updateElementVisble;
    private ElementUpdateModel ElementUpdateModel = new();
    private List<MapShares.Property.ElementProperty> ElementProperties = [];

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

        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("AddEventListener", DotNetObj, ViewContainerRef, "click", nameof(ViewContainerClick));
        await JSRuntime.InvokeVoidAsync("AddMouseMoveEventListener", DotNetObj, ViewContainerRef, nameof(MouseMoveOnMapContainer));

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

    public async Task LoadMap(MapDataDto? mapData)
    {
        if (mapData == null)
        {
            nodes.Clear();
            edges.Clear();
            zones.Clear();
            elements.Clear();
            robots.ReplaceAll([]);
            RobotCurrentPathRef.UpdatePath([]);
            RobotPathRef.UpdatePath([]);
            Resolution = 1.0;
            OriginY = 0;
            OriginX = 0;
            OldOriginY = 0;
            ImageOriginY = 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
        {
            nodes = [.. mapData.Nodes.Select(node => new NodeModel(node))];
            edges = [.. mapData.Edges.Select(edge => new EdgeModel(edge, nodes.First(n => n.Id == edge.StartNodeId), nodes.First(n => n.Id == edge.EndNodeId)))];
            zones = [.. mapData.Zones.Select(zone => new ZoneModel(zone))];
            elements = [.. mapData.Elements.Select(element => new ElementModel(element, nodes.First(n => n.Id == element.NodeId)))];
            Resolution = mapData.Resolution;
            OldOriginY = mapData.OriginY;
            OriginY = -mapData.ImageHeight * Resolution - mapData.OriginY;
            OriginX = mapData.OriginX;
            ImageOriginY = mapData.OriginY;
            ImageHeight = mapData.ImageHeight * Resolution;
            ImageWidth = mapData.ImageWidth * Resolution;
            MapGridRef.Resize(OriginX, OriginY, ImageHeight, ImageWidth);

            using var Http = HttpFactory.CreateClient("MapManagerAPI");

            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();
        }
        StateHasChanged();
    }

    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 - OldOriginY;

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

    [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 - OldOriginY;
        await SetViewMovement(Left - mouseX * scaleChange, Top - (ImageHeight - mouseY) * scaleChange);
    }

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

    [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 (IsFocusRobot) return;
        switch (buttons)
        {
            case 4:
                await SetViewMovement(Left + movementX, Top + movementY);
                break;
        }
    }

    public void LoadRobots(IEnumerable<RobotInfomationDto> robotDtos)
    {
        if (robotDtos.Any())
        {
            var robotModel = robotDtos.Select(robot => new RobotVisualizationModel()
                {
                    RobotId = robot.RobotId,
                    RobotName = robot.Name,
                    X = robot.AgvPosition.X,
                    Y = robot.AgvPosition.Y,
                    Theta = robot.AgvPosition.Theta,
                }).ToArray();
            robots.ReplaceAll(robotModel);
        }
        else robots.ReplaceAll([]);
        StateHasChanged();
    }

    public void RobotSelectedChange(string robotId)
    {
        if (robotId != RobotSelectedId) RobotSelectedId = robotId;
    }

    public void FocusRobot(bool value) => IsFocusRobot = value;

    public async Task FocusRobotAsync()
    {
        var robot = robots.FirstOrDefault(r => r.RobotId == RobotSelectedId);
        if (robot is null) return;
        double mapX = (ViewContainerRectWidth - ImageWidth * Scale) / 2 + ((ImageWidth / 2 + OriginX) * Scale) - robot.X * Scale;
        double mapY = (ViewContainerRectHeight - ImageHeight * Scale) / 2 + ((ImageHeight / 2 + OriginY) * Scale) + robot.Y * Scale;
        await SetViewMovement(mapX, mapY);
    }

    public async Task SetRobotPosition(List<RobotInfomationDto> robotStates)
    {
        foreach (var robot in robotStates)
        {
            var robotModel = robots.FirstOrDefault(rm => rm.RobotId == robot.RobotId);
            if (robotModel is not null)
            {
                robotModel.Update(robot.AgvPosition.X, robot.AgvPosition.Y, robot.AgvPosition.Theta, robot.Loads);
                if (IsFocusRobot && robot.RobotId == RobotSelectedId) await FocusRobotAsync();
            }
            else
            {
                robots.Add(new()
                    {
                        RobotId = robot.RobotId,
                        RobotName = robot.Name,
                        X = robot.AgvPosition.X,
                        Y = robot.AgvPosition.Y,
                        Theta = robot.AgvPosition.Theta,
                        Loads = robot.Loads
                    });
                StateHasChanged();
            }
            if (robot.RobotId == RobotSelectedId)
            {
                RobotPathRef.UpdatePath([.. robot.Navigation.RobotPath]);
                RobotCurrentPathRef.UpdatePath([.. robot.Navigation.RobotBasePath]);
                RobotLaserScanerRef.SetData(robot.Navigation.LaserScaner);
            }
        }
    }

    public void ElementStateUpdated(IEnumerable<ElementDto> elementsState)
    {
        foreach (var elmenet in elementsState)
        {
            var elemetModel = elements.FirstOrDefault(e => e.Id == elmenet.Id);
            if (elemetModel is not null)
            {
                elemetModel.Update(elmenet);
            }
            else
            {
                var node = nodes.FirstOrDefault(n => n.Id == elmenet.NodeId);
                if (node is not null) elements.Add(new ElementModel(elmenet, node));
            }
        }
        StateHasChanged();
    }

    private void DefaultIntValueChanged(Guid id, int value)
    {
        var property = ElementProperties.FirstOrDefault(p => p.Id == id);
        if (property is null) return;
        property.DefaultValue = value.ToString();
    }

    private void DefaultDoubleValueChanged(Guid id, double value)
    {
        var property = ElementProperties.FirstOrDefault(p => p.Id == id);
        if (property is null) return;
        property.DefaultValue = value.ToString();
    }

    private void DefaultBooleanValueChanged(Guid id, bool value)
    {
        var property = ElementProperties.FirstOrDefault(p => p.Id == id);
        if (property is null) return;
        property.DefaultValue = value.ToString();
    }

    private void ElementDoubleClick(ElementModel model)
    {
        ElementUpdateModel.Id = model.Id;
        ElementUpdateModel.Name = model.Name;
        ElementUpdateModel.IsOpen = model.IsOpen;
        ElementUpdateModel.OffsetX = model.OffsetX;
        ElementUpdateModel.OffsetY = model.OffsetY;
        ElementUpdateModel.Content = model.Content;

        if (model is not null && !string.IsNullOrEmpty(model.Content))
        {
            var properties = JsonSerializer.Deserialize<List<MapShares.Property.ElementProperty>>(model.Content, JsonOptionExtends.Read);
            if (properties is not null) ElementProperties = [.. properties];
        }

        updateElementVisble = true;
        StateHasChanged();
    }

    private async Task UpdateElement()
    {
        var selectedElement = elements.FirstOrDefault(e => e.Id == ElementUpdateModel.Id);
        if (selectedElement is null) return;

        using var Http = HttpFactory.CreateClient("MapManagerAPI");
        var result = await (await Http.PutAsJsonAsync($"api/Elements", new ElementUpdateModel()
            {
                Id = ElementUpdateModel.Id,
                Name = ElementUpdateModel.Name,
                IsOpen = ElementUpdateModel.IsOpen,
                OffsetX = ElementUpdateModel.OffsetX,
                OffsetY = ElementUpdateModel.OffsetY,
                Content = JsonSerializer.Serialize(ElementProperties, JsonOptionExtends.Write),
            })).Content.ReadFromJsonAsync<MessageResult<ElementDto>>();
        if (result == null) Snackbar.Add("Lỗi giao tiếp với hệ thống", Severity.Error);
        else if (!result.IsSuccess) Snackbar.Add(result.Message, Severity.Error);
        else if (result.Data is null) Snackbar.Add("Lỗi dữ liệu", Severity.Error);
        else
        {
            selectedElement.Update(result.Data);
            Snackbar.Add("Cập nhật thành công", Severity.Success);
        }

        updateElementVisble = false;
        StateHasChanged();
    }
}
