// Urho3D editor view & camera functions

WeakHandle previewCamera;
Node@ cameraNode;
Camera@ camera;

Node@ gridNode;
CustomGeometry@ grid;

UIElement@ viewportUI; // holds the viewport ui, convienent for clearing and hiding
uint setViewportCursor = 0; // used to set cursor in post update
uint resizingBorder = 0; // current border that is dragging
uint viewportMode = VIEWPORT_SINGLE;
int  viewportBorderOffset = 2; // used to center borders over viewport seams,  should be half of width
int  viewportBorderWidth = 4; // width of a viewport resize border
IntRect viewportArea; // the area where the editor viewport is. if we ever want to have the viewport not take up the whole screen this abstracts that
IntRect viewportUIClipBorder = IntRect(27, 60, 0, 0); // used to clip viewport borders, the borders are ugly when going behind the transparent toolbars
RenderPath@ renderPath; // Renderpath to use on all views
String renderPathName;
bool mouseWheelCameraPosition = false;
bool contextMenuActionWaitFrame = false;

const uint VIEWPORT_BORDER_H     = 0x00000001;
const uint VIEWPORT_BORDER_H1    = 0x00000002;
const uint VIEWPORT_BORDER_H2    = 0x00000004;
const uint VIEWPORT_BORDER_V     = 0x00000010;
const uint VIEWPORT_BORDER_V1    = 0x00000020;
const uint VIEWPORT_BORDER_V2    = 0x00000040;

const uint VIEWPORT_SINGLE       = 0x00000000;
const uint VIEWPORT_TOP          = 0x00000100;
const uint VIEWPORT_BOTTOM       = 0x00000200;
const uint VIEWPORT_LEFT         = 0x00000400;
const uint VIEWPORT_RIGHT        = 0x00000800;
const uint VIEWPORT_TOP_LEFT     = 0x00001000;
const uint VIEWPORT_TOP_RIGHT    = 0x00002000;
const uint VIEWPORT_BOTTOM_LEFT  = 0x00004000;
const uint VIEWPORT_BOTTOM_RIGHT = 0x00008000;

// Combinations for easier testing
const uint VIEWPORT_BORDER_H_ANY = 0x00000007;
const uint VIEWPORT_BORDER_V_ANY = 0x00000070;
const uint VIEWPORT_SPLIT_H      = 0x0000f300;
const uint VIEWPORT_SPLIT_V      = 0x0000fc00;
const uint VIEWPORT_SPLIT_HV     = 0x0000f000;
const uint VIEWPORT_TOP_ANY      = 0x00003300;
const uint VIEWPORT_BOTTOM_ANY   = 0x0000c200;
const uint VIEWPORT_LEFT_ANY     = 0x00005400;
const uint VIEWPORT_RIGHT_ANY    = 0x0000c800;
const uint VIEWPORT_QUAD         = 0x0000f000;

enum EditMode
{
    EDIT_MOVE = 0,
    EDIT_ROTATE,
    EDIT_SCALE,
    EDIT_SELECT,
    EDIT_SPAWN
}

enum AxisMode
{
    AXIS_WORLD = 0,
    AXIS_LOCAL
}

enum SnapScaleMode
{
    SNAP_SCALE_FULL = 0,
    SNAP_SCALE_HALF,
    SNAP_SCALE_QUARTER
}

// Holds info about a viewport such as camera settings and splits up shared resources
class ViewportContext
{
    float cameraYaw = 0;
    float cameraPitch = 0;
    Camera@ camera;
    Node@ cameraNode;
    SoundListener@ soundListener;
    Viewport@ viewport;
    bool enabled = false;
    uint index = 0;
    uint viewportId = 0;
    UIElement@ viewportContextUI;
    UIElement@ statusBar;
    Text@ cameraPosText;

    Window@ settingsWindow;
    LineEdit@ cameraPosX;
    LineEdit@ cameraPosY;
    LineEdit@ cameraPosZ;
    LineEdit@ cameraRotX;
    LineEdit@ cameraRotY;
    LineEdit@ cameraRotZ;
    LineEdit@ cameraZoom;
    LineEdit@ cameraOrthoSize;
    CheckBox@ cameraOrthographic;

    ViewportContext(IntRect viewRect, uint index_, uint viewportId_)
    {
        cameraNode = Node();
        camera = cameraNode.CreateComponent("Camera");
        camera.fillMode = fillMode;
        soundListener = cameraNode.CreateComponent("SoundListener");
        viewport = Viewport(editorScene, camera, viewRect, renderPath);
        index = index_;
        viewportId = viewportId_;
        camera.viewMask = 0xffffffff; // It's easier to only have 1 gizmo active this viewport is shared with the gizmo
    }

    void ResetCamera()
    {
        cameraNode.position = Vector3(0, 5, -10);
        // Look at the origin so user can see the scene.
        cameraNode.rotation = Quaternion(Vector3(0, 0, 1), -cameraNode.position);
        ReacquireCameraYawPitch();
        UpdateSettingsUI();
    }

    void ReacquireCameraYawPitch()
    {
        cameraYaw = cameraNode.rotation.yaw;
        cameraPitch = cameraNode.rotation.pitch;
    }

    void CreateViewportContextUI()
    {
        Font@ font = cache.GetResource("Font", "Fonts/Anonymous Pro.ttf");

        viewportContextUI = UIElement();
        viewportUI.AddChild(viewportContextUI);
        viewportContextUI.SetPosition(viewport.rect.left, viewport.rect.top);
        viewportContextUI.SetFixedSize(viewport.rect.width, viewport.rect.height);
        viewportContextUI.clipChildren = true;

        statusBar = BorderImage("ToolBar");
        statusBar.style = "EditorToolBar";
        viewportContextUI.AddChild(statusBar);

        statusBar.SetLayout(LM_HORIZONTAL);
        statusBar.SetAlignment(HA_LEFT, VA_BOTTOM);
        statusBar.layoutSpacing = 4;
        statusBar.opacity = uiMaxOpacity;

        Button@ settingsButton = CreateSmallToolBarButton("Settings");
        statusBar.AddChild(settingsButton);

        cameraPosText = Text();
        statusBar.AddChild(cameraPosText);

        cameraPosText.SetFont(font, 11);
        cameraPosText.color = Color(1, 1, 0);
        cameraPosText.textEffect = TE_SHADOW;
        cameraPosText.priority = -100;

        settingsWindow = LoadEditorUI("UI/EditorViewport.xml");
        settingsWindow.opacity = uiMaxOpacity;
        settingsWindow.visible = false;
        viewportContextUI.AddChild(settingsWindow);

        cameraPosX = settingsWindow.GetChild("PositionX", true);
        cameraPosY = settingsWindow.GetChild("PositionY", true);
        cameraPosZ = settingsWindow.GetChild("PositionZ", true);
        cameraRotX = settingsWindow.GetChild("RotationX", true);
        cameraRotY = settingsWindow.GetChild("RotationY", true);
        cameraRotZ = settingsWindow.GetChild("RotationZ", true);
        cameraOrthographic = settingsWindow.GetChild("Orthographic", true);
        cameraZoom = settingsWindow.GetChild("Zoom", true);
        cameraOrthoSize = settingsWindow.GetChild("OrthoSize", true);

        SubscribeToEvent(cameraPosX, "TextChanged", "HandleSettingsLineEditTextChange");
        SubscribeToEvent(cameraPosY, "TextChanged", "HandleSettingsLineEditTextChange");
        SubscribeToEvent(cameraPosZ, "TextChanged", "HandleSettingsLineEditTextChange");
        SubscribeToEvent(cameraRotX, "TextChanged", "HandleSettingsLineEditTextChange");
        SubscribeToEvent(cameraRotY, "TextChanged", "HandleSettingsLineEditTextChange");
        SubscribeToEvent(cameraRotZ, "TextChanged", "HandleSettingsLineEditTextChange");
        SubscribeToEvent(cameraZoom, "TextChanged", "HandleSettingsLineEditTextChange");
        SubscribeToEvent(cameraOrthoSize, "TextChanged", "HandleSettingsLineEditTextChange");
        SubscribeToEvent(cameraOrthographic, "Toggled", "HandleOrthographicToggled");

        SubscribeToEvent(settingsButton, "Released", "ToggleViewportSettingsWindow");
        SubscribeToEvent(settingsWindow.GetChild("ResetCamera", true), "Released", "ResetCamera");
        SubscribeToEvent(settingsWindow.GetChild("CopyTransform", true), "Released", "HandleCopyTransformClicked");
        SubscribeToEvent(settingsWindow.GetChild("CloseButton", true), "Released", "CloseViewportSettingsWindow");
        SubscribeToEvent(settingsWindow.GetChild("Refresh", true), "Released", "UpdateSettingsUI");
        HandleResize();
    }

