// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the Apache 2.0 License.
// See the LICENSE file in the project root for more information.
//
// Copyright (c) Jeff Hardy 2010-2012.
//

using System;
using System.Collections;
using System.Diagnostics;
using Community.CsharpSqlite;
using IronPython.Runtime;
using IronPython.Runtime.Operations;
using IronPython.Runtime.Types;

using sqlite3_stmt = Community.CsharpSqlite.Sqlite3.Vdbe;

namespace IronPython.SQLite
{
    internal enum StatementType
    {
        Unknown,
        Select,
        Insert,
        Update,
        Delete,
        Replace,
        Other
    }

    [DebuggerDisplay("{sql}")]
    internal class Statement
    {
        private readonly Guid uniqueid;
        private Sqlite3.sqlite3 db;
        internal sqlite3_stmt st;

        private object current = null, nextRow = null;
        private bool started = false;
        private string sql;
        private bool bound = false;
        internal bool in_use = false;

        public string Tail { get; private set; }

        public Statement(PythonSQLite.Connection connection, string operation)
        {
            this.uniqueid = Guid.NewGuid();

            this.db = connection.db;
            this.sql = operation;

            this.st = null;
            string tail = null;
            if(Sqlite3.sqlite3_prepare(this.db, this.sql, -1, ref this.st, ref tail) != Sqlite3.SQLITE_OK /*TODO: || too much sql */)
            {
                Sqlite3.sqlite3_finalize(st);
                this.st = null;
                throw PythonSQLite.GetSqliteError(this.db, null);
            }

            this.Tail = tail;
        }

        private Statement(Sqlite3.sqlite3 db, sqlite3_stmt stmt, string operation, string tail)
        {
            this.uniqueid = Guid.NewGuid();

            this.db = db;
            this.sql = operation;

            this.st = stmt;
            this.Tail = tail;
        }

        ~Statement()
        {
            if(this.st != null)
            {
                Sqlite3.sqlite3_finalize(this.st);
            }

            this.st = null;
        }

        private StatementType _type = StatementType.Unknown;
        public StatementType StatementType
        {
            get
            {
                if(this._type != StatementType.Unknown)
                    return _type;

                string s = this.sql.TrimStart();

                if(s.StartsWith("select", StringComparison.InvariantCultureIgnoreCase))
                    this._type = StatementType.Select;
                else if(s.StartsWith("insert", StringComparison.InvariantCultureIgnoreCase))
                    this._type = StatementType.Insert;
                else if(s.StartsWith("update", StringComparison.InvariantCultureIgnoreCase))
                    this._type = StatementType.Update;
                else if(s.StartsWith("delete", StringComparison.InvariantCultureIgnoreCase))
                    this._type = StatementType.Delete;
                else if(s.StartsWith("replace", StringComparison.InvariantCultureIgnoreCase))
                    this._type = StatementType.Replace;
                else
                    this._type = StatementType.Other;

                return this._type;
            }
        }

        public void BindParameters(CodeContext context, object parameters)
        {
            if(bound)
                this.ClearParameters();

            int num_params_needed = Sqlite3.sqlite3_bind_parameter_count(this.st);

            if(parameters == null)
            {
                if(num_params_needed > 0)
                    throw PythonSQLite.MakeProgrammingError("parameters are required but not specified.");
                else
                    return;
            }

            if(parameters is IDictionary)
                BindParameters(context, (IDictionary)parameters, num_params_needed);
            else if(parameters is IList)
                BindParameters(context, (IList)parameters, num_params_needed);
            else
                throw PythonSQLite.MakeProgrammingError("unknown parameter type");

            bound = true;
        }

        private void BindParameters(CodeContext context, IDictionary args, int num_params_needed)
        {
            for(int i = 1; i <= num_params_needed; ++i)
            {
                string binding_name = Sqlite3.sqlite3_bind_parameter_name(this.st, i);
                if(string.IsNullOrEmpty(binding_name))
                    throw PythonSQLite.MakeProgrammingError("Binding {0} has no name, but you supplied a dictionary (which has only names).".Format(i));

                // remove the leading colon
                binding_name = binding_name.Substring(1);

                if(args.Contains(binding_name))
                    BindParameter(context, i, maybeAdapt(context, args[binding_name]));
                else
                    throw PythonSQLite.MakeProgrammingError("You did not supply a value for binding {0}.".Format(i));
            }
        }

        private void BindParameters(CodeContext context, IList args, int num_params_needed)
        {
            if(num_params_needed != args.Count)
                throw PythonSQLite.MakeProgrammingError("Incorrect number of bindings supplied.");

            for(int i = 0; i < args.Count; ++i)
            {
                BindParameter(context, i + 1, maybeAdapt(context, args[i]));
            }
        }

