// GdbBacktrace.cs
//
// Authors: Lluis Sanchez Gual <lluis@novell.com>
//          Jeffrey Stedfast <jeff@xamarin.com>
//
// Copyright (c) 2008 Novell, Inc (http://www.novell.com)
// Copyright (c) 2012 Xamarin Inc. (http://www.xamarin.com)
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

using Mono.Debugging.Backend;
using Mono.Debugging.Client;
using System;
using System.Collections.Generic;
using System.Globalization;

namespace AvalonStudio.Debugging.GDB
{
    internal class GdbBacktrace : IBacktrace, IObjectValueSource
    {
        private int fcount;
        private StackFrame firstFrame;
        private GdbSession session;
        private DissassemblyBuffer[] disBuffers;
        private int currentFrame = -1;
        private long threadId;

        public GdbBacktrace(GdbSession session, long threadId, int count, ResultData firstFrame)
        {
            this.session = session;
            fcount = count;
            this.threadId = threadId;

            if (firstFrame != null)
                this.firstFrame = CreateFrame(firstFrame);
        }

        public int FrameCount
        {
            get
            {
                return fcount;
            }
        }

        public StackFrame[] GetStackFrames(int firstIndex, int lastIndex)
        {
            List<StackFrame> frames = new List<StackFrame>();
            if (firstIndex == 0 && firstFrame != null)
            {
                frames.Add(firstFrame);
                firstIndex++;
            }

            if (lastIndex >= fcount)
                lastIndex = fcount - 1;

            if (firstIndex > lastIndex)
                return frames.ToArray();

            session.SelectThread(threadId);

            GdbCommandResult res = session.RunCommand("-stack-list-frames", firstIndex.ToString(), lastIndex.ToString());

            if (res.Status == CommandStatus.Done)
            {
                ResultData stack = res.GetObject("stack");
                for (int n = 0; n < stack.Count; n++)
                {
                    ResultData frd = stack.GetObject(n);
                    frames.Add(CreateFrame(frd.GetObject("frame")));
                }
            }

            return frames.ToArray();
        }

        public ObjectValue[] GetLocalVariables(int frameIndex, EvaluationOptions options)
        {
            List<ObjectValue> values = new List<ObjectValue>();
            SelectFrame(frameIndex);

            GdbCommandResult res = session.RunCommand("-stack-list-locals", "0");
            if (res.Status == CommandStatus.Done)
            {
                foreach (ResultData data in res.GetObject("locals"))
                    values.Add(CreateVarObject(data.GetValue("name")));
            }

            return values.ToArray();
        }

        public ObjectValue[] GetParameters(int frameIndex, EvaluationOptions options)
        {
            List<ObjectValue> values = new List<ObjectValue>();
            SelectFrame(frameIndex);
            GdbCommandResult res = session.RunCommand("-stack-list-arguments", "0", frameIndex.ToString(), frameIndex.ToString());

            if (res.Status == CommandStatus.Done)
            {
                foreach (ResultData data in res.GetObject("stack-args").GetObject(0).GetObject("frame").GetObject("args"))
                    values.Add(CreateVarObject(data.GetValue("name")));
            }

            return values.ToArray();
        }

        public ObjectValue GetThisReference(int frameIndex, EvaluationOptions options)
        {
            return null;
        }

        public ObjectValue[] GetAllLocals(int frameIndex, EvaluationOptions options)
        {
            List<ObjectValue> locals = new List<ObjectValue>();
            locals.AddRange(GetParameters(frameIndex, options));
            locals.AddRange(GetLocalVariables(frameIndex, options));
            return locals.ToArray();
        }

        public ObjectValue[] GetExpressionValues(int frameIndex, string[] expressions, EvaluationOptions options)
        {
            List<ObjectValue> values = new List<ObjectValue>();
            SelectFrame(frameIndex);
            foreach (string exp in expressions)
                values.Add(CreateVarObject(exp));
            return values.ToArray();
        }

        public ExceptionInfo GetException(int frameIndex, EvaluationOptions options)
        {
            return null;
        }