    void HandleResize()
    {
        viewportContextUI.SetPosition(viewport.rect.left, viewport.rect.top);
        viewportContextUI.SetFixedSize(viewport.rect.width, viewport.rect.height);
        if (viewport.rect.left < 34)
        {
            statusBar.layoutBorder = IntRect(34 - viewport.rect.left, 4, 4, 8);
            IntVector2 pos = settingsWindow.position;
            pos.x = 32 - viewport.rect.left;
            settingsWindow.position = pos;
        }
        else
        {
            statusBar.layoutBorder = IntRect(8, 4, 4, 8);
            IntVector2 pos = settingsWindow.position;
            pos.x = 5;
            settingsWindow.position = pos;
        }

        statusBar.SetFixedSize(viewport.rect.width, 22);
    }

    void ToggleOrthographic()
    {
        SetOrthographic(!camera.orthographic);
    }

    void SetOrthographic(bool orthographic)
    {
        camera.orthographic = orthographic;
        UpdateSettingsUI();
    }

    void Update(float timeStep)
    {
        Vector3 cameraPos = cameraNode.position;
        String xText(cameraPos.x);
        String yText(cameraPos.y);
        String zText(cameraPos.z);
        xText.Resize(8);
        yText.Resize(8);
        zText.Resize(8);

        cameraPosText.text = String(
            "Pos: " + xText + " " + yText + " " + zText +
            " Zoom: " + camera.zoom);

        cameraPosText.size = cameraPosText.minSize;
    }

    void ToggleViewportSettingsWindow()
    {
        if (settingsWindow.visible)
            CloseViewportSettingsWindow();
        else
            OpenViewportSettingsWindow();
    }

    void OpenViewportSettingsWindow()
    {
        UpdateSettingsUI();
        /* settingsWindow.position = */
        settingsWindow.visible = true;
        settingsWindow.BringToFront();
    }

    void CloseViewportSettingsWindow()
    {
        settingsWindow.visible = false;
    }

    void UpdateSettingsUI()
    {
        cameraPosX.text = String(Floor(cameraNode.position.x * 1000) / 1000);
        cameraPosY.text = String(Floor(cameraNode.position.y * 1000) / 1000);
        cameraPosZ.text = String(Floor(cameraNode.position.z * 1000) / 1000);
        cameraRotX.text = String(Floor(cameraNode.rotation.pitch * 1000) / 1000);
        cameraRotY.text = String(Floor(cameraNode.rotation.yaw * 1000) / 1000);
        cameraRotZ.text = String(Floor(cameraNode.rotation.roll * 1000) / 1000);
        cameraZoom.text = String(Floor(camera.zoom * 1000) / 1000);
        cameraOrthoSize.text = String(Floor(camera.orthoSize * 1000) / 1000);
        cameraOrthographic.checked = camera.orthographic;
    }

    void HandleOrthographicToggled(StringHash eventType, VariantMap& eventData)
    {
        SetOrthographic(cameraOrthographic.checked);
    }

    void HandleSettingsLineEditTextChange(StringHash eventType, VariantMap& eventData)
    {
        LineEdit@ element = eventData["Element"].GetPtr();
        if (element.text == "")
            return;

        if (element is cameraRotX ||  element is cameraRotY || element is cameraRotZ)
        {
            Vector3 euler = cameraNode.rotation.eulerAngles;
            if (element is cameraRotX)
                euler.x = element.text.ToFloat();
            else if (element is cameraRotY)
                euler.y = element.text.ToFloat();
            else if (element is cameraRotZ)
                euler.z = element.text.ToFloat();

            cameraNode.rotation = Quaternion(euler);
        }
        else if (element is cameraPosX ||  element is cameraPosY || element is cameraPosZ)
        {
            Vector3 pos = cameraNode.position;
            if (element is cameraPosX)
                pos.x = element.text.ToFloat();
            else if (element is cameraPosY)
                pos.y = element.text.ToFloat();
            else if (element is cameraPosZ)
                pos.z = element.text.ToFloat();

            cameraNode.position = pos;
        }
        else if (element is cameraZoom)
            camera.zoom = element.text.ToFloat();
        else if (element is cameraOrthoSize)
            camera.orthoSize = element.text.ToFloat();
    }
    void HandleCopyTransformClicked(StringHash eventType, VariantMap& eventData)
    {
        if (editNode !is null)
        {
            editNode.position = cameraNode.position;
            editNode.rotation = cameraNode.rotation;
        }
    }
}

Array<ViewportContext@> viewports;
ViewportContext@ activeViewport;

Text@ editorModeText;
Text@ renderStatsText;

EditMode editMode = EDIT_MOVE;
AxisMode axisMode = AXIS_WORLD;
FillMode fillMode = FILL_SOLID;
SnapScaleMode snapScaleMode = SNAP_SCALE_FULL;

float viewNearClip = 0.1;
float viewFarClip = 1000.0;
float viewFov = 45.0;

float cameraBaseSpeed = 10;
float cameraBaseRotationSpeed = 0.2;
float cameraShiftSpeedMultiplier = 5;
float newNodeDistance = 20;
float moveStep = 0.5;
float rotateStep = 5;
float scaleStep = 0.1;
float snapScale = 1.0;
bool limitRotation = false;
bool moveSnap = false;
bool rotateSnap = false;
bool scaleSnap = false;
bool renderingDebug = false;
bool physicsDebug = false;
bool octreeDebug = false;
int pickMode = PICK_GEOMETRIES;
bool orbiting = false;

enum MouseOrbitMode
{
    ORBIT_RELATIVE = 0,
    ORBIT_WRAP
}

bool toggledMouseLock = false;
int mouseOrbitMode = ORBIT_RELATIVE;

bool showGrid = true;
bool grid2DMode = false;
uint gridSize = 16;
uint gridSubdivisions = 3;
float gridScale = 8.0;
Color gridColor(0.1, 0.1, 0.1);
Color gridSubdivisionColor(0.05, 0.05, 0.05);
Color gridXColor(0.5, 0.1, 0.1);
Color gridYColor(0.1, 0.5, 0.1);
Color gridZColor(0.1, 0.1, 0.5);

Array<int> pickModeDrawableFlags = {
    DRAWABLE_GEOMETRY,
    DRAWABLE_LIGHT,
    DRAWABLE_ZONE
};

Array<String> editModeText = {
    "Move",
    "Rotate",
    "Scale",
    "Select",
    "Spawn"
};

Array<String> axisModeText = {
    "World",
    "Local"
};

Array<String> pickModeText = {
    "Geometries",
    "Lights",
    "Zones",
    "Rigidbodies",
    "UI-elements"
};

Array<String> fillModeText = {
    "Solid",
    "Wire",
    "Point"
};

void SetRenderPath(const String&in newRenderPathName)
{
    renderPath = null;
    renderPathName = newRenderPathName.Trimmed();

    if (renderPathName.length > 0)
    {
        File@ file = cache.GetFile(renderPathName);
        if (file !is null)
        {
            XMLFile@ xml = XMLFile();
            if (xml.Load(file))
            {
                renderPath = RenderPath();
                if (!renderPath.Load(xml))
                    renderPath = null;
            }
        }
    }
    
    // If renderPath is null, the engine default will be used
    for (uint i = 0; i < renderer.numViewports; ++i)
        renderer.viewports[i].renderPath = renderPath;

    if (materialPreview !is null && materialPreview.viewport !is null)
        materialPreview.viewport.renderPath = renderPath;
}

