using System.Collections.Generic;
using System.Linq;

namespace Ink.Parsed
{
	public class Divert : Parsed.Object
	{
		public Parsed.Path target { get; protected set; }
        public Parsed.Object targetContent { get; protected set; }
        public List<Expression> arguments { get; protected set; }
		public Runtime.Divert runtimeDivert { get; protected set; }
        public bool isFunctionCall { get; set; }
        public bool isEmpty { get; set; }
        public bool isTunnel { get; set; }
        public bool isThread { get; set; }
        public bool isEnd {
            get {
                return target != null && target.dotSeparatedComponents == "END";
            }
        }
        public bool isDone {
            get {
                return target != null && target.dotSeparatedComponents == "DONE";
            }
        }

        public Divert (Parsed.Path target, List<Expression> arguments = null)
		{
			this.target = target;
            this.arguments = arguments;

            if (arguments != null) {
                AddContent (arguments.Cast<Parsed.Object> ().ToList ());
            }
		}

        public Divert (Parsed.Object targetContent)
        {
            this.targetContent = targetContent;
        }

		public override Runtime.Object GenerateRuntimeObject ()
		{
            // End = end flow immediately
            // Done = return from thread or instruct the flow that it's safe to exit
            if (isEnd) {
                return Runtime.ControlCommand.End ();
            }
            if (isDone) {
                return Runtime.ControlCommand.Done ();
            }

            runtimeDivert = new Runtime.Divert ();

            // Normally we resolve the target content during the
            // Resolve phase, since we expect all runtime objects to
            // be available in order to find the final runtime path for
            // the destination. However, we need to resolve the target
            // (albeit without the runtime target) early so that
            // we can get information about the arguments - whether
            // they're by reference - since it affects the code we
            // generate here.
            ResolveTargetContent ();


            CheckArgumentValidity ();

            // Passing arguments to the knot
            bool requiresArgCodeGen = arguments != null && arguments.Count > 0;
            if ( requiresArgCodeGen || isFunctionCall || isTunnel || isThread ) {

                var container = new Runtime.Container ();

                // Generate code for argument evaluation
                // This argument generation is coded defensively - it should
                // attempt to generate the code for all the parameters, even if
                // they don't match the expected arguments. This is so that the
                // parameter objects themselves are generated correctly and don't
                // get into a state of attempting to resolve references etc
                // without being generated.
                if (requiresArgCodeGen) {

                    // Function calls already in an evaluation context
                    if (!isFunctionCall) {
                        container.AddContent (Runtime.ControlCommand.EvalStart());
                    }

                    List<FlowBase.Argument> targetArguments = null;
                    if( targetContent )
                        targetArguments = (targetContent as FlowBase).arguments;

                    for (var i = 0; i < arguments.Count; ++i) {
                        Expression argToPass = arguments [i];
                        FlowBase.Argument argExpected = null;
                        if( targetArguments != null && i < targetArguments.Count )
                            argExpected = targetArguments [i];

                        // Pass by reference: argument needs to be a variable reference
                        if (argExpected != null && argExpected.isByReference) {

                            var varRef = argToPass as VariableReference;
                            if (varRef == null) {
                                Error ("Expected variable name to pass by reference to 'ref " + argExpected.identifier + "' but saw " + argToPass.ToString ());
                                break;
                            }

                            // Check that we're not attempting to pass a read count by reference
                            var targetPath = new Path(varRef.pathIdentifiers);
                            Parsed.Object targetForCount = targetPath.ResolveFromContext (this);
                            if (targetForCount != null) {
                                Error ("can't pass a read count by reference. '" + targetPath.dotSeparatedComponents+"' is a knot/stitch/label, but '"+target.dotSeparatedComponents+"' requires the name of a VAR to be passed.");
                                break;
                            }

                            var varPointer = new Runtime.VariablePointerValue (varRef.name);
                            container.AddContent (varPointer);
                        }

                        // Normal value being passed: evaluate it as normal
                        else {
                            argToPass.GenerateIntoContainer (container);
                        }
                    }

                    // Function calls were already in an evaluation context
                    if (!isFunctionCall) {
                        container.AddContent (Runtime.ControlCommand.EvalEnd());
                    }
                }


                // Starting a thread? A bit like a push to the call stack below... but not.
                // It sort of puts the call stack on a thread stack (argh!) - forks the full flow.
                if (isThread) {
                    container.AddContent(Runtime.ControlCommand.StartThread());
                }

                // If this divert is a function call, tunnel, we push to the call stack
                // so we can return again
                else if (isFunctionCall || isTunnel) {
                    runtimeDivert.pushesToStack = true;
                    runtimeDivert.stackPushType = isFunctionCall ? Runtime.PushPopType.Function : Runtime.PushPopType.Tunnel;
                }

                // Jump into the "function" (knot/stitch)
                container.AddContent (runtimeDivert);

                return container;
            }

            // Simple divert
            else {
                return runtimeDivert;
            }
		}


