// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.


using System;
using System.Diagnostics;
using System.Collections;              // for ArrayList
using System.Collections.Generic;      // for Dictionary
using System.IO;
using System.Windows;                  // for Rect                        WindowsBase.dll
using System.Windows.Media;            // for Geometry, Brush, ImageData. PresentationCore.dll
using System.Windows.Media.Imaging;

using System.Windows.Xps.Serialization;
using System.Printing;
using System.Printing.Interop;
using System.Security;
using System.Text;
using MS.Utility;

namespace Microsoft.Internal.AlphaFlattener
{
    /// <summary>
    ///
    /// </summary>
    internal class MetroDevice0
    {
        [Flags]
        private enum DeviceState
        {
            NoChange       = 0,
            Init           = 1,
            DocStarted     = 2,
            PageStarted    = 4
        };

        private enum PushType
        {
            Opacity,
            Clip,
            Transform
        };

        private DeviceState     _state;

        private CanvasPrimitive _page;          // Page
        private CanvasPrimitive _root;          // Current root
        private BrushProxy      _opacityMask;   // Current layer OpacityMask
        private double          _opacity;       // Current layer Opacity
        private Geometry        _clip;          // Current layer Clip

        private Stack           _stack;

#if UNIT_TEST
        static private bool     s_first = true;
#endif
        /// <summary>
        ///
        /// </summary>
        public MetroDevice0()
        {
#if UNIT_TEST
            if (s_first)
            {
                s_first = false;

                RectangleIntersection.UnitTest();
            }
#endif

            _state = DeviceState.Init;
        }

        private void AssertState(DeviceState state, DeviceState next)
        {
            if ((_state & state) != state)
            {
                throw new InvalidOperationException();
            }

            if (next != DeviceState.NoChange)
            {
                _state = next;
            }
        }

        /// <summary>
        ///
        /// </summary>
        public void StartDocument()
        {
            AssertState(DeviceState.Init, DeviceState.DocStarted);
        }

        /// <summary>
        ///
        /// </summary>
        public void EndDocument()
        {
            AssertState(DeviceState.DocStarted, DeviceState.Init);
        }


        /// <summary>
        ///
        /// </summary>
        public void AbortDocument()
        {
            _state = DeviceState.Init;
        }

        /// <summary>
        ///
        /// </summary>
        public bool StartPage()
        {
            AssertState(DeviceState.DocStarted, DeviceState.PageStarted);

            _page = new CanvasPrimitive();
            _root = _page;
            _stack = new Stack();

            _opacity     = 1;
            _opacityMask = null;
            _clip        = null;

            return true;
        }

        /// <summary>
        /// Start Tree/alpha flattening and send result to ILegacyDevice interface
        /// </summary>
        public void FlushPage(ILegacyDevice sink, double width, double height, Nullable<OutputQuality> outputQuality)
        {
            AssertState(DeviceState.PageStarted, DeviceState.DocStarted);

            if (_stack.Count != 0)
            {
                throw new InvalidOperationException();
            }

            if (sink != null)
            {
                Flattener.Convert(_root, sink, width, height, 96, 96, outputQuality);
            }
        }