        public ValidationResult ValidateExpression(int frameIndex, string expression, EvaluationOptions options)
        {
            return new ValidationResult(true, null);
        }

        public CompletionData GetExpressionCompletionData(int frameIndex, string exp)
        {
            SelectFrame(frameIndex);

            bool pointer = exp.EndsWith("->");
            int i;

            if (pointer || exp.EndsWith("."))
            {
                exp = exp.Substring(0, exp.Length - (pointer ? 2 : 1));
                i = 0;
                while (i < exp.Length)
                {
                    ObjectValue val = CreateVarObject(exp);
                    if (!val.IsUnknown && !val.IsError)
                    {
                        CompletionData data = new CompletionData();
                        foreach (ObjectValue cv in val.GetAllChildren())
                            data.Items.Add(new CompletionItem(cv.Name, cv.Flags));
                        data.ExpressionLength = 0;
                        return data;
                    }
                    i++;
                }
                return null;
            }

            i = exp.Length - 1;
            bool lastWastLetter = false;
            while (i >= 0)
            {
                char c = exp[i--];
                if (!char.IsLetterOrDigit(c) && c != '_')
                    break;
                lastWastLetter = !char.IsDigit(c);
            }

            if (lastWastLetter)
            {
                string partialWord = exp.Substring(i + 1);

                CompletionData cdata = new CompletionData();
                cdata.ExpressionLength = partialWord.Length;

                // Local variables
                GdbCommandResult res = session.RunCommand("-stack-list-locals", "0");
                foreach (ResultData data in res.GetObject("locals"))
                {
                    string name = data.GetValue("name");
                    if (name.StartsWith(partialWord))
                        cdata.Items.Add(new CompletionItem(name, ObjectValueFlags.Variable));
                }

                // Parameters
                res = session.RunCommand("-stack-list-arguments", "0", frameIndex.ToString(), frameIndex.ToString());
                foreach (ResultData data in res.GetObject("stack-args").GetObject(0).GetObject("frame").GetObject("args"))
                {
                    string name = data.GetValue("name");
                    if (name.StartsWith(partialWord))
                        cdata.Items.Add(new CompletionItem(name, ObjectValueFlags.Parameter));
                }

                if (cdata.Items.Count > 0)
                    return cdata;
            }
            return null;
        }

        private ObjectValue CreateVarObject(string exp)
        {
            try
            {
                session.SelectThread(threadId);
                exp = exp.Replace("\"", "\\\"");
                GdbCommandResult res = session.RunCommand("-var-create", "-", "*", "\"" + exp + "\"");

                if (res.Status == CommandStatus.Done)
                {
                    string vname = res.GetValue("name");
                    var result = CreateObjectValue(exp, res);

                    session.RegisterTempVariableObject(vname, result);

                    return result;
                }
                else
                {
                    return ObjectValue.CreateUnknown(exp);
                }
            }
            catch
            {
                return ObjectValue.CreateUnknown(exp);
            }
        }

        private ObjectValue CreateObjectValue(string name, ResultData data)
        {
            string vname = data.GetValue("name");
            string typeName = data.GetValue("type");
            string value = data.GetValue("value");
            int nchild = data.GetInt("numchild");

            ObjectValue val;
            ObjectValueFlags flags = ObjectValueFlags.Variable;

            // There can be 'public' et al children for C++ structures
            if (typeName == null)
                typeName = "none";

            if (typeName.EndsWith("]"))
            {
                val = ObjectValue.CreateArray(this, new ObjectPath(vname), typeName, nchild, flags, null);
            }
            else if (value == "{...}" || typeName.EndsWith("*") || nchild > 0)
            {
                val = ObjectValue.CreateObject(this, new ObjectPath(vname), typeName, value, flags, null);
            }
            else
            {
                val = ObjectValue.CreatePrimitive(this, new ObjectPath(vname), typeName, new EvaluationResult(value), flags);
            }
            val.Name = name;
            return val;
        }

