using System;
using System.IO;
using AutoIngest.Core;
using ImageMagick;
using ImageMagick.Drawing;

namespace AutoIngest.Engine
{
    /// <summary>
    /// Burns visible branding into a photo in place. Called from <see cref="ImageConverter"/>
    /// BEFORE metadata stripping (watermarks are pixels, not metadata, so they survive anything),
    /// and BEFORE EXIF copyright is written. Two independent overlays:
    ///
    ///   - Logo: a PNG composited onto the image at an anchor, with adjustable opacity and an
    ///     optional scale-to-width. Drawn first so the text can sit on top of it.
    ///   - Text: the seller's store name / handle / URL drawn as a single line at an anchor,
    ///     with adjustable opacity baked into the fill color.
    ///
    /// Anchors use percentage-based margins (not fixed pixels) so the watermark lands sensibly on
    /// any photo size. Everything is wrapped: a missing logo file, an unreadable image, or a font
    /// problem logs and skips that one overlay — never throws out of the convert pipeline.
    /// </summary>
    public static class BrandingRenderer
    {
        const double MARGIN_PCT = 0.02; // 2% of image dimension, inset from the chosen edge/corner

        /// <summary>
        /// Applies whichever overlays <paramref name="branding"/> enables to <paramref name="image"/>.
        /// No-op (and safe) when branding is disabled or both overlays are off. Mutates the image
        /// in place.
        /// </summary>
        public static void Apply(MagickImage image, BrandingConfig? branding)
        {
            if (branding == null || !branding.Enabled) return;

            if (branding.LogoWatermarkEnabled)
                TryApplyLogo(image, branding);

            if (branding.TextWatermarkEnabled)
                TryApplyText(image, branding);
        }

        static void TryApplyLogo(MagickImage image, BrandingConfig b)
        {
            if (string.IsNullOrWhiteSpace(b.LogoPath) || !File.Exists(b.LogoPath))
                return;

            try
            {
                using var logo = new MagickImage(b.LogoPath);

                // Scale: either to a % of the host image's width, or leave at native size. Clamp
                // the scale so a fat logo can't exceed 60% of the photo width (keeps it a mark,
                // not a takeover). image.Width is uint; work in int after the cast.
                int hostW = (int)image.Width;
                if (b.LogoScalePercent > 0)
                {
                    int targetW = (int)(hostW * (b.LogoScalePercent / 100.0));
                    int maxW = (int)(hostW * 0.6);
                    if (targetW > maxW) targetW = maxW;
                    if (targetW > 0 && Math.Abs((int)logo.Width - targetW) > 1)
                        logo.Scale((uint)targetW, 0);
                }

                // Opacity: multiply the alpha channel by a fraction in [0.05, 1.0]. Magick.NET's
                // Evaluate with a double is the cleanest overload (Percentage ctor is explicit).
                double frac = Clamp(b.LogoOpacity / 100.0, 0.05, 1.0);
                if (frac < 1.0)
                {
                    logo.HasAlpha = true;
                    logo.Evaluate(Channels.Alpha, EvaluateOperator.Multiply, frac);
                }

                var (x, y) = Anchor((int)image.Width, (int)image.Height,
                                    (int)logo.Width, (int)logo.Height, b.LogoPosition);
                image.Composite(logo, x, y, CompositeOperator.Over);
            }
            catch (MagickException)
            {
                // Bad/corrupt logo file — skip the overlay, keep converting.
            }
            catch (IOException)
            {
                // Logo file existed at the check but couldn't be read (locked, etc.) — skip.
            }
        }