        // When the divert is to a target that's actually a variable name
        // rather than an explicit knot/stitch name, try interpretting it
        // as such by getting the variable name.
        public string PathAsVariableName()
        {
            return target.firstComponent;
        }


        void ResolveTargetContent()
        {
            if (isEmpty || isEnd) {
                return;
            }

            if (targetContent == null) {

                // Is target of this divert a variable name that will be de-referenced
                // at runtime? If so, there won't be any further reference resolution
                // we can do at this point.
                var variableTargetName = PathAsVariableName ();
                if (variableTargetName != null) {
                    var flowBaseScope = ClosestFlowBase ();
                    var resolveResult = flowBaseScope.ResolveVariableWithName (variableTargetName, fromNode: this);
                    if (resolveResult.found) {

                        // Make sure that the flow was typed correctly, given that we know that this
                        // is meant to be a divert target
                        if (resolveResult.isArgument) {
                            var argument = resolveResult.ownerFlow.arguments.Where (a => a.identifier.name == variableTargetName).First();
                            if ( !argument.isDivertTarget ) {
                                Error ("Since '" + argument.identifier + "' is used as a variable divert target (on "+this.debugMetadata+"), it should be marked as: -> " + argument.identifier, resolveResult.ownerFlow);
                            }
                        }

                        runtimeDivert.variableDivertName = variableTargetName;
                        return;

                    }
                }

                targetContent = target.ResolveFromContext (this);
            }
        }

        public override void ResolveReferences(Story context)
		{
            if (isEmpty || isEnd || isDone) {
                return;
            }

            if (targetContent) {
                runtimeDivert.targetPath = targetContent.runtimePath;
            }

            // Resolve children (the arguments)
            base.ResolveReferences (context);

            // May be null if it's a built in function (e.g. TURNS_SINCE)
            // or if it's a variable target.
            var targetFlow = targetContent as FlowBase;
            if (targetFlow) {
                if (!targetFlow.isFunction && this.isFunctionCall) {
                    base.Error (targetFlow.identifier + " hasn't been marked as a function, but it's being called as one. Do you need to delcare the knot as '== function " + targetFlow.identifier + " =='?");
                } else if (targetFlow.isFunction && !this.isFunctionCall && !(this.parent is DivertTarget)) {
                    base.Error (targetFlow.identifier + " can't be diverted to. It can only be called as a function since it's been marked as such: '" + targetFlow.identifier + "(...)'");
                }
            }

            // Check validity of target content
            bool targetWasFound = targetContent != null;
            bool isBuiltIn = false;
            bool isExternal = false;

            if (target.numberOfComponents == 1 ) {

                // BuiltIn means TURNS_SINCE, CHOICE_COUNT, RANDOM or SEED_RANDOM
                isBuiltIn = FunctionCall.IsBuiltIn (target.firstComponent);

                // Client-bound function?
                isExternal = context.IsExternal (target.firstComponent);

                if (isBuiltIn || isExternal) {
                    if (!isFunctionCall) {
                        base.Error (target.firstComponent + " must be called as a function: ~ " + target.firstComponent + "()");
                    }
                    if (isExternal) {
                        runtimeDivert.isExternal = true;
                        if( arguments != null )
                            runtimeDivert.externalArgs = arguments.Count;
                        runtimeDivert.pushesToStack = false;
                        runtimeDivert.targetPath = new Runtime.Path (this.target.firstComponent);
                        CheckExternalArgumentValidity (context);
                    }
                    return;
                }
            }

            // Variable target?
            if (runtimeDivert.variableDivertName != null) {
                return;
            }

            if( !targetWasFound && !isBuiltIn && !isExternal )
                Error ("target not found: '" + target + "'");
		}

