// Copyright (c) 2012-2024 Wojciech Figat. All rights reserved.

using System;
using System.ComponentModel;
using FlaxEditor.CustomEditors;
using FlaxEditor.GUI.Dialogs;
using FlaxEngine;
using FlaxEngine.GUI;

// ReSharper disable InconsistentNaming
// ReSharper disable UnusedMember.Local
// ReSharper disable UnusedMember.Global
#pragma warning disable 649
#pragma warning disable 414

namespace FlaxEditor.Tools.Terrain
{
    /// <summary>
    /// Terrain creator dialog. Allows user to specify initial terrain properties perform proper setup.
    /// </summary>
    /// <seealso cref="FlaxEditor.GUI.Dialogs.Dialog" />
    sealed class CreateTerrainDialog : Dialog
    {
        private enum ChunkSizes
        {
            _31 = 31,
            _63 = 63,
            _127 = 127,
            _254 = 254,
            _255 = 255,
            _511 = 511,
        }

        private class Options
        {
            [EditorOrder(100), EditorDisplay("Layout", "Number Of Patches"), DefaultValue(typeof(Int2), "1,1"), Limit(0, 512), Tooltip("Amount of terrain patches in each direction (X and Z). Each terrain patch contains a grid of 16 chunks. Patches can be later added or removed from terrain using a terrain editor tool.")]
            public Int2 NumberOfPatches = new Int2(1, 1);

            [EditorOrder(110), EditorDisplay("Layout"), DefaultValue(ChunkSizes._127), Tooltip("The size of the chunk (amount of quads per edge for the highest LOD).")]
            public ChunkSizes ChunkSize = ChunkSizes._127;

            [EditorOrder(120), EditorDisplay("Layout", "LOD Count"), DefaultValue(6), Limit(1, FlaxEngine.Terrain.MaxLODs), Tooltip("The maximum Level Of Details count. The actual amount of LODs may be lower due to provided chunk size (each LOD has 4 times less quads).")]
            public int LODCount = 6;

            [EditorOrder(130), EditorDisplay("Layout"), DefaultValue(null), Tooltip("The default material used for terrain rendering (chunks can override this). It must have Domain set to terrain.")]
            public MaterialBase Material;

            [EditorOrder(210), EditorDisplay("Collision", "Collision LOD"), DefaultValue(-1), Limit(-1, 100, 0.1f), Tooltip("Terrain geometry LOD index used for collision.")]
            public int CollisionLOD = -1;

            [EditorOrder(300), EditorDisplay("Import Data"), DefaultValue(null), Tooltip("Custom heightmap texture to import. Used as a source for height field values (from channel Red).")]
            public Texture Heightmap;

            [EditorOrder(310), EditorDisplay("Import Data"), DefaultValue(5000.0f), Tooltip("Custom heightmap texture values scale. Applied to adjust the normalized heightmap values into the world units.")]
            public float HeightmapScale = 5000.0f;

            [EditorOrder(320), EditorDisplay("Import Data"), DefaultValue(null), Tooltip("Custom terrain splat map used as a source of the terrain layers weights. Each channel from RGBA is used as an independent layer weight for terrain layers compositing.")]
            public Texture Splatmap1;

            [EditorOrder(330), EditorDisplay("Import Data"), DefaultValue(null), Tooltip("Custom terrain splat map used as a source of the terrain layers weights. Each channel from RGBA is used as an independent layer weight for terrain layers compositing.")]
            public Texture Splatmap2;

            [EditorOrder(400), EditorDisplay("Transform", "Position"), DefaultValue(typeof(Double3), "0,0,0"), Tooltip("Position of the terrain (importer offset it on the Y axis.)")]
            public Double3 Position = new Double3(0.0f, 0.0f, 0.0f);

            [EditorOrder(410), EditorDisplay("Transform", "Rotation"), DefaultValue(typeof(Quaternion), "0,0,0,1"), Tooltip("Orientation of the terrain")]
            public Quaternion Orientation = Quaternion.Identity;

            [EditorOrder(420), EditorDisplay("Transform", "Scale"), DefaultValue(1.0f), Limit(0.0001f, float.MaxValue, 0.01f), Tooltip("Scale of the terrain")]
            public float Scale = 1.0f;
        }

        private readonly Options _options = new Options();
        private bool _isDone;
        private bool _isWorking;
        private FlaxEngine.Terrain _terrain;
        private CustomEditorPresenter _editor;

