#if MYSQL_6_10

// Copyright ?2004, 2016 Oracle and/or its affiliates. All rights reserved.
//
// MySQL Connector/NET is licensed under the terms of the GPLv2
// <http://www.gnu.org/licenses/old-licenses/gpl-2.0.html>, like most 
// MySQL Connectors. There are special exceptions to the terms and 
// conditions of the GPLv2 as it is applied to this software, see the 
// FLOSS License Exception
// <http://www.mysql.com/about/legal/licensing/foss-exception.html>.
//
// This program is free software; you can redistribute it and/or modify 
// it under the terms of the GNU General Public License as published 
// by the Free Software Foundation; version 2 of the License.
//
// This program is distributed in the hope that it will be useful, but 
// WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 
// or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 
// for more details.
//
// You should have received a copy of the GNU General Public License along 
// with this program; if not, write to the Free Software Foundation, Inc., 
// 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA

using System;
using System.Data;
using System.Globalization;
using System.Linq;
using System.Text;
using Externals.MySql.Data.Common;
using Externals.MySql.Data.Types;

namespace Externals.MySql.Data.MySqlClient
{
    /// <summary>
    /// Summary description for StoredProcedure.
    /// </summary>
    internal class StoredProcedure : PreparableStatement
    {
        private string _outSelect;

        // Prefix used for to generate inout or output parameters names
        internal const string ParameterPrefix = "_cnet_param_";
        private string resolvedCommandText;

        public StoredProcedure(MySqlCommand cmd, string text)
          : base(cmd, text)
        {
        }

        private MySqlParameter GetReturnParameter()
        {
            return Parameters?.Cast<MySqlParameter>().FirstOrDefault(p => p.Direction == ParameterDirection.ReturnValue);
        }

        public bool ServerProvidingOutputParameters { get; private set; }

        public override string ResolvedCommandText
        {
            get { return resolvedCommandText; }
        }

        internal string GetCacheKey(string spName)
        {
            string retValue = String.Empty;
            StringBuilder key = new StringBuilder(spName);
            key.Append("(");
            string delimiter = "";
            foreach (MySqlParameter p in command.Parameters)
            {
                if (p.Direction == ParameterDirection.ReturnValue)
                    retValue = "?=";
                else
                {
                    key.AppendFormat(CultureInfo.InvariantCulture, "{0}?", delimiter);
                    delimiter = ",";
                }
            }
            key.Append(")");
            return retValue + key.ToString();
        }

        private ProcedureCacheEntry GetParameters(string procName)
        {
            string procCacheKey = GetCacheKey(procName);
            ProcedureCacheEntry entry = Connection.ProcedureCache.GetProcedure(Connection, procName, procCacheKey);
            return entry;
        }

        public static string GetFlags(string dtd)
        {
            int x = dtd.Length - 1;
            while (x > 0 && (Char.IsLetterOrDigit(dtd[x]) || dtd[x] == ' '))
                x--;
            string dtdSubstring = dtd.Substring(x);
            return StringUtility.ToUpperInvariant(dtdSubstring);
        }

        private string FixProcedureName(string name)
        {
            string[] parts = name.Split('.');
            for (int i = 0; i < parts.Length; i++)
                if (!parts[i].StartsWith("`", StringComparison.Ordinal))
                    parts[i] = String.Format("`{0}`", parts[i]);
            if (parts.Length == 1) return parts[0];
            return String.Format("{0}.{1}", parts[0], parts[1]);
        }

        private MySqlParameter GetAndFixParameter(string spName, MySqlSchemaRow param, bool realAsFloat, MySqlParameter returnParameter)
        {
            string mode = (string)param["PARAMETER_MODE"];
            string pName = (string)param["PARAMETER_NAME"];

            if (param["ORDINAL_POSITION"].Equals(0))
            {
                if (returnParameter == null)
                    throw new InvalidOperationException(
                        String.Format(Resources.RoutineRequiresReturnParameter, spName));
                pName = returnParameter.ParameterName;
            }

            // make sure the parameters given to us have an appropriate type set if it's not already
            MySqlParameter p = command.Parameters.GetParameterFlexible(pName, true);
            if (!p.TypeHasBeenSet)
            {
                string datatype = (string)param["DATA_TYPE"];
                bool unsigned = GetFlags(param["DTD_IDENTIFIER"].ToString()).IndexOf("UNSIGNED") != -1;
                p.MySqlDbType = MetaData.NameToType(datatype, unsigned, realAsFloat, Connection);
            }
            return p;
        }

        private MySqlParameterCollection CheckParameters(string spName)
        {
            MySqlParameterCollection newParms = new MySqlParameterCollection(command);
            MySqlParameter returnParameter = GetReturnParameter();

            ProcedureCacheEntry entry = GetParameters(spName);
            if (entry.procedure == null || entry.procedure.Rows.Count == 0)
                throw new InvalidOperationException(String.Format(Resources.RoutineNotFound, spName));

            bool realAsFloat = entry.procedure.Rows[0]["SQL_MODE"].ToString().IndexOf("REAL_AS_FLOAT") != -1;

            foreach (MySqlSchemaRow param in entry.parameters.Rows)
                newParms.Add(GetAndFixParameter(spName, param, realAsFloat, returnParameter));
            return newParms;
        }

        public override void Resolve(bool preparing)
        {
            // check to see if we are already resolved
            if (ResolvedCommandText != null) return;

            ServerProvidingOutputParameters = Driver.SupportsOutputParameters && preparing;

            // first retrieve the procedure definition from our
            // procedure cache
            string spName = commandText;
            if (spName.IndexOf(".") == -1 && !String.IsNullOrEmpty(Connection.Database))
                spName = Connection.Database + "." + spName;
            spName = FixProcedureName(spName);

            MySqlParameter returnParameter = GetReturnParameter();

            MySqlParameterCollection parms = command.Connection.Settings.CheckParameters ?
                CheckParameters(spName) : Parameters;

            string setSql = SetUserVariables(parms, preparing);
            string callSql = CreateCallStatement(spName, returnParameter, parms);
            string outSql = CreateOutputSelect(parms, preparing);
            resolvedCommandText = String.Format("{0}{1}{2}", setSql, callSql, outSql);
        }