        private void BindParameter(CodeContext context, int index, object arg)
        {
            int rc;
            if(arg == null)
                rc = Sqlite3.sqlite3_bind_null(st, index);
            else if(arg is int)
                rc = Sqlite3.sqlite3_bind_int(st, index, (int)arg);
            else if(arg is bool)
                rc = Sqlite3.sqlite3_bind_int(st, index, (bool)arg ? 1 : 0);
            else if(arg is long)
                rc = Sqlite3.sqlite3_bind_int64(st, index, (long)arg);
            else if (arg is System.Numerics.BigInteger)
                rc = Sqlite3.sqlite3_bind_int64(st, index, (long)((System.Numerics.BigInteger)arg));
            else if(arg is float)
                rc = Sqlite3.sqlite3_bind_double(st, index, (float)arg);
            else if(arg is double)
                rc = Sqlite3.sqlite3_bind_double(st, index, (double)arg);
            else if(arg is string)
                rc = Sqlite3.sqlite3_bind_text(st, index, (string)arg, -1, Sqlite3.SQLITE_TRANSIENT);
            else if(arg is byte[])
                rc = Sqlite3.sqlite3_bind_blob(this.st, index, (byte[])arg, -1, Sqlite3.SQLITE_TRANSIENT);
            else
                throw PythonSQLite.MakeInterfaceError("Unable to bind parameter {0} - unsupported type {1}".Format(index, arg.GetType()));

            if(rc != Sqlite3.SQLITE_OK)
                throw PythonSQLite.MakeInterfaceError("Unable to bind parameter {0}: {1}".Format(index, Sqlite3.sqlite3_errmsg(db)));
        }

        private object maybeAdapt(CodeContext context, object value)
        {
            return needsAdaptation(context, value) ? adaptValue(context, value) : value;
        }

        private bool needsAdaptation(CodeContext context, object value)
        {
            // TODO The check for primitive types could probably be cached like pysqlite does
            if(value == null ||
                value is int ||
                value is bool ||
                value is long ||
                value is System.Numerics.BigInteger ||
                value is float ||
                value is double ||
                value is string ||
                value is byte[])
            {
                object proto = DynamicHelpers.GetPythonTypeFromType(typeof(PythonSQLite.PrepareProtocol));
                object type = DynamicHelpers.GetPythonType(value);

                object key = new PythonTuple(new[] { type, proto });

                return PythonSQLite.adapters.ContainsKey(key);
            }
            else
            {
                return true;
            }
        }

        private object adaptValue(CodeContext context, object value)
        {
            object proto = DynamicHelpers.GetPythonTypeFromType(typeof(PythonSQLite.PrepareProtocol));
            object type = DynamicHelpers.GetPythonType(value);

            object key = new PythonTuple(new[] { type, proto });

            object adapter;
            if(PythonSQLite.adapters.TryGetValue(key, out adapter))
            {
                object adapted = PythonCalls.Call(context, adapter, value);
                return adapted;
            }

            // TODO: Use proto? Any value whatsoever?

            object conform;
            if(context.LanguageContext.Operations.TryGetMember(value, "__conform__", out conform))
            {
                object adapted = PythonCalls.Call(context, conform, proto);
                if(adapted != null)
                {
                    return adapted;
                }
            }

            return value;
        }

        public int RawStep()
        {
            return Util.Step(st);
        }

        public int SqliteFinalize()
        {
            int rc = Sqlite3.SQLITE_OK;

            if(this.st != null)
            {
                rc = Sqlite3.sqlite3_finalize(this.st);
                this.st = null;
            }

            this.in_use = false;

            return rc;
        }

        public int Reset()
        {
            int rc = Sqlite3.SQLITE_OK;

            if(this.in_use && this.st != null)
            {
                rc = Sqlite3.sqlite3_reset(this.st);

                if(rc == Sqlite3.SQLITE_OK)
                    this.in_use = false;
            }

            return rc;
        }

        private void ClearParameters()
        {
            if(Sqlite3.sqlite3_clear_bindings(this.st) != Sqlite3.SQLITE_OK)
                throw PythonSQLite.GetSqliteError(this.db, null);
        }

        internal void MarkDirty()
        {
            this.in_use = true;
        }

        internal int Recompile(CodeContext context, object parameters)
        {
            sqlite3_stmt new_st = null;
            string tail = null;

            int rc = Sqlite3.sqlite3_prepare(this.db, this.sql, -1, ref new_st, ref tail);
            if(rc == Sqlite3.SQLITE_OK)
            {
                Statement new_stmt = new Statement(this.st.db, new_st, this.sql, tail);
                new_stmt.BindParameters(context, parameters);

                Sqlite3.sqlite3_finalize(this.st);
                this.st = new_st;
            }

            return rc;
        }
    }
}