        public void DrawGeometry(Brush brush, Pen pen, Geometry geometry)
        {
            // Ignore total transparent primitive
            if (Utility.IsTransparent(_opacity) || ((brush == null) && (pen == null || pen.Brush == null)) || (geometry == null))
            {
                return;
            }

            // Split if having both pen and brush
            if ((brush != null) && (pen != null))
            // if (!Utility.IsOpaque(_opacity) || (_opacityMask != null))
            {
                // Push a canvas to handle geometry with brush + pen properly
                Push(Matrix.Identity, null, 1.0, null, Rect.Empty, false);

                DrawGeometry(brush, null, geometry);
                DrawGeometry(null, pen, geometry);

                Pop();

                return;
            }

            AssertState(DeviceState.PageStarted, DeviceState.NoChange);

            GeometryPrimitive g = new GeometryPrimitive();

            g.Geometry    = geometry;
            g.Clip        = _clip;
            g.Opacity     = _opacity;
            g.OpacityMask = _opacityMask;

            int needBounds = 0; // 1 for fill, 2 for stroke

            if (brush != null)
            {
                // Fix bug 1427695: Need bounds for non-SolidColorBrushes to enable rebuilding Brush from BrushProxy.
                if (!(brush is SolidColorBrush))
                {
                    needBounds |= 1;
                }
            }

            if ((pen != null) && (pen.Brush != null))
            {
                if (!(pen.Brush is SolidColorBrush))
                {
                    needBounds |= 2;
                }
            }

            if (g.OpacityMask != null)
            {
                if (g.OpacityMask.BrushList == null && !(g.OpacityMask.Brush is SolidColorBrush))
                {
                    if (pen != null)
                    {
                        needBounds |= 2;
                    }
                    else
                    {
                        needBounds |= 1;
                    }
                }
            }

            Rect bounds = g.GetRectBounds((needBounds & 1) != 0);

            if (brush != null)
            {
                g.Brush = BrushProxy.CreateBrush(brush, bounds);
            }

            if ((needBounds & 2) != 0)
            {
                bounds = geometry.GetRenderBounds(pen);
            }

            if ((pen != null) && (pen.Brush != null))
            {
                g.Pen = PenProxy.CreatePen(pen, bounds);
            }

            if (g.OpacityMask != null)
            {
                if (!g.OpacityMask.MakeBrushAbsolute(bounds))
                {
                    // Fix bug 1463955: Brush has become empty; replace with transparent brush.
                    g.OpacityMask = BrushProxy.CreateColorBrush(Colors.Transparent);
                }
            }

            // Optimization: Unfold primitive DrawingBrush when possible to avoid rasterizing it.
            Primitive primitive = g.UnfoldDrawingBrush();

            if (primitive != null)
            {
                _root.Children.Add(primitive);
            }
        }

        public void DrawImage(ImageSource image, Rect rectangle)
        {
            if (image == null)
                return;

            AssertState(DeviceState.PageStarted, DeviceState.NoChange);

            ImagePrimitive g = new ImagePrimitive();

            g.Image   = new ImageProxy((BitmapSource)image);

            g.DstRect     = rectangle;
            g.Clip        = _clip;
            g.Opacity     = _opacity;
            g.OpacityMask = _opacityMask;

            _root.Children.Add(g);
        }

        public void DrawGlyphRun(Brush foreground, GlyphRun glyphRun)
        {
            if (foreground == null)
                return;

            AssertState(DeviceState.PageStarted, DeviceState.NoChange);

            GlyphPrimitive g = new GlyphPrimitive();

            bool needBounds = false;

            if (foreground != null && !(foreground is SolidColorBrush))
            {
                needBounds = true;
            }

            g.GlyphRun    = glyphRun;
            g.Brush       = BrushProxy.CreateBrush(foreground, g.GetRectBounds(needBounds));
            g.Clip        = _clip;
            g.Opacity     = _opacity;
            g.OpacityMask = _opacityMask;

            // Optimization: Unfold primitive DrawingBrush when possible to avoid rasterizing it.
            Primitive primitive = g.UnfoldDrawingBrush();

            if (primitive != null)
            {
                _root.Children.Add(primitive);
            }
        }

