/*
  ==============================================================================

   This file is part of the JUCE library.
   Copyright (c) 2022 - Raw Material Software Limited

   JUCE is an open source library subject to commercial or open-source
   licensing.

   By using JUCE, you agree to the terms of both the JUCE 7 End-User License
   Agreement and JUCE Privacy Policy.

   End User License Agreement: www.juce.com/juce-7-licence
   Privacy Policy: www.juce.com/juce-privacy-policy

   Or: You may also use this code under the terms of the GPL v3 (see
   www.gnu.org/licenses).

   JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
   EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
   DISCLAIMED.

  ==============================================================================
*/

#include "../../Application/jucer_Headers.h"

//==============================================================================
String joinLinesIntoSourceFile (StringArray& lines)
{
    while (lines.size() > 10 && lines [lines.size() - 1].isEmpty())
        lines.remove (lines.size() - 1);

    return lines.joinIntoString (getPreferredLineFeed()) + getPreferredLineFeed();
}

String replaceLineFeeds (const String& content, const String& lineFeed)
{
    StringArray lines;
    lines.addLines (content);

    return lines.joinIntoString (lineFeed);
}

String getLineFeedForFile (const String& fileContent)
{
    auto t = fileContent.getCharPointer();

    while (! t.isEmpty())
    {
        switch (t.getAndAdvance())
        {
            case 0:     break;
            case '\n':  return "\n";
            case '\r':  if (*t == '\n') return "\r\n";
            default:    continue;
        }
    }

    return {};
}

String trimCommentCharsFromStartOfLine (const String& line)
{
    return line.trimStart().trimCharactersAtStart ("*/").trimStart();
}