void CreateCamera()
{
    // Set the initial viewport rect
    viewportArea = IntRect(0, 0, graphics.width, graphics.height);

    SetViewportMode(viewportMode);
    SetActiveViewport(viewports[0]);

    // Note: the camera is not inside the scene, so that it is not listed, and does not get deleted
    ResetCamera();

    SubscribeToEvent("PostRenderUpdate", "HandlePostRenderUpdate");
    SubscribeToEvent("UIMouseClick", "ViewMouseClick");
    SubscribeToEvent("MouseMove", "ViewMouseMove");
    SubscribeToEvent("UIMouseClickEnd", "ViewMouseClickEnd");
    SubscribeToEvent("BeginViewUpdate", "HandleBeginViewUpdate");
    SubscribeToEvent("EndViewUpdate", "HandleEndViewUpdate");
    SubscribeToEvent("BeginViewRender", "HandleBeginViewRender");
    SubscribeToEvent("EndViewRender", "HandleEndViewRender");

    // Set initial renderpath if defined
    SetRenderPath(renderPathName);
}

// Create any UI associated with changing the editor viewports
void CreateViewportUI()
{
    if (viewportUI is null)
    {
        viewportUI = UIElement();
        ui.root.AddChild(viewportUI);
    }

    viewportUI.SetFixedSize(viewportArea.width, viewportArea.height);
    viewportUI.position = IntVector2(viewportArea.top, viewportArea.left);
    viewportUI.clipChildren = true;
    viewportUI.clipBorder = viewportUIClipBorder;
    viewportUI.RemoveAllChildren();
    viewportUI.priority = -2000;

    Array<BorderImage@> borders;

    IntRect top;
    IntRect bottom;
    IntRect left;
    IntRect right;
    IntRect topLeft;
    IntRect topRight;
    IntRect bottomLeft;
    IntRect bottomRight;

    for (uint i = 0; i < viewports.length; ++i)
    {
        ViewportContext@ vc = viewports[i];
        vc.CreateViewportContextUI();

        if (vc.viewportId & VIEWPORT_TOP > 0)
            top = vc.viewport.rect;
        else if (vc.viewportId & VIEWPORT_BOTTOM > 0)
            bottom = vc.viewport.rect;
        else if (vc.viewportId & VIEWPORT_LEFT > 0)
            left = vc.viewport.rect;
        else if (vc.viewportId & VIEWPORT_RIGHT > 0)
            right = vc.viewport.rect;
        else if (vc.viewportId & VIEWPORT_TOP_LEFT > 0)
            topLeft = vc.viewport.rect;
        else if (vc.viewportId & VIEWPORT_TOP_RIGHT > 0)
            topRight = vc.viewport.rect;
        else if (vc.viewportId & VIEWPORT_BOTTOM_LEFT > 0)
            bottomLeft = vc.viewport.rect;
        else if (vc.viewportId & VIEWPORT_BOTTOM_RIGHT > 0)
            bottomRight = vc.viewport.rect;
    }

    // Creates resize borders based on the mode set
    if (viewportMode == VIEWPORT_QUAD) // independent borders for quad isn't easy
    {
        borders.Push(CreateViewportDragBorder(VIEWPORT_BORDER_V, topLeft.right - viewportBorderOffset, topLeft.top, viewportBorderWidth, viewportArea.height));
        borders.Push(CreateViewportDragBorder(VIEWPORT_BORDER_H, topLeft.left, topLeft.bottom-viewportBorderOffset, viewportArea.width, viewportBorderWidth));
    }
    else
    {
        // Figures what borders to create based on mode
        if (viewportMode & (VIEWPORT_LEFT|VIEWPORT_RIGHT) > 0)
        {
            borders.Push(
                viewportMode & VIEWPORT_LEFT > 0 ?
                    CreateViewportDragBorder(VIEWPORT_BORDER_V, left.right-viewportBorderOffset, left.top, viewportBorderWidth, left.height) :
                    CreateViewportDragBorder(VIEWPORT_BORDER_V, right.left-viewportBorderOffset, right.top, viewportBorderWidth, right.height)
                );
        }
        else
        {
            if (viewportMode & (VIEWPORT_TOP_LEFT|VIEWPORT_TOP_RIGHT) > 0)
                borders.Push(CreateViewportDragBorder(VIEWPORT_BORDER_V1, topLeft.right-viewportBorderOffset, topLeft.top, viewportBorderWidth, topLeft.height));
            if (viewportMode & (VIEWPORT_BOTTOM_LEFT|VIEWPORT_BOTTOM_RIGHT) > 0)
                borders.Push(CreateViewportDragBorder(VIEWPORT_BORDER_V2, bottomLeft.right-viewportBorderOffset, bottomLeft.top, viewportBorderWidth, bottomLeft.height));
        }

        if (viewportMode & (VIEWPORT_TOP|VIEWPORT_BOTTOM) > 0)
        {
            borders.Push(
                viewportMode & VIEWPORT_TOP > 0 ?
                    CreateViewportDragBorder(VIEWPORT_BORDER_H, top.left, top.bottom-viewportBorderOffset, top.width, viewportBorderWidth) :
                    CreateViewportDragBorder(VIEWPORT_BORDER_H, bottom.left, bottom.top-viewportBorderOffset, bottom.width, viewportBorderWidth)
                );
        }
        else
        {
            if (viewportMode & (VIEWPORT_TOP_LEFT|VIEWPORT_BOTTOM_LEFT) > 0)
                borders.Push(CreateViewportDragBorder(VIEWPORT_BORDER_H1, topLeft.left, topLeft.bottom-viewportBorderOffset, topLeft.width, viewportBorderWidth));
            if (viewportMode & (VIEWPORT_TOP_RIGHT|VIEWPORT_BOTTOM_RIGHT) > 0)
                borders.Push(CreateViewportDragBorder(VIEWPORT_BORDER_H2, topRight.left, topRight.bottom-viewportBorderOffset, topRight.width, viewportBorderWidth));
        }
    }
}

BorderImage@ CreateViewportDragBorder(uint value, int posX, int posY, int sizeX, int sizeY)
{
    BorderImage@ border = BorderImage();
    viewportUI.AddChild(border);
    border.name = "border";
    border.style = "ViewportBorder";
    border.vars["VIEWMODE"] = value;
    border.SetFixedSize(sizeX, sizeY); // relevant size gets set by viewport later
    border.position = IntVector2(posX, posY);
    border.opacity = uiMaxOpacity;
    SubscribeToEvent(border, "DragMove", "HandleViewportBorderDragMove");
    SubscribeToEvent(border, "DragEnd", "HandleViewportBorderDragEnd");
    return border;
}

void SetFillMode(FillMode fillMode_)
{
    fillMode = fillMode_;
    for (uint i = 0; i < viewports.length; ++i)
        viewports[i].camera.fillMode = fillMode_;
}


// Sets the viewport mode
void SetViewportMode(uint mode = VIEWPORT_SINGLE)
{
    // Remember old viewport positions
    Array<Vector3> cameraPositions;
    Array<Quaternion> cameraRotations;
    for (uint i = 0; i < viewports.length; ++i)
    {
        cameraPositions.Push(viewports[i].cameraNode.position);
        cameraRotations.Push(viewports[i].cameraNode.rotation);
    }

    viewports.Clear();
    viewportMode = mode;

    // Always have quad a
    {
        uint viewport = 0;
        ViewportContext@ vc = ViewportContext(
            IntRect(
                0,
                0,
                mode & (VIEWPORT_LEFT|VIEWPORT_TOP_LEFT) > 0 ? viewportArea.width / 2 : viewportArea.width,
                mode & (VIEWPORT_TOP|VIEWPORT_TOP_LEFT) > 0 ? viewportArea.height / 2 : viewportArea.height),
            viewports.length + 1,
            viewportMode & (VIEWPORT_TOP|VIEWPORT_LEFT|VIEWPORT_TOP_LEFT)
        );
        viewports.Push(vc);
    }

    uint topRight = viewportMode & (VIEWPORT_RIGHT|VIEWPORT_TOP_RIGHT);
    if (topRight > 0)
    {
        ViewportContext@ vc = ViewportContext(
            IntRect(
                viewportArea.width/2,
                0,
                viewportArea.width,
                mode & VIEWPORT_TOP_RIGHT > 0 ? viewportArea.height / 2 : viewportArea.height),
            viewports.length + 1,
            topRight
        );
        viewports.Push(vc);
    }

    uint bottomLeft = viewportMode & (VIEWPORT_BOTTOM|VIEWPORT_BOTTOM_LEFT);
    if (bottomLeft > 0)
    {
        ViewportContext@ vc = ViewportContext(
            IntRect(
                0,
                viewportArea.height / 2,
                mode & (VIEWPORT_BOTTOM_LEFT) > 0 ? viewportArea.width / 2 : viewportArea.width,
                viewportArea.height),
            viewports.length + 1,
            bottomLeft
        );
        viewports.Push(vc);
    }

    uint bottomRight = viewportMode & (VIEWPORT_BOTTOM_RIGHT);
    if (bottomRight > 0)
    {
        ViewportContext@ vc = ViewportContext(
            IntRect(
                viewportArea.width / 2,
                viewportArea.height / 2,
                viewportArea.width,
                viewportArea.height),
            viewports.length + 1,
            bottomRight
        );
        viewports.Push(vc);
    }

    renderer.numViewports = viewports.length;
    for (uint i = 0; i < viewports.length; ++i)
        renderer.viewports[i] = viewports[i].viewport;

    // Restore camera positions as applicable. Default new viewports to the last camera position
    if (cameraPositions.length > 0)
    {
        for (uint i = 0; i < viewports.length; ++i)
        {
            uint src = i;
            if (src >= cameraPositions.length)
                src = cameraPositions.length - 1;
            viewports[i].cameraNode.position = cameraPositions[src];
            viewports[i].cameraNode.rotation = cameraRotations[src];
        }
    }

    ReacquireCameraYawPitch();
    UpdateViewParameters();
    UpdateCameraPreview();
    CreateViewportUI();
}

