using System.Collections.Generic;
using System.Data;
using System.Linq;
using FluentMigrator.Model;
using FluentMigrator.Runner.Processors.SQLite;

namespace NzbDrone.Core.Datastore.Migration.Framework
{
    // Modeled after the FluentMigrator SchemaDumper class.
    // The original implementation had bad support for escaped identifiers, amongst other things.
    public class SqliteSchemaDumper
    {
        public SqliteSchemaDumper(SQLiteProcessor processor)
        {
            Processor = processor;
        }

        public SQLiteProcessor Processor { get; set; }

        protected internal virtual TableDefinition ReadTableSchema(string sqlSchema)
        {
            var reader = new SqliteSyntaxReader(sqlSchema);

            var result = ParseCreateTableStatement(reader);

            return result;
        }

        protected internal virtual IndexDefinition ReadIndexSchema(string sqlSchema)
        {
            var reader = new SqliteSyntaxReader(sqlSchema);

            var result = ParseCreateIndexStatement(reader);

            return result;
        }

        protected virtual TableDefinition ParseCreateTableStatement(SqliteSyntaxReader reader)
        {
            var table = new TableDefinition();

            while (reader.Read() != SqliteSyntaxReader.TokenType.StringToken || reader.ValueToUpper != "TABLE")
            {
            }

            if (reader.Read() == SqliteSyntaxReader.TokenType.StringToken && reader.ValueToUpper == "IF")
            {
                reader.Read(); // NOT
                reader.Read(); // EXISTS
            }
            else
            {
                reader.Rollback();
            }

            table.Name = ParseIdentifier(reader);

            // Find Column List
            reader.SkipTillToken(SqliteSyntaxReader.TokenType.ListStart);

            // Split the list.
            var list = reader.ReadList();

            foreach (var columnReader in list)
            {
                columnReader.SkipWhitespace();

                if (columnReader.Read() == SqliteSyntaxReader.TokenType.StringToken)
                {
                    if (columnReader.ValueToUpper == "PRIMARY")
                    {
                        columnReader.SkipTillToken(SqliteSyntaxReader.TokenType.ListStart);
                        if (columnReader.Read() == SqliteSyntaxReader.TokenType.Identifier)
                        {
                            var pk = table.Columns.First(v => v.Name == columnReader.Value);
                            pk.IsPrimaryKey = true;
                            pk.IsNullable = true;
                            pk.IsUnique = true;
                            if (columnReader.Buffer.ToUpperInvariant().Contains("AUTOINCREMENT"))
                            {
                                pk.IsIdentity = true;
                            }

                            continue;
                        }
                    }

                    if (columnReader.ValueToUpper == "CONSTRAINT" ||
                        columnReader.ValueToUpper == "PRIMARY" || columnReader.ValueToUpper == "UNIQUE" ||
                        columnReader.ValueToUpper == "CHECK" || columnReader.ValueToUpper == "FOREIGN")
                    {
                        continue;
                    }
                }

                columnReader.Rollback();

                var column = ParseColumnDefinition(columnReader);
                column.TableName = table.Name;
                table.Columns.Add(column);
            }

            return table;
        }

        protected virtual ColumnDefinition ParseColumnDefinition(SqliteSyntaxReader reader)
        {
            var column = new ColumnDefinition();

            column.Name = ParseIdentifier(reader);

            reader.TrimBuffer();

            reader.Read();
            if (reader.Type != SqliteSyntaxReader.TokenType.End)
            {
                column.Type = GetDbType(reader.Value);

                var upper = reader.Buffer.ToUpperInvariant();
                column.IsPrimaryKey = upper.Contains("PRIMARY KEY");
                column.IsIdentity = upper.Contains("AUTOINCREMENT");
                column.IsNullable = !upper.Contains("NOT NULL") && !upper.Contains("PRIMARY KEY");
                column.IsUnique = upper.Contains("UNIQUE") || upper.Contains("PRIMARY KEY");
            }

            return column;
        }