        public void Push(Matrix transform, Geometry clip, double opacity, Brush opacityMask, Rect maskBounds, bool onePrimitive)
        {
            AssertState(DeviceState.PageStarted, DeviceState.NoChange);

            opacity = Utility.NormalizeOpacity(opacity);

            if (!Utility.IsValid(transform))
            {
                // treat as invisible subtree
                opacity = 0.0;
                transform = Matrix.Identity;
            }

            _stack.Push(_root);
            _stack.Push(_clip);
            _stack.Push(_opacity);
            _stack.Push(_opacityMask);

            bool noTrans = transform.IsIdentity;

            if (onePrimitive && noTrans && (opacityMask == null))
            {
                bool empty;

                _clip        = Utility.Intersect(_clip, clip, Matrix.Identity, out empty);
                _opacity    *= opacity;
                _opacityMask = null;

                if (empty)
                {
                    _opacity = 0;
                }
            }
            else
            {
                CanvasPrimitive c = new CanvasPrimitive();

                if (noTrans)
                {
                    c.Transform = Matrix.Identity;
                }
                else
                {
                    c.Transform = transform;

                    Matrix invertTransform = transform;
                    invertTransform.Invert();

                    _clip = Utility.TransformGeometry(_clip, invertTransform); // transform inherited clip to this level
                }

                bool empty;

                c.Clip    = Utility.Intersect(clip, _clip, Matrix.Identity, out empty); // Combined with inherited attributes
                c.Opacity = opacity * _opacity;

                if (empty)
                {
                    c.Opacity = 0; // Invisible
                }

                if (opacityMask != null)
                {
                    double op;

                    if (Utility.ExtractOpacityMaskOpacity(opacityMask, out op, maskBounds))
                    {
                        c.Opacity = opacity * _opacity * op;
                    }
                    else
                    {
                        c.OpacityMask = BrushProxy.CreateOpacityMaskBrush(opacityMask, maskBounds);
                    }
                }

                _root.Children.Add(c);

                _root = c;

                _clip        = null;
                _opacity     = 1.0;
                _opacityMask = null;
            }
        }

        public void Pop()
        {
            AssertState(DeviceState.PageStarted, DeviceState.NoChange);

            _opacityMask = _stack.Pop() as BrushProxy;
            _opacity     = (double) _stack.Pop();
            _clip        = _stack.Pop() as Geometry;
            _root        = _stack.Pop() as CanvasPrimitive;
        }

#if COMMENT
        void Comment(string message)
        {
        }
#endif
    }

    /// <summary>
    /// Main interface to NGCSerializationManager, and glue flatteners and GDIExporter together
    ///
    /// 1) Implements IMetroDrawingContext interface
    /// 2) Passes drawing primitives to MetroDevice0 to build a primitive tree
    /// 3) In FlushPage, calls tree flattener to flat primitive tree to a linear display list
    /// 4) In FlushPage, calls alpha flattener to resolve transparency and send down result on the fly to GDIExporter which implements the ILegacyDevice interface
    /// 5) In FlushPage, calls GDIExporter.EndPage to finish printing a page
    /// 6) Supporting Testing hook to serialize alpha flattening result
    /// </summary>
    internal class MetroToGdiConverter : IMetroDrawingContext
    {
        static protected object         s_TestingHook;

        protected  MetroDevice0         m_Flattener;
        protected  ILegacyDevice        m_GDIExporter;
        protected  PrintQueue           m_PrintQueue;
        protected  PrintTicketConverter m_Converter; // Expensive to create, cache it
        protected  PrintTicketCache     m_printTicketCache; // Cache for per ticket data that is expensive to fetch

        protected  byte[]             m_Devmode;

        // settings captured from current PrintTicket
        protected  double             m_PageWidth;
        protected  double             m_PageHeight;

        protected Nullable<OutputQuality> m_OutputQuality;


        // PrintTicket XML Strings start at 4KB so we don't want the keys
        // from chewing all memory.  Limit to a small amount
        private const int s_PrintTicketCacheMaxCount = 10;

        public MetroToGdiConverter(PrintQueue queue)
        {
            ArgumentNullException.ThrowIfNull(queue);

            m_PrintQueue  = queue;
            m_Flattener   = new MetroDevice0();
            m_GDIExporter = queue.GetLegacyDevice();

            m_printTicketCache = new PrintTicketCache(s_PrintTicketCacheMaxCount);
        }

