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

   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.

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

namespace juce
{
namespace build_tools
{
    void overwriteFileIfDifferentOrThrow (const File& file, const MemoryOutputStream& newData)
    {
        if (! overwriteFileWithNewDataIfDifferent (file, newData))
            throw SaveError (file);
    }

    void overwriteFileIfDifferentOrThrow (const File& file, const String& newData)
    {
        if (! overwriteFileWithNewDataIfDifferent (file, newData))
            throw SaveError (file);
    }

    String replacePreprocessorDefs (const StringPairArray& definitions, String sourceString)
    {
        for (int i = 0; i < definitions.size(); ++i)
        {
            const String key (definitions.getAllKeys()[i]);
            const String value (definitions.getAllValues()[i]);

            sourceString = sourceString.replace ("${" + key + "}", value);
        }

        return sourceString;
    }

    String getXcodePackageType (ProjectType::Target::Type type)
    {
        using Type = ProjectType::Target::Type;

        switch (type)
        {
            case Type::GUIApp:
            case Type::StandalonePlugIn:
                return "APPL";

            case Type::VSTPlugIn:
            case Type::VST3PlugIn:
            case Type::AudioUnitPlugIn:
            case Type::UnityPlugIn:
                return "BNDL";

            case Type::AudioUnitv3PlugIn:
                return "XPC!";

            case Type::AAXPlugIn:
                return "TDMw";

            case Type::ConsoleApp:
            case Type::StaticLibrary:
            case Type::DynamicLibrary:
            case Type::LV2PlugIn:
            case Type::LV2TurtleProgram:
            case Type::SharedCodeTarget:
            case Type::AggregateTarget:
            case Type::unspecified:
            default:
                return {};
        }
    }

    String getXcodeBundleSignature (ProjectType::Target::Type type)
    {
        using Type = ProjectType::Target::Type;

        switch (type)
        {
            case Type::GUIApp:
            case Type::VSTPlugIn:
            case Type::VST3PlugIn:
            case Type::AudioUnitPlugIn:
            case Type::StandalonePlugIn:
            case Type::AudioUnitv3PlugIn:
            case Type::UnityPlugIn:
                return "????";

            case Type::AAXPlugIn:
                return "PTul";

            case Type::ConsoleApp:
            case Type::StaticLibrary:
            case Type::DynamicLibrary:
            case Type::LV2PlugIn:
            case Type::LV2TurtleProgram:
            case Type::SharedCodeTarget:
            case Type::AggregateTarget:
            case Type::unspecified:
            default:
                return {};
        }
    }

    static unsigned int calculateHash (const String& s, const unsigned int hashMultiplier)
    {
        auto t = s.toUTF8();
        unsigned int hash = 0;

        while (*t != 0)
            hash = hashMultiplier * hash + (unsigned int) *t++;

        return hash;
    }

    static unsigned int findBestHashMultiplier (const StringArray& strings)
    {
        unsigned int v = 31;

        for (;;)
        {
            SortedSet<unsigned int> hashes;
            bool collision = false;

            for (int i = strings.size(); --i >= 0;)
            {
                auto hash = calculateHash (strings[i], v);

                if (hashes.contains (hash))
                {
                    collision = true;
                    break;
                }

                hashes.add (hash);
            }

            if (! collision)
                break;

            v += 2;
        }

        return v;
    }

    String makeValidIdentifier (String s, bool makeCamelCase, bool removeColons, bool allowTemplates, bool allowAsterisks)
    {
        if (s.isEmpty())
            return "unknown";

        if (removeColons)
            s = s.replaceCharacters (".,;:/@", "______");
        else
            s = s.replaceCharacters (".,;/@", "_____");

        for (int i = s.length(); --i > 0;)
            if (CharacterFunctions::isLetter (s[i])
                && CharacterFunctions::isLetter (s[i - 1])
                && CharacterFunctions::isUpperCase (s[i])
                && ! CharacterFunctions::isUpperCase (s[i - 1]))
                s = s.substring (0, i) + " " + s.substring (i);

        String allowedChars ("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_ 0123456789");
        if (allowTemplates)
            allowedChars += "<>";

        if (! removeColons)
            allowedChars += ":";

        if (allowAsterisks)
            allowedChars += "*";

        StringArray words;
        words.addTokens (s.retainCharacters (allowedChars), false);
        words.trim();

        auto n = words[0];

        if (makeCamelCase)
            n = n.toLowerCase();

        for (int i = 1; i < words.size(); ++i)
        {
            if (makeCamelCase && words[i].length() > 1)
                n << words[i].substring (0, 1).toUpperCase()
                  << words[i].substring (1).toLowerCase();
            else
                n << words[i];
        }

        if (CharacterFunctions::isDigit (n[0]))
            n = "_" + n;

        if (isReservedKeyword (n))
            n << '_';

        return n;
    }