        protected virtual IndexDefinition ParseCreateIndexStatement(SqliteSyntaxReader reader)
        {
            var index = new IndexDefinition();

            reader.Read();

            reader.Read();
            index.IsUnique = reader.ValueToUpper == "UNIQUE";

            while (reader.ValueToUpper != "INDEX")
            {
                reader.Read();
            }

            if (reader.Read() == SqliteSyntaxReader.TokenType.StringToken && reader.ValueToUpper == "IF")
            {
                reader.Read(); // NOT
                reader.Read(); // EXISTS
            }
            else
            {
                reader.Rollback();
            }

            index.Name = ParseIdentifier(reader);

            reader.Read(); // ON

            index.TableName = ParseIdentifier(reader);

            // Find Column List
            reader.SkipTillToken(SqliteSyntaxReader.TokenType.ListStart);

            // Split the list.
            var list = reader.ReadList();

            foreach (var columnReader in list)
            {
                var column = new IndexColumnDefinition();
                column.Name = ParseIdentifier(columnReader);

                while (columnReader.Read() == SqliteSyntaxReader.TokenType.StringToken)
                {
                    if (columnReader.ValueToUpper == "COLLATE")
                    {
                        columnReader.Read(); // Skip Collation name
                    }
                    else if (columnReader.ValueToUpper == "DESC")
                    {
                        column.Direction = Direction.Descending;
                    }
                }

                index.Columns.Add(column);
            }

            return index;
        }

        protected virtual string ParseIdentifier(SqliteSyntaxReader reader)
        {
            reader.Read();

            if (reader.Type != SqliteSyntaxReader.TokenType.Identifier &&
                reader.Type != SqliteSyntaxReader.TokenType.StringToken &&
                reader.Type != SqliteSyntaxReader.TokenType.StringLiteral)
            {
                throw reader.CreateSyntaxException("Expected Identifier but found {0}", reader.Type);
            }

            return reader.Value;
        }

        public virtual IList<TableDefinition> ReadDbSchema()
        {
            var tables = ReadTables();
            foreach (var table in tables)
            {
                table.Indexes = ReadIndexes(table.SchemaName, table.Name);

                //table.ForeignKeys = ReadForeignKeys(table.SchemaName, table.Name);
            }

            return tables;
        }

        protected virtual DataSet Read(string template, params object[] args)
        {
            return Processor.Read(template, args);
        }

        protected virtual IList<TableDefinition> ReadTables()
        {
            const string sqlCommand = @"SELECT name, sql FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' AND name NOT LIKE '_litestream_%' ORDER BY name;";
            var dtTable = Read(sqlCommand).Tables[0];

            var tableDefinitionList = new List<TableDefinition>();

            foreach (DataRow dr in dtTable.Rows)
            {
                var sql = dr["sql"].ToString();
                var table = ReadTableSchema(sql);

                tableDefinitionList.Add(table);
            }

            return tableDefinitionList;
        }

        /// <summary>
        /// Get DbType from string type definition
        /// </summary>
        /// <param name="typeNum"></param>
        /// <returns></returns>
        public static DbType? GetDbType(string typeNum)
        {
            switch (typeNum.ToUpper())
            {
                case "BLOB":
                    return DbType.Binary;
                case "INTEGER":
                    return DbType.Int32;
                case "BIGINT":
                    return DbType.Int64;
                case "NUMERIC":
                    return DbType.Double;
                case "TEXT":
                    return DbType.String;
                case "DATETIME":
                    return DbType.DateTime;
                case "UNIQUEIDENTIFIER":
                    return DbType.Guid;
                case "SMALLINT":
                    return DbType.Int16;
                case "TINYINT":
                    return DbType.Boolean;
                default:
                    return null;
            }
        }

        protected virtual IList<IndexDefinition> ReadIndexes(string schemaName, string tableName)
        {
            var sqlCommand = string.Format(@"SELECT type, name, sql FROM sqlite_master WHERE tbl_name = '{0}' AND type = 'index' AND name NOT LIKE 'sqlite_auto%';", tableName);
            var table = Read(sqlCommand).Tables[0];

            IList<IndexDefinition> indexes = new List<IndexDefinition>();

            foreach (DataRow dr in table.Rows)
            {
                var sql = dr["sql"].ToString();
                var index = ReadIndexSchema(sql);
                indexes.Add(index);
            }

            return indexes;
        }
    }
}
