/********************************************************
 * ADO.NET 2.0 Data Provider for SQLite Version 3.X
 * Written by Robert Simpson (robert@blackcastlesoft.com)
 * 
 * Released to the public domain, use at your own risk!
 ********************************************************/

#if USE_ENTITY_FRAMEWORK_6
namespace System.Data.SQLite.EF6
#else
namespace System.Data.SQLite.Linq
#endif
{
  using System;
  using System.Collections.Generic;
  using System.Data.Common;
  using System.Diagnostics;
  using System.Globalization;
  using System.Text;

#if USE_ENTITY_FRAMEWORK_6
  using System.Data.Entity.Core.Common;
  using System.Data.Entity.Core.Metadata.Edm;
  using System.Data.Entity.Core.Common.CommandTrees;
#else
  using System.Data.Metadata.Edm;
  using System.Data.Common.CommandTrees;
#endif

  internal sealed class SQLiteProviderServices : DbProviderServices, ISQLiteSchemaExtensions
  {
    internal static readonly SQLiteProviderServices Instance = new SQLiteProviderServices();

    protected override DbCommandDefinition CreateDbCommandDefinition(DbProviderManifest manifest, DbCommandTree commandTree)
    {
      DbCommand prototype = CreateCommand(manifest, commandTree);
      DbCommandDefinition result = this.CreateCommandDefinition(prototype);
      return result;
    }

    private DbCommand CreateCommand(DbProviderManifest manifest, DbCommandTree commandTree)
    {
      if (manifest == null)
        throw new ArgumentNullException("manifest");

      if (commandTree == null)
        throw new ArgumentNullException("commandTree");

      SQLiteCommand command = new SQLiteCommand();
      try
      {
        List<DbParameter> parameters;
        CommandType commandType;

        command.CommandText = SqlGenerator.GenerateSql((SQLiteProviderManifest)manifest, commandTree, out parameters, out commandType);
        command.CommandType = commandType;

        // Get the function (if any) implemented by the command tree since this influences our interpretation of parameters
        EdmFunction function = null;
        if (commandTree is DbFunctionCommandTree)
        {
          function = ((DbFunctionCommandTree)commandTree).EdmFunction;
        }

        // Now make sure we populate the command's parameters from the CQT's parameters:
        foreach (KeyValuePair<string, TypeUsage> queryParameter in commandTree.Parameters)
        {
          SQLiteParameter parameter;

          // Use the corresponding function parameter TypeUsage where available (currently, the SSDL facets and 
          // type trump user-defined facets and type in the EntityCommand).
          FunctionParameter functionParameter;
          if (null != function && function.Parameters.TryGetValue(queryParameter.Key, false, out functionParameter))
          {
            parameter = CreateSqlParameter((SQLiteProviderManifest)manifest, functionParameter.Name, functionParameter.TypeUsage, functionParameter.Mode, DBNull.Value);
          }
          else
          {
            parameter = CreateSqlParameter((SQLiteProviderManifest)manifest, queryParameter.Key, queryParameter.Value, ParameterMode.In, DBNull.Value);
          }

          command.Parameters.Add(parameter);
        }

        // Now add parameters added as part of SQL gen (note: this feature is only safe for DML SQL gen which
        // does not support user parameters, where there is no risk of name collision)
        if (null != parameters && 0 < parameters.Count)
        {
          if (!(commandTree is DbInsertCommandTree) &&
            !(commandTree is DbUpdateCommandTree) &&
            !(commandTree is DbDeleteCommandTree))
          {
            throw new InvalidOperationException("SqlGenParametersNotPermitted");
          }

          foreach (DbParameter parameter in parameters)
          {
            command.Parameters.Add(parameter);
          }
        }

        return command;
      }
      catch
      {
        command.Dispose();
        throw;
      }
    }

    protected override string GetDbProviderManifestToken(DbConnection connection)
    {
        if (connection == null)
            throw new ArgumentNullException("connection");

        if (String.IsNullOrEmpty(connection.ConnectionString))
            throw new ArgumentNullException("ConnectionString");

        return connection.ConnectionString;
    }

    protected override DbProviderManifest GetDbProviderManifest(string versionHint)
    {
      return new SQLiteProviderManifest(versionHint);
    }

    /// <summary>
    /// Creates a SQLiteParameter given a name, type, and direction
    /// </summary>
    internal static SQLiteParameter CreateSqlParameter(SQLiteProviderManifest manifest, string name, TypeUsage type, ParameterMode mode, object value)
    {
      int? size;

      //
      // NOTE: Adjust the parameter type so that it will work with textual
      //       GUIDs.  Please see ticket [a4d9c7ee94] for more details.
      //
      if ((manifest != null) && !manifest._binaryGuid &&
          (MetadataHelpers.GetPrimitiveTypeKind(type) == PrimitiveTypeKind.Guid))
      {
          type = TypeUsage.CreateStringTypeUsage(
              PrimitiveType.GetEdmPrimitiveType(PrimitiveTypeKind.String),
              false, true);
      }

      SQLiteParameter result = new SQLiteParameter(name, value);

      // .Direction
      ParameterDirection direction = MetadataHelpers.ParameterModeToParameterDirection(mode);
      if (result.Direction != direction)
      {
        result.Direction = direction;
      }

      // .Size and .DbType
      // output parameters are handled differently (we need to ensure there is space for return
      // values where the user has not given a specific Size/MaxLength)
      bool isOutParam = mode != ParameterMode.In;
      DbType sqlDbType = GetSqlDbType(type, isOutParam, out size);
      if (result.DbType != sqlDbType)
      {
        result.DbType = sqlDbType;
      }

      // Note that we overwrite 'facet' parameters where either the value is different or
      // there is an output parameter.
      if (size.HasValue && (isOutParam || result.Size != size.Value))
      {
        result.Size = size.Value;
      }

      // .IsNullable
      bool isNullable = MetadataHelpers.IsNullable(type);
      if (isOutParam || isNullable != result.IsNullable)
      {
        result.IsNullable = isNullable;
      }

      return result;
    }


    /// <summary>
    /// Determines DbType for the given primitive type. Extracts facet
    /// information as well.
    /// </summary>
    private static DbType GetSqlDbType(TypeUsage type, bool isOutParam, out int? size)
    {
      // only supported for primitive type
      PrimitiveTypeKind primitiveTypeKind = MetadataHelpers.GetPrimitiveTypeKind(type);

      size = default(int?);

      switch (primitiveTypeKind)
      {
        case PrimitiveTypeKind.Binary:
          // for output parameters, ensure there is space...
          size = GetParameterSize(type, isOutParam);
          return GetBinaryDbType(type);

        case PrimitiveTypeKind.Boolean:
          return DbType.Boolean;

        case PrimitiveTypeKind.Byte:
          return DbType.Byte;

        case PrimitiveTypeKind.Time:
          return DbType.Time;

        case PrimitiveTypeKind.DateTimeOffset:
          return DbType.DateTimeOffset;

        case PrimitiveTypeKind.DateTime:
          return DbType.DateTime;

        case PrimitiveTypeKind.Decimal:
          return DbType.Decimal;

        case PrimitiveTypeKind.Double:
          return DbType.Double;

        case PrimitiveTypeKind.Guid:
          return DbType.Guid;

        case PrimitiveTypeKind.Int16:
          return DbType.Int16;

        case PrimitiveTypeKind.Int32:
          return DbType.Int32;

        case PrimitiveTypeKind.Int64:
          return DbType.Int64;

        case PrimitiveTypeKind.SByte:
          return DbType.SByte;

        case PrimitiveTypeKind.Single:
          return DbType.Single;

        case PrimitiveTypeKind.String:
          size = GetParameterSize(type, isOutParam);
          return GetStringDbType(type);

        default:
          Debug.Fail("unknown PrimitiveTypeKind " + primitiveTypeKind);
          return DbType.Object;
      }
    }

    /// <summary>
    /// Determines preferred value for SqlParameter.Size. Returns null
    /// where there is no preference.
    /// </summary>
    private static int? GetParameterSize(TypeUsage type, bool isOutParam)
    {
      int maxLength;
      if (MetadataHelpers.TryGetMaxLength(type, out maxLength))
      {
        // if the MaxLength facet has a specific value use it
        return maxLength;
      }
      else if (isOutParam)
      {
        // if the parameter is a return/out/inout parameter, ensure there 
        // is space for any value
        return int.MaxValue;
      }
      else
      {
        // no value
        return default(int?);
      }
    }

    /// <summary>
    /// Chooses the appropriate DbType for the given string type.
    /// </summary>
    private static DbType GetStringDbType(TypeUsage type)
    {
      Debug.Assert(type.EdmType.BuiltInTypeKind == BuiltInTypeKind.PrimitiveType &&
        PrimitiveTypeKind.String == ((PrimitiveType)type.EdmType).PrimitiveTypeKind, "only valid for string type");

      DbType dbType;

      // Specific type depends on whether the string is a unicode string and whether it is a fixed length string.
      // By default, assume widest type (unicode) and most common type (variable length)
      bool unicode;
      bool fixedLength;
      if (!MetadataHelpers.TryGetIsFixedLength(type, out fixedLength))
      {
        fixedLength = false;
      }

      if (!MetadataHelpers.TryGetIsUnicode(type, out unicode))
      {
        unicode = true;
      }

      if (fixedLength)
      {
        dbType = (unicode ? DbType.StringFixedLength : DbType.AnsiStringFixedLength);
      }
      else
      {
        dbType = (unicode ? DbType.String : DbType.AnsiString);
      }
      return dbType;
    }

    /// <summary>
    /// Chooses the appropriate DbType for the given binary type.
    /// </summary>
    private static DbType GetBinaryDbType(TypeUsage type)
    {
      Debug.Assert(type.EdmType.BuiltInTypeKind == BuiltInTypeKind.PrimitiveType &&
        PrimitiveTypeKind.Binary == ((PrimitiveType)type.EdmType).PrimitiveTypeKind, "only valid for binary type");

      // Specific type depends on whether the binary value is fixed length. By default, assume variable length.
      bool fixedLength;
      if (!MetadataHelpers.TryGetIsFixedLength(type, out fixedLength))
      {
        fixedLength = false;
      }

      return DbType.Binary;
      //            return fixedLength ? DbType.Binary : DbType.VarBinary;
    }

    #region ISQLiteSchemaExtensions Members

    /// <summary>
    /// Creates temporary tables on the connection so schema information can be queried
    /// </summary>
    /// <remarks>
    /// There's a lot of work involved in getting schema information out of SQLite, but LINQ expects to
    /// be able to query on schema tables.  Therefore we need to "fake" it by generating temporary tables
    /// filled with the schema of the current connection.  We get away with making this information static
    /// because schema information seems to always be queried on a new connection object, so the schema is
    /// always fresh.
    /// </remarks>
    /// <param name="cnn">The connection upon which to build the schema tables.</param>
    void ISQLiteSchemaExtensions.BuildTempSchema(SQLiteConnection cnn)
    {
      string[] arr = new string[] { "TABLES", "COLUMNS", "VIEWS", "VIEWCOLUMNS", "INDEXES", "INDEXCOLUMNS", "FOREIGNKEYS", "CATALOGS" };

      using (DataTable table = cnn.GetSchema("Tables", new string[] { "temp", null, String.Format("SCHEMA{0}", arr[0]) }))
      {
        if (table.Rows.Count > 0) return;
      }

      for (int n = 0; n < arr.Length; n++)
      {
        using (DataTable table = cnn.GetSchema(arr[n]))
        {
          DataTableToTable(cnn, table, String.Format("SCHEMA{0}", arr[n]));
        }
      }

      using (SQLiteCommand cmd = cnn.CreateCommand())
      {
        cmd.CommandText = Properties.Resources.SQL_CONSTRAINTS;
        cmd.ExecuteNonQuery();

        cmd.CommandText = Properties.Resources.SQL_CONSTRAINTCOLUMNS;
        cmd.ExecuteNonQuery();
      }
    }

    /// <summary>
    /// Turn a datatable into a table in the temporary database for the connection
    /// </summary>
    /// <param name="cnn">The connection to make the temporary table in</param>
    /// <param name="table">The table to write out</param>
    /// <param name="dest">The temporary table name to write to</param>
    private void DataTableToTable(SQLiteConnection cnn, DataTable table, string dest)
    {
      StringBuilder sql = new StringBuilder();
      SQLiteCommandBuilder builder = new SQLiteCommandBuilder();

      using (SQLiteCommand cmd = cnn.CreateCommand())
      using (DataTable source = new DataTable())
      {
        sql.AppendFormat(CultureInfo.InvariantCulture, "CREATE TEMP TABLE {0} (", builder.QuoteIdentifier(dest));
        string separator = String.Empty;
        SQLiteConnectionFlags flags = cnn.Flags;
        foreach (DataColumn dc in table.Columns)
        {
          DbType dbtypeName = SQLiteConvert.TypeToDbType(dc.DataType);
          string typeName = SQLiteConvert.DbTypeToTypeName(cnn, dbtypeName, flags);

          sql.AppendFormat(CultureInfo.InvariantCulture, "{2}{0} {1} COLLATE NOCASE", builder.QuoteIdentifier(dc.ColumnName), typeName, separator);
          separator = ", ";
        }
        sql.Append(")");

        cmd.CommandText = sql.ToString();
        cmd.ExecuteNonQuery();

        cmd.CommandText = String.Format("SELECT * FROM TEMP.{0} WHERE 1=2", builder.QuoteIdentifier(dest));
        using (SQLiteDataAdapter adp = new SQLiteDataAdapter(cmd))
        {
          builder.DataAdapter = adp;

          adp.Fill(source);

          foreach (DataRow row in table.Rows)
          {
            object[] arr = row.ItemArray;

            source.Rows.Add(arr);
          }
          adp.Update(source);
        }
      }
    }

    #endregion
  }
}

