#region Copyright notice and license

// Copyright 2018 gRPC authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#endregion

using System;
using System.IO;
using System.Text;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;

namespace Grpc.Tools
{
    // Abstract class for language-specific analysis behavior, such
    // as guessing the generated files the same way protoc does.
    internal abstract class GeneratorServices
    {
        protected readonly TaskLoggingHelper Log;
        protected GeneratorServices(TaskLoggingHelper log) { Log = log; }

        // Obtain a service for the given language (csharp, cpp).
        public static GeneratorServices GetForLanguage(string lang, TaskLoggingHelper log)
        {
            if (lang.EqualNoCase("csharp")) { return new CSharpGeneratorServices(log); }
            if (lang.EqualNoCase("cpp")) { return new CppGeneratorServices(log); }

            log.LogError("Invalid value '{0}' for task property 'Generator'. " +
                "Supported generator languages: CSharp, Cpp.", lang);
            return null;
        }

        // Guess whether item's metadata suggests gRPC stub generation.
        // When "gRPCServices" is not defined, assume gRPC is not used.
        // When defined, C# uses "none" to skip gRPC, C++ uses "false", so
        // recognize both. Since the value is tightly coupled to the scripts,
        // we do not try to validate the value; scripts take care of that.
        // It is safe to assume that gRPC is requested for any other value.
        protected bool GrpcOutputPossible(ITaskItem proto)
        {
            string gsm = proto.GetMetadata(Metadata.GrpcServices);
            return !gsm.EqualNoCase("") && !gsm.EqualNoCase("none")
                && !gsm.EqualNoCase("false");
        }

        public abstract string[] GetPossibleOutputs(ITaskItem proto);
    };

    // C# generator services.
    internal class CSharpGeneratorServices : GeneratorServices
    {
        public CSharpGeneratorServices(TaskLoggingHelper log) : base(log) { }

        public override string[] GetPossibleOutputs(ITaskItem protoItem)
        {
            bool doGrpc = GrpcOutputPossible(protoItem);
            var outputs = new string[doGrpc ? 2 : 1];
            string basename = Path.GetFileNameWithoutExtension(protoItem.ItemSpec);

            string outdir = protoItem.GetMetadata(Metadata.OutputDir);
            string filename = LowerUnderscoreToUpperCamelProtocWay(basename);
            outputs[0] = Path.Combine(outdir, filename) + ".cs";

            if (doGrpc)
            {
                // Override outdir if kGrpcOutputDir present, default to proto output.
                string grpcdir = protoItem.GetMetadata(Metadata.GrpcOutputDir);
                filename = LowerUnderscoreToUpperCamelGrpcWay(basename);
                outputs[1] = Path.Combine(
                    grpcdir != "" ? grpcdir : outdir, filename) + "Grpc.cs";
            }
            return outputs;
        }

        // This is how the gRPC codegen currently construct its output filename.
        // See src/compiler/generator_helpers.h:118.
        string LowerUnderscoreToUpperCamelGrpcWay(string str)
        {
            var result = new StringBuilder(str.Length, str.Length);
            bool cap = true;
            foreach (char c in str)
            {
                if (c == '_')
                {
                    cap = true;
                }
                else if (cap)
                {
                    result.Append(char.ToUpperInvariant(c));
                    cap = false;
                }
                else
                {
                    result.Append(c);
                }
            }
            return result.ToString();
        }

        // This is how the protoc codegen constructs its output filename.
        // See protobuf/compiler/csharp/csharp_helpers.cc:137.
        // Note that protoc explicitly discards non-ASCII letters.
        string LowerUnderscoreToUpperCamelProtocWay(string str)
        {
            var result = new StringBuilder(str.Length, str.Length);
            bool cap = true;
            foreach (char c in str)
            {
                char upperC = char.ToUpperInvariant(c);
                bool isAsciiLetter = 'A' <= upperC && upperC <= 'Z';
                if (isAsciiLetter || ('0' <= c && c <= '9'))
                {
                    result.Append(cap ? upperC : c);
                }
                cap = !isAsciiLetter;
            }
            return result.ToString();
        }
    };

    // C++ generator services.
    internal class CppGeneratorServices : GeneratorServices
    {
        public CppGeneratorServices(TaskLoggingHelper log) : base(log) { }

        public override string[] GetPossibleOutputs(ITaskItem protoItem)
        {
            bool doGrpc = GrpcOutputPossible(protoItem);
            string root = protoItem.GetMetadata(Metadata.ProtoRoot);
            string proto = protoItem.ItemSpec;
            string filename = Path.GetFileNameWithoutExtension(proto);
            // E. g., ("foo/", "foo/bar/x.proto") => "bar"
            string relative = GetRelativeDir(root, proto);

            var outputs = new string[doGrpc ? 4 : 2];
            string outdir = protoItem.GetMetadata(Metadata.OutputDir);
            string fileStem = Path.Combine(outdir, relative, filename);
            outputs[0] = fileStem + ".pb.cc";
            outputs[1] = fileStem + ".pb.h";
            if (doGrpc)
            {
                // Override outdir if kGrpcOutputDir present, default to proto output.
                outdir = protoItem.GetMetadata(Metadata.GrpcOutputDir);
                if (outdir != "")
                {
                    fileStem = Path.Combine(outdir, relative, filename);
                }
                outputs[2] = fileStem + "_grpc.pb.cc";
                outputs[3] = fileStem + "_grpc.pb.h";
            }
            return outputs;
        }

        // Calculate part of proto path relative to root. Protoc is very picky
        // about them matching exactly, so can be we. Expect root be exact prefix
        // to proto, minus some slash normalization.
        string GetRelativeDir(string root, string proto)
        {
            string protoDir = Path.GetDirectoryName(proto);
            string rootDir = EndWithSlash(Path.GetDirectoryName(EndWithSlash(root)));
            if (rootDir == s_dotSlash)
            {
                // Special case, otherwise we can return "./" instead of "" below!
                return protoDir;
            }
            if (Platform.IsFsCaseInsensitive)
            {
                protoDir = protoDir.ToLowerInvariant();
                rootDir = rootDir.ToLowerInvariant();
            }
            protoDir = EndWithSlash(protoDir);
            if (!protoDir.StartsWith(rootDir))
            {
                Log.LogWarning("Protobuf item '{0}' has the ProtoRoot metadata '{1}' " +
                  "which is not prefix to its path. Cannot compute relative path.",
                  proto, root);
                return "";
            }
            return protoDir.Substring(rootDir.Length);
        }

        // './' or '.\', normalized per system.
        static string s_dotSlash = "." + Path.DirectorySeparatorChar;

        static string EndWithSlash(string str)
        {
            if (str == "")
            {
                return s_dotSlash;
            }
            else if (str[str.Length - 1] != '\\' && str[str.Length - 1] != '/')
            {
                return str + Path.DirectorySeparatorChar;
            }
            else
            {
                return str;
            }
        }
    };
}