        // Returns false if there's an error
        void CheckArgumentValidity()
        {
            if (isEmpty)
                return;

            // Argument passing: Check for errors in number of arguments
            var numArgs = 0;
            if (arguments != null && arguments.Count > 0)
                numArgs = arguments.Count;

            // Missing content?
            // Can't check arguments properly. It'll be due to some
            // other error though, so although there's a problem and
            // we report false, we don't need to report a specific error.
            // It may also be because it's a valid call to an external
            // function, that we check at the resolve stage.
            if (targetContent == null) {
                return;
            }

            FlowBase targetFlow = targetContent as FlowBase;

            // No error, crikey!
            if (numArgs == 0 && (targetFlow == null || !targetFlow.hasParameters)) {
                return;
            }

            if (targetFlow == null && numArgs > 0) {
                Error ("target needs to be a knot or stitch in order to pass arguments");
                return;
            }

            if (targetFlow.arguments == null && numArgs > 0) {
                Error ("target (" + targetFlow.name + ") doesn't take parameters");
                return;
            }

            if( this.parent is DivertTarget ) {
                if (numArgs > 0)
                    Error ("can't store arguments in a divert target variable");
                return;
            }

            var paramCount = targetFlow.arguments.Count;
            if (paramCount != numArgs) {

                string butClause;
                if (numArgs == 0) {
                    butClause = "but there weren't any passed to it";
                } else if (numArgs < paramCount) {
                    butClause = "but only got " + numArgs;
                } else {
                    butClause = "but got " + numArgs;
                }
                Error ("to '" + targetFlow.identifier + "' requires " + paramCount + " arguments, "+butClause);
                return;
            }

            // Light type-checking for divert target arguments
            for (int i = 0; i < paramCount; ++i) {
                FlowBase.Argument flowArg = targetFlow.arguments [i];
                Parsed.Expression divArgExpr = arguments [i];

                // Expecting a divert target as an argument, let's do some basic type checking
                if (flowArg.isDivertTarget) {

                    // Not passing a divert target or any kind of variable reference?
                    var varRef = divArgExpr as VariableReference;
                    if (!(divArgExpr is DivertTarget) && varRef == null ) {
                        Error ("Target '" + targetFlow.identifier + "' expects a divert target for the parameter named -> " + flowArg.identifier + " but saw " + divArgExpr, divArgExpr);
                    }

                    // Passing 'a' instead of '-> a'?
                    // i.e. read count instead of divert target
                    else if (varRef != null) {

                        // Unfortunately have to manually resolve here since we're still in code gen
                        var knotCountPath = new Path(varRef.pathIdentifiers);
                        Parsed.Object targetForCount = knotCountPath.ResolveFromContext (varRef);
                        if (targetForCount != null) {
                            Error ("Passing read count of '" + knotCountPath.dotSeparatedComponents + "' instead of a divert target. You probably meant '" + knotCountPath + "'");
                        }
                    }
                }
            }

            if (targetFlow == null) {
                Error ("Can't call as a function or with arguments unless it's a knot or stitch");
                return;
            }

            return;
        }

        void CheckExternalArgumentValidity(Story context)
        {
            string externalName = target.firstComponent;
            ExternalDeclaration external = null;
            var found = context.externals.TryGetValue(externalName, out external);
            System.Diagnostics.Debug.Assert (found, "external not found");

            int externalArgCount = external.argumentNames.Count;
            int ownArgCount = 0;
            if (arguments != null) {
                ownArgCount = arguments.Count;
            }

            if (ownArgCount != externalArgCount) {
                Error ("incorrect number of arguments sent to external function '" + externalName + "'. Expected " + externalArgCount + " but got " + ownArgCount);
            }
        }

        public override void Error (string message, Object source = null, bool isWarning = false)
        {
            // Could be getting an error from a nested Divert
            if (source != this && source) {
                base.Error (message, source);
                return;
            }

            if (isFunctionCall) {
                base.Error ("Function call " + message, source, isWarning);
            } else {
                base.Error ("Divert " + message, source, isWarning);
            }
        }

        public override string ToString ()
        {
            if (target != null)
                return target.ToString ();
            else
                return "-> <empty divert>";
        }

	}
}

