#!/usr/bin/env rdmd

/* Copyright (c) 2021, Jean Pierre Cimalando
 *
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without modification,
 * are permitted provided that the following conditions are met:
 *
 *     * Redistributions of source code must retain the above copyright notice,
 *       this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above copyright notice,
 *       this list of conditions and the following disclaimer in the documentation
 *       and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER
 * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * SPDX-License-Identifier: BSD-2-Clause
 */

import std.conv;
import std.algorithm;
import std.getopt;
import std.process;
import std.regex;
import std.uni;
import std.ascii;
import std.string;
import std.array;
import std.stdio;
import core.stdc.stdlib;

enum string prologue = `#if defined(__GNUC__)
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wunused-parameter"
#endif
`;

enum string epilogue = `
#if defined(__GNUC__)
#pragma GCC diagnostic pop
#endif
#undef FAUSTFLOAT
#undef FAUSTCLASS
`;

void main(string[] args)
{
    struct Opts {
        string outputPath;
        string className = "mydsp";
        string superclassName = null;
        bool oneSample = false;
        bool doublePrecision = false;
        bool inPlace = false;
        bool vectorize = false;
        bool mathApproximation = false;
        string processName = null;
        string[] importDirs;
    }

    Opts opts;

    auto optInfo = getopt(args,
        "out|o", "Output path", &opts.outputPath,
        "cn", "Class name", &opts.className,
        "scn", "Superclass name", &opts.superclassName,
        "os", "One sample", &opts.oneSample,
        "double", "Double precision", &opts.doublePrecision,
        "inpl", "In-place", &opts.inPlace,
        "vec", "Vectorization", &opts.vectorize,
        "mapp", "Math approximation", &opts.mathApproximation,
        "pn", "Process name", &opts.processName,
        "import-dir|I", "Import directory", &opts.importDirs);

    if (optInfo.helpWanted)
    {
        defaultGetoptPrinter("MyFaust", optInfo.options);
        return;
    }

    if (args.length != 2)
    {
        writeln("You must indicate exactly 1 input file.");
        exit(1);
    }

    string[] cmd = [getFaust()];
    cmd ~= "-cn";
    cmd ~= opts.className;
    if (opts.superclassName)
    {
        cmd ~= "-scn";
        cmd ~= opts.superclassName;
    }
    if (opts.oneSample)
        cmd ~= "-os";
    if (opts.doublePrecision)
        cmd ~= "-double";
    if (opts.inPlace)
        cmd ~= "-inpl";
    if (opts.vectorize)
        cmd ~= "-vec";
    if (opts.mathApproximation)
        cmd ~= "-mapp";
    if (opts.processName)
    {
        cmd ~= "-pn";
        cmd ~= opts.processName;
    }
    foreach (string dir; opts.importDirs)
    {
        cmd ~= "-I";
        cmd ~= dir;
    }
    cmd ~= args[1];

    auto result = execute(cmd, null, Config.stderrPassThrough|Config.suppressConsole);
    if (result.status != 0)
        exit(1);

    string code = result.output;
    Parameter[] params = findParameters(code);

    code = removeVirtualKeyword(code);
    foreach (string method; ["metadata", "getInputRate", "getOutputRate", "clone", "buildUserInterface"])
        code = removeMethod(code, method);
    foreach (string method; ["getNumInputs", "getNumOutputs"])
        code = makeMethodStaticConstexpr(code, method);
    if (opts.oneSample)
    {
        foreach (string method; ["getNumIntControls", "getNumRealControls"])
            code = makeMethodStaticConstexpr(code, method);
    }
    if (!opts.superclassName)
        code = removeSuperclass(code, opts.oneSample ? "one_sample_dsp" : "dsp");
    code = makePointerArgsConst(code, opts.oneSample);
    code = addParameters(code, params);

    foreach (string method; ["compute", "classInit", "instanceConstants", "instanceResetUserInterface", "instanceClear", "init", "instanceInit"])
        code = addMethodStartEnd(code, method);
    code = addClassStartEnd(code);

    code = cleanupWhitespace(code);

    File outFile = stdout;
    if (opts.outputPath)
        outFile = File(opts.outputPath, "w");
    outFile.writef("%s%s%s", prologue, code, epilogue);
    outFile.flush();
}

