using System;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using BepInEx.ConsoleUtil;
using HarmonyLib;
using Microsoft.Win32.SafeHandles;
using UnityInjector.ConsoleUtil;

namespace BepInEx
{
	internal class WindowsConsoleDriver : IConsoleDriver
	{
		public TextWriter StandardOut { get; private set; }
		public TextWriter ConsoleOut { get; private set; }

		public bool ConsoleActive { get; private set; }
		public bool ConsoleIsExternal => true;

		public void Initialize(bool alreadyActive)
		{
			ConsoleActive = alreadyActive;

			StandardOut = Console.Out;
		}

		// Apparently on some versions of Unity (e.g. 2018.4) using old mono causes crashes on game close if
		// IntPtr overload is used for file streams (check #139).
		// On the other hand, not all Unity games come with SafeFileHandle overload for FileStream
		// As such, we're trying to use SafeFileHandle when it's available and go back to IntPtr overload if not available
		private static readonly ConstructorInfo FileStreamCtor = new[]
		{
			AccessTools.Constructor(typeof(FileStream), new[] { typeof(SafeFileHandle), typeof(FileAccess) }),
			AccessTools.Constructor(typeof(FileStream), new[] { typeof(IntPtr), typeof(FileAccess) }),
		}.FirstOrDefault(m => m != null);

		private static FileStream OpenFileStream(IntPtr handle)
		{
			var fileHandle = new SafeFileHandle(handle, false);
			var ctorParams = AccessTools.ActualParameters(FileStreamCtor, new object[] { fileHandle, fileHandle.DangerousGetHandle(), FileAccess.Write });
			return (FileStream)Activator.CreateInstance(typeof(FileStream), ctorParams);
		}

		public void CreateConsole(uint codepage)
		{
			ConsoleWindow.Attach();

			// Make sure of ConsoleEncoding helper class because on some Monos
			// Encoding.GetEncoding throws NotImplementedException on most codepages
			// NOTE: We don't set Console.OutputEncoding because it resets any existing Console.Out writers
			ConsoleEncoding.ConsoleCodePage = codepage;

			// If stdout exists, write to it, otherwise make it the same as console out
			// Not sure if this is needed? Does the original Console.Out still work?
			var stdout = GetOutHandle();
			if (stdout == IntPtr.Zero)
			{
				StandardOut = TextWriter.Null;
				ConsoleOut = TextWriter.Null;
				return;
			}

			var originalOutStream = OpenFileStream(stdout);
			StandardOut = new StreamWriter(originalOutStream, new UTF8Encoding(false))
			{
				AutoFlush = true
			};

			var consoleOutStream = OpenFileStream(ConsoleWindow.ConsoleOutHandle);
			// Can't use Console.OutputEncoding because it can be null (i.e. not preference by user)
			ConsoleOut = new StreamWriter(consoleOutStream, ConsoleEncoding.OutputEncoding)
			{
				AutoFlush = true
			};
			ConsoleActive = true;
		}

		private IntPtr GetOutHandle()
		{
			switch (ConsoleManager.ConfigConsoleOutRedirectType.Value)
			{
				case ConsoleManager.ConsoleOutRedirectType.ConsoleOut:
					return ConsoleWindow.ConsoleOutHandle;
				case ConsoleManager.ConsoleOutRedirectType.StandardOut:
					return ConsoleWindow.OriginalStdoutHandle;
				case ConsoleManager.ConsoleOutRedirectType.Auto:
				default:
					return ConsoleWindow.OriginalStdoutHandle != IntPtr.Zero ? ConsoleWindow.OriginalStdoutHandle : ConsoleWindow.ConsoleOutHandle;
			}
		}

		public void DetachConsole()
		{
			ConsoleWindow.Detach();

			ConsoleOut.Close();
			ConsoleOut = null;

			ConsoleActive = false;
		}

		public void SetConsoleColor(ConsoleColor color)
		{
			SafeConsole.ForegroundColor = color;
			Kon.ForegroundColor = color;
		}

		public void SetConsoleTitle(string title)
		{
			ConsoleWindow.Title = title;
		}
	}
}