using UnityEditor;
using UnityEngine;

namespace Getly.Publisher
{
    /// <summary>
    /// The drawing vocabulary. Everything the window renders goes through here.
    ///
    /// Unity's IMGUI gives you `HelpBox` and a bold label and not much else, and
    /// a tool built out of those looks like a debug panel — which is what a
    /// publisher decides about the marketplace behind it. So: a header band, a
    /// hairline rule, an eyebrow, a card, a framed artifact.
    ///
    /// The house rules this obeys, from the site: NO shadows (a 1px hairline
    /// does the separating), monochrome except one accent reserved for what
    /// actually blocks a sale, section headings lighter than the data labels
    /// under them, and a framed mascot rather than a small floating thumbnail.
    /// </summary>
    public static class GetlyUI
    {
        public const float Gutter = 14f;
        public const float MascotSize = 116f;

        /// <summary>
        /// How wide the content column is, in points, this frame.
        ///
        /// IMGUI does not clip a child to its parent: a group given a width
        /// still lets a child that reports a larger minimum draw straight past
        /// it, and a long description in a text area reports a very large one.
        /// The result is a horizontal scrollbar, labels that stop wrapping, and
        /// the framed mascot pushed off the edge. So the width is stated
        /// explicitly on the widgets that would otherwise push.
        /// </summary>
        public static float ContentWidth { get; private set; } = 400f;

        public static void SetContentWidth(float width)
        {
            ContentWidth = Mathf.Max(200f, width);
        }

        private static Texture2D _pixel;

        /// <summary>A 1x1 white texture, tinted at draw time. Cheaper than one per colour.</summary>
        private static Texture2D Pixel
        {
            get
            {
                if (_pixel == null)
                {
                    _pixel = new Texture2D(1, 1) { hideFlags = HideFlags.HideAndDontSave };
                    _pixel.SetPixel(0, 0, Color.white);
                    _pixel.Apply();
                }
                return _pixel;
            }
        }

        public static void Fill(Rect rect, Color color)
        {
            if (Event.current.type != EventType.Repaint) return;
            var previous = GUI.color;
            GUI.color = color;
            GUI.DrawTexture(rect, Pixel);
            GUI.color = previous;
        }

        /// <summary>A 1px separator. The only thing allowed to divide two areas.</summary>
        public static void Rule(float topMargin = 0f, float bottomMargin = 0f)
        {
            if (topMargin > 0f) GUILayout.Space(topMargin);
            var rect = GUILayoutUtility.GetRect(GUIContent.none, GUIStyle.none, GUILayout.Height(1f));
            Fill(rect, GetlyBrand.Hairline);
            if (bottomMargin > 0f) GUILayout.Space(bottomMargin);
        }

        /// <summary>A hairline box around whatever was just laid out.</summary>
        public static void Outline(Rect rect, Color color)
        {
            Fill(new Rect(rect.x, rect.y, rect.width, 1f), color);
            Fill(new Rect(rect.x, rect.yMax - 1f, rect.width, 1f), color);
            Fill(new Rect(rect.x, rect.y, 1f, rect.height), color);
            Fill(new Rect(rect.xMax - 1f, rect.y, 1f, rect.height), color);
        }

        // ---- type ---------------------------------------------------------------
        // Section headings are LIGHTER than the data labels beneath them. Reversing
        // that is what makes an interface read as a form rather than a product.

        private static GUIStyle _eyebrow, _display, _heading, _body, _muted, _label, _mono;

        /// <summary>Uppercase, tracked, small. Names a region without competing with it.</summary>
        public static GUIStyle Eyebrow => _eyebrow ?? (_eyebrow = new GUIStyle(EditorStyles.miniLabel)
        {
            fontSize = 10,
            fontStyle = FontStyle.Normal,
            normal = { textColor = GetlyBrand.Muted },
            padding = new RectOffset(0, 0, 0, 2),
        });

        /// <summary>The one title on the screen.</summary>
        public static GUIStyle Display => _display ?? (_display = new GUIStyle(EditorStyles.label)
        {
            fontSize = 17,
            fontStyle = FontStyle.Normal,
            normal = { textColor = GetlyBrand.Ink },
            wordWrap = true,
        });

        /// <summary>Section title. Deliberately not bold.</summary>
        public static GUIStyle Heading => _heading ?? (_heading = new GUIStyle(EditorStyles.label)
        {
            fontSize = 13,
            fontStyle = FontStyle.Normal,
            normal = { textColor = GetlyBrand.Ink },
            wordWrap = true,
        });

        /// <summary>A data label — the name of a finding, a store, a file. Bolder than a heading.</summary>
        public static GUIStyle Label => _label ?? (_label = new GUIStyle(EditorStyles.label)
        {
            fontSize = 12,
            fontStyle = FontStyle.Bold,
            normal = { textColor = GetlyBrand.Ink },
            wordWrap = true,
        });

