<#@ assembly name="System.Core"    #>
<#@ assembly name="System.Data"    #>
<#@ import namespace="System.Collections.Generic" #>
<#@ import namespace="System.Data" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.IO"   #>
<#
	AppDomain.CurrentDomain.AssemblyResolve += (_,args) =>
	{
		foreach (var a in Assemblies)
			if (args.Name.ToLower().IndexOf(a.Key.ToLower()) >= 0)
				return System.Reflection.Assembly.LoadFile(a.Value);
	
		if (DataProviderAssembly != null && args.Name.Split(',')[0] == System.IO.Path.GetFileNameWithoutExtension(DataProviderAssembly))
			return System.Reflection.Assembly.LoadFile(DataProviderAssembly);

		return null;
	};
#><#+

static Dictionary<string,string> Assemblies = new Dictionary<string,string>();

static Action<GeneratedTextTransformation,string> WriteComment        = (tt,s) => tt.WriteLine("//{0}", s);
static Action<GeneratedTextTransformation,string> WriteSummary        = (tt,s) => 
{
	if (!string.IsNullOrWhiteSpace(s))
	{
		tt.WriteLine("/// <summary>");
		tt.WriteLine("/// {0}", s);
		tt.WriteLine("/// </summary>");
	}
};
static Action<GeneratedTextTransformation,string> WriteUsing          = (tt,s) => tt.WriteLine("using {0};", s);
static Action<GeneratedTextTransformation,string> WriteBeginNamespace = (tt,s) => { tt.WriteLine("namespace {0}", s); tt.WriteLine("{"); };
static Action<GeneratedTextTransformation>        WriteEndNamespace   =  tt    => tt.WriteLine("}");
static Action<GeneratedTextTransformation,string,string> WriteBeginClass = (tt,cl,bc) =>
{
	tt.Write("public partial class {0}", cl);
	if (!string.IsNullOrEmpty(bc))
		tt.Write(" : {0}", bc);
	tt.WriteLine("");
	tt.WriteLine("{");
};
static Action<GeneratedTextTransformation>        WriteEndClass       =  tt   => tt.WriteLine("}");
static Func<string,string,string>                 MakeGenericType     = (c,t) => string.Format("{0}<{1}>", c, t);
static Func<string,string>                        MakeType            = t     => t;

delegate void WriteTablePropertyAction(GeneratedTextTransformation tt, string name, string pname, int maxlen, int maxplen, string desc);

static WriteTablePropertyAction WriteTableProperty = (tt,name,pname,maxlen,maxplen,desc) =>
{
	WriteSummary(tt,desc);
	tt.WriteLine("public Table<{0}>{1} {2}{3} {{ get {{ return this.GetTable<{0}>();{1} }} }}", name, tt.LenDiff(maxlen, name), pname, tt.LenDiff(maxplen, pname));
};
static Action<GeneratedTextTransformation,string> WriteAttribute      = (tt,a) => tt.Write("[{0}]", a);
static Action<GeneratedTextTransformation>        WriteAttributeLine  =  tt    => tt.WriteLine("");

static string ConnectionString;
static string ConnectionType;
static string DataProviderAssembly = null;

string DatabaseName             = null;
string DataContextName          = null;
string Namespace                = "DataModel";
string BaseDataContextClass     = "DbManager";
string BaseEntityClass          = null;
string OneToManyAssociationType = "IEnumerable<{0}>";

string OwnerToInclude           = null;
string[] DatabaseQuote          = null;

bool   RenderField              = false;
bool   RenderBackReferences     = true;
bool   RenderForeignKeys        = true;

bool   IsMetadataLoaded;

int MaxColumnTypeLen;
int MaxColumnMemberLen;

