/*
 *    The contents of this file are subject to the Initial
 *    Developer's Public License Version 1.0 (the "License");
 *    you may not use this file except in compliance with the
 *    License. You may obtain a copy of the License at
 *    https://github.com/FirebirdSQL/NETProvider/blob/master/license.txt.
 *
 *    Software distributed under the License is distributed on
 *    an "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, either
 *    express or implied. See the License for the specific
 *    language governing rights and limitations under the License.
 *
 *    All Rights Reserved.
 */

//$Authors = Jiri Cincura (jiri@cincura.net)

using System.Collections.Generic;
using System.Data.Common;
using System.Data.Entity.Core.Common.CommandTrees;
using System.Data.Entity.Core.Metadata.Edm;
using System.Linq;
using System.Text;
using FirebirdSql.Data.FirebirdClient;

namespace EntityFramework.Firebird.SqlGen
{
	internal static class DmlSqlGenerator
	{
		#region Static Fields

		private const int CommandTextBuilderInitialCapacity = 256;

		#endregion

		#region Static Methods

		internal static string GenerateUpdateSql(DbUpdateCommandTree tree, out List<DbParameter> parameters, bool generateParameters = true)
		{
			var commandText = new StringBuilder(CommandTextBuilderInitialCapacity);
			var translator = new ExpressionTranslator(commandText, tree, null != tree.Returning, generateParameters);
			var first = true;

			commandText.Append("UPDATE ");
			tree.Target.Expression.Accept(translator);
			commandText.AppendLine();

			// set c1 = ..., c2 = ..., ...
			commandText.Append("SET ");

			foreach (DbSetClause setClause in tree.SetClauses)
			{
				if (first)
				{
					first = false;
				}
				else
				{
					commandText.Append(", ");
				}

				setClause.Property.Accept(translator);
				commandText.Append(" = ");
				setClause.Value.Accept(translator);

				translator.RegisterMemberValue(setClause.Property, setClause.Value);
			}

			if (first)
			{
				// If first is still true, it indicates there were no set
				// clauses. Introduce a fake set clause so that:
				// - we acquire the appropriate locks
				// - server-gen columns (e.g. timestamp) get recomputed

				var table = ((DbScanExpression)tree.Target.Expression).Target;
				// hope this column isn't indexed to not waste power
				var someColumn = table.ElementType.Members.Last(x => !MetadataHelpers.IsStoreGenerated(x));
				commandText.AppendFormat("{0} = {0}", GenerateMemberSql(someColumn));
			}
			commandText.AppendLine();

			// where c1 = ..., c2 = ...
			commandText.Append("WHERE ");
			tree.Predicate.Accept(translator);
			commandText.AppendLine();

			// generate returning sql
			GenerateReturningSql(commandText, tree, translator, tree.Returning);

			parameters = translator.Parameters;
			return commandText.ToString();
		}

		internal static string GenerateDeleteSql(DbDeleteCommandTree tree, out List<DbParameter> parameters, bool generateParameters = true)
		{
			var commandText = new StringBuilder(CommandTextBuilderInitialCapacity);
			var translator = new ExpressionTranslator(commandText, tree, false, generateParameters);

			commandText.Append("DELETE FROM ");
			tree.Target.Expression.Accept(translator);
			commandText.AppendLine();

			// where c1 = ... AND c2 = ...
			commandText.Append("WHERE ");
			tree.Predicate.Accept(translator);

			parameters = translator.Parameters;
			return commandText.ToString();
		}

		internal static string GenerateInsertSql(DbInsertCommandTree tree, out List<DbParameter> parameters, bool generateParameters = true)
		{
			var commandText = new StringBuilder(CommandTextBuilderInitialCapacity);
			var translator = new ExpressionTranslator(commandText, tree, null != tree.Returning, generateParameters);
			var first = true;

			commandText.Append("INSERT INTO ");
			tree.Target.Expression.Accept(translator);

			if (tree.SetClauses.Any())
			{
				// (c1, c2, c3, ...)
				commandText.Append("(");

				foreach (DbSetClause setClause in tree.SetClauses)
				{
					if (first)
					{
						first = false;
					}
					else
					{
						commandText.Append(", ");
					}
					setClause.Property.Accept(translator);
				}
				commandText.AppendLine(")");

				// values c1, c2, ...
				first = true;
				commandText.Append("VALUES (");
				foreach (DbSetClause setClause in tree.SetClauses)
				{
					if (first)
					{
						first = false;
					}
					else
					{
						commandText.Append(", ");
					}

					setClause.Value.Accept(translator);

					translator.RegisterMemberValue(setClause.Property, setClause.Value);
				}
				commandText.AppendLine(")");
			}
			else
			{
				commandText.AppendLine("DEFAULT VALUES");
			}
			// generate returning sql
			GenerateReturningSql(commandText, tree, translator, tree.Returning);

			parameters = translator.Parameters;
			return commandText.ToString();
		}