    String makeBinaryDataIdentifierName (const File& file)
    {
        return makeValidIdentifier (file.getFileName()
                                            .replaceCharacters (" .", "__")
                                            .retainCharacters ("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_0123456789"),
                                    false, true, false);
    }

    void writeDataAsCppLiteral (const MemoryBlock& mb, OutputStream& out,
                                bool breakAtNewLines, bool allowStringBreaks)
    {
        const int maxCharsOnLine = 250;

        auto data = (const unsigned char*) mb.getData();
        int charsOnLine = 0;

        bool canUseStringLiteral = mb.getSize() < 32768; // MS compilers can't handle big string literals..

        if (canUseStringLiteral)
        {
            unsigned int numEscaped = 0;

            for (size_t i = 0; i < mb.getSize(); ++i)
            {
                auto num = (unsigned int) data[i];

                if (! ((num >= 32 && num < 127) || num == '\t' || num == '\r' || num == '\n'))
                {
                    if (++numEscaped > mb.getSize() / 4)
                    {
                        canUseStringLiteral = false;
                        break;
                    }
                }
            }
        }

        if (! canUseStringLiteral)
        {
            out << "{ ";

            for (size_t i = 0; i < mb.getSize(); ++i)
            {
                auto num = (int) (unsigned int) data[i];
                out << num << ',';

                charsOnLine += 2;

                if (num >= 10)
                {
                    ++charsOnLine;

                    if (num >= 100)
                        ++charsOnLine;
                }

                if (charsOnLine >= maxCharsOnLine)
                {
                    charsOnLine = 0;
                    out << newLine;
                }
            }

            out << "0,0 };";
        }
        else
        {
            out << "\"";
            writeEscapeChars (out, (const char*) data, (int) mb.getSize(),
                              maxCharsOnLine, breakAtNewLines, false, allowStringBreaks);
            out << "\";";
        }
    }

    void createStringMatcher (OutputStream& out, const String& utf8PointerVariable,
                              const StringArray& strings, const StringArray& codeToExecute, const int indentLevel)
    {
        jassert (strings.size() == codeToExecute.size());
        auto indent = String::repeatedString (" ", indentLevel);
        auto hashMultiplier = findBestHashMultiplier (strings);

        out << indent << "unsigned int hash = 0;" << newLine
            << newLine
            << indent << "if (" << utf8PointerVariable << " != nullptr)" << newLine
            << indent << "    while (*" << utf8PointerVariable << " != 0)" << newLine
            << indent << "        hash = " << (int) hashMultiplier << " * hash + (unsigned int) *" << utf8PointerVariable << "++;" << newLine
            << newLine
            << indent << "switch (hash)" << newLine
            << indent << "{" << newLine;

        for (int i = 0; i < strings.size(); ++i)
        {
            out << indent << "    case 0x" << hexString8Digits ((int) calculateHash (strings[i], hashMultiplier))
                << ":  " << codeToExecute[i] << newLine;
        }

        out << indent << "    default: break;" << newLine
            << indent << "}" << newLine << newLine;
    }

    String unixStylePath (const String& path)       { return path.replaceCharacter ('\\', '/'); }
    String windowsStylePath (const String& path)    { return path.replaceCharacter ('/', '\\'); }

    String currentOSStylePath (const String& path)
    {
       #if JUCE_WINDOWS
        return windowsStylePath (path);
       #else
        return unixStylePath (path);
       #endif
    }

    bool isAbsolutePath (const String& path)
    {
        return File::isAbsolutePath (path)
                || path.startsWithChar ('/') // (needed because File::isAbsolutePath will ignore forward-slashes on Windows)
                || path.startsWithChar ('$')
                || path.startsWithChar ('~')
                || (CharacterFunctions::isLetter (path[0]) && path[1] == ':')
                || path.startsWithIgnoreCase ("smb:");
    }

    String getRelativePathFrom (const File& file, const File& sourceFolder)
    {
       #if ! JUCE_WINDOWS
        // On a non-windows machine, we can't know if a drive-letter path may be relative or not.
        if (CharacterFunctions::isLetter (file.getFullPathName()[0]) && file.getFullPathName()[1] == ':')
            return file.getFullPathName();
       #endif

        return file.getRelativePathFrom (sourceFolder);
    }

    void writeStreamToFile (const File& file, const std::function<void (MemoryOutputStream&)>& writer)
    {
        MemoryOutputStream mo;
        writer (mo);
        overwriteFileIfDifferentOrThrow (file, mo);
    }
}
}