static Action<GeneratedTextTransformation,Column,int[],string[]> RenderColumn = (tt,c,maxLens,attrs) =>
{
	WriteSummary(tt,c.Description);
	
	if (maxLens.Sum() > 0)
	{
		if (attrs.Any(_ => _ != null))
		{
			tt.Write("[");

			for (var i = 0; i < attrs.Length; i++)
			{
				if (attrs[i] != null)
				{
					tt.Write(attrs[i]);
					tt.WriteSpace(maxLens[i] - attrs[i].Length);

					if (attrs.Skip(i + 1).Any(_ => _ != null))
						tt.Write(", ");
					else if (maxLens.Skip(i + 1).Any(_ => _ > 0))
						tt.WriteSpace(2);
				}
				else if (maxLens[i] > 0)
				{
					tt.WriteSpace(maxLens[i]);
					
					if (maxLens.Skip(i + 1).Any(_ => _ > 0))
						tt.WriteSpace(2);
				}
			}

			tt.Write("] ");
		}
		else
		{
			tt.WriteSpace(maxLens.Sum() + (maxLens.Where(_ => _ > 0).Count() - 1) * 2 + 3);
		}
	}

	tt.Write("public {0}{1} {2}", c.Type, tt.LenDiff(tt.MaxColumnTypeLen, c.Type), c.MemberName);

	if (tt.RenderField)
	{
		tt.Write(";");
		if (c.ColumnType != null)
			tt.Write(tt.LenDiff(tt.MaxColumnMemberLen, c.MemberName));
	}
	else
		tt.Write("{0} {{ get; set; }}", tt.LenDiff(tt.MaxColumnMemberLen, c.MemberName));

	if (c.ColumnType != null)
	{
		tt.Write(" // {0}", c.ColumnType);
		
		if (c.Length != 0)
			tt.Write("({0})", c.Length);

		if (c.Precision != 0)
		{
			if (c.Scale == 0)
				tt.Write("({0})", c.Precision);
			else
				tt.Write("({0},{1})", c.Precision, c.Scale);
		}
	}

	tt.WriteLine("");
};

static Action<GeneratedTextTransformation,ForeignKey> RenderForeignKey = (tt,key) =>
{
	WriteComment(tt, " " + key.KeyName);
	tt.WriteLine("[Association(ThisKey=\"{0}\", OtherKey=\"{1}\", CanBeNull={2})]",
		string.Join(", ", (from c in key.ThisColumns  select c.MemberName).ToArray()),
		string.Join(", ", (from c in key.OtherColumns select c.MemberName).ToArray()),
		key.CanBeNull ? "true" : "false");

	if (key.Attributes.Count > 0)
	{
		WriteAttribute(tt, string.Join(", ", key.Attributes.Distinct().ToArray()));
		WriteAttributeLine(tt);
	}

	tt.Write("public ");

	if (key.AssociationType == AssociationType.OneToMany)
		tt.Write(tt.OneToManyAssociationType, key.OtherTable.ClassName);
	else
		tt.Write(key.OtherTable.ClassName);

	tt.Write(" ");
	tt.Write(key.MemberName);

	if (tt.RenderField)
		tt.WriteLine(";");
	else
		tt.WriteLine(" { get; set; }");
};

static Action<GeneratedTextTransformation,Table, bool> RenderTable = (tt,t, renderForeignKeys) =>
{
	WriteSummary(tt,t.Description);

	if (t.IsView)
	{
		WriteComment(tt, " View");
	}

	RenderTableAttributes(tt, t);

	WriteBeginClass(tt, t.ClassName, t.BaseClassName);
	
	tt.PushIndent("\t");

	if (t.Columns.Count > 0)
	{
		tt.MaxColumnTypeLen   = t.Columns.Values.Max(_ => _.Type.Length);
		tt.MaxColumnMemberLen = t.Columns.Values.Max(_ => _.MemberName.Length);

		var maxLens = new int[]
		{
			t.Columns.Values.Max(_ => _.MemberName == _.ColumnName ? 0 : "MapField('')".Length + _.ColumnName.Length),
			t.Columns.Values.Max(_ => _.IsNullable                 ? "Nullable".Length : _.IsIdentity ? "Identity".Length : 0),
			t.Columns.Values.Max(_ => _.IsIdentity && _.IsNullable ? "Identity".Length : 0),
			t.Columns.Values.Max(_ => _.IsPrimaryKey               ? string.Format("PrimaryKey({0})", _.PKIndex).Length : 0),
			t.Columns.Values.Max(_ => _.Attributes.Count == 0      ? 0 : string.Join(", ", _.Attributes.Distinct().ToArray()).Length),
		};

		foreach (var c in from c in t.Columns.Values orderby c.ID select c)
		{
			var attrs = new string[]
			{
				c.MemberName == c.ColumnName ? null : string.Format("MapField(\"{0}\")", c.ColumnName),
				c.IsNullable                 ? "Nullable" : c.IsIdentity ? "Identity" : null,
				c.IsIdentity && c.IsNullable ? "Identity" : null,
				c.IsPrimaryKey               ? string.Format("PrimaryKey({0})", c.PKIndex) : null,
				c.Attributes.Count == 0      ? null : string.Join(", ", c.Attributes.Distinct().ToArray()),
			};

			RenderColumn(tt, c, maxLens, attrs);
		}
	}

	if (renderForeignKeys && t.ForeignKeys.Count > 0)
	{
		foreach (var key in t.ForeignKeys.Values.Where(k => tt.RenderBackReferences || k.BackReference != null))
		{
			tt.WriteLine("");
			RenderForeignKey(tt, key);
		}
	}

	tt.PopIndent();
	WriteEndClass(tt);
};