        /// <summary>
        /// Initializes a new instance of the <see cref="CreateTerrainDialog"/> class.
        /// </summary>
        public CreateTerrainDialog()
        : base("Create terrain")
        {
            const float TotalWidth = 450;
            const float EditorHeight = 600;
            Width = TotalWidth;

            // Header and help description
            var headerLabel = new Label
            {
                Text = "New Terrain",
                AnchorPreset = AnchorPresets.HorizontalStretchTop,
                Offsets = new Margin(0, 0, 0, 40),
                Parent = this,
                Font = new FontReference(Style.Current.FontTitle)
            };
            var infoLabel = new Label
            {
                Text = "Specify options for new terrain.\nIt will be added to the first opened scene.\nMany of the following settings can be adjusted later.\nYou can also create terrain at runtime from code.",
                HorizontalAlignment = TextAlignment.Near,
                Margin = new Margin(7),
                AnchorPreset = AnchorPresets.HorizontalStretchTop,
                Offsets = new Margin(10, -20, 45, 70),
                Parent = this
            };

            // Buttons
            const float ButtonsWidth = 60;
            const float ButtonsHeight = 24;
            const float ButtonsMargin = 8;
            var importButton = new Button
            {
                Text = "Create",
                AnchorPreset = AnchorPresets.BottomRight,
                Offsets = new Margin(-ButtonsWidth - ButtonsMargin, ButtonsWidth, -ButtonsHeight - ButtonsMargin, ButtonsHeight),
                Parent = this
            };
            importButton.Clicked += OnSubmit;
            var cancelButton = new Button
            {
                Text = "Cancel",
                AnchorPreset = AnchorPresets.BottomRight,
                Offsets = new Margin(-ButtonsWidth - ButtonsMargin - ButtonsWidth - ButtonsMargin, ButtonsWidth, -ButtonsHeight - ButtonsMargin, ButtonsHeight),
                Parent = this
            };
            cancelButton.Clicked += OnCancel;

            // Settings editor
            var settingsEditor = new CustomEditorPresenter(null);
            settingsEditor.Panel.AnchorPreset = AnchorPresets.HorizontalStretchTop;
            settingsEditor.Panel.Offsets = new Margin(2, 2, infoLabel.Bottom + 2, EditorHeight);
            settingsEditor.Panel.Parent = this;
            _editor = settingsEditor;

            _dialogSize = new Float2(TotalWidth, settingsEditor.Panel.Bottom);

            settingsEditor.Select(_options);
        }

        private void OnCreate()
        {
            var scene = Level.GetScene(0);
            if (scene == null)
                throw new InvalidOperationException("No scene found to add terrain to it!");

            // Create terrain object and setup some options
            var terrain = new FlaxEngine.Terrain();
            terrain.Setup(_options.LODCount, (int)_options.ChunkSize);
            terrain.Transform = new Transform(_options.Position, _options.Orientation, new Float3(_options.Scale));
            terrain.Material = _options.Material;
            terrain.CollisionLOD = _options.CollisionLOD;
            if (_options.Heightmap)
                terrain.Position -= new Vector3(0, _options.HeightmapScale * 0.5f, 0);

            // Add to scene (even if generation fails user gets a terrain in the scene)
            terrain.Parent = scene;
            Editor.Instance.Scene.MarkSceneEdited(scene);

            // Show loading label
            var label = new Label
            {
                AnchorPreset = AnchorPresets.StretchAll,
                Offsets = Margin.Zero,
                Text = "Generating terrain...",
                BackgroundColor = Style.Current.ForegroundDisabled,
                Parent = this,
            };

            // Lock UI
            _editor.Panel.Enabled = false;
            _isWorking = true;
            _isDone = false;

            // Start async work
            _terrain = terrain;
            var thread = new System.Threading.Thread(Generate)
            {
                Name = "Terrain Generator"
            };
            thread.Start();
        }

        private void Generate()
        {
            _isWorking = true;
            _isDone = false;

            // Call tool to generate the terrain patches from the input data
            if (TerrainTools.GenerateTerrain(_terrain, ref _options.NumberOfPatches, _options.Heightmap, _options.HeightmapScale, _options.Splatmap1, _options.Splatmap2))
            {
                Editor.LogError("Failed to generate terrain. See log for more info.");
            }

            _isWorking = false;
            _isDone = true;
        }

        /// <inheritdoc />
        public override void OnSubmit()
        {
            if (_isWorking)
                return;

            OnCreate();
        }

        /// <inheritdoc />
        public override void OnCancel()
        {
            if (_isWorking)
                return;

            base.OnCancel();
        }

        /// <inheritdoc />
        public override void Update(float deltaTime)
        {
            if (_isDone)
            {
                Editor.Instance.SceneEditing.Select(_terrain);

                _terrain = null;
                _isDone = false;
                Close(DialogResult.OK);
                return;
            }

            base.Update(deltaTime);
        }

        /// <inheritdoc />
        protected override bool CanCloseWindow(ClosingReason reason)
        {
            if (_isWorking && reason == ClosingReason.User)
                return false;

            return base.CanCloseWindow(reason);
        }
    }
}