// Create a preview viewport if a camera component is selected
void UpdateCameraPreview()
{
    previewCamera = null;
    StringHash cameraType("Camera");

    for (uint i = 0; i < selectedComponents.length; ++i)
    {
        if (selectedComponents[i].type == cameraType)
        {
            // Take the first encountered camera
            previewCamera = selectedComponents[i];
            break;
        }
    }
    // Also try nodes if not found from components
    if (previewCamera.Get() is null)
    {
        for (uint i = 0; i < selectedNodes.length; ++i)
        {
            previewCamera = selectedNodes[i].GetComponent("Camera");
            if (previewCamera.Get() !is null)
                break;
        }
    }

    // Remove extra viewport if it exists and no camera is selected
    if (previewCamera.Get() is null)
    {
        if (renderer.numViewports > viewports.length)
            renderer.numViewports = viewports.length;
    }
    else
    {
        if (renderer.numViewports < viewports.length + 1)
            renderer.numViewports = viewports.length + 1;

        int previewWidth = graphics.width / 4;
        int previewHeight = previewWidth * 9 / 16;
        int previewX = graphics.width - 10 - previewWidth;
        int previewY = graphics.height - 30 - previewHeight;

        Viewport@ previewView = Viewport();
        previewView.scene = editorScene;
        previewView.camera = previewCamera.Get();
        previewView.rect = IntRect(previewX, previewY, previewX + previewWidth, previewY + previewHeight);
        previewView.renderPath = renderPath;
        renderer.viewports[viewports.length] = previewView;
    }
}

void HandleViewportBorderDragMove(StringHash eventType, VariantMap& eventData)
{
    UIElement@ dragBorder = eventData["Element"].GetPtr();
    if (dragBorder is null)
        return;

    uint hPos;
    uint vPos;

    // Moves border to new cursor position, restricts motion to 1 axis, and keeps borders within view area
    if (resizingBorder & VIEWPORT_BORDER_V_ANY > 0)
    {
        hPos = Clamp(ui.cursorPosition.x, 150, viewportArea.width-150);
        vPos = dragBorder.position.y;
        dragBorder.position = IntVector2(hPos, vPos);
    }
    if (resizingBorder & VIEWPORT_BORDER_H_ANY > 0)
    {
        vPos = Clamp(ui.cursorPosition.y, 150, viewportArea.height-150);
        hPos = dragBorder.position.x;
        dragBorder.position = IntVector2(hPos, vPos);
    }

    // Move all dependent borders
    Array<UIElement@> borders = viewportUI.GetChildren();
    for (uint i = 0; i < borders.length; ++i)
    {
        BorderImage@ border = borders[i];
        if (border is null || border is dragBorder || border.name != "border")
            continue;

        uint borderViewMode = border.vars["VIEWMODE"].GetUInt();
        if (resizingBorder == VIEWPORT_BORDER_H)
        {
            if (borderViewMode == VIEWPORT_BORDER_V1)
            {
                border.SetFixedHeight(vPos);
            }
            else if (borderViewMode == VIEWPORT_BORDER_V2)
            {
                border.position = IntVector2(border.position.x, vPos);
                border.SetFixedHeight(viewportArea.height - vPos);
            }
        }
        else if (resizingBorder == VIEWPORT_BORDER_V)
        {
            if (borderViewMode == VIEWPORT_BORDER_H1)
            {
                border.SetFixedWidth(hPos);
            }
            else if (borderViewMode == VIEWPORT_BORDER_H2)
            {
                border.position = IntVector2(hPos, border.position.y);
                border.SetFixedWidth(viewportArea.width - hPos);
            }
        }
    }
}

