//
// Copyright (C) 2009 the Open Toolkit (http://www.opentk.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 System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using System.Xml.Linq;
using System.Diagnostics;

namespace CHeaderToXML
{
    // Todo: Array parameters are copied as-is, e.g.: int foo[4] -> <param type="int" name="foo[4]" />.
    // This should become <param type="int*" name="foo" count="4" />.
    // Todo: Fails to parse ES extension headers, which mix enum and function definitions.

    // Parses ES and CL header files.
    sealed class ESCLParser
    {
        Regex extensions = new Regex("(ARB|EXT|AMD|NV|OES|QCOM)", RegexOptions.RightToLeft | RegexOptions.Compiled);
        Regex array_size = new Regex(@"\[.+\]", RegexOptions.RightToLeft | RegexOptions.Compiled);
        Regex EnumToken  = new Regex(@"^#define \w+\s+\(?-?\w+\s?<?<?\s?-?\w*\)?$", RegexOptions.Compiled);

        public ESCLParser()
        {
        }

        public string Prefix {get; set;}
        public string Version {get; set;}

        public IEnumerable<XElement> Parse(string filename)
        {
            return Parse(File.ReadAllLines(filename));
        }

        public IEnumerable<XElement> Parse(string[] lines)
        {
            char[] splitters = new char[] { ' ', '\t', ',', '(', ')', ';', '\n', '\r' };

            // Line splitter
            Func<string, string[]> split = line =>
                line.Split(splitters, StringSplitOptions.RemoveEmptyEntries);

            // Adds new enum to the accumulator (acc)
            Func<string, List<XElement>, List<XElement>> enum_name = (line, acc) =>
            {
                Func<string, string[]> get_tokens = (_) =>
                    line.Trim("/*. ".ToCharArray()).Split(" _-+".ToCharArray(), StringSplitOptions.RemoveEmptyEntries).Select(t =>
                    {
                        switch (t.ToLower())
                        {
                            case ("bitfield"): return "Flags";
                            default:
                                if (t.ToLower() == Prefix)
                                    return "";
                                else
                                    return t;
                        }
                        /* gmcs bug 336258 */ return "";
                    }).ToArray();

                Func<string[], string> get_name = tokens =>
                {
                    // Some comments do not indicate enums. Cull them!
                    if (tokens[0].StartsWith("$"))
                        return null;
                    
                    // Some names consist of more than one tokens. Concatenate them.
                    return tokens.Aggregate(
                        "",
                        (string n, string token) =>
                        {
                            n += String.IsNullOrEmpty(token) ? "" : Char.ToUpper(token[0]).ToString() + token.Substring(1);
                            return n;
                        },
                        n => n);
                };

                Func<string, string> translate_name = name =>
                {
                    if (String.IsNullOrEmpty(name))
                        return name;

                    // Patch some names that are known to be problematic
                    if (name.EndsWith("FlagsFlags"))
                        name = name.Replace("FlagsFlags", "Flags");

                    switch (name)
                    {
                        case "OpenGLEScoreversions":
                        case "EGLVersioning":
                        case "OpenCLVersion": return "Version";
                        case "ShaderPrecision-SpecifiedTypes": return "ShaderPrecision";
                        case "Texturecombine+dot3": return "TextureCombine";
                        case "MacroNamesAndCorrespondingValuesDefinedByOpenCL": return null;
                        default: return name;
                    }
                };

                Func<string, List<XElement>> add_enum = @enum =>
                {
                    switch (@enum)
                    {
                        case null:
                        case "":
                            return acc;
                        default:
                            acc.Add(new XElement("enum", new XAttribute("name", @enum)));
                            return acc;
                    }
                };

                return add_enum(translate_name(get_name(get_tokens(line))));
            };

            // Adds new token to last enum in accumulator
            Func<string, List<XElement>, List<XElement>> enum_token = (line, acc) =>
            {
                if (EnumToken.IsMatch(line))
                {
                    if (acc.Count == 0 || acc.Last().Name.LocalName != "enum")
                        acc.Add(new XElement("enum", new XAttribute("name", "Unknown")));

                    var tokens = split(line);
                    // Some constants are defined bitshifts, e.g. (1 << 2). If a constant contains parentheses
                    // we assume it is a bitshift. Otherwise, we assume it is single value, separated by space
                    // (e.g. 0xdeadbeef).
                    if (line.Contains("("))
                        tokens[2] = "(" + line.Split('(')[1];

                    // Check whether this is an include guard (e.g. #define __OPENCL_CL_H)
                    if (tokens[1].StartsWith("__"))
                        return acc;

                    // Check whether this is a known header define like WIN32_LEAN_AND_MEAN
                    switch (tokens[1])
                    {
                        case "WIN32_LEAN_AND_MEAN":
                        case "APIENTRY":
                        case "GLAPI":
                            return acc;
                    }

                    acc[acc.Count - 1].Add(new XElement("token",
                        new XAttribute("name", tokens[1].Substring(Prefix.Length + 1)),   // remove prefix
                        new XAttribute("value", tokens[2])));
                }
                return acc;
            };

            // Parses a function declaration
            var function_string = ""; // Used to concatenate functions that are split in different lines. (e.g. "void\nclFoo(int /* a */,\nint b);")
            Func<string, List<XElement>, List<XElement>> function = (line, acc) =>
            {
                if (!line.EndsWith(";"))
                {
                    function_string += line + " ";
                    return acc;
                }

                line = function_string + line;
                function_string = "";

                Func<string, string> GetExtension = name =>
                {
                    var match = extensions.Match(name);
                    return match != null && String.IsNullOrEmpty(match.Value) ? "Core" : match.Value;
                };

                var words = line.Split(splitters, StringSplitOptions.RemoveEmptyEntries);
                //var words = line.Replace("/*", "").Replace("*/", "").Split(" ()".ToCharArray(), StringSplitOptions.RemoveEmptyEntries);
                // ES does not start methods with 'extern', while CL does.
                // Remove the 'extern' keyword to create a single code-path.
                if (words[0] == "extern")
                    words = words.Skip(1).ToArray();

                string rettype = null;
                string funcname = null;
                GetFunctionNameAndType(words, out funcname, out rettype);

                var paramaters_string = Regex.Match(line, @"\(.*\)").Captures[0].Value.TrimStart('(').TrimEnd(')');

                // This regex matches function parameters.
                // The first part matches functions pointers in the following format:
                // '[return type] (*[function pointer name])([parameter list]) [parameter name]
                // where [parameter name] may or may not be in comments.
                // The second part (before the '|') matches parameters of the following formats:
                // '[return type] [parameter name]', '[return type] * [parameter name]', 'const [return type]* [parameter name]'
                // where [parameter name] can either be inside comments (/* ... */) or not.
                var get_param = new Regex(@"(\w+\s\(\*\w+\)\s*\(.*\)\s*(/\*.*?\*/|\w+)? | (const\s)?\w+\s*\**\s*(/\*.*?\*/|\w+(\[.*?\])?)),?", RegexOptions.IgnorePatternWhitespace);

                var fun =
                    new
                    {
                        Name = funcname,
                        Return = rettype,
                        Version = Version,
                        Extension = GetExtension(funcname),
                        Profile = String.Empty,
                        Parameters =
                            from item in get_param.Matches(paramaters_string).OfType<Match>().Select(m => m.Captures[0].Value.TrimEnd(','))
                                //paramaters_string.Split(",".ToCharArray(), StringSplitOptions.RemoveEmptyEntries)
                            let tokens = item.Trim().Split(' ')
                            let is_function_pointer = item.Contains("(*") // This only occurs in function pointers, e.g. void (*pfn_notify)() or void (*user_func)()
                            let param_name =
                                is_function_pointer ? tokens[1].TrimStart('(', '*').Split(')')[0] :
                                (tokens.Last().Trim() != "*/" ? tokens.Last() : tokens[tokens.Length - 2]).Trim()
                            let param_type =
                                is_function_pointer ? "IntPtr" :
                                (from t in tokens where t.Trim() != "const" && t.Trim() != "unsigned" select t).First().Trim()
                            let has_array_size = array_size.IsMatch(param_name)
                            let indirection_level =
                                is_function_pointer ? 0 :
                                (from c in param_name where c == '*' select c).Count() +
                                (from c in param_type where c == '*' select c).Count() +
                                (from t in tokens where t == "***" select t).Count() * 3 +
                                (from t in tokens where t == "**" select t).Count() * 2 +
                                (from t in tokens where t == "*" select t).Count() +
                                (has_array_size ? 1 : 0)
                            let pointers = new string[] { "*", "*", "*", "*" } // for adding indirection levels (pointers) to param_type
                            where tokens.Length > 1
                            select new
                            {
                                Name = (has_array_size ? array_size.Replace(param_name, "") : param_name).Replace("*", ""), // Pointers are placed into the parameter Type, not Name
                                Type =
                                    is_function_pointer ? param_type :
                                    (tokens.Contains("unsigned") && !param_type.StartsWith("byte") ? "u" : "") +    // Make sure we don't ignore the unsigned part of unsigned parameters (e.g. unsigned int -> uint)
                                    param_type.Replace("*", "") + String.Join("", pointers, 0, indirection_level),  // Normalize pointer indirection level (place as many asterisks as in indirection_level variable)
                                Count = has_array_size ? Int32.Parse(array_size.Match(param_name).Value.Trim('[', ']')) : 0,
                                Flow = param_name.EndsWith("ret") ? "out" : "in"
                            }
                    };

                XElement func = new XElement("function", new XAttribute("name", fun.Name));
                func.Add(new XAttribute("extension", fun.Extension));
                func.Add(new XAttribute("profile", fun.Profile));
                func.Add(new XAttribute("category", fun.Version));
                func.Add(new XAttribute("version", fun.Version));

                func.Add(new XElement("returns", new XAttribute("type", fun.Return)));
                foreach (var p in fun.Parameters)
                {
                    var param = new XElement("param", new XAttribute("type", p.Type), new XAttribute("name", p.Name));
                    if (p.Count > 0)
                        param.Add(new XAttribute("count", p.Count));
                    param.Add(new XAttribute("flow", p.Flow));
                    func.Add(param);
                }

                acc.Add(func);
                return acc;
            };

            Func<string, bool> is_comment = line => line.StartsWith("/*") || line.StartsWith("//");
            Func<string, bool> is_enum = line => {
                if (!is_comment(line))
                    return false;

                // Some enum tokens are commented out and should not be confused with new enum declarations.
                // Since tokens are always in ALL_CAPS, while enum names always contain at least one lower case
                // character, we'll try to use this information to distinguish between the two.
                // Warning: rather fragile.
                if (Regex.IsMatch(line, @"/\*\s+([A-Z]+_?[0-9]*_?)+\s+\*/"))
                        return false;
                
                var toks = split(line);
                return toks.Length > 1;// && toks[1].StartsWith("GL");
            };
            Func<string, bool> is_function = line =>
                (line.StartsWith("GL_APICALL") || line.StartsWith("GL_API") ||
                line.StartsWith("GLAPI") || line.StartsWith("EGLAPI") ||
                line.StartsWith("extern CL_API_ENTRY"));


            var signatures = lines.Aggregate(
                new List<XElement>(),
                (List<XElement> acc, string line) =>
                {
                    return
                        is_function(line) || !String.IsNullOrEmpty(function_string) ? function(line, acc) :
                        is_enum(line) ? enum_name(line, acc) :
                        enum_token(line, acc);
                },
                acc =>
                    from elem in acc
                    where !elem.IsEmpty
                    select elem);

            return signatures;
        }

        void GetFunctionNameAndType(string[] words, out string funcname, out string rettype)
        {
            funcname = null;
            rettype  = null;

            bool inRettype = false;
            bool quit = false;
            for (int i = 0; !quit && i < words.Length; ++i) {
                switch (words [i]) {
                    case "const":
                        // ignore
                        break;
                    case "GLAPI":   // ES 1.0
                    case "GL_API": // ES 1.1
                    case "GL_APICALL":  // ES 2.0
                    case "CL_API_ENTRY": // CL 1.0
                        inRettype = true;
                        break;
                    case "APIENTRY":  // ES 1.0
                    case "GL_APIENTRY":  // ES 1.1 & 2.0
                    case "CL_API_CALL": // CL 1.0
                        inRettype = false;
                        funcname = words [i+1].Substring(Prefix.Length);
                        quit = true;
                        break;
                    default:
                        if (inRettype)
                            rettype += words [i];
                        break;
                }
            }
        }
    }
}