using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;
using xTile.Dimensions;
using xTile.Display;
using xTile.Layers;
using xTile.Tiles;
using Rectangle = xTile.Dimensions.Rectangle;

namespace StardewModdingAPI.Framework.Rendering
{
    /// <summary>A map display device which reimplements the default logic.</summary>
    /// <remarks>This is an exact copy of <see cref="XnaDisplayDevice"/>, except that private fields are protected and all methods are virtual.</remarks>
    [SuppressMessage("ReSharper", "InconsistentNaming", Justification = $"Field naming deliberately matches {nameof(XnaDisplayDevice)} to minimize differences.")]
    internal class SXnaDisplayDevice : IDisplayDevice
    {
        /*********
        ** Fields
        *********/
        protected readonly ContentManager m_contentManager;
        protected readonly GraphicsDevice m_graphicsDevice;
        protected SpriteBatch m_spriteBatchAlpha;
        protected SpriteBatch m_spriteBatchAdditive;
        protected readonly Dictionary<TileSheet, Texture2D> m_tileSheetTextures;
        protected Vector2 m_tilePosition;
        protected Microsoft.Xna.Framework.Rectangle m_sourceRectangle;
        protected readonly Color m_modulationColour;


        /*********
        ** Public methods
        *********/
        /// <summary>Construct an instance.</summary>
        /// <param name="contentManager">The content manager through which to load tiles.</param>
        /// <param name="graphicsDevice">The graphics device with which to render tiles.</param>
        public SXnaDisplayDevice(ContentManager contentManager, GraphicsDevice graphicsDevice)
        {
            this.m_contentManager = contentManager;
            this.m_graphicsDevice = graphicsDevice;
            this.m_spriteBatchAlpha = new SpriteBatch(graphicsDevice);
            this.m_spriteBatchAdditive = new SpriteBatch(graphicsDevice);
            this.m_tileSheetTextures = new Dictionary<TileSheet, Texture2D>();
            this.m_tilePosition = new Vector2();
            this.m_sourceRectangle = new Microsoft.Xna.Framework.Rectangle();
            this.m_modulationColour = Color.White;
        }

        /// <summary>Load a tilesheet texture.</summary>
        /// <param name="tileSheet">The tilesheet instance.</param>
        public virtual void LoadTileSheet(TileSheet tileSheet)
        {
            Texture2D texture2D = this.m_contentManager.Load<Texture2D>(tileSheet.ImageSource);
            this.m_tileSheetTextures[tileSheet] = texture2D;
        }

        /// <summary>Unload a tilesheet texture.</summary>
        /// <param name="tileSheet">The tilesheet instance.</param>
        public virtual void DisposeTileSheet(TileSheet tileSheet)
        {
            this.m_tileSheetTextures.Remove(tileSheet);
        }

        /// <summary>Prepare to render to the screen.</summary>
        /// <param name="b">The sprite batch being rendered.</param>
        public virtual void BeginScene(SpriteBatch b)
        {
            this.m_spriteBatchAlpha = b;
        }

        /// <summary>Set the clipping region.</summary>
        /// <param name="clippingRegion">The clipping region.</param>
        public virtual void SetClippingRegion(Rectangle clippingRegion)
        {
            int backBufferWidth = this.m_graphicsDevice.PresentationParameters.BackBufferWidth;
            int backBufferHeight = this.m_graphicsDevice.PresentationParameters.BackBufferHeight;
            int x = this.Clamp(clippingRegion.X, 0, backBufferWidth);
            int y = this.Clamp(clippingRegion.Y, 0, backBufferHeight);
            int num1 = this.Clamp(clippingRegion.X + clippingRegion.Width, 0, backBufferWidth);
            int num2 = this.Clamp(clippingRegion.Y + clippingRegion.Height, 0, backBufferHeight);
            int width = num1 - x;
            int height = num2 - y;
            this.m_graphicsDevice.Viewport = new Viewport(x, y, width, height);
        }

        /// <summary>Draw a tile to the screen.</summary>
        /// <param name="tile">The tile to draw.</param>
        /// <param name="location">The tile position to draw.</param>
        /// <param name="layerDepth">The layer depth at which to draw.</param>
        public virtual void DrawTile(Tile? tile, Location location, float layerDepth)
        {
            if (tile == null)
                return;
            Rectangle tileImageBounds = tile.TileSheet.GetTileImageBounds(tile.TileIndex);
            Texture2D tileSheetTexture = this.m_tileSheetTextures[tile.TileSheet];
            if (tileSheetTexture.IsDisposed)
                return;
            this.m_tilePosition.X = location.X;
            this.m_tilePosition.Y = location.Y;
            this.m_sourceRectangle.X = tileImageBounds.X;
            this.m_sourceRectangle.Y = tileImageBounds.Y;
            this.m_sourceRectangle.Width = tileImageBounds.Width;
            this.m_sourceRectangle.Height = tileImageBounds.Height;
            this.m_spriteBatchAlpha.Draw(tileSheetTexture, this.m_tilePosition, this.m_sourceRectangle, this.m_modulationColour, 0.0f, Vector2.Zero, Layer.zoom, SpriteEffects.None, layerDepth);
        }

        /// <summary>Finish drawing to the screen.</summary>
        public virtual void EndScene() { }

        /// <summary>Snap a value to the given range.</summary>
        /// <param name="nValue">The value to normalize.</param>
        /// <param name="nMin">The minimum value.</param>
        /// <param name="nMax">The maximum value.</param>
        protected int Clamp(int nValue, int nMin, int nMax)
        {
            return Math.Min(Math.Max(nValue, nMin), nMax);
        }
    }
}