        public ObjectValue[] GetChildren(ObjectPath path, int index, int count, EvaluationOptions options)
        {
            List<ObjectValue> children = new List<ObjectValue>();
            session.SelectThread(threadId);
            GdbCommandResult res = session.RunCommand("-var-list-children", "2", path.Join("."));
            ResultData cdata = res.GetObject("children");

            // The response may not contain the "children" list at all.
            if (cdata == null)
                return children.ToArray();

            if (index == -1)
            {
                index = 0;
                count = cdata.Count;
            }

            for (int n = index; n < cdata.Count && n < index + count; n++)
            {
                ResultData data = cdata.GetObject(n);
                ResultData child = data.GetObject("child");

                string name = child.GetValue("exp");
                if (name.Length > 0 && char.IsNumber(name[0]))
                    name = "[" + name + "]";

                // C++ structures may contain typeless children named
                // "public", "private" and "protected".
                if (child.GetValue("type") == null)
                {
                    ObjectPath childPath = new ObjectPath(child.GetValue("name").Split('.'));
                    ObjectValue[] subchildren = GetChildren(childPath, -1, -1, options);
                    children.AddRange(subchildren);
                }
                else
                {
                    ObjectValue val = CreateObjectValue(name, child);
                    children.Add(val);
                }
            }
            return children.ToArray();
        }

        public EvaluationResult SetValue(ObjectPath path, string value, EvaluationOptions options)
        {
            session.SelectThread(threadId);
            session.RunCommand("-var-assign", path.Join("."), value);
            return new EvaluationResult(value);
        }

        public ObjectValue GetValue(ObjectPath path, EvaluationOptions options)
        {
            throw new NotSupportedException();
        }

        private void SelectFrame(int frame)
        {
            session.SelectThread(threadId);
            if (frame != currentFrame)
            {
                session.RunCommand("-stack-select-frame", frame.ToString());
                currentFrame = frame;
            }
        }

        private StackFrame CreateFrame(ResultData frameData)
        {
            string lang = "Native";
            string func = frameData.GetValue("func");
            string sadr = frameData.GetValue("addr");

            if (func == "??" && session.IsMonoProcess)
            {
                // Try to get the managed func name
                try
                {
                    ResultData data = session.RunCommand("-data-evaluate-expression", "mono_pmip(" + sadr + ")");
                    string val = data.GetValue("value");
                    if (val != null)
                    {
                        int i = val.IndexOf('"');
                        if (i != -1)
                        {
                            func = val.Substring(i).Trim('"', ' ');
                            lang = "Mono";
                        }
                    }
                }
                catch
                {
                }
            }

            int line = -1;
            string sline = frameData.GetValue("line");
            if (sline != null)
                line = int.Parse(sline);

            string sfile = frameData.GetValue("fullname");
            if (sfile == null)
                sfile = frameData.GetValue("file");
            if (sfile == null)
                sfile = frameData.GetValue("from");
            var loc = new SourceLocation(func ?? "?", sfile, line, -1, -1, -1, null);

            long addr;
            if (!string.IsNullOrEmpty(sadr))
                addr = long.Parse(sadr.Substring(2), NumberStyles.HexNumber);
            else
                addr = 0;

            return new StackFrame(addr, loc, lang);
        }

        public AssemblyLine[] Disassemble(int frameIndex, int firstLine, int count)
        {
            SelectFrame(frameIndex);
            if (disBuffers == null)
                disBuffers = new DissassemblyBuffer[fcount];

            DissassemblyBuffer buffer = disBuffers[frameIndex];
            if (buffer == null)
            {
                ResultData data = session.RunCommand("-stack-info-frame");
                long addr = long.Parse(data.GetObject("frame").GetValue("addr").Substring(2), NumberStyles.HexNumber);
                buffer = new GdbDissassemblyBuffer(session, addr);
                disBuffers[frameIndex] = buffer;
            }

            return buffer.GetLines(firstLine, firstLine + count - 1);
        }

        public object GetRawValue(ObjectPath path, EvaluationOptions options)
        {
            return null;
        }

        public void SetRawValue(ObjectPath path, object value, EvaluationOptions options)
        {
        }
    }
}