<#@ assembly name="System.Core" #>
<#@ assembly name="System.Data" #>
<#@ assembly name="System.Data.Entity.Design" #>
<#@ import namespace="System.Globalization" #>
<#@ import namespace="System.IO" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Text" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ import namespace="System.Data.Entity.Design.PluralizationServices" #>
<#@ output extension=".txt" #>
<# EnvDTE.DTE Dte; 
    //this relies on the nuget packages: T4EnvDte and T4MultiFile
#>
<#@ include file="MultipleOutputHelper.ttinclude" #>
<#@ include file="EnvDteHelper.ttinclude" #>
Main File Output
<#+ 

public static void Generate(ITextTemplatingEngineHost host, EnvDTE.DTE dte, StringBuilder generationEnvironment, string targetProjectName, IEnumerable<string> tables, string cString, bool doMultiFile)
{ 
    generationEnvironment.AppendLine(host.TemplateFile);
    Action<int,string> appendLine = (indentLevels, text) => generationEnvironment.AppendLine(String.Join(string.Empty,Enumerable.Repeat("    ",indentLevels)) + text);
    var useOptions = false;
    var manager = Manager.Create(host, generationEnvironment);

    var projects = RecurseSolutionProjects(dte);
    var targetProject = projects.First(p => p.Name==targetProjectName);
    var targetProjectFolder = Path.GetDirectoryName(targetProject.FullName);
    var pluralizer = PluralizationService.CreateService(new CultureInfo("en")); // https://msdn.microsoft.com/en-us/library/system.data.entity.design.pluralizationservices.pluralizationservice(v=vs.110).aspx

    generationEnvironment.AppendLine("Main file output");
    foreach(var p in projects)
    {
        generationEnvironment.AppendLine(p.Name + " " + p.FullName);
    }

    using(var cn = new System.Data.SqlClient.SqlConnection(cString))
    {
        cn.Open();
        foreach(var tableName in tables)
        {
            manager.StartNewFile(Path.Combine(targetProjectFolder,tableName + ".generated.fs"),targetProject); 
            var typeName = pluralizer.Singularize(tableName);
            var columns = new List<ColumnDescription>();
            using(var cmd= new System.Data.SqlClient.SqlCommand("sp_help " + tableName,cn))
            using(var r = cmd.ExecuteReader())
            {
                r.NextResult(); // ignore the first tables
                while(r.Read())
                {
                    // columns and info
                    var columnName = r["Column_name"].ToString();
                    var type = r["Type"].ToString();
                    // var computed = r["Computed"];
                    var length = Convert.ToInt32(r["Length"]);
                    // var prec = r["Prec"];
                    columns.Add(new ColumnDescription{ColumnName=columnName, Type=type, Length=length, Nullable = r["Nullable"].ToString() =="yes"});
                }
            }

            columns = new List<ColumnDescription>( columns.OrderBy(c => c.ColumnName));
            generationEnvironment.AppendLine ("namespace Pm.Schema.DataModels." + pluralizer.Pluralize(typeName) + " // Generated by item in namespace " + manager.DefaultProjectNamespace );

            generationEnvironment.AppendLine(string.Empty);
            generationEnvironment.AppendLine("open System");
            generationEnvironment.AppendLine("open System.ComponentModel");
            generationEnvironment.AppendLine("open System.Linq.Expressions");
            generationEnvironment.AppendLine(string.Empty);

            GenerateInterface(typeName, columns, appendLine,writeable:false, useOptions:useOptions);
            GenerateInterface(typeName, columns, appendLine,writeable:true, useOptions:useOptions);
            GenerateRecord(typeName, columns, appendLine, useOptions);
            GenerateModule(typeName, columns, appendLine, useOptions);
            GenerateClass(typeName, columns, appendLine, useOptions);

            manager.EndBlock();
        }
    }

    manager.Process(doMultiFile);
}