final class Parameter
{
    string name;
    string var;
    bool readonly;
};

Parameter[] findParameters(string code)
{
    Parameter[] params;

    auto expr = regex(`->add(Button|CheckButton|VerticalSlider|HorizontalSlider|NumEntry|HorizontalBargraph|VerticalBargraph)\("([^"]*)", &([a-zA-Z0-9_]+)`);

    foreach (string line; code.lineSplitter)
    {
        auto match = line.matchFirst(expr);
        if (match)
        {
            Parameter param = new Parameter;
            param.name = match[2];
            param.var = match[3];
            param.readonly = ["HorizontalBargraph", "VerticalBargraph"].canFind(match[1]);
            params ~= param;
        }
    }

    return params;
}

string removeVirtualKeyword(string code)
{
    auto expr = regex(`\bvirtual\s*\b`);
    return code.replaceAll(expr, "");
}

string removeMethod(string code, string method)
{
    string[] newLines;
    newLines.reserve(1024);

    auto expr = regex(`^(\s*).*\b` ~ method.escaper.to!string ~ `\s*\(.*\{\s*$`);

    bool inMethod = false;
    string eom = null;

    foreach (string line; code.lineSplitter)
    {
        if (inMethod)
        {
            if (line.stripRight == eom)
                inMethod = false;
        }
        else
        {
            auto match = line.matchFirst(expr);
            if (match)
            {
                inMethod = true;
                eom = match[1] ~ '}';
            }
            else
                newLines ~= line;
        }
    }

    return newLines.join('\n');
}

string makeMethodStaticConstexpr(string code, string method)
{
    string[] newLines;
    newLines.reserve(1024);

    auto expr = regex(`^(\s*)(.*\b` ~ method.escaper.to!string ~ `\s*\(.*\{.*)$`);

    foreach (string line; code.lineSplitter)
    {
        auto match = line.matchFirst(expr);
        if (match)
            newLines ~= match[1] ~ "static constexpr " ~ match[2];
        else
            newLines ~= line;
    }

    return newLines.join('\n');
}

string removeSuperclass(string code, string superClassName)
{
    auto expr = regex(`\s*:\s*public\s+` ~ superClassName.escaper.to!string ~ `\s*`);
    return code.replaceFirst(expr, " ");
}

string makePointerArgsConst(string code, bool oneSample)
{
    if (!oneSample)
    {
        auto expr = regex(`\bvoid\s+compute\s*\(\s*([^,)]+)\s*,\s*([^,)]+)\s*,\s*([^,)]+)\s*\)`);
        auto match = code.matchFirst(expr);

        string arg1 = match[1];
        string arg2 = match[2];
        string arg3 = match[3];

        auto exprArg = regex(`^FAUSTFLOAT\s*\*\s*\*`);
        arg2 = arg2.replaceFirst(exprArg, "FAUSTFLOAT const* const*");
        arg3 = arg3.replaceFirst(exprArg, "FAUSTFLOAT* const*");

        ulong start = match[0].ptr - code.ptr;
        ulong end = start + match[0].length;
        code = format("%svoid compute(%s, %s, %s)%s", code[0..start], arg1, arg2, arg3, code[end..$]);

        //
        auto exprStmt = regex(`FAUSTFLOAT\s*\*\s*(input\d+)`);
        code = code.replaceAll(exprStmt, "FAUSTFLOAT const* $1");
    }
    else
    {
        auto expr = regex(`\bvoid\s+compute\s*\(\s*([^,)]+)\s*,\s*([^,)]+)\s*,\s*([^,)]+)\s*,\s*([^,)]+)\s*\)`);
        auto match = code.matchFirst(expr);

        string arg1 = match[1];
        string arg2 = match[2];
        string arg3 = match[3];
        string arg4 = match[4];

        auto exprArg = regex(`^(FAUSTFLOAT|int)\s*\*`);
        arg1 = arg1.replaceFirst(exprArg, "$1 const*");
        arg3 = arg3.replaceFirst(exprArg, "$1 const*");
        arg4 = arg4.replaceFirst(exprArg, "$1 const*");

        ulong start = match[0].ptr - code.ptr;
        ulong end = start + match[0].length;
        code = format("%svoid compute(%s, %s, %s, %s)%s", code[0..start], arg1, arg2, arg3, arg4, code[end..$]);
    }

    return code;
}