        public static GUIStyle Body => _body ?? (_body = new GUIStyle(EditorStyles.label)
        {
            fontSize = 12,
            normal = { textColor = GetlyBrand.Ink },
            wordWrap = true,
            richText = false,
        });

        public static GUIStyle Muted => _muted ?? (_muted = new GUIStyle(EditorStyles.label)
        {
            fontSize = 11,
            normal = { textColor = GetlyBrand.Muted },
            wordWrap = true,
        });

        /// <summary>
        /// Secondary detail: file paths, counts, character tallies.
        ///
        /// NOT monospace, despite what it used to claim — the editor ships no
        /// monospace font we can rely on, and assigning miniLabel's own font
        /// back to itself achieved nothing but a comment that lied.
        /// </summary>
        public static GUIStyle Mono => _mono ?? (_mono = new GUIStyle(EditorStyles.miniLabel)
        {
            fontSize = 10,
            normal = { textColor = GetlyBrand.Muted },
            wordWrap = true,
        });

        /// <summary>Styles cache colours, and the editor skin can change under us.</summary>
        public static void InvalidateStyles()
        {
            _eyebrow = _display = _heading = _body = _muted = _label = _mono = _textArea = null;
        }

        // ---- structures ---------------------------------------------------------

        /// <summary>
        /// The band at the top of the window: mark on the left, state on the right.
        ///
        /// Never a lone title floating with an empty right side — the house rule
        /// that applies to every block we ship.
        /// </summary>
        public static Rect BeginHeader()
        {
            var rect = GUILayoutUtility.GetRect(GUIContent.none, GUIStyle.none, GUILayout.Height(0f));
            return rect;
        }

        /// <summary>Draw the mark: the chip, framed, beside the wordmark.</summary>
        public static void Mark(string subtitle = null)
        {
            using (new GUILayout.HorizontalScope())
            {
                var chipRect = GUILayoutUtility.GetRect(26f, 26f, GUILayout.Width(26f), GUILayout.Height(26f));
                if (GetlyBrand.Chip != null) GUI.DrawTexture(chipRect, GetlyBrand.Chip, ScaleMode.ScaleToFit);
                Outline(chipRect, GetlyBrand.Hairline);

                GUILayout.Space(8f);

                using (new GUILayout.VerticalScope())
                {
                    GUILayout.Space(string.IsNullOrEmpty(subtitle) ? 4f : 0f);
                    GUILayout.Label("Getly", Heading);
                    if (!string.IsNullOrEmpty(subtitle)) GUILayout.Label(subtitle, Mono);
                }

                GUILayout.FlexibleSpace();
            }
        }

        /// <summary>
        /// A bordered block. Returns the rect so the caller can outline it after
        /// its contents have decided how tall it is.
        /// </summary>
        public static Rect BeginCard(Color? tint = null)
        {
            var scope = EditorGUILayout.BeginVertical();
            if (tint.HasValue) Fill(scope, tint.Value);
            GUILayout.Space(10f);
            return scope;
        }

        public static void EndCard(Rect rect, Color? border = null)
        {
            GUILayout.Space(10f);
            EditorGUILayout.EndVertical();
            Outline(rect, border ?? GetlyBrand.Hairline);
        }

        /// <summary>Horizontal padding inside a card, so text never touches its own border.</summary>
        public static GUILayout.HorizontalScope CardRow()
        {
            var scope = new GUILayout.HorizontalScope();
            GUILayout.Space(12f);
            return scope;
        }

        /// <summary>
        /// A severity mark: a small filled square, not a shouting icon.
        ///
        /// Unity's own MessageType icons are loud enough to make nine warnings
        /// look like a catastrophe, which is how a seller learns to stop reading
        /// them.
        /// </summary>
        public static void SeverityDot(Color color)
        {
            var rect = GUILayoutUtility.GetRect(8f, 8f, GUILayout.Width(8f), GUILayout.Height(8f));
            rect.y += 5f;
            Fill(rect, color);
        }

        /// <summary>
        /// The mascot as a FRAMED artifact — square, hairlined, sized.
        ///
        /// A small floating thumbnail above a heading is the pattern this brand
        /// spent a redesign removing.
        /// </summary>
        public static void FramedMascot(float size = MascotSize)
        {
            var rect = GUILayoutUtility.GetRect(size, size, GUILayout.Width(size), GUILayout.Height(size));
            Fill(rect, GetlyBrand.Surface);
            if (GetlyBrand.Mascot != null) GUI.DrawTexture(rect, GetlyBrand.Mascot, ScaleMode.ScaleToFit);
            Outline(rect, GetlyBrand.Hairline);
        }