String createAlphaNumericUID()
{
    String uid;
    const char chars[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
    Random r;

    uid << chars[r.nextInt (52)]; // make sure the first character is always a letter

    for (int i = 5; --i >= 0;)
    {
        r.setSeedRandomly();
        uid << chars [r.nextInt (62)];
    }

    return uid;
}

String createGUID (const String& seed)
{
    auto hex = MD5 ((seed + "_guidsalt").toUTF8()).toHexString().toUpperCase();

    return "{" + hex.substring (0, 8)
         + "-" + hex.substring (8, 12)
         + "-" + hex.substring (12, 16)
         + "-" + hex.substring (16, 20)
         + "-" + hex.substring (20, 32)
         + "}";
}

String escapeSpaces (const String& s)
{
    return s.replace (" ", "\\ ");
}

String escapeQuotesAndSpaces (const String& s)
{
    return escapeSpaces (s).replace ("'", "\\'").replace ("\"", "\\\"");
}

String addQuotesIfContainsSpaces (const String& text)
{
    return (text.containsChar (' ') && ! text.isQuotedString()) ? text.quoted() : text;
}

void setValueIfVoid (Value value, const var& defaultValue)
{
    if (value.getValue().isVoid())
        value = defaultValue;
}

//==============================================================================
StringPairArray parsePreprocessorDefs (const String& text)
{
    StringPairArray result;
    auto s = text.getCharPointer();

    while (! s.isEmpty())
    {
        String token, value;
        s.incrementToEndOfWhitespace();

        while ((! s.isEmpty()) && *s != '=' && ! s.isWhitespace())
            token << s.getAndAdvance();

        s.incrementToEndOfWhitespace();

        if (*s == '=')
        {
            ++s;

            while ((! s.isEmpty()) && *s == ' ')
                ++s;

            while ((! s.isEmpty()) && ! s.isWhitespace())
            {
                if (*s == ',')
                {
                    ++s;
                    break;
                }

                if (*s == '\\' && (s[1] == ' ' || s[1] == ','))
                    ++s;

                value << s.getAndAdvance();
            }
        }

        if (token.isNotEmpty())
            result.set (token, value);
    }

    return result;
}

StringPairArray mergePreprocessorDefs (StringPairArray inheritedDefs, const StringPairArray& overridingDefs)
{
    for (int i = 0; i < overridingDefs.size(); ++i)
        inheritedDefs.set (overridingDefs.getAllKeys()[i], overridingDefs.getAllValues()[i]);

    return inheritedDefs;
}

String createGCCPreprocessorFlags (const StringPairArray& defs)
{
    String s;

    for (int i = 0; i < defs.size(); ++i)
    {
        auto def = defs.getAllKeys()[i];
        auto value = defs.getAllValues()[i];

        if (value.isNotEmpty())
            def << "=" << value;

        s += " \"" + ("-D" + def).replace ("\"", "\\\"") + "\"";
    }

    return s;
}

StringArray getSearchPathsFromString (const String& searchPath)
{
    StringArray s;
    s.addTokens (searchPath, ";\r\n", StringRef());
    return getCleanedStringArray (s);
}

StringArray getCommaOrWhitespaceSeparatedItems (const String& sourceString)
{
    StringArray s;
    s.addTokens (sourceString, ", \t\r\n", StringRef());
    return getCleanedStringArray (s);
}

StringArray getCleanedStringArray (StringArray s)
{
    s.trim();
    s.removeEmptyStrings();
    return s;
}

//==============================================================================
void autoScrollForMouseEvent (const MouseEvent& e, bool scrollX, bool scrollY)
{
    if (Viewport* const viewport = e.eventComponent->findParentComponentOfClass<Viewport>())
    {
        const MouseEvent e2 (e.getEventRelativeTo (viewport));
        viewport->autoScroll (scrollX ? e2.x : 20, scrollY ? e2.y : 20, 8, 16);
    }
}

//==============================================================================
int indexOfLineStartingWith (const StringArray& lines, const String& text, int index)
{
    const int len = text.length();

    for (const String* i = lines.begin() + index, * const e = lines.end(); i < e; ++i)
    {
        if (CharacterFunctions::compareUpTo (i->getCharPointer().findEndOfWhitespace(),
                                             text.getCharPointer(), len) == 0)
            return index;

        ++index;
    }

    return -1;
}

//==============================================================================
bool fileNeedsCppSyntaxHighlighting (const File& file)
{
    if (file.hasFileExtension (sourceOrHeaderFileExtensions))
        return true;

    // This is a bit of a bodge to deal with libc++ headers with no extension..
    char fileStart[128] = { 0 };
    FileInputStream fin (file);
    fin.read (fileStart, sizeof (fileStart) - 4);

    return CharPointer_UTF8::isValidString (fileStart, sizeof (fileStart))
             && String (fileStart).trimStart().startsWith ("// -*- C++ -*-");
}

//==============================================================================
void writeAutoGenWarningComment (OutputStream& outStream)
{
    outStream << "/*" << newLine << newLine
              << "    IMPORTANT! This file is auto-generated each time you save your" << newLine
              << "    project - if you alter its contents, your changes may be overwritten!" << newLine
              << newLine;
}

//==============================================================================
StringArray getJUCEModules() noexcept
{
    static StringArray juceModuleIds =
    {
        "juce_analytics",
        "juce_audio_basics",
        "juce_audio_devices",
        "juce_audio_formats",
        "juce_audio_plugin_client",
        "juce_audio_processors",
        "juce_audio_utils",
        "juce_box2d",
        "juce_core",
        "juce_cryptography",
        "juce_data_structures",
        "juce_dsp",
        "juce_events",
        "juce_graphics",
        "juce_gui_basics",
        "juce_gui_extra",
        "juce_opengl",
        "juce_osc",
        "juce_product_unlocking",
        "juce_video"
    };

    return juceModuleIds;
}

bool isJUCEModule (const String& moduleID) noexcept
{
    return getJUCEModules().contains (moduleID);
}

StringArray getModulesRequiredForConsole() noexcept
{
    return
    {
        "juce_core",
        "juce_data_structures",
        "juce_events"
    };
}

StringArray getModulesRequiredForComponent() noexcept
{
    return
    {
        "juce_core",
        "juce_data_structures",
        "juce_events",
        "juce_graphics",
        "juce_gui_basics"
    };
}

StringArray getModulesRequiredForAudioProcessor() noexcept
{
    return
    {
        "juce_audio_basics",
        "juce_audio_devices",
        "juce_audio_formats",
        "juce_audio_plugin_client",
        "juce_audio_processors",
        "juce_audio_utils",
        "juce_core",
        "juce_data_structures",
        "juce_events",
        "juce_graphics",
        "juce_gui_basics",
        "juce_gui_extra"
    };
}

bool isPIPFile (const File& file) noexcept
{
    for (auto line : StringArray::fromLines (file.loadFileAsString()))
    {
        auto trimmedLine = trimCommentCharsFromStartOfLine (line);

        if (trimmedLine.startsWith ("BEGIN_JUCE_PIP_METADATA"))
            return true;
    }

    return false;
}

bool isValidJUCEExamplesDirectory (const File& directory) noexcept
{
    if (! directory.exists() || ! directory.isDirectory() || ! directory.containsSubDirectories())
        return false;

    return directory.getChildFile ("Assets").getChildFile ("juce_icon.png").existsAsFile();
}

bool isJUCEFolder (const File& f)
{
    return isJUCEModulesFolder (f.getChildFile ("modules"));
}

bool isJUCEModulesFolder (const File& f)
{
    return f.isDirectory() && f.getChildFile ("juce_core").isDirectory();
}

//==============================================================================
static bool isDivider (const String& line)
{
    auto afterIndent = line.trim();

    if (afterIndent.startsWith ("//") && afterIndent.length() > 20)
    {
        afterIndent = afterIndent.substring (5);

        if (afterIndent.containsOnly ("=")
            || afterIndent.containsOnly ("/")
            || afterIndent.containsOnly ("-"))
        {
            return true;
        }
    }

    return false;
}

static int getIndexOfCommentBlockStart (const StringArray& lines, int endIndex)
{
    auto endLine = lines[endIndex];

    if (endLine.contains ("*/"))
    {
        for (int i = endIndex; i >= 0; --i)
            if (lines[i].contains ("/*"))
                return i;
    }

     if (endLine.trim().startsWith ("//") && ! isDivider (endLine))
     {
         for (int i = endIndex; i >= 0; --i)
             if (! lines[i].startsWith ("//") || isDivider (lines[i]))
                 return i + 1;
     }

    return -1;
}

int findBestLineToScrollToForClass (StringArray lines, const String& className, bool isPlugin)
{
    for (auto line : lines)
    {
        if (line.contains ("struct " + className) || line.contains ("class " + className)
            || (isPlugin && line.contains ("public AudioProcessor") && ! line.contains ("AudioProcessorEditor")))
        {
            auto index = lines.indexOf (line);

            auto commentBlockStartIndex = getIndexOfCommentBlockStart (lines, index - 1);

            if (commentBlockStartIndex != -1)
                index = commentBlockStartIndex;

            if (isDivider (lines[index - 1]))
                index -= 1;

            return index;
        }
    }

    return 0;
}

//==============================================================================
static var parseJUCEHeaderMetadata (const StringArray& lines)
{
    auto* o = new DynamicObject();
    var result (o);

    for (auto& line : lines)
    {
        auto trimmedLine = trimCommentCharsFromStartOfLine (line);

        auto colon = trimmedLine.indexOfChar (':');

        if (colon >= 0)
        {
            auto key = trimmedLine.substring (0, colon).trim();
            auto value = trimmedLine.substring (colon + 1).trim();

            o->setProperty (key, value);
        }
    }

    return result;
}

static String parseMetadataItem (const StringArray& lines, int& index)
{
    String result = lines[index++];

    while (index < lines.size())
    {
        auto continuationLine = trimCommentCharsFromStartOfLine (lines[index]);

        if (continuationLine.isEmpty() || continuationLine.indexOfChar (':') != -1
            || continuationLine.startsWith ("END_JUCE_"))
            break;

        result += " " + continuationLine;
        ++index;
    }

    return result;
}

var parseJUCEHeaderMetadata (const File& file)
{
    StringArray lines;
    file.readLines (lines);

    for (int i = 0; i < lines.size(); ++i)
    {
        auto trimmedLine = trimCommentCharsFromStartOfLine (lines[i]);

        if (trimmedLine.startsWith ("BEGIN_JUCE_"))
        {
            StringArray desc;
            auto j = i + 1;

            while (j < lines.size())
            {
                if (trimCommentCharsFromStartOfLine (lines[j]).startsWith ("END_JUCE_"))
                    return parseJUCEHeaderMetadata (desc);

                desc.add (parseMetadataItem (lines, j));
            }
        }
    }

    return {};
}