void HandleViewportBorderDragEnd(StringHash eventType, VariantMap& eventData)
{
    // Sets the new viewports by checking all the dependencies
    Array<UIElement@> children = viewportUI.GetChildren();
    Array<BorderImage@> borders;

    BorderImage@ borderV;
    BorderImage@ borderV1;
    BorderImage@ borderV2;
    BorderImage@ borderH;
    BorderImage@ borderH1;
    BorderImage@ borderH2;

    for (uint i = 0; i < children.length; ++i)
    {
        if (children[i].name == "border")
        {
            BorderImage@ border = children[i];
            uint mode = border.vars["VIEWMODE"].GetUInt();
            if (mode == VIEWPORT_BORDER_V)
                borderV = border;
            else if (mode == VIEWPORT_BORDER_V1)
                borderV1 = border;
            else if (mode == VIEWPORT_BORDER_V2)
                borderV2 = border;
            else if (mode == VIEWPORT_BORDER_H)
                borderH = border;
            else if (mode == VIEWPORT_BORDER_H1)
                borderH1 = border;
            else if (mode == VIEWPORT_BORDER_H2)
                borderH2 = border;
        }
    }

    IntRect top;
    IntRect bottom;
    IntRect left;
    IntRect right;
    IntRect topLeft;
    IntRect topRight;
    IntRect bottomLeft;
    IntRect bottomRight;

    for (uint i = 0; i < viewports.length; ++i)
    {
        ViewportContext@ vc = viewports[i];
        if (vc.viewportId & VIEWPORT_TOP > 0)
            top = vc.viewport.rect;
        else if (vc.viewportId & VIEWPORT_BOTTOM > 0)
            bottom = vc.viewport.rect;
        else if (vc.viewportId & VIEWPORT_LEFT > 0)
            left = vc.viewport.rect;
        else if (vc.viewportId & VIEWPORT_RIGHT > 0)
            right = vc.viewport.rect;
        else if (vc.viewportId & VIEWPORT_TOP_LEFT > 0)
            topLeft = vc.viewport.rect;
        else if (vc.viewportId & VIEWPORT_TOP_RIGHT > 0)
            topRight = vc.viewport.rect;
        else if (vc.viewportId & VIEWPORT_BOTTOM_LEFT > 0)
            bottomLeft = vc.viewport.rect;
        else if (vc.viewportId & VIEWPORT_BOTTOM_RIGHT > 0)
            bottomRight = vc.viewport.rect;
    }

    if (borderV !is null)
    {
        if (viewportMode & VIEWPORT_LEFT > 0)
            left.right = borderV.position.x + viewportBorderOffset;
        if (viewportMode & VIEWPORT_TOP_LEFT > 0)
            topLeft.right = borderV.position.x + viewportBorderOffset;
        if (viewportMode & VIEWPORT_TOP_RIGHT > 0)
            topRight.left = borderV.position.x + viewportBorderOffset;
        if (viewportMode & VIEWPORT_RIGHT > 0)
            right.left = borderV.position.x + viewportBorderOffset;
        if (viewportMode & VIEWPORT_BOTTOM_LEFT > 0)
            bottomLeft.right = borderV.position.x + viewportBorderOffset;
        if (viewportMode & VIEWPORT_BOTTOM_RIGHT > 0)
            bottomRight.left = borderV.position.x + viewportBorderOffset;
    }
    else
    {
        if (borderV1 !is null)
        {
            if (viewportMode & VIEWPORT_TOP_LEFT > 0)
                topLeft.right = borderV1.position.x + viewportBorderOffset;
            if (viewportMode & VIEWPORT_TOP_RIGHT > 0)
                topRight.left = borderV1.position.x + viewportBorderOffset;
        }
        if (borderV2 !is null)
        {
            if (viewportMode & VIEWPORT_BOTTOM_LEFT > 0)
                bottomLeft.right = borderV2.position.x + viewportBorderOffset;
            if (viewportMode & VIEWPORT_BOTTOM_RIGHT > 0)
                bottomRight.left = borderV2.position.x + viewportBorderOffset;
        }
    }

    if (borderH !is null)
    {
        if (viewportMode & VIEWPORT_TOP > 0)
            top.bottom = borderH.position.y + viewportBorderOffset;
        if (viewportMode & VIEWPORT_TOP_LEFT > 0)
            topLeft.bottom = borderH.position.y + viewportBorderOffset;
        if (viewportMode & VIEWPORT_BOTTOM_LEFT > 0)
            bottomLeft.top = borderH.position.y + viewportBorderOffset;
        if (viewportMode & VIEWPORT_BOTTOM > 0)
            bottom.top = borderH.position.y + viewportBorderOffset;
        if (viewportMode & VIEWPORT_TOP_RIGHT > 0)
            topRight.bottom = borderH.position.y + viewportBorderOffset;
        if (viewportMode & VIEWPORT_BOTTOM_RIGHT > 0)
            bottomRight.top = borderH.position.y + viewportBorderOffset;
    }
    else
    {
        if (borderH1 !is null)
        {
            if (viewportMode & VIEWPORT_TOP_LEFT > 0)
                topLeft.bottom = borderH1.position.y+viewportBorderOffset;
            if (viewportMode & VIEWPORT_BOTTOM_LEFT > 0)
                bottomLeft.top = borderH1.position.y+viewportBorderOffset;
        }
        if (borderH2 !is null)
        {
            if (viewportMode & VIEWPORT_TOP_RIGHT > 0)
                topRight.bottom = borderH2.position.y+viewportBorderOffset;
            if (viewportMode & VIEWPORT_BOTTOM_RIGHT > 0)
                bottomRight.top = borderH2.position.y+viewportBorderOffset;
        }
    }

    // Applies the calculated changes
    for (uint i = 0; i < viewports.length; ++i)
    {
        ViewportContext@ vc = viewports[i];
        if (vc.viewportId & VIEWPORT_TOP > 0)
            vc.viewport.rect = top;
        else if (vc.viewportId & VIEWPORT_BOTTOM > 0)
            vc.viewport.rect = bottom;
        else if (vc.viewportId & VIEWPORT_LEFT > 0)
            vc.viewport.rect = left;
        else if (vc.viewportId & VIEWPORT_RIGHT > 0)
            vc.viewport.rect = right;
        else if (vc.viewportId & VIEWPORT_TOP_LEFT > 0)
            vc.viewport.rect = topLeft;
        else if (vc.viewportId & VIEWPORT_TOP_RIGHT > 0)
            vc.viewport.rect = topRight;
        else if (vc.viewportId & VIEWPORT_BOTTOM_LEFT > 0)
            vc.viewport.rect = bottomLeft;
        else if (vc.viewportId & VIEWPORT_BOTTOM_RIGHT > 0)
            vc.viewport.rect = bottomRight;
        vc.HandleResize();
    }

    // End drag state
    resizingBorder = 0;
    setViewportCursor = 0;
}

void SetViewportCursor()
{
    if (setViewportCursor & VIEWPORT_BORDER_V_ANY > 0)
        ui.cursor.shape = CS_RESIZEHORIZONTAL;
    else if (setViewportCursor & VIEWPORT_BORDER_H_ANY > 0)
        ui.cursor.shape = CS_RESIZEVERTICAL;
}

void SetActiveViewport(ViewportContext@ context)
{
    // Sets the global variables to the current context
    @cameraNode = context.cameraNode;
    @camera = context.camera;
    @audio.listener = context.soundListener;

    // Camera is created before gizmo, this gets called again after UI is created
    if (gizmo !is null)
        gizmo.viewMask = camera.viewMask;

    @activeViewport = context;

    // If a mode is changed while in a drag or hovering over a border these can get out of sync
    resizingBorder = 0;
    setViewportCursor = 0;
}

void ResetCamera()
{
    for (uint i = 0; i < viewports.length; ++i)
        viewports[i].ResetCamera();
}

void ReacquireCameraYawPitch()
{
    for (uint i = 0; i < viewports.length; ++i)
        viewports[i].ReacquireCameraYawPitch();
}

void UpdateViewParameters()
{
    for (uint i = 0; i < viewports.length; ++i)
    {
        viewports[i].camera.nearClip = viewNearClip;
        viewports[i].camera.farClip = viewFarClip;
        viewports[i].camera.fov = viewFov;
    }
}

void CreateGrid()
{
    if (gridNode !is null)
        gridNode.Remove();

    gridNode = Node();
    grid = gridNode.CreateComponent("CustomGeometry");
    grid.numGeometries = 1;
    grid.material = cache.GetResource("Material", "Materials/VColUnlit.xml");
    grid.viewMask = 0x80000000; // Editor raycasts use viewmask 0x7fffffff
    grid.occludee = false;

    UpdateGrid();
}

void HideGrid()
{
    if (grid !is null)
        grid.enabled = false;
}

void ShowGrid()
{
    if (grid !is null)
    {
        grid.enabled = true;

        if (editorScene.octree !is null)
            editorScene.octree.AddManualDrawable(grid);
    }
}

void UpdateGrid(bool updateGridGeometry = true)
{
    showGrid ? ShowGrid() : HideGrid();
    gridNode.scale = Vector3(gridScale, gridScale, gridScale);

    if (!updateGridGeometry)
        return;

    uint size = uint(Floor(gridSize / 2) * 2);
    float halfSizeScaled = size / 2;
    float scale = 1.0;
    uint subdivisionSize = uint(Pow(2.0f, float(gridSubdivisions)));

    if (subdivisionSize > 0)
    {
        size *= subdivisionSize;
        scale /= subdivisionSize;
    }

    uint halfSize = size / 2;

    grid.BeginGeometry(0, LINE_LIST);
    float lineOffset = -halfSizeScaled;
    for (uint i = 0; i <= size; ++i)
    {
        bool lineCenter = i == halfSize;
        bool lineSubdiv = !Equals(Mod(i, subdivisionSize), 0.0);

        if (!grid2DMode)
        {
            grid.DefineVertex(Vector3(lineOffset, 0.0, halfSizeScaled));
            grid.DefineColor(lineCenter ? gridZColor : (lineSubdiv ? gridSubdivisionColor : gridColor));
            grid.DefineVertex(Vector3(lineOffset, 0.0, -halfSizeScaled));
            grid.DefineColor(lineCenter ? gridZColor : (lineSubdiv ? gridSubdivisionColor : gridColor));

            grid.DefineVertex(Vector3(-halfSizeScaled, 0.0, lineOffset));
            grid.DefineColor(lineCenter ? gridXColor : (lineSubdiv ? gridSubdivisionColor : gridColor));
            grid.DefineVertex(Vector3(halfSizeScaled, 0.0, lineOffset));
            grid.DefineColor(lineCenter ? gridXColor : (lineSubdiv ? gridSubdivisionColor : gridColor));
        }
        else
        {
            grid.DefineVertex(Vector3(lineOffset, halfSizeScaled, 0.0));
            grid.DefineColor(lineCenter ? gridYColor : (lineSubdiv ? gridSubdivisionColor : gridColor));
            grid.DefineVertex(Vector3(lineOffset, -halfSizeScaled, 0.0));
            grid.DefineColor(lineCenter ? gridYColor : (lineSubdiv ? gridSubdivisionColor : gridColor));

            grid.DefineVertex(Vector3(-halfSizeScaled, lineOffset, 0.0));
            grid.DefineColor(lineCenter ? gridXColor : (lineSubdiv ? gridSubdivisionColor : gridColor));
            grid.DefineVertex(Vector3(halfSizeScaled, lineOffset, 0.0));
            grid.DefineColor(lineCenter ? gridXColor : (lineSubdiv ? gridSubdivisionColor : gridColor));
        }

        lineOffset  += scale;
    }
    grid.Commit();
}