        /// <summary>
        /// Copy on the left, the framed mascot on the right.
        ///
        /// The layout rule the whole site follows: a block is either centred or
        /// it is text-left with something FILLED on the right. Never text with
        /// emptiness beside it.
        /// </summary>
        public static void SplitWithMascot(System.Action drawLeft, float mascotSize = MascotSize)
        {
            using (new GUILayout.HorizontalScope())
            {
                // No FlexibleSpace here. Inside a scroll view it absorbs every
                // spare pixel in the viewport and shoves the rest of the window
                // to the bottom — which is not a subtle bug, it is a 200px hole
                // in the middle of the layout.
                var leftWidth = Mathf.Max(120f, ContentWidth - mascotSize - Gutter);
                using (new GUILayout.VerticalScope(GUILayout.Width(leftWidth)))
                {
                    drawLeft();
                }
                GUILayout.Space(Gutter);
                using (new GUILayout.VerticalScope(GUILayout.Width(mascotSize)))
                {
                    FramedMascot(mascotSize);
                }
            }
        }

        /// <summary>
        /// Shorten a string to fit, with an ellipsis.
        ///
        /// A store name is whatever the seller typed. Unbounded, it runs past
        /// the window edge and takes the control beside it with it.
        /// </summary>
        public static string Elide(string text, GUIStyle style, float maxWidth)
        {
            if (string.IsNullOrEmpty(text)) return text;
            if (style.CalcSize(new GUIContent(text)).x <= maxWidth) return text;
            for (var length = text.Length - 1; length > 1; length--)
            {
                var candidate = text.Substring(0, length).TrimEnd() + "…";
                if (style.CalcSize(new GUIContent(candidate)).x <= maxWidth) return candidate;
            }
            return "…";
        }

        /// <summary>A primary action. Tall enough to be the obvious thing to press.</summary>
        public static bool PrimaryButton(string text, bool enabled = true)
        {
            using (new EditorGUI.DisabledScope(!enabled))
            {
                return GUILayout.Button(text, GUILayout.Height(30f));
            }
        }

        public static bool SecondaryButton(string text, float width = 0f, bool enabled = true)
        {
            using (new EditorGUI.DisabledScope(!enabled))
            {
                return width > 0f
                    ? GUILayout.Button(text, GUILayout.Height(22f), GUILayout.Width(width))
                    : GUILayout.Button(text, GUILayout.Height(22f));
            }
        }

        /// <summary>
        /// A line of guidance under a control.
        ///
        /// Every disabled control in this tool says why it is disabled. A dead
        /// button with no explanation is the most common way software gets
        /// reported as broken.
        /// </summary>
        public static void Hint(string text)
        {
            GUILayout.Space(4f);
            GUILayout.Label(text, Muted);
        }

        /// <summary>
        /// A labelled field, label above.
        ///
        /// Unity's built-in inline label puts the caption in a fixed left
        /// column sized for the Inspector, which at this width leaves a text
        /// box a third of the window wide and every field a different shape.
        /// </summary>
        public static string TextField(string label, string value)
        {
            GUILayout.Label(label, Body);
            return EditorGUILayout.TextField(value, GUILayout.Width(ContentWidth));
        }

        private static GUIStyle _textArea;

        /// <summary>
        /// A text area that WRAPS.
        ///
        /// EditorStyles.textArea does not wrap in this context, so a
        /// description runs off the right edge of its own box — which looks
        /// like the text was lost.
        /// </summary>
        public static string TextArea(string label, string value, float height)
        {
            if (_textArea == null) _textArea = new GUIStyle(EditorStyles.textArea) { wordWrap = true };
            GUILayout.Label(label, Body);
            return EditorGUILayout.TextArea(value, _textArea, GUILayout.Width(ContentWidth), GUILayout.Height(height));
        }

        public static float FloatField(string label, float value)
        {
            GUILayout.Label(label, Body);
            return EditorGUILayout.FloatField(value, GUILayout.Width(ContentWidth));
        }

        /// <summary>A wrapping paragraph that cannot push the layout wider than it is.</summary>
        public static void Paragraph(string text, GUIStyle style, float width = 0f)
        {
            GUILayout.Label(text, style, GUILayout.Width(width > 0f ? width : ContentWidth));
        }

        /// <summary>Something went wrong. Stated in words, in the one accent colour.</summary>
        public static void Problem(string text)
        {
            var card = BeginCard();
            using (CardRow())
            {
                SeverityDot(GetlyBrand.Alert);
                GUILayout.Space(8f);
                GUILayout.Label(text, Body);
                GUILayout.Space(12f);
            }
            EndCard(card, GetlyBrand.Alert);
        }
    }
}
