/*
 * Apply.cs --
 *
 * Copyright (c) 2007-2012 by Joe Mistachkin.  All rights reserved.
 *
 * See the file "license.terms" for information on usage and redistribution of
 * this file, and for a DISCLAIMER OF ALL WARRANTIES.
 *
 * RCS: @(#) $Id: $
 */

using System;
using Eagle._Attributes;
using Eagle._Components.Private;
using Eagle._Components.Public;
using Eagle._Constants;
using Eagle._Containers.Public;
using Eagle._Interfaces.Public;

namespace Eagle._Commands
{
    [ObjectId("2bf60b1f-86bd-4271-952a-847b72b613c4")]
    [CommandFlags(CommandFlags.Safe | CommandFlags.Standard)]
    [ObjectGroup("procedure")]
    internal sealed class Apply : Core
    {
        #region Private Static Methods
        private string NextName(
            Interpreter interpreter
            )
        {
            //
            // NOTE: Create and return a per-interpreter unique name.
            //
            return StringList.MakeList(
                this.Name, GlobalState.NextId(interpreter));
        }
        #endregion

        ///////////////////////////////////////////////////////////////////////////////////////////////

        public Apply(
            ICommandData commandData
            )
            : base(commandData)
        {
            // do nothing.
        }

        ///////////////////////////////////////////////////////////////////////////////////////////////