void CreateStatsBar()
{
    Font@ font = cache.GetResource("Font", "Fonts/Anonymous Pro.ttf");

    editorModeText = Text();
    ui.root.AddChild(editorModeText);
    renderStatsText = Text();
    ui.root.AddChild(renderStatsText);

    if (ui.root.width >= 1200)
    {
        SetupStatsBarText(editorModeText, font, 35, 64, HA_LEFT, VA_TOP);
        SetupStatsBarText(renderStatsText, font, -4, 64, HA_RIGHT, VA_TOP);
    }
    else
    {
        SetupStatsBarText(editorModeText, font, 35, 64, HA_LEFT, VA_TOP);
        SetupStatsBarText(renderStatsText, font, 35, 78, HA_LEFT, VA_TOP);
    }
}

void SetupStatsBarText(Text@ text, Font@ font, int x, int y, HorizontalAlignment hAlign, VerticalAlignment vAlign)
{
    text.position = IntVector2(x, y);
    text.horizontalAlignment = hAlign;
    text.verticalAlignment = vAlign;
    text.SetFont(font, 11);
    text.color = Color(1, 1, 0);
    text.textEffect = TE_SHADOW;
    text.priority = -100;
}

void UpdateStats(float timeStep)
{
    editorModeText.text = String(
        "Mode: " + editModeText[editMode] +
        "  Axis: " + axisModeText[axisMode] +
        "  Pick: " + pickModeText[pickMode] +
        "  Fill: " + fillModeText[fillMode] +
        "  Updates: " + (runUpdate ? "Running" : "Paused"));

    renderStatsText.text = String(
        "Tris: " + renderer.numPrimitives +
        "  Batches: " + renderer.numBatches +
        "  Lights: " + renderer.numLights[true] +
        "  Shadowmaps: " + renderer.numShadowMaps[true] +
        "  Occluders: " + renderer.numOccluders[true]);

    editorModeText.size = editorModeText.minSize;
    renderStatsText.size = renderStatsText.minSize;
}

void UpdateViewports(float timeStep)
{
    for(uint i = 0; i < viewports.length; ++i)
    {
        ViewportContext@ viewportContext = viewports[i];
        viewportContext.Update(timeStep);
    }
}

void SetMouseMode(bool enable)
{
    if (enable)
    {
        if (mouseOrbitMode == ORBIT_RELATIVE)
        {
            input.mouseMode = MM_RELATIVE;
            ui.cursor.visible = false;
        }
        else if (mouseOrbitMode == ORBIT_WRAP)
            input.mouseMode = MM_WRAP;
    }
    else
    {
        input.mouseMode = MM_ABSOLUTE;
        ui.cursor.visible = true;
    }
}

void SetMouseLock()
{
    toggledMouseLock = true;
    SetMouseMode(true);
    FadeUI();
}

void ReleaseMouseLock()
{
    if (toggledMouseLock)
    {
        toggledMouseLock = false;
        SetMouseMode(false);
    }
}

void UpdateView(float timeStep)
{
    if (ui.HasModalElement() || ui.focusElement !is null)
    {
        ReleaseMouseLock();
        return;
    }

    // Move camera
    if (!input.keyDown[KEY_LCTRL])
    {
        float speedMultiplier = 1.0;
        if (input.keyDown[KEY_LSHIFT])
            speedMultiplier = cameraShiftSpeedMultiplier;

        if (input.keyDown['W'] || input.keyDown[KEY_UP])
        {
            cameraNode.Translate(Vector3(0, 0, cameraBaseSpeed) * timeStep * speedMultiplier);
            FadeUI();
        }
        if (input.keyDown['S'] || input.keyDown[KEY_DOWN])
        {
            cameraNode.Translate(Vector3(0, 0, -cameraBaseSpeed) * timeStep * speedMultiplier);
            FadeUI();
        }
        if (input.keyDown['A'] || input.keyDown[KEY_LEFT])
        {
            cameraNode.Translate(Vector3(-cameraBaseSpeed, 0, 0) * timeStep * speedMultiplier);
            FadeUI();
        }
        if (input.keyDown['D'] || input.keyDown[KEY_RIGHT])
        {
            cameraNode.Translate(Vector3(cameraBaseSpeed, 0, 0) * timeStep * speedMultiplier);
            FadeUI();
        }
        if (input.keyDown['E'] || input.keyDown[KEY_PAGEUP])
        {
            cameraNode.Translate(Vector3(0, cameraBaseSpeed, 0) * timeStep * speedMultiplier, TS_WORLD);
            FadeUI();
        }
        if (input.keyDown['Q'] || input.keyDown[KEY_PAGEDOWN])
        {
            cameraNode.Translate(Vector3(0, -cameraBaseSpeed, 0) * timeStep * speedMultiplier, TS_WORLD);
            FadeUI();
        }
        if (input.mouseMoveWheel != 0 && ui.GetElementAt(ui.cursor.position) is null)
        {
            if (mouseWheelCameraPosition)
            {
                cameraNode.Translate(Vector3(0, 0, -cameraBaseSpeed) * -input.mouseMoveWheel*20 * timeStep *
                    speedMultiplier);
            }
            else
            {
                float zoom = camera.zoom + -input.mouseMoveWheel *.1 * speedMultiplier;
                camera.zoom = Clamp(zoom, .1, 30);
            }
        }
    }

    // Rotate/orbit/pan camera
    if (input.mouseButtonDown[MOUSEB_RIGHT] || input.mouseButtonDown[MOUSEB_MIDDLE])
    {
        SetMouseLock();

        IntVector2 mouseMove = input.mouseMove;
        if (mouseMove.x != 0 || mouseMove.y != 0)
        {
            if (input.keyDown[KEY_LSHIFT] && input.mouseButtonDown[MOUSEB_MIDDLE])
            {
                cameraNode.Translate(Vector3(-mouseMove.x, mouseMove.y, 0) * timeStep * cameraBaseSpeed * 0.5);
            }
            else
            {
                activeViewport.cameraYaw += mouseMove.x * cameraBaseRotationSpeed;
                activeViewport.cameraPitch += mouseMove.y * cameraBaseRotationSpeed;

                if (limitRotation)
                    activeViewport.cameraPitch = Clamp(activeViewport.cameraPitch, -90.0, 90.0);

                Quaternion q = Quaternion(activeViewport.cameraPitch, activeViewport.cameraYaw, 0);
                cameraNode.rotation = q;
                if (input.mouseButtonDown[MOUSEB_MIDDLE] && (selectedNodes.length > 0 || selectedComponents.length > 0))
                {
                    Vector3 centerPoint = SelectedNodesCenterPoint();
                    Vector3 d = cameraNode.worldPosition - centerPoint;
                    cameraNode.worldPosition = centerPoint - q * Vector3(0.0, 0.0, d.length);
                    orbiting = true;
                }
            }
        }
    }
    else
        ReleaseMouseLock();

    if (orbiting && !input.mouseButtonDown[MOUSEB_MIDDLE])
        orbiting = false;

    // Move/rotate/scale object
    if (!editNodes.empty && editMode != EDIT_SELECT && input.keyDown[KEY_LCTRL])
    {
        Vector3 adjust(0, 0, 0);
        if (input.keyDown[KEY_UP])
            adjust.z = 1;
        if (input.keyDown[KEY_DOWN])
            adjust.z = -1;
        if (input.keyDown[KEY_LEFT])
            adjust.x = -1;
        if (input.keyDown[KEY_RIGHT])
            adjust.x = 1;
        if (input.keyDown[KEY_PAGEUP])
            adjust.y = 1;
        if (input.keyDown[KEY_PAGEDOWN])
            adjust.y = -1;
        if (editMode == EDIT_SCALE)
        {
            if (input.keyDown[KEY_KP_PLUS])
                adjust = Vector3(1, 1, 1);
            if (input.keyDown[KEY_KP_MINUS])
                adjust = Vector3(-1, -1, -1);
        }

        if (adjust == Vector3(0, 0, 0))
            return;

        bool moved = false;
        adjust *= timeStep * 10;

        switch (editMode)
        {
        case EDIT_MOVE:
            if (!moveSnap)
                moved = MoveNodes(adjust * moveStep);
            break;

        case EDIT_ROTATE:
            if (!rotateSnap)
                moved = RotateNodes(adjust * rotateStep);
            break;

        case EDIT_SCALE:
            if (!scaleSnap)
                moved = ScaleNodes(adjust * scaleStep);
            break;
        }

        if (moved)
            UpdateNodeAttributes();
    }

    // If not dragging
    if (resizingBorder == 0)
    {
        UIElement@ uiElement = ui.GetElementAt(ui.cursorPosition);
        if (uiElement !is null && uiElement.vars.Contains("VIEWMODE"))
        {
            setViewportCursor = uiElement.vars["VIEWMODE"].GetUInt();
            if (input.mouseButtonDown[MOUSEB_LEFT])
                resizingBorder = setViewportCursor;
        }
    }
}

