using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Xml;
using System.Xml.Linq;
using System.Xml.Schema;
using Web.Core;
using Patcher.Data.Command;
using Patcher.DB;

namespace Patcher.Data.Patch
{
	abstract class AbstractPatch
	{
	
		private static class Checker
		{
			private static XmlSchemaSet LoadXsd()
			{
				XmlSchemaSet schemaSet = new XmlSchemaSet();
				using(Stream xsdStream = Resources.ResourcesManager.GetResource("IPatch.xsd"))
				{
					using(XmlReader xsdReader = XmlReader.Create(xsdStream))
					{
						schemaSet.Add("", xsdReader);
					}
				}
				schemaSet.Compile();
				return schemaSet;
			}

			private static readonly XmlSchemaSet Xsd = LoadXsd();
			
			public static void Check(XDocument document)
			{
				document.Validate(Xsd, (sender, e) => { throw new ApplicationException("Intermediate XML validation failed: " + e.Message, e.Exception); });
			}

		}
		
		public static AbstractPatch LoadById(PatchId id, Context context)
		{
			return Cache<AbstractPatch>.instance.get(new KeyValuePair<PatchId, Context>(id, context), () => _LoadById(id, context));
		}
	
		private static AbstractPatch _LoadById(PatchId id, Context context)
		{
			XDocument data;
			using(Stream xmlStream = context.loadPatch(id))
			{
				using(XmlReader reader = XmlReader.Create(xmlStream))
				{
					data = XDocument.Load(reader);
				}
			}
			Checker.Check(data);
			XElement version = data.Root.Element("version");
			string number = version.Element("number").Value;
			string author = version.Element("author").Value;
			if((number != id.version.ToString()) || (author != id.name))
			{
				throw new ApplicationException(string.Format("Versions mismatch on patch #{0} from {1} (got #{2} from {3})", id.version, id.name, number, author));
			}

			HashSet<string> restrictToEnvironments;
			if(data.Root.Element("restrictToEnvironments") != null)
			{
				restrictToEnvironments = new HashSet<string>(from elem in data.Root.Element("restrictToEnvironments").Elements() select elem.Value);
			} else
			{
				restrictToEnvironments = new HashSet<string>();
			}

			XElement commandSet;
			bool isStrictCommandSet;
			{
				XElement strictCommandSet = data.Root.Element("strictCommandSet");
				XElement looseCommandSet = data.Root.Element("looseCommandSet");
				if (strictCommandSet != null && looseCommandSet == null)
				{
					commandSet = strictCommandSet;
					isStrictCommandSet = true;
				} else if (strictCommandSet == null && looseCommandSet != null)
				{
					commandSet = looseCommandSet;
					isStrictCommandSet = false;
				} else if (strictCommandSet != null && looseCommandSet != null)
				{
					throw new ApplicationException("Malformed XML: both strictCommandSet and looseCommandSet found");
				} else
				{
					throw new ApplicationException("Malformed XML: no CommandSet found");
				}
			}

			var commands = (from elem in commandSet.Elements() select AbstractCommand.Create(elem)).ToArray();

			if(isStrictCommandSet)
			{
				var isPersistent = commands.OfType<AbstractPersistentCommand>().Any();
				if (isPersistent)
				{
					if (commands.Length != 1)
					{
						throw new ApplicationException("More than one persistent command");
					}
					if(context.DbDriver.IsDDLTransactional)
					{
						return new AtomicPatch(commands, true, restrictToEnvironments, context);
					} else
					{
						return new PersistentPatch(commands[0], restrictToEnvironments, context);
					}
				}
				else
				{
					return new AtomicPatch(commands, true, restrictToEnvironments, context);
				}
			} else
			{
				return new AtomicPatch(commands, false, restrictToEnvironments, context);
			}
		}

		private readonly HashSet<string> restrictToEnvironments;

		protected readonly Context context;

		protected AbstractPatch(HashSet<string> restrictToEnvironments, Context context)
		{
			this.restrictToEnvironments = restrictToEnvironments;
			this.context = context;
		}

		public abstract XDocument Apply(Transaction transaction);

		public abstract void Rollback(Transaction transaction, XDocument rollbackInfo);
		
		public bool DoesSupportEnvironment(string environmentName)
		{
			if(restrictToEnvironments.Any())
			{
				return restrictToEnvironments.Contains(environmentName);
			} else
			{
				return true;
			}
		}
	
	}
}