static Action<GeneratedTextTransformation,Table> RenderTableAttributes = (tt,t) =>
{
	if (t.Attributes.Count > 0)
	{
		WriteAttribute(tt, string.Join(", ", t.Attributes.Distinct().ToArray()));
		WriteAttributeLine(tt);
	}

	string tbl = "TableName(";

	if (!string.IsNullOrEmpty(tt.DatabaseName))
		tbl += string.Format("Database=\"{0}\", ", tt.DatabaseName);

	if (!string.IsNullOrEmpty(t.Owner))
		tbl += string.Format("Owner=\"{0}\", ", t.Owner);

	tbl += string.Format("Name=\"{0}\")", t.TableName);

	WriteAttribute(tt, tbl);
	WriteAttributeLine(tt);
};

List<string> Usings = new List<string>()
{
	"System",
	"BLToolkit.Data",
	"BLToolkit.Data.Linq",
	"BLToolkit.DataAccess",
	"BLToolkit.Mapping",
};

static Action<GeneratedTextTransformation> RenderUsing = tt =>
{
	var q =
		from ns in tt.Usings.Distinct()
		group ns by ns.Split('.')[0];

	var groups =
		(from ns in q where ns.Key == "System"                select ns).Concat
		(from ns in q where ns.Key != "System" orderby ns.Key select ns);

	foreach (var gr in groups)
	{
		foreach (var ns in from s in gr orderby s select s)
			WriteUsing(tt, ns);

		tt.WriteLine("");
	}
};

Action<GeneratedTextTransformation> BeforeGenerateModel = _ => {};
Action<GeneratedTextTransformation> AfterGenerateModel  = _ => {};

Action<GeneratedTextTransformation> BeforeWriteTableProperty = _ => {};
Action<GeneratedTextTransformation> AfterWriteTableProperty  = _ => {};

void GenerateModel()
{
	if (ConnectionString != null) ConnectionString = ConnectionString.Trim();
	if (DataContextName  != null) DataContextName  = DataContextName. Trim();

	if (string.IsNullOrEmpty(ConnectionString)) { Error("ConnectionString cannot be empty."); return; }

	if (string.IsNullOrEmpty(DataContextName))
		DataContextName = "DataContext";

	LoadMetadata();

	BeforeGenerateModel(this);

	WriteComment(this, "---------------------------------------------------------------------------------------------------");
	WriteComment(this, " <auto-generated>");
	WriteComment(this, "    This code was generated by BLToolkit template for T4.");
	WriteComment(this, "    Changes to this file may cause incorrect behavior and will be lost if the code is regenerated.");
	WriteComment(this, " </auto-generated>");
	WriteComment(this, "---------------------------------------------------------------------------------------------------");

	RenderUsing(this);

	WriteBeginNamespace(this, Namespace);
	PushIndent("\t");

	WriteBeginClass(this, DataContextName, BaseDataContextClass);

	var tlist   = (from t in Tables.Values orderby t.TableName select t).ToList();
	var maxlen  = tlist.Max(_ => _.ClassName.Length);
	var maxplen = tlist.Max(_ => (_.DataContextPropertyName ?? _.ClassName).Length);

	PushIndent("\t");

	BeforeWriteTableProperty(this);

	foreach (var t in tlist)
		WriteTableProperty(this, t.ClassName, t.DataContextPropertyName ?? t.ClassName, maxlen, maxplen, t.Description);

	AfterWriteTableProperty(this);

	PopIndent();

	WriteEndClass(this);

	foreach (var t in tlist)
	{
		WriteLine("");
		RenderTable(this, t, RenderForeignKeys);
	}

	PopIndent();
	WriteEndNamespace(this);

	AfterGenerateModel(this);
}

string LenDiff(int max, string str)
{
	var s = "";

	while (max-- > str.Length)
		s += " ";

	return s;
}

void WriteSpace(int len)
{
	while (len-- > 0)
		Write(" ");
}

List<T> CreateList<T>(T item)
{
	return new List<T>();
}