        #region IExecute Members
        public override ReturnCode Execute(
            Interpreter interpreter,
            IClientData clientData,
            ArgumentList arguments,
            ref Result result
            )
        {
            ReturnCode code = ReturnCode.Ok;

            if (interpreter != null)
            {
                if (arguments != null)
                {
                    if (arguments.Count >= 2)
                    {
                        //
                        // NOTE: lambdaExpr must be a two element list {args body} or a three element
                        //       list {args body namespace}.
                        //
                        StringList lambdaExpr = null;

                        code = Parser.SplitList(
                            interpreter, arguments[1], 0,
                            Length.Invalid, true, ref lambdaExpr,
                            ref result);

                        if (code == ReturnCode.Ok)
                        {
                            if ((lambdaExpr.Count == 2) || (lambdaExpr.Count == 3))
                            {
                                //
                                // NOTE: Parse the arguments into a list and make sure there are enough
                                //       supplied to satisfy the request.
                                //
                                StringList list = null;

                                code = Parser.SplitList(
                                    interpreter, lambdaExpr[0], 0,
                                    Length.Invalid, true, ref list,
                                    ref result);

                                if (code == ReturnCode.Ok)
                                {
                                    StringPairList list2 = new StringPairList();

                                    for (int argumentIndex = 0; argumentIndex < list.Count; argumentIndex++)
                                    {
                                        StringList list3 = null;

                                        code = Parser.SplitList(
                                            interpreter, list[argumentIndex], 0,
                                            Length.Invalid, true, ref list3,
                                            ref result);

                                        if (code != ReturnCode.Ok)
                                            break;

                                        if (list3.Count > 2)
                                        {
                                            result = String.Format(
                                                "too many fields in argument specifier \"{0}\"",
                                                list[argumentIndex]);

                                            code = ReturnCode.Error;
                                            break;
                                        }
                                        else if ((list3.Count == 0) || String.IsNullOrEmpty(list3[0]))
                                        {
                                            result = "argument with no name";
                                            code = ReturnCode.Error;
                                            break;
                                        }
                                        else if (!Parser.IsSimpleScalarVariableName(list3[0],
                                                String.Format(Interpreter.ArgumentNotSimpleError, list3[0]),
                                                String.Format(Interpreter.ArgumentNotScalarError, list3[0]), ref result))
                                        {
                                            code = ReturnCode.Error;
                                            break;
                                        }

                                        string argName = list3[0];
                                        string argDefault = (list3.Count >= 2) ? list3[1] : null;

                                        list2.Add(new StringPair(argName, argDefault));
                                    }

                                    if (code == ReturnCode.Ok)
                                    {
                                        //
                                        // NOTE: We *MUST* have the formal arguments in an actual ArgumentList
                                        //       container now.  The variadic and optional argument semantics
                                        //       depend on it.
                                        //
                                        ArgumentList formalArguments = new ArgumentList(
                                            list2, ArgumentFlags.NameOnly);

                                        //
                                        // NOTE: Compare lambda argument count with the total outer argument
                                        //       count minus the "apply" and "lambdaExpr" arguments.
                                        //
                                        bool hasArgs = formalArguments.IsVariadic(true);
                                        int totalArgs = hasArgs ? formalArguments.Count - 1 : formalArguments.Count;
                                        int optionalArgs = formalArguments.GetOptionalCount();

                                        if ((((arguments.Count - 2) >= (totalArgs - optionalArgs)) &&
                                             ((arguments.Count - 2) <= totalArgs)) ||
                                            (hasArgs && ((arguments.Count - 2) >= (totalArgs - optionalArgs))))
                                        {
                                            string name = NextName(interpreter);

                                            ICallFrame frame = interpreter.NewProcedureCallFrame(
                                                name, CallFrameFlags.Procedure | CallFrameFlags.Lambda,
                                                this, arguments);

                                            VariableDictionary variables = frame.Variables;

                                            frame.ProcedureArguments = new ArgumentList(arguments[0]);

                                            for (int argumentIndex = 0; argumentIndex < formalArguments.Count; argumentIndex++)
                                            {
                                                string varName = formalArguments[argumentIndex].Name;

                                                if (!variables.ContainsKey(varName))
                                                {
                                                    ArgumentFlags flags = ArgumentFlags.None;
                                                    string varValue;

                                                    if (hasArgs && (argumentIndex == (formalArguments.Count - 1)))
                                                    {
                                                        //
                                                        // NOTE: This argument is part of an argument list.
                                                        //
                                                        flags |= ArgumentFlags.ArgumentList;

                                                        //
                                                        // NOTE: Build the list for the final formal argument value,
                                                        //       which consists of all the remaining argument values.
                                                        //
                                                        StringList args = new StringList();

                                                        for (int argsArgumentIndex = argumentIndex + 2;
                                                            argsArgumentIndex < arguments.Count; argsArgumentIndex++)
                                                        {
                                                            //
                                                            // NOTE: Sync up argument names/flags for debugging (below).
                                                            //
                                                            arguments[argsArgumentIndex].Name = varName + Characters.Space + args.Count;
                                                            arguments[argsArgumentIndex].Flags |= flags;

                                                            args.Add(arguments[argsArgumentIndex]);
                                                        }

                                                        varValue = args.ToString();
                                                    }
                                                    else
                                                    {
                                                        if ((argumentIndex + 2) < arguments.Count)
                                                        {
                                                            //
                                                            // NOTE: Sync up argument name for debugging (below).
                                                            //
                                                            arguments[argumentIndex + 2].Name = varName;

                                                            //
                                                            // NOTE: Use the value supplied by the caller.
                                                            //
                                                            varValue = arguments[argumentIndex + 2];
                                                        }
                                                        else
                                                        {
                                                            //
                                                            // NOTE: We cannot sync up the argument name here
                                                            //       because we are out-of-bounds on that list
                                                            //       and it cannot be extended (i.e. it would
                                                            //       break [info level]); therefore, we punt
                                                            //       on that for now.  Use the default value
                                                            //       for this argument, if any; otherwise, use
                                                            //       an empty string.
                                                            //
                                                            object @default = formalArguments[argumentIndex].Default;

                                                            varValue = (@default != null) ?
                                                                @default.ToString() : Argument.NoValue;
                                                        }
                                                    }

                                                    code = interpreter.SetVariableValue2(VariableFlags.Argument, frame,
                                                        varName, null, varValue, null, ref result);

                                                    if (code != ReturnCode.Ok)
                                                        break;

                                                    //
                                                    // BUGFIX: Now, also keep track of this argument in the procedure
                                                    //         arguments list.  Primarily because we do not want to
                                                    //         have to redo this logic later (i.e. for [scope]).
                                                    //
                                                    frame.ProcedureArguments.Add(new Argument(flags, varName, varValue));
                                                }
                                            }

                                            //
                                            // NOTE: Make sure we succeeded in creating the call frame.
                                            //
                                            if (code == ReturnCode.Ok)
                                            {
                                                ICallFrame savedFrame = null;
                                                interpreter.PushProcedureCallFrame(frame, true, ref savedFrame);

#if DEBUGGER && DEBUGGER_EXECUTE
                                                if (DebuggerOps.CanHitBreakpoints(EngineFlags.None, interpreter))
                                                    code = interpreter.CheckBreakpoints(
                                                        code, BreakpointType.BeforeLambdaBody, this.Name, null, null,
                                                        this, null, clientData, arguments, ref result);
#endif

                                                if (code == ReturnCode.Ok)
                                                {
                                                    interpreter.ReturnCode = ReturnCode.Ok;

                                                    code = interpreter.EvaluateScript(lambdaExpr[1], arguments[1], ref result);

#if DEBUGGER && DEBUGGER_EXECUTE
                                                    if (DebuggerOps.CanHitBreakpoints(EngineFlags.None, interpreter))
                                                        code = interpreter.CheckBreakpoints(
                                                            code, BreakpointType.AfterLambdaBody, this.Name, null, null,
                                                            this, null, clientData, arguments, ref result);
#endif

                                                    //
                                                    // BUGFIX: If an opaque object handle is being returned, add
                                                    //         a reference to it now.
                                                    //
                                                    if ((code == ReturnCode.Ok) || (code == ReturnCode.Return))
                                                        code = interpreter.AddObjectReference(code, result, ref result);

                                                    if (code == ReturnCode.Return)
                                                        code = Engine.UpdateReturnInformation(interpreter);
                                                    else if (code == ReturnCode.Error)
                                                        Engine.AddErrorInformation(interpreter, result,
                                                            String.Format("{0}    (lambda term \"{1}\" line {2})",
                                                                Environment.NewLine, FormatOps.Ellipsis(arguments[1]),
                                                                Interpreter.GetErrorLine(interpreter)));
                                                }

                                                /* IGNORED */
                                                interpreter.PopProcedureCallFrame(frame, ref savedFrame);
                                            }
                                        }
                                        else
                                        {
                                            result = String.Format(
                                                "wrong # args: should be \"apply lambdaExpr {0}\"",
                                                formalArguments.ToRawString(ToStringFlags.Decorated,
                                                    Characters.Space.ToString()));

                                            code = ReturnCode.Error;
                                        }
                                    }
                                }
                            }
                            else
                            {
                                result =  String.Format(
                                    "can't interpret \"{0}\" as a lambda expression",
                                    arguments[1]);

                                code = ReturnCode.Error;
                            }
                        }
                    }
                    else
                    {
                        result = "wrong # args: should be \"apply lambdaExpr ?arg1 arg2 ...?\"";
                        code = ReturnCode.Error;
                    }
                }
                else
                {
                    result = "invalid argument list";
                    code = ReturnCode.Error;
                }
            }
            else
            {
                result = "invalid interpreter";
                code = ReturnCode.Error;
            }

            return code;
        }
        #endregion
    }
}