		// Generates SQL describing a member
		// Requires: member must belong to an entity type (a safe requirement for DML
		// SQL gen, where we only access table columns)
		internal static string GenerateMemberSql(EdmMember member)
		{
			return SqlGenerator.QuoteIdentifier(member.Name);
		}

		private static void GenerateReturningSql(
			StringBuilder commandText,
			DbModificationCommandTree tree,
			ExpressionTranslator translator,
			DbExpression returning)
		{
			// Nothing to do if there is no Returning expression
			if (returning == null)
			{
				return;
			}

			var table = ((DbScanExpression)tree.Target.Expression).Target;
			var columnsToFetch =
				table.ElementType.Members
					.Where(m => MetadataHelpers.IsStoreGenerated(m))
					.Except((!(tree is DbInsertCommandTree) ? table.ElementType.KeyMembers : Enumerable.Empty<EdmMember>()));

			var startBlock = new StringBuilder();
			var separator = string.Empty;

			startBlock.Append("EXECUTE BLOCK ");
			if (translator.Parameters.Any())
			{
				startBlock.AppendLine("(");
				separator = string.Empty;
				foreach (FbParameter param in translator.Parameters)
				{
					startBlock.Append(separator);
					startBlock.Append(param.ParameterName.Replace("@", string.Empty));
					startBlock.Append(" ");
					var member = translator.MemberValues.First(m => m.Value.Contains(param)).Key;
					startBlock.Append(SqlGenerator.GetSqlPrimitiveType(member.TypeUsage));
					if (param.FbDbType == FbDbType.VarChar || param.FbDbType == FbDbType.Char)
						startBlock.Append(" CHARACTER SET UTF8");
					startBlock.Append(" = ");
					startBlock.Append(param.ParameterName);

					separator = ", ";
				}
				startBlock.AppendLine();
				startBlock.Append(") ");
			}

			startBlock.AppendLine("RETURNS (");
			separator = string.Empty;
			foreach (var m in columnsToFetch)
			{
				startBlock.Append(separator);
				startBlock.Append(GenerateMemberSql(m));
				startBlock.Append(" ");
				startBlock.Append(SqlGenerator.GetSqlPrimitiveType(m.TypeUsage));

				separator = ", ";
			}
			startBlock.AppendLine(")");
			startBlock.AppendLine("AS BEGIN");

			var newCommand = ChangeParamsToPSQLParams(commandText.ToString(), translator.Parameters.Select(p => p.ParameterName).ToArray());
			commandText.Remove(0, commandText.Length);
			commandText.Insert(0, newCommand);
			commandText.Insert(0, startBlock.ToString());

			commandText.Append("RETURNING ");
			separator = string.Empty;
			foreach (var m in columnsToFetch)
			{
				commandText.Append(separator);
				commandText.Append(GenerateMemberSql(m));

				separator = ", ";
			}
			commandText.Append(" INTO ");
			separator = string.Empty;
			foreach (var m in columnsToFetch)
			{
				commandText.Append(separator);
				commandText.Append(":" + GenerateMemberSql(m));

				separator = ", ";
			}

			commandText.AppendLine(";");
			commandText.AppendLine("IF (ROW_COUNT > 0) THEN");
			commandText.AppendLine("  SUSPEND;");
			commandText.AppendLine("END");
		}

		private static string ChangeParamsToPSQLParams(string commandText, string[] parametersUsed)
		{
			var command = new StringBuilder(commandText);
			foreach (var param in parametersUsed)
			{
				command.Replace(param, ":" + param.Remove(0, 1));
			}
			return command.ToString();
		}

		#endregion
	}
}