void SteppedObjectManipulation(int key)
{
    if (editNodes.empty || editMode == EDIT_SELECT)
        return;

    // Do not react in non-snapped mode, because that is handled in frame update
    if (editMode == EDIT_MOVE && !moveSnap)
        return;
    if (editMode == EDIT_ROTATE && !rotateSnap)
        return;
    if (editMode == EDIT_SCALE && !scaleSnap)
        return;

    Vector3 adjust(0, 0, 0);
    if (key == KEY_UP)
        adjust.z = 1;
    if (key == KEY_DOWN)
        adjust.z = -1;
    if (key == KEY_LEFT)
        adjust.x = -1;
    if (key == KEY_RIGHT)
        adjust.x = 1;
    if (key == KEY_PAGEUP)
        adjust.y = 1;
    if (key == KEY_PAGEDOWN)
        adjust.y = -1;
    if (editMode == EDIT_SCALE)
    {
        if (key == KEY_KP_PLUS)
            adjust = Vector3(1, 1, 1);
        if (key == KEY_KP_MINUS)
            adjust = Vector3(-1, -1, -1);
    }

    if (adjust == Vector3(0, 0, 0))
        return;

    bool moved = false;

    switch (editMode)
    {
    case EDIT_MOVE:
        moved = MoveNodes(adjust);
        break;

    case EDIT_ROTATE:
        {
            float rotateStepScaled = rotateStep * snapScale;
            moved = RotateNodes(adjust * rotateStepScaled);
        }
        break;

    case EDIT_SCALE:
        {
            float scaleStepScaled = scaleStep * snapScale;
            moved = ScaleNodes(adjust * scaleStepScaled);
        }
        break;
    }

    if (moved)
        UpdateNodeAttributes();
}

void HandlePostRenderUpdate()
{
    DebugRenderer@ debug = editorScene.debugRenderer;
    if (debug is null || orbiting)
        return;

    // Visualize the currently selected nodes
    for (uint i = 0; i < selectedNodes.length; ++i)
        DrawNodeDebug(selectedNodes[i], debug);

    // Visualize the currently selected components
    for (uint i = 0; i < selectedComponents.length; ++i)
        selectedComponents[i].DrawDebugGeometry(debug, false);

    // Visualize the currently selected UI-elements
    for (uint i = 0; i < selectedUIElements.length; ++i)
        ui.DebugDraw(selectedUIElements[i]);

    if (renderingDebug)
        renderer.DrawDebugGeometry(false);
    if (physicsDebug && editorScene.physicsWorld !is null)
        editorScene.physicsWorld.DrawDebugGeometry(true);
    if (octreeDebug && editorScene.octree !is null)
        editorScene.octree.DrawDebugGeometry(true);

    if (setViewportCursor | resizingBorder > 0)
    {
        SetViewportCursor();
        if (resizingBorder == 0)
            setViewportCursor = 0;
    }

    ViewRaycast(false);
}

void DrawNodeDebug(Node@ node, DebugRenderer@ debug, bool drawNode = true)
{
    if (drawNode)
        debug.AddNode(node, 1.0, false);

    // Exception for the scene to avoid bringing the editor to its knees: drawing either the whole hierarchy or the subsystem-
    // components can have a large performance hit. Also do not draw terrain child nodes due to their large amount
    // (TerrainPatch component itself draws nothing as debug geometry)
    if (node !is editorScene && node.GetComponent("Terrain") is null)
    {
        for (uint j = 0; j < node.numComponents; ++j)
            node.components[j].DrawDebugGeometry(debug, false);

        // To avoid cluttering the view, do not draw the node axes for child nodes
        for (uint k = 0; k < node.numChildren; ++k)
            DrawNodeDebug(node.children[k], debug, false);
    }
}

void ViewMouseMove()
{
    // setting mouse position based on mouse position
    if (ui.IsDragging()) { }
    else if (ui.focusElement !is null || input.mouseButtonDown[MOUSEB_LEFT|MOUSEB_MIDDLE|MOUSEB_RIGHT])
        return;

    IntVector2 pos = ui.cursor.position;
    for (uint i = 0; i < viewports.length; ++i)
    {
        ViewportContext@ vc = viewports[i];
        if (vc !is activeViewport && vc.viewport.rect.IsInside(pos) == INSIDE)
            SetActiveViewport(vc);
    }
}

void ViewMouseClick()
{
    ViewRaycast(true);
}

Ray GetActiveViewportCameraRay()
{
    IntRect view = activeViewport.viewport.rect;
    return camera.GetScreenRay(
        float(ui.cursorPosition.x - view.left) / view.width,
        float(ui.cursorPosition.y - view.top) / view.height
    );
}

void ViewMouseClickEnd()
{
    // checks to close open popup windows
    IntVector2 pos = ui.cursorPosition;
    if (contextMenu !is null && contextMenu.enabled)
    {
        if (contextMenuActionWaitFrame)
            contextMenuActionWaitFrame = false;
        else
        {
            if (!contextMenu.IsInside(pos, true))
                CloseContextMenu();
        }
    }
    if (quickMenu !is null && quickMenu.enabled)
    {
        bool enabled = quickMenu.IsInside(pos, true);
        quickMenu.enabled = enabled;
        quickMenu.visible = enabled;
    }
}