        static void TryApplyText(MagickImage image, BrandingConfig b)
        {
            string text = BuildText(b);
            if (string.IsNullOrWhiteSpace(text)) return;

            try
            {
                int hostW = (int)image.Width;
                int hostH = (int)image.Height;

                // Point size scales with the image so the watermark reads on both a 4000px and a
                // 800px photo. Floor at 12 so it can't vanish on tiny images.
                double pointSize = Math.Max(12, hostW * 0.022);

                // Opacity → alpha fraction in [0.05, 1.0]; bake into the fill color's alpha byte.
                double frac = Clamp(b.TextOpacity / 100.0, 0.05, 1.0);
                var fill = new MagickColor("white") { A = (ushort)(Quantum.Max * frac) };
                var shadow = new MagickColor("black") { A = (ushort)(Quantum.Max * frac) };

                // Measure the text so we can anchor the box. FontTypeMetrics needs the same point
                // size we'll draw with; chain it off a Drawables configured identically.
                var metrics = new Drawables()
                    .FontTypeMetrics(text);

                // FontTypeMetrics can return null if the font machinery can't measure (e.g. no
                // font available). Fall back to a rough estimate so we still place something.
                double textWidth = metrics?.TextWidth ?? text.Length * pointSize * 0.5;
                double textHeight = metrics?.TextHeight ?? pointSize * 1.2;
                double ascent = metrics?.Ascent ?? pointSize;

                int tw = (int)Math.Ceiling(textWidth);
                int th = (int)Math.Ceiling(textHeight);
                var (tx, boxTop) = Anchor(hostW, hostH, tw, th, b.TextPosition);

                // Text() takes the baseline origin; Anchor gave us the box top, so add the ascent.
                double baselineY = boxTop + ascent;

                // Shadow first (offset 1px down-right for legibility), then the main fill on top.
                new Drawables()
                    .FontPointSize(pointSize)
                    .FillColor(shadow)
                    .Text(tx + 1, baselineY + 1, text)
                    .Draw(image);

                new Drawables()
                    .FontPointSize(pointSize)
                    .FillColor(fill)
                    .Text(tx, baselineY, text)
                    .Draw(image);
            }
            catch (MagickException)
            {
                // Font/render problem — skip the text overlay, keep converting.
            }
        }

        /// <summary>
        /// Builds the single watermark line from whichever store fields are populated.
        /// e.g. "ReelDeals  ·  @reeldeals  ·  ebay.com/usr/reeldeals". Empty parts are skipped.
        /// </summary>
        static string BuildText(BrandingConfig b)
        {
            var parts = new System.Collections.Generic.List<string>();
            if (!string.IsNullOrWhiteSpace(b.StoreName)) parts.Add(b.StoreName.Trim());
            if (!string.IsNullOrWhiteSpace(b.SellerHandle))
            {
                string h = b.SellerHandle.Trim();
                if (!h.StartsWith("@", StringComparison.Ordinal)) h = "@" + h;
                parts.Add(h);
            }
            if (!string.IsNullOrWhiteSpace(b.StoreUrl)) parts.Add(b.StoreUrl.Trim());
            return string.Join("  \u00b7  ", parts); // middle dot separator
        }

        /// <summary>
        /// Resolves the top-left pixel of a box of size (w,h) placed at <paramref name="pos"/>
        /// within a host of size (hostW, hostH), with a percentage margin from the edges.
        /// </summary>
        static (int x, int y) Anchor(int hostW, int hostH, int w, int h, WatermarkPosition pos)
        {
            int mx = (int)(hostW * MARGIN_PCT);
            int my = (int)(hostH * MARGIN_PCT);
            return pos switch
            {
                WatermarkPosition.TopLeft     => (mx, my),
                WatermarkPosition.TopRight    => (hostW - w - mx, my),
                WatermarkPosition.BottomLeft  => (mx, hostH - h - my),
                WatermarkPosition.BottomRight => (hostW - w - mx, hostH - h - my),
                WatermarkPosition.Center      => ((hostW - w) / 2, (hostH - h) / 2),
                _                              => (hostW - w - mx, hostH - h - my),
            };
        }

        static double Clamp(double v, double min, double max) =>
            v < min ? min : v > max ? max : v;
    }
}