        /// <param name="ticket"></param>
        /// <param name="ticketXMLString">If you wish to read/write to the cache,
        /// this must be set to the result of ticket.ToXmlString().
        /// This an optional performance enhancement.</param>
        private byte[] GetDevmode(PrintTicket ticket, String ticketXMLString)
        {
            Debug.Assert(ticket != null);

            // The cached devmode is not modified by internal printing code
            // and not exposed to client code so it is safe to pass the byte array
            // by reference instead of making a copy
            byte[] result = null;

            if (ticketXMLString != null &&  m_printTicketCache.TryGetDevMode(ticketXMLString, out result))
            {
            }
            // 10ms slowpath.
            else
            {
                Toolbox.EmitEvent(EventTrace.Event.WClientDRXGetDevModeBegin);

                result = ConvertPrintTicketToDevMode(ticket);

                if (ticketXMLString != null)
                {
                    m_printTicketCache.CacheDevMode(ticketXMLString, result);
                }

                Toolbox.EmitEvent(EventTrace.Event.WClientDRXGetDevModeEnd);
            }

            return result;
        }

        /// <summary>
        /// Captures settings from complete PrintTickets.
        /// </summary>
        /// <param name="ticket"></param>
        /// <param name="ticketXMLString">If you wish to read/write to the cache,
        /// this must be set to the result of ticket.ToXmlString().
        /// This an optional performance enhancement.</param>
        /// <remarks>
        /// PrintTicket changes are communicated through StartDocument and StartPage, which receive
        /// complete tickets (the merge of tickets at all levels). Lack of change is indicated by
        /// null. Here we capture settings from PrintTicket for use in flattening and by GDIExporter.
        /// </remarks>
        private void CaptureTicketSettings(PrintTicket ticket, String ticketXMLString)
        {
            if (ticket == null)
            {
                Debug.Assert(ticketXMLString == null);

                // ticket has not changed since last capture; keep existing settings
            }
            else
            {
                m_OutputQuality = ticket.OutputQuality;

                if (ticketXMLString != null && m_printTicketCache.TryGetPageSize(ticketXMLString, out m_PageWidth, out m_PageHeight))
                {
                }
                // 100ms slowpath.
                else
                {
                    //
                    // Fix bug: 1396328, 1394678: Incorrect page dimensions used for clipping.
                    //
                    // PrintTicket contains the application-requested page dimensions. The printer
                    // may print to a page of different dimensions; that information is returned in
                    // PrintCapabilities, and is what we need to use for clipping.
                    //
                    PrintCapabilities capabilities = m_PrintQueue.GetPrintCapabilities(ticket);

                    m_PageWidth = capabilities.OrientedPageMediaWidth.GetValueOrDefault(816);
                    m_PageHeight = capabilities.OrientedPageMediaHeight.GetValueOrDefault(1056);

                    if(ticketXMLString != null)
                    {
                        m_printTicketCache.CachePageSize(ticketXMLString, m_PageWidth, m_PageHeight);
                    }
                }
            }
        }

        public int StartDocument(string jobName, PrintTicket ticket)
        {
            int jobIdentifier = 0;

            Toolbox.EmitEvent(EventTrace.Event.WClientDRXStartDocBegin);

            if (ticket == null)
            {
                ticket = m_PrintQueue.UserPrintTicket;
            }

            // Do not cache the calculation since StartDocument is only called
            // once per document, instead of per page, and we don't want to
            // hog precious cache entries.
            CaptureTicketSettings(ticket, null/*dont cache*/);

            m_Flattener.StartDocument();

            if (s_TestingHook == null)
            {
                m_Devmode = GetDevmode(ticket, null/*dont cache*/);

                jobIdentifier = m_GDIExporter.StartDocument(m_PrintQueue.FullName, jobName, Configuration.OutputFile, m_Devmode);
            }

            Toolbox.EmitEvent(EventTrace.Event.WClientDRXStartDocEnd);

            return jobIdentifier;
        }

        public void EndDocument()
        {
            EndDocument(abort:false);
        }