public static void GenerateInterface(string typeName, IEnumerable<ColumnDescription> columns, Action<int,string> appendLine, bool writeable, bool useOptions)
{
    appendLine(0, TypeComment(columns.Count()));
    appendLine(0,"type I" + typeName + (writeable ? "RW" : string.Empty) + " =");

    foreach(var cd in columns)
    {
        appendLine(1, "abstract member " + cd.ColumnName + ":" + MapSqlType(cd.Type, cd.Nullable, useOptions) + " with get" + (writeable ? ",set" : string.Empty));
    }

    appendLine(0,string.Empty);
}

public static void GenerateRecord(string typeName, IEnumerable<ColumnDescription> columns, Action<int,string> appendLine, bool useOptions)
{
    appendLine(0, TypeComment(columns.Count()));
    if (!useOptions)
    {
        appendLine(0,"[<NoComparison>]");
    }

    appendLine(0, "type " + typeName + "Record =");
    appendLine(1, "{");

    foreach(var cd in columns)
    {
        appendLine(1, ColumnComment(cd));
        appendLine(1, cd.ColumnName + ":" + MapSqlType(cd.Type,cd.Nullable,useOptions));
    }

    appendLine(1,"}");
    appendLine(1,"interface I" + typeName + " with");

    foreach(var cd in columns )
    {
        appendLine(2, ColumnComment(cd));
        appendLine(2, "member x." + cd.ColumnName + " with get () = x." + cd.ColumnName);
    }

    appendLine(1,"static member Zero () = ");
    appendLine(2,"{");

    foreach(var cd in columns )
    {
        var mapped = MapSqlType(cd.Type,cd.Nullable,useOptions);
        appendLine(2, cd.ColumnName + " = " + GetDefaultValue(mapped));
    }

    appendLine(2,"}");
    appendLine(0,string.Empty);
}

public static void GenerateModule(string typeName, IEnumerable<ColumnDescription> columns, Action<int,string> appendLine, bool useOptions)
{
    var camelType = toCamel(typeName);
    appendLine(0, "module " + typeName + "Helpers =");
    appendLine(1, "let ToRecord (i" + typeName + ":I" + typeName + ") =");
    appendLine(2, "{");

    foreach(var cd in columns )
    {
        var mapped = MapSqlType(cd.Type,cd.Nullable,useOptions);
        appendLine(2, cd.ColumnName + " = i" + typeName + "." + cd.ColumnName);
    }

    appendLine(2, "}");
    appendLine(0,string.Empty);

    appendLine(1, "let toRecord " + camelType + " =");
    appendLine(2, "{");

    foreach(var cd in columns )
    {
        var mapped = MapSqlType(cd.Type,cd.Nullable,useOptions);
        appendLine(2, cd.ColumnName + " = " + camelType + "." + cd.ColumnName);
    }

    appendLine(2, "}");
    appendLine(0,string.Empty);

    appendLine(1, "let inline toRecordStp (" + camelType + ": ^a) =");
    appendLine(2, "{");

    foreach(var cd in columns )
    {
        var mapped = MapSqlType(cd.Type,cd.Nullable,useOptions);
        appendLine(2, cd.ColumnName + " = (^a: (member " + cd.ColumnName + ": _) " + camelType + ")");
    }
    appendLine(2, "}");
    appendLine(0,string.Empty);

}