Func<System.Data.IDbConnection> GetConnectionObject = () =>
{
	Type connType = null;

	if (DataProviderAssembly != null)
	{
		try
		{
			var assembly = System.Reflection.Assembly.LoadFile(DataProviderAssembly);
			connType = assembly.GetType(ConnectionType) 
				?? assembly.GetType(ConnectionType.Substring(0, ConnectionType.IndexOf(",")));
		}
		catch
		{
		}
	}

	if (connType == null)
		connType = Type.GetType(ConnectionType);

	return (System.Data.IDbConnection)Activator.CreateInstance(connType);
};

System.Data.IDbConnection GetConnection()
{
	var conn = GetConnectionObject();

	conn.ConnectionString = ConnectionString;
	conn.Open();

	return conn;
}

void LoadMetadata()
{
	if (IsMetadataLoaded)
		return;

	IsMetadataLoaded = true;

	if (!string.IsNullOrEmpty(DataProviderAssembly) && !DataProviderAssembly.Contains(":") && DataProviderAssembly.Contains(".."))
	{
		try
		{
			string path = this.Host.ResolvePath("");
			DataProviderAssembly = Path.GetFullPath(Path.Combine(path, DataProviderAssembly));
		}
		catch 
		{
		}
	}

	BeforeLoadMetadata(this);
	LoadServerMetadata();

	if (DatabaseQuote != null)
	{
		foreach (var t in Tables.Values)
		{
			t.TableName = string.Format("{1}{0}{2}", t.TableName, DatabaseQuote.FirstOrDefault(), DatabaseQuote.Skip(1).FirstOrDefault() ?? DatabaseQuote.FirstOrDefault());
			foreach (var c in t.Columns.Values)
			{
				c.ColumnName = string.Format("{1}{0}{2}", c.ColumnName, DatabaseQuote.FirstOrDefault(), DatabaseQuote.Skip(1).FirstOrDefault() ?? DatabaseQuote.FirstOrDefault());
			}
		}
	}

	foreach (var t in Tables.Values)
	{
		if (t.ClassName.Contains(" "))
		{
			var ss = t.ClassName.Split(' ').Where(_ => _.Trim().Length > 0).Select(_ => char.ToUpper(_[0]) + _.Substring(1));
			t.ClassName = string.Join("", ss.ToArray());
		}
	}

	foreach (var t in Tables.Values)
		foreach (var key in t.ForeignKeys.Values.ToList())
			if (!key.KeyName.EndsWith("_BackReference"))
				key.OtherTable.ForeignKeys.Add(key.KeyName + "_BackReference", key.BackReference = new ForeignKey
				{
					KeyName         = key.KeyName    + "_BackReference",
					MemberName      = key.MemberName + "_BackReference",
					AssociationType = AssociationType.Auto,
					OtherTable      = t,
					ThisColumns     = key.OtherColumns,
					OtherColumns    = key.ThisColumns,
				});

	foreach (var t in Tables.Values)
	{
		foreach (var key in t.ForeignKeys.Values)
		{
			if (key.BackReference != null && key.AssociationType == AssociationType.Auto)
			{
				if (key.ThisColumns.All(_ => _.IsPrimaryKey))
				{
					if (t.Columns.Values.Count(_ => _.IsPrimaryKey) == key.ThisColumns.Count)
						key.AssociationType = AssociationType.OneToOne;
					else
						key.AssociationType = AssociationType.ManyToOne;
				}
				else
					key.AssociationType = AssociationType.ManyToOne;

				key.CanBeNull = key.ThisColumns.All(_ => _.IsNullable);
			}
		}
	}

	foreach (var t in Tables.Values)
	{
		foreach (var key in t.ForeignKeys.Values)
		{
			var name = key.MemberName;

			if (key.BackReference != null && key.ThisColumns.Count == 1 && key.ThisColumns[0].MemberName.ToLower().EndsWith("id"))
			{
				name = key.ThisColumns[0].MemberName;
				name = name.Substring(0, name.Length - "id".Length);

				if (!t.ForeignKeys.Values.Select(_ => _.MemberName).Concat(
					 t.Columns.    Values.Select(_ => _.MemberName)).Concat(
					 new[] { t.ClassName }).Any(_ => _ == name))
				{
					name = key.MemberName;;
				}
			}
			
			if (name == key.MemberName)
			{
				if (name.StartsWith("FK_"))
					name = name.Substring(3);

				if (name.EndsWith("_BackReference"))
					name = name.Substring(0, name.Length - "_BackReference".Length);

				name = string.Join("", name.Split('_').Where(_ => _.Length > 0 && _ != t.TableName).ToArray());

				if (name.Length > 0)
					name = key.AssociationType == AssociationType.OneToMany ? PluralizeAssociationName(name) : SingularizeAssociationName(name);
			}

			if (name.Length != 0 &&
				!t.ForeignKeys.Values.Select(_ => _.MemberName).Concat(
				 t.Columns.    Values.Select(_ => _.MemberName)).Concat(
				 new[] { t.ClassName }).Any(_ => _ == name))
			{
				key.MemberName = name;
			}
		}
	}

	if (Tables.Values.SelectMany(_ => _.ForeignKeys.Values).Any(_ => _.AssociationType == AssociationType.OneToMany))
		Usings.Add("System.Collections.Generic");

	var keyWords = new HashSet<string>
	{
		"abstract", "as",       "base",     "bool",    "break",     "byte",     "case",       "catch",     "char",    "checked",
		"class",    "const",    "continue", "decimal", "default",   "delegate", "do",         "double",    "else",    "enum",
		"event",    "explicit", "extern",   "false",   "finally",   "fixed",    "float",      "for",       "foreach", "goto",
		"if",       "implicit", "in",       "int",     "interface", "internal", "is",         "lock",      "long",    "new",
		"null",     "object",   "operator", "out",     "override",  "params",   "private",    "protected", "public",  "readonly",
		"ref",      "return",   "sbyte",    "sealed",  "short",     "sizeof",   "stackalloc", "static",    "struct",  "switch",
		"this",     "throw",    "true",     "try",     "typeof",    "uint",     "ulong",      "unchecked", "unsafe",  "ushort",
		"using",    "virtual",  "volatile", "void",    "while"
	};

	foreach (var t in Tables.Values)
	{
		if (keyWords.Contains(t.ClassName))
			t.ClassName = "@" + t.ClassName;

		if (keyWords.Contains(t.DataContextPropertyName))
			t.DataContextPropertyName = "@" + t.DataContextPropertyName;

		foreach (var col in t.Columns.Values)
			if (keyWords.Contains(col.MemberName))
				col.MemberName = "@" + col.MemberName;
	}


	AfterLoadMetadata(this);
}