        public void EndDocument(bool abort)
        {
            Toolbox.EmitEvent(EventTrace.Event.WClientDRXEndDocBegin);

            if (abort)
            {
                m_Flattener.AbortDocument();
            }
            else
            {
                m_Flattener.EndDocument();
            }

            if (s_TestingHook == null)
            {
                m_GDIExporter.EndDocument();
            }

            DisposePrintTicketConverter();

            Toolbox.EmitEvent(EventTrace.Event.WClientDRXEndDocEnd);
        }


        public void CreateDeviceContext(string jobName, PrintTicket ticket)
        {
            if (ticket == null)
            {
                ticket = m_PrintQueue.UserPrintTicket;
            }

            // Do not cache the calculation since we aren't called once per page,
            // and we don't want to hog precious cache entries.
            CaptureTicketSettings(ticket, null/*dont cache*/);

            m_Devmode = GetDevmode(ticket, null/*dont cache*/);

            m_GDIExporter.CreateDeviceContext(m_PrintQueue.FullName, jobName, m_Devmode);
        }

        public void DeleteDeviceContext()
        {
            m_GDIExporter.DeleteDeviceContext();
        }

        public void StartDocumentWithoutCreatingDC(String jobName)
        {
            m_Flattener.StartDocument();

            String printerName = m_PrintQueue.FullName;

            m_GDIExporter.StartDocumentWithoutCreatingDC(printerName, jobName, Configuration.OutputFile);
        }

        public string ExtEscGetName()
        {
            return m_GDIExporter.ExtEscGetName();
        }

        public bool ExtEscMXDWPassThru()
        {
            return m_GDIExporter.ExtEscMXDWPassThru();
        }

        public void AbortDocument()
        {
            m_Flattener.AbortDocument();

            m_GDIExporter.EndDocument();

            DisposePrintTicketConverter();
        }

        public void StartPage(PrintTicket ticket)
        {
            Toolbox.EmitEvent(EventTrace.Event.WClientDRXStartPageBegin);

            String printTicketXMLStr = (ticket == null) ? null : ticket.ToXmlString();

            CaptureTicketSettings(ticket, printTicketXMLStr);

            if (m_Flattener.StartPage())
            {
                if (s_TestingHook == null)
                {
                    if (ticket != null)
                    {
                        m_GDIExporter.StartPage(GetDevmode(ticket, printTicketXMLStr), Configuration.RasterizationDPI);
                    }
                    else
                    {
                        m_GDIExporter.StartPage(null, Configuration.RasterizationDPI);
                    }
                }
            }

            Toolbox.EmitEvent(EventTrace.Event.WClientDRXStartPageEnd);
        }

        public void FlushPage()
        {
            Toolbox.EmitEvent(EventTrace.Event.WClientDRXFlushPageStart);

            ILegacyDevice ioc = m_GDIExporter;

            if (s_TestingHook != null)
            {
                DrawingContext dc = s_TestingHook as DrawingContext;

                if (dc != null)
                {
                    // Send alpha flattening output to a DrawingContext for testing
                    ioc = new OutputContext(dc);
                }
            }

            m_Flattener.FlushPage(ioc, m_PageWidth, m_PageHeight, m_OutputQuality);

            if (s_TestingHook == null)
            {
                Toolbox.EmitEvent(EventTrace.Event.WClientDRXEndPageBegin);
                m_GDIExporter.EndPage();
                Toolbox.EmitEvent(EventTrace.Event.WClientDRXEndPageEnd);
            }

            Toolbox.EmitEvent(EventTrace.Event.WClientDRXFlushPageStop);
        }

        void IMetroDrawingContext.DrawGeometry(Brush brush, Pen pen, Geometry geometry)
        {
            m_Flattener.DrawGeometry(brush, pen, geometry);
        }

        void IMetroDrawingContext.DrawImage(ImageSource image, Rect rectangle)
        {
            m_Flattener.DrawImage(image, rectangle);
        }

        void IMetroDrawingContext.DrawGlyphRun(Brush foreground, GlyphRun glyphRun)
        {
            m_Flattener.DrawGlyphRun(foreground, glyphRun);
        }