public static void GenerateClass(string typeName, IEnumerable<ColumnDescription> columns, Action<int,string> appendLine, bool useOptions)
{
    appendLine(0, TypeComment(columns.Count()));
    appendLine(0, "type "+ typeName + "N (model:" + typeName + "Record) = ");
    appendLine(0, string.Empty);
    appendLine(1, "let propertyChanged = new Event<_, _>()");
    appendLine(0, string.Empty);

    appendLine(0, string.Empty);
    foreach(var cd in columns) // https://fadsworld.wordpress.com/2011/05/18/f-quotations-for-inotifypropertychanged/
    {
        var camel = toCamel(cd.ColumnName);
        appendLine(1, "let mutable "+ camel + " = model." + cd.ColumnName);
    }

    appendLine(0, string.Empty);

    appendLine(1, "interface I" + typeName + " with");

    foreach(var cd in columns)
    {
        appendLine(2, "member x." + cd.ColumnName + " with get () = x." + cd.ColumnName);
    }

    appendLine(1, "interface I" + typeName + "RW with" );

    foreach(var cd in columns)
    {
        appendLine(2, "member x." + cd.ColumnName + " with get () = x." + cd.ColumnName + " and set v = x." + cd.ColumnName + " <- v");
    }

    appendLine(0, string.Empty);
    appendLine(1, "member x.MakeRecord () =");
    appendLine(2, "{");

    foreach(var cd in columns)
    {
        appendLine(2, cd.ColumnName + " = x." + cd.ColumnName);
    }

    appendLine(2, "}");

    appendLine(0, string.Empty);

    appendLine(1, "interface INotifyPropertyChanged with");
    appendLine(2, "[<CLIEvent>]");
    appendLine(2, "member x.PropertyChanged = propertyChanged.Publish");
    appendLine(1, "abstract member RaisePropertyChanged : string -> unit");
    appendLine(1, "default x.RaisePropertyChanged(propertyName : string) = propertyChanged.Trigger(x, PropertyChangedEventArgs(propertyName))");

    appendLine(0, string.Empty);

    foreach(var cd in columns)
    {
        var camel = toCamel(cd.ColumnName);
        appendLine(0,string.Empty);
        appendLine(1, ColumnComment(cd));
        appendLine(1, "member x."+ cd.ColumnName);
        appendLine(2, "with get() = " + camel);
        appendLine(2, "and set v = ");
        appendLine(3, camel +" <- v");
        appendLine(3, "x.RaisePropertyChanged \"" + cd.ColumnName +"\"");
    }
}

public class ColumnDescription
{
    public string ColumnName{get;set;}
    public string Type {get;set;}
    public int Length {get;set;}
    public bool Nullable{get;set;}
}

static string MapSqlType(string type, bool nullable, bool useOptions)
{
    switch (type.ToLower()){
        case "char":
        case "nchar":
        case "nvarchar":
        case "varchar": return "string";
        case "bit": return nullable ? (useOptions ? "bool option" : "bool Nullable") : "bool";
        case "date":
        case "datetime":
        case "smalldatetime": return nullable ? (useOptions ? "DateTime option" : "DateTime Nullable") : "DateTime";
        case "int": return nullable ? (useOptions ? "int option" : "int Nullable") : "int";
        case "decimal": return nullable ? (useOptions ? "decimal option": "decimal Nullable") : "decimal";
        default : return type ?? string.Empty;
    }
}

static string GetDefaultValue(string mappedType)
{
    if(mappedType.EndsWith("Nullable"))
        return "Nullable()";
    if(mappedType.EndsWith("option"))
        return "None";

    switch(mappedType.ToLower()){
        case "int": return "0";
        case "bool": return "false";
        case "decimal": return "0m";
        case "datetime": return "System.DateTime.MinValue";
        default : return "null";
    }
}

static string ColumnComment(ColumnDescription cd)
{
    return "/// " + (cd.Type ?? "null") + " (" + cd.Length + ") " + (cd.Nullable? "null" : "not null");
}

static string TypeComment(int columnCount)
{
    return "/// " + columnCount + " properties";
}

static string toCamel(string s) // https://github.com/ayoung/Newtonsoft.Json/blob/master/Newtonsoft.Json/Utilities/StringUtils.cs
{ 
    if (string.IsNullOrEmpty(s))
        return s;

    if (!char.IsUpper(s[0]))
        return s;
    string camelCase = char.ToLower(s[0], CultureInfo.InvariantCulture).ToString(CultureInfo.InvariantCulture);
    if (s.Length > 1)
        camelCase += s.Substring(1);

    return camelCase;
}
#>