Func<string,string> PluralizeAssociationName   = _ => _ + "s";
Func<string,string> SingularizeAssociationName = _ => _;

Action<GeneratedTextTransformation> BeforeLoadMetadata = _ => {};
Action<GeneratedTextTransformation> AfterLoadMetadata  = _ => {};

Dictionary<string,Table> Tables = new Dictionary<string,Table>();

public partial class Table
{
	public string       Owner;
	public string       TableName;
	public string       ClassName;
	public string       DataContextPropertyName;
	public string       BaseClassName;
	public bool         IsView;
	public string		Description;
	public List<string> Attributes = new List<string>();

	public Dictionary<string,Column>     Columns     = new Dictionary<string,Column>();
	public Dictionary<string,ForeignKey> ForeignKeys = new Dictionary<string,ForeignKey>();
}

public partial class Column
{
	public int          ID;
	public string       ColumnName; // Column name in database
	public string       MemberName; // Member name of the generated class
	public bool         IsNullable;
	public bool         IsIdentity;
	public string       Type;       // Type of the generated member
	public string       ColumnType; // Type of the column in database
	public bool         IsClass;
	public DbType       DbType;
	public SqlDbType    SqlDbType;
	public long         Length;
	public int          Precision;
	public int          Scale;
	public string		Description;

	public int          PKIndex = -1;
	public List<string> Attributes = new List<string>();

	public bool IsPrimaryKey { get { return PKIndex >= 0; } }
}

public enum AssociationType
{
	Auto,
	OneToOne,
	OneToMany,
	ManyToOne,
}

public partial class ForeignKey
{
	public string       KeyName;
	public string       MemberName;
	public Table        OtherTable;
	public List<Column> ThisColumns  = new List<Column>();
	public List<Column> OtherColumns = new List<Column>();
	public List<string> Attributes   = new List<string>();
	public bool         CanBeNull    = true;
	public ForeignKey   BackReference;

	private AssociationType _associationType = AssociationType.Auto;
	public  AssociationType  AssociationType
	{
		get { return _associationType; }
		set
		{
			_associationType = value;

			if (BackReference != null)
			{
				switch (value)
				{
					case AssociationType.Auto      : BackReference.AssociationType = AssociationType.Auto;      break;
					case AssociationType.OneToOne  : BackReference.AssociationType = AssociationType.OneToOne;  break;
					case AssociationType.OneToMany : BackReference.AssociationType = AssociationType.ManyToOne; break;
					case AssociationType.ManyToOne : BackReference.AssociationType = AssociationType.OneToMany; break;
				}
			}
		}
	}
}

#>