string addParameters(string code, Parameter[] params)
{
    string[] addedLines;

    foreach (Parameter param; params)
    {
        string camelName = param.name.camelify;
        addedLines ~= "";
        addedLines ~= format("\tFAUSTFLOAT get%s() const { return %s; }", camelName, param.var);
        if (!param.readonly)
            addedLines ~= format("\tvoid set%s(FAUSTFLOAT value) { %s = value; }", camelName, param.var);
    }

    return addToClass(code, addedLines.join('\n'));
}

string addMethodStartEnd(string code, string method)
{
    string[] newLines;
    newLines.reserve(1024);

    auto expr = regex(`^(\s*).*\b` ~ method.escaper.to!string ~ `\s*\(.*\{\s*$`);

    bool inMethod = false;
    string eom = null;

    foreach (string line; code.lineSplitter)
    {
        if (inMethod)
        {
            if (line.stripRight == eom)
            {
                newLines ~= "\t\t//[End:" ~ method ~ "]";
                inMethod = false;
            }
            newLines ~= line;
        }
        else
        {
            newLines ~= line;
            auto match = line.matchFirst(expr);
            if (match)
            {
                inMethod = true;
                eom = match[1] ~ '}';
                newLines ~= "\t\t//[Begin:" ~ method ~ "]";
            }
        }
    }

    return newLines.join('\n');
}

string addClassStartEnd(string code)
{
    {
        auto expr = regex(`^class\b.*$`, "m");
        auto match = code.matchLast(expr);
        ulong start = match[0].ptr - code.ptr;
        ulong end = start + match[0].length;
        code = code[0..start] ~ "\n//[Before:class]\n" ~ match[0] ~ "\n\t//[Begin:class]\n" ~ code[end..$];
    }

    {
        auto expr = regex(`^\};`, "m");
        auto match = code.matchLast(expr);
        ulong start = match[0].ptr - code.ptr;
        ulong end = start + match[0].length;
        code = code[0..start] ~ "\n\t//[End:class]\n" ~ match[0] ~ "\n//[After:class]\n" ~ code[end..$];
    }

    return code;
}

string addToClass(string code, string addend)
{
    if (addend.empty)
        return code;

    auto expr = regex(`^\};`, "m");
    auto match = code.matchLast(expr);
    ulong index = match[0].ptr - code.ptr;
    return code[0..index] ~ addend ~ '\n' ~ code[index..$];
}

string cleanupWhitespace(string code)
{
    string[] newLines;
    newLines.reserve(1024);

    foreach (string line; code.lineSplitter)
    {
        newLines ~= line.stripRight;
    }

    return newLines.join('\n');
}

string camelify(string name)
{
    dchar[] result;
    result.reserve(name.length);

    bool isIdentifierChar(dchar ch)
    {
        return std.ascii.isAlphaNum(ch) || ch == '_';
    }

    dchar[] temp = name.to!(dchar[]);

    foreach (ref dchar uniChar; temp)
    {
        uniChar = [uniChar].normalize!NFD[0];
        if (!isIdentifierChar(uniChar))
            uniChar = ' ';
    }

    foreach (dchar[] part; temp.split(' '))
    {
        if (!part.empty)
            part[0] = std.uni.toUpper(part[0]);
        result ~= part;
    }

    return result.to!string;
}

Captures!S matchLast(S, R)(S input, R expr)
{
    Captures!S last;
    foreach (Captures!S current; input.matchAll(expr))
        last = current;
    return last;
}

string getFaust()
{
    char *env = getenv("FAUST");
    return env ? env.to!string : "faust";
}