        private string SetUserVariables(MySqlParameterCollection parms, bool preparing)
        {
            StringBuilder setSql = new StringBuilder();

            if (ServerProvidingOutputParameters) return setSql.ToString();

            string delimiter = String.Empty;
            foreach (MySqlParameter p in parms)
            {
                if (p.Direction != ParameterDirection.InputOutput) continue;

                string pName = "@" + p.BaseName;
                string uName = "@" + ParameterPrefix + p.BaseName;
                string sql = String.Format("SET {0}={1}", uName, pName);

                if (command.Connection.Settings.AllowBatch && !preparing)
                {
                    setSql.AppendFormat(CultureInfo.InvariantCulture, "{0}{1}", delimiter, sql);
                    delimiter = "; ";
                }
                else
                {
                    MySqlCommand cmd = new MySqlCommand(sql, command.Connection);
                    cmd.Parameters.Add(p);
                    cmd.ExecuteNonQuery();
                }
            }
            if (setSql.Length > 0)
                setSql.Append("; ");
            return setSql.ToString();
        }

        private string CreateCallStatement(string spName, MySqlParameter returnParameter, MySqlParameterCollection parms)
        {
            StringBuilder callSql = new StringBuilder();

            string delimiter = String.Empty;
            foreach (MySqlParameter p in parms)
            {
                if (p.Direction == ParameterDirection.ReturnValue) continue;

                string pName = "@" + p.BaseName;
                string uName = "@" + ParameterPrefix + p.BaseName;

                bool useRealVar = p.Direction == ParameterDirection.Input || ServerProvidingOutputParameters;
                callSql.AppendFormat(CultureInfo.InvariantCulture, "{0}{1}", delimiter, useRealVar ? pName : uName);
                delimiter = ", ";
            }

            if (returnParameter == null)
                return String.Format("CALL {0}({1})", spName, callSql.ToString());
            else
                return String.Format("SET @{0}{1}={2}({3})", ParameterPrefix, returnParameter.BaseName, spName, callSql.ToString());
        }

        private string CreateOutputSelect(MySqlParameterCollection parms, bool preparing)
        {
            StringBuilder outSql = new StringBuilder();

            string delimiter = String.Empty;
            foreach (MySqlParameter p in parms)
            {
                if (p.Direction == ParameterDirection.Input) continue;
                if ((p.Direction == ParameterDirection.InputOutput ||
                    p.Direction == ParameterDirection.Output) &&
                    ServerProvidingOutputParameters) continue;
                string pName = "@" + p.BaseName;
                string uName = "@" + ParameterPrefix + p.BaseName;

                outSql.AppendFormat(CultureInfo.InvariantCulture, "{0}{1}", delimiter, uName);
                delimiter = ", ";
            }

            if (outSql.Length == 0) return String.Empty;

            if (command.Connection.Settings.AllowBatch && !preparing)
                return String.Format(";SELECT {0}", outSql.ToString());

            _outSelect = String.Format("SELECT {0}", outSql.ToString());
            return String.Empty;
        }

        internal void ProcessOutputParameters(MySqlDataReader reader)
        {
            // We apparently need to always adjust our output types since the server
            // provided data types are not always right
            AdjustOutputTypes(reader);

            if ((reader.CommandBehavior & CommandBehavior.SchemaOnly) != 0)
                return;

            // now read the output parameters data row
            reader.Read();

            string prefix = "@" + StoredProcedure.ParameterPrefix;

            for (int i = 0; i < reader.FieldCount; i++)
            {
                string fieldName = reader.GetName(i);
                if (fieldName.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
                    fieldName = fieldName.Remove(0, prefix.Length);
                MySqlParameter parameter = command.Parameters.GetParameterFlexible(fieldName, true);
                parameter.Value = reader.GetValue(i);
            }
        }

        private void AdjustOutputTypes(MySqlDataReader reader)
        {
            // since MySQL likes to return user variables as strings
            // we reset the types of the readers internal value objects
            // this will allow those value objects to parse the string based
            // return values
            for (int i = 0; i < reader.FieldCount; i++)
            {
                string fieldName = reader.GetName(i);
                if (fieldName.IndexOf(StoredProcedure.ParameterPrefix) != -1)
                    fieldName = fieldName.Remove(0, StoredProcedure.ParameterPrefix.Length + 1);
                MySqlParameter parameter = command.Parameters.GetParameterFlexible(fieldName, true);

                IMySqlValue v = MySqlField.GetIMySqlValue(parameter.MySqlDbType);
                if (v is MySqlBit)
                {
                    MySqlBit bit = (MySqlBit)v;
                    bit.ReadAsString = true;
                    reader.ResultSet.SetValueObject(i, bit);
                }
                else
                    reader.ResultSet.SetValueObject(i, v);
            }
        }

        public override void Close(MySqlDataReader reader)
        {
            base.Close(reader);
            if (String.IsNullOrEmpty(_outSelect)) return;
            if ((reader.CommandBehavior & CommandBehavior.SchemaOnly) != 0) return;

            MySqlCommand cmd = new MySqlCommand(_outSelect, command.Connection);
            using (MySqlDataReader rdr = cmd.ExecuteReader(reader.CommandBehavior))
            {
                ProcessOutputParameters(rdr);
            }
        }
    }
}

#endif