        void IMetroDrawingContext.Push(
            Matrix transform,
            Geometry clip,
            double opacity,
            Brush opacityMask,
            Rect maskBounds,
            bool onePrimitive,

            // serialization attributes
            String nameAttr,
            Visual node,
            Uri navigateUri,
            EdgeMode edgeMode
            )
        {
            m_Flattener.Push(transform, clip, opacity, opacityMask, maskBounds, onePrimitive);
        }

        void IMetroDrawingContext.Pop()
        {
            m_Flattener.Pop();
        }

        void IMetroDrawingContext.Comment(string message)
        {
        }

        private byte[] ConvertPrintTicketToDevMode(PrintTicket ticket)
        {
            if (m_Converter == null)
            {
                m_Converter = new PrintTicketConverter(m_PrintQueue.FullName, m_PrintQueue.ClientPrintSchemaVersion);
            }

            return m_Converter.ConvertPrintTicketToDevMode(ticket, BaseDevModeType.UserDefault);
        }


        private void DisposePrintTicketConverter()
        {
            if (m_Converter != null)
            {
                m_Converter.Dispose();
                m_Converter = null;
            }
        }

        /// <summary>
        /// Called before StartDocument to by-pass GDIExporter and send result to DrawingContext
        /// </summary>
        /// <param name="obj"></param>
        static public void TestingHook(Object obj)
        {
            if (obj != null)
            {
                s_TestingHook = obj as DrawingContext;
            }
        }
    }

    // Cache for per ticket data that is expensive to fetch
    internal class PrintTicketCache
    {
        public PrintTicketCache(int maxEntries)
        {
            if(maxEntries < 1)
            {
                throw new ArgumentOutOfRangeException("maxEntries", maxEntries, string.Empty);
            }

            this.m_innerCache = new MS.Internal.Printing.MostFrequentlyUsedCache<string, CachePacket>(maxEntries);
        }

        public void CachePageSize(string ticket, double width, double height)
        {
            ArgumentNullException.ThrowIfNull(ticket);

            EnsurePacketForKey(ticket).PageSize = new Size(width, height);
        }

        public void CacheDevMode(string ticket, byte [] devMode)
        {
            ArgumentNullException.ThrowIfNull(ticket);
            ArgumentNullException.ThrowIfNull(devMode);

            EnsurePacketForKey(ticket).DevMode = devMode;
        }

        public bool TryGetPageSize(string ticket, out double width, out double height)
        {
            ArgumentNullException.ThrowIfNull(ticket);

            CachePacket packet;
            if(this.m_innerCache.TryGetValue(ticket, out packet))
            {
                if(packet.PageSize.HasValue)
                {
                    width = packet.PageSize.Value.Width;
                    height = packet.PageSize.Value.Height;
                    return true;
                }
            }

            width = 0.0;
            height = 0.0;
            return false;
        }

        public bool TryGetDevMode(string ticket, out byte [] devMode)
        {
            ArgumentNullException.ThrowIfNull(ticket);

            CachePacket packet;
            if(this.m_innerCache.TryGetValue(ticket, out packet))
            {
                devMode = packet.DevMode;
                if(devMode != null)
                {
                    return true;
                }
            }

            devMode = null;
            return false;
        }

        private CachePacket EnsurePacketForKey(string ticket)
        {
            CachePacket packet;
            if(!this.m_innerCache.TryGetValue(ticket, out packet))
            {
                packet = new CachePacket();
                this.m_innerCache.CacheValue(ticket, packet);
            }

            return packet;
        }

        class CachePacket
        {
            // "null" for any field means it hasn't been set yet.
            // This implies that the fields cannot have real values of "null".
            public Nullable<Size> PageSize;
            public byte [] DevMode;
        }

        private MS.Internal.Printing.MostFrequentlyUsedCache<string, CachePacket> m_innerCache;
    }
} // end of namespace