void ViewRaycast(bool mouseClick)
{
    // Ignore if UI has modal element
    if (ui.HasModalElement())
        return;

    // Ignore if mouse is grabbed by other operation
    if (input.mouseGrabbed)
        return;

    IntVector2 pos = ui.cursorPosition;
    UIElement@ elementAtPos = ui.GetElementAt(pos, pickMode != PICK_UI_ELEMENTS);
    if (editMode == EDIT_SPAWN)
    {
        if(mouseClick && input.mouseButtonPress[MOUSEB_LEFT] && elementAtPos is null)
            SpawnObject();
        return;
    }

    // Do not raycast / change selection if hovering over the gizmo
    if (IsGizmoSelected())
        return;

    DebugRenderer@ debug = editorScene.debugRenderer;

    if (pickMode == PICK_UI_ELEMENTS)
    {
        bool leftClick = mouseClick && input.mouseButtonPress[MOUSEB_LEFT];
        bool multiselect = input.qualifierDown[QUAL_CTRL];

        // Only interested in user-created UI elements
        if (elementAtPos !is null && elementAtPos !is editorUIElement && elementAtPos.GetElementEventSender() is editorUIElement)
        {
            ui.DebugDraw(elementAtPos);

            if (leftClick)
                SelectUIElement(elementAtPos, multiselect);
        }
        // If clicked on emptiness in non-multiselect mode, clear the selection
        else if (leftClick && !multiselect && ui.GetElementAt(pos) is null)
            hierarchyList.ClearSelection();

        return;
    }

    // Do not raycast / change selection if hovering over a UI element when not in PICK_UI_ELEMENTS Mode
    if (elementAtPos !is null)
        return;

    Ray cameraRay = GetActiveViewportCameraRay();
    Component@ selectedComponent;

    if (pickMode < PICK_RIGIDBODIES)
    {
        if (editorScene.octree is null)
            return;

        RayQueryResult result = editorScene.octree.RaycastSingle(cameraRay, RAY_TRIANGLE, camera.farClip,
            pickModeDrawableFlags[pickMode], 0x7fffffff);
        if (result.drawable !is null)
        {
            Drawable@ drawable = result.drawable;
            // If selecting a terrain patch, select the parent terrain instead
            if (drawable.typeName != "TerrainPatch")
            {
                selectedComponent = drawable;
                if (debug !is null)
                {
                    debug.AddNode(drawable.node, 1.0, false);
                    drawable.DrawDebugGeometry(debug, false);
                }
            }
            else if (drawable.node.parent !is null)
                selectedComponent = drawable.node.parent.GetComponent("Terrain");
        }
    }
    else
    {
        if (editorScene.physicsWorld is null)
            return;

        // If we are not running the actual physics update, refresh collisions before raycasting
        if (!runUpdate)
            editorScene.physicsWorld.UpdateCollisions();

        PhysicsRaycastResult result = editorScene.physicsWorld.RaycastSingle(cameraRay, camera.farClip);
        if (result.body !is null)
        {
            RigidBody@ body = result.body;
            if (debug !is null)
            {
                debug.AddNode(body.node, 1.0, false);
                body.DrawDebugGeometry(debug, false);
            }
            selectedComponent = body;
        }
    }

    if (mouseClick && input.mouseButtonPress[MOUSEB_LEFT])
    {
        bool multiselect = input.qualifierDown[QUAL_CTRL];
        if (selectedComponent !is null)
        {
            if (input.qualifierDown[QUAL_SHIFT])
            {
                // If we are selecting components, but have nodes in existing selection, do not multiselect to prevent confusion
                if (!selectedNodes.empty)
                    multiselect = false;
                SelectComponent(selectedComponent, multiselect);
            }
            else
            {
                // If we are selecting nodes, but have components in existing selection, do not multiselect to prevent confusion
                if (!selectedComponents.empty)
                    multiselect = false;
                SelectNode(selectedComponent.node, multiselect);
            }
        }
        else
        {
            // If clicked on emptiness in non-multiselect mode, clear the selection
            if (!multiselect)
               SelectComponent(null, false);
        }
    }
}

Vector3 GetNewNodePosition()
{
    return cameraNode.position + cameraNode.worldRotation * Vector3(0, 0, newNodeDistance);
}

int GetShadowResolution()
{
    if (!renderer.drawShadows)
        return 0;
    int level = 1;
    int res = renderer.shadowMapSize;
    while (res > 512)
    {
        res >>= 1;
        ++level;
    }
    if (level > 3)
        level = 3;

    return level;
}

void SetShadowResolution(int level)
{
    if (level <= 0)
    {
        renderer.drawShadows = false;
        return;
    }
    else
    {
        renderer.drawShadows = true;
        renderer.shadowMapSize = 256 << level;
    }
}

void ToggleRenderingDebug()
{
    renderingDebug = !renderingDebug;
}

void TogglePhysicsDebug()
{
    physicsDebug = !physicsDebug;
}

void ToggleOctreeDebug()
{
    octreeDebug = !octreeDebug;
}

bool StopTestAnimation()
{
    testAnimState = null;
    return true;
}

void LocateNode(Node@ node)
{
    if (node is null || node is editorScene)
        return;

    Vector3 center = node.worldPosition;
    float distance = newNodeDistance;

    for (uint i = 0; i < node.numComponents; ++i)
    {
        // Determine view distance from drawable component's bounding box. Skip skybox, as its box is very large, as well as lights
        Drawable@ drawable = cast<Drawable>(node.components[i]);
        if (drawable !is null && cast<Skybox>(drawable) is null && cast<Light>(drawable) is null)
        {
            BoundingBox box = drawable.worldBoundingBox;
            center = box.center;
            // Ensure the object fits on the screen
            distance = Max(distance, newNodeDistance + box.size.length);
            break;
        }
    }

    if (distance > viewFarClip)
        distance = viewFarClip;

    cameraNode.worldPosition = center - cameraNode.worldDirection * distance;
}

Vector3 SelectedNodesCenterPoint()
{
    Vector3 centerPoint;
    uint count = selectedNodes.length;
    for (uint i = 0; i < selectedNodes.length; ++i)
        centerPoint += selectedNodes[i].worldPosition;

    for (uint i = 0; i < selectedComponents.length; ++i)
    {
        Drawable@ drawable = cast<Drawable>(selectedComponents[i]);
        count++;
        if (drawable !is null)
            centerPoint += drawable.node.LocalToWorld(drawable.boundingBox.center);
        else
            centerPoint += selectedComponents[i].node.worldPosition;
    }

    if (count > 0)
        return centerPoint / count;
    else
        return centerPoint;
}

Vector3 GetScreenCollision(IntVector2 pos)
{
    Ray cameraRay = camera.GetScreenRay(float(pos.x) / activeViewport.viewport.rect.width, float(pos.y) / activeViewport.viewport.rect.height);
    Vector3 res = cameraNode.position + cameraRay.direction * Vector3(0, 0, newNodeDistance);

    bool physicsFound = false;
    if (editorScene.physicsWorld !is null)
    {
        if (!runUpdate)
            editorScene.physicsWorld.UpdateCollisions();

        PhysicsRaycastResult result = editorScene.physicsWorld.RaycastSingle(cameraRay, camera.farClip);

        if (result.body !is null)
        {
            physicsFound = true;
            result.position;
        }
    }

    if (editorScene.octree is null)
        return res;

    RayQueryResult result = editorScene.octree.RaycastSingle(cameraRay, RAY_TRIANGLE, camera.farClip,
        DRAWABLE_GEOMETRY, 0x7fffffff);

    if (result.drawable !is null)
    {
        // take the closer of the results
        if (physicsFound && (cameraNode.position - res).length < (cameraNode.position - result.position).length)
            return res;
        else
            return result.position;
    }

    return res;
}

Drawable@ GetDrawableAtMousePostion()
{
    IntVector2 pos = ui.cursorPosition;
    Ray cameraRay = camera.GetScreenRay(float(pos.x) / activeViewport.viewport.rect.width, float(pos.y) / activeViewport.viewport.rect.height);

    if (editorScene.octree is null)
        return null;

    RayQueryResult result = editorScene.octree.RaycastSingle(cameraRay, RAY_TRIANGLE, camera.farClip, DRAWABLE_GEOMETRY, 0x7fffffff);

    return result.drawable;
}

void HandleBeginViewUpdate(StringHash eventType, VariantMap& eventData)
{
    // Hide gizmo and grid from any camera other then active viewport
    if (eventData["Camera"].GetPtr() !is camera)
    {
        if (gizmo !is null)
            gizmo.viewMask = 0;
    }
    if (eventData["Camera"].GetPtr() is previewCamera.Get())
    {
        if (grid !is null)
            grid.viewMask = 0;
    }
}

void HandleEndViewUpdate(StringHash eventType, VariantMap& eventData)
{
    // Restore gizmo and grid after camera view update
    if (eventData["Camera"].GetPtr() !is camera)
    {
        if (gizmo !is null)
            gizmo.viewMask = 0x80000000;
    }
    if (eventData["Camera"].GetPtr() is previewCamera.Get())
    {
        if (grid !is null)
            grid.viewMask = 0x80000000;
    }
}

bool debugWasEnabled = true;

void HandleBeginViewRender(StringHash eventType, VariantMap& eventData)
{
    // Hide debug geometry from preview camera
    if (eventData["Camera"].GetPtr() is previewCamera.Get())
    {
        DebugRenderer@ debug = editorScene.GetComponent("DebugRenderer");
        if (debug !is null)
        {
            suppressSceneChanges = true; // Do not want UI update now
            debugWasEnabled = debug.enabled;
            debug.enabled = false;
            suppressSceneChanges = false;
        }
    }
}

void HandleEndViewRender(StringHash eventType, VariantMap& eventData)
{
    // Restore debug geometry after preview camera render
    if (eventData["Camera"].GetPtr() is previewCamera.Get())
    {
        DebugRenderer@ debug = editorScene.GetComponent("DebugRenderer");
        if (debug !is null)
        {
            suppressSceneChanges = true; // Do not want UI update now
            debug.enabled = debugWasEnabled;
            suppressSceneChanges = false;
        }
    }
}
