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

   This file is part of the JUCE library.
   Copyright (c) 2020 - 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 6 End-User License
   Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020).

   End User License Agreement: www.juce.com/juce-6-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.

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

#pragma once


//==============================================================================
struct ClassDatabase
{
    //==============================================================================
    struct MemberInfo
    {
        enum CodeLocationType
        {
            declaration = 0,
            addedToParent,
            setBoundsParamX,
            setBoundsParamY,
            setBoundsParamW,
            setBoundsParamH,

            // WARNING! When you change any of these, also update the copy that lives in the live editing code

            numCodeLocationTypes
        };

        MemberInfo() {}

        MemberInfo (const MemberInfo& other)
            : name (other.name), type (other.type)
        {
            for (int i = 0; i < numCodeLocationTypes; ++i)
                locations[i] = other.locations[i];
        }

        MemberInfo (const String& nm, const String& ty)
            : name (nm), type (ty)
        {
        }

        MemberInfo (const ValueTree& v)
            : name (v [Ids::name].toString()),
              type (v [Ids::class_].toString())
        {
            for (int i = 0; i < numCodeLocationTypes; ++i)
                locations[i] = v [getIdentifierForCodeLocationType (i)].toString();
        }

        const String& getName() const   { return name; }
        const String& getType() const   { return type; }

        const SourceCodeRange& getLocation (CodeLocationType t) const
        {
            return locations[t];
        }

        void setLocation (CodeLocationType t, const SourceCodeRange& range)
        {
            locations[t] = range;
        }

        void mergeWith (const MemberInfo& other)
        {
            jassert (name == other.name);
            if (other.type.isNotEmpty())
                type = other.type;

            for (int i = 0; i < numCodeLocationTypes; ++i)
                if (other.locations[i].isValid())
                    locations[i] = other.locations[i];
        }

        void nudgeAllCodeRanges (const String& file, const int insertPoint, const int delta)
        {
            for (int i = 0; i < numCodeLocationTypes; ++i)
                locations[i].nudge (file, insertPoint, delta);
        }

        void fileContentChanged (const String& file)
        {
            for (int i = 0; i < numCodeLocationTypes; ++i)
                locations[i].fileContentChanged (file);
        }

        ValueTree toValueTree() const
        {
            ValueTree m (Ids::MEMBER);
            m.setProperty (Ids::name, name, nullptr);
            m.setProperty (Ids::class_, type, nullptr);

            for (int i = 0; i < numCodeLocationTypes; ++i)
                locations[i].writeToValueTree (m, getIdentifierForCodeLocationType (i));

            return m;
        }

    private:
        String name, type;
        SourceCodeRange locations [numCodeLocationTypes];

        static Identifier getIdentifierForCodeLocationType (int typeIndex)
        {
            // (These need to remain in order)
            static_assert (setBoundsParamX + 1 == setBoundsParamY && setBoundsParamY + 1 == setBoundsParamW
                            && setBoundsParamW + 1 == setBoundsParamH, "");

            static const Identifier ids[] =
            {
                "declaration",
                "addedToParent",
                "setBoundsParamX",
                "setBoundsParamY",
                "setBoundsParamW",
                "setBoundsParamH"
            };

            return ids [typeIndex];
        }
    };

    //==============================================================================
    struct MethodInfo
    {
        MethodInfo() {}

        MethodInfo (const MethodInfo& other)
            : name (other.name), returnType (other.returnType),
              declaration (other.declaration), definition (other.definition),
              numArgs (other.numArgs), flags (other.flags)
        {
        }

        String name, returnType;
        SourceCodeRange declaration, definition;
        int numArgs, flags;

        enum
        {
            isConstructor = 1,
            isDefaultConstructor = 2,
            isTemplated = 4,
            isPublic = 8
        };

        MethodInfo (const ValueTree& v)
            : name (v[Ids::name].toString()),
              returnType (v[Ids::returnType].toString()),
              declaration (v[Ids::declaration].toString()),
              definition (v[Ids::definition].toString()),
              numArgs (v[Ids::numArgs]),
              flags (v[Ids::flags])
        {
        }

        ValueTree toValueTree() const
        {
            ValueTree m (Ids::METHOD);
            m.setProperty (Ids::name, name, nullptr);
            m.setProperty (Ids::returnType, returnType, nullptr);
            m.setProperty (Ids::numArgs, numArgs, nullptr);
            m.setProperty (Ids::flags, flags, nullptr);
            declaration.writeToValueTree (m, Ids::declaration);
            definition.writeToValueTree (m, Ids::definition);
            return m;
        }

        void nudgeAllCodeRanges (const String& file, const int insertPoint, const int delta)
        {
            declaration.nudge (file, insertPoint, delta);
            definition.nudge (file, insertPoint, delta);
        }

        void fileContentChanged (const String& file)
        {
            declaration.fileContentChanged (file);
            definition.fileContentChanged (file);
        }
    };

    //==============================================================================
    struct InstantiationFlags
    {
        InstantiationFlags()
            : isAbstract (false),
              inAnonymousNamespace (false),
              noDefaultConstructor (false)
        {}

        bool canBeInstantiated() const noexcept
        {
            return ! (isAbstract || inAnonymousNamespace || noDefaultConstructor);
        }

        String getReasonForUnavailability() const
        {
            if (isAbstract)             return "This class is abstract";
            if (noDefaultConstructor)   return "This class has no default constructor";
            if (inAnonymousNamespace)   return "This class is declared inside an anonymous namespace";
            return String();
        }

        bool isDisallowed (const InstantiationFlags& disallowedFlags) const
        {
            return ! ((disallowedFlags.isAbstract && isAbstract)
                      || (disallowedFlags.inAnonymousNamespace && inAnonymousNamespace)
                      || (disallowedFlags.noDefaultConstructor && noDefaultConstructor));
        }

        bool isAbstract;
        bool inAnonymousNamespace;
        bool noDefaultConstructor;
    };

    //==============================================================================
    struct Class
    {
        Class() {}
        ~Class() {}

        Class (const Class& other)
            : className (other.className), members (other.members),
              methods (other.methods), classDeclaration (other.classDeclaration),
              instantiationFlags (other.instantiationFlags)
        {
        }

        Class (const String& name, const InstantiationFlags& flags,
               const Array<MemberInfo>& m,
               const Array<MethodInfo>& meth,
               const SourceCodeRange& classDeclarationRange)
            : className (name),
              members (m), methods (meth),
              classDeclaration (classDeclarationRange),
              instantiationFlags (flags)
        {
        }

        Class& operator= (const Class& other)
        {
            className = other.className;
            members = other.members;
            methods = other.methods;
            classDeclaration = other.classDeclaration;
            instantiationFlags = other.instantiationFlags;
            return *this;
        }

        const String& getName() const noexcept          { return className; }

        const InstantiationFlags& getInstantiationFlags() const
        {
            return instantiationFlags;
        }

        void setInstantiationFlags (const InstantiationFlags& newFlags)
        {
            instantiationFlags = newFlags;
        }

        const SourceCodeRange& getClassDeclarationRange() const
        {
            return classDeclaration;
        }

        const MemberInfo* findMember (const String& memberName) const
        {
            for (auto& m : members)
                if (m.getName() == memberName)
                    return &m;

            return nullptr;
        }

        MemberInfo* findMember (const String& memberName)
        {
            return const_cast<MemberInfo*> (static_cast<const Class&>(*this).findMember (memberName));
        }

        const MethodInfo* getDefaultConstructor() const
        {
            for (const MethodInfo& m : methods)
                if ((m.flags & MethodInfo::isDefaultConstructor) != 0)
                    return &m;

            return nullptr;
        }

        const MethodInfo* getConstructor() const
        {
            if (const MethodInfo* m = getDefaultConstructor())
                return m;

            for (const MethodInfo& m : methods)
                if ((m.flags & MethodInfo::isConstructor) != 0)
                    return &m;

            return nullptr;
        }

        const MethodInfo* getResizedMethod() const
        {
            for (const MethodInfo& m : methods)
                if (m.name == "resized" && m.numArgs == 0)
                    return &m;

            return nullptr;
        }

        File getMainSourceFile() const
        {
            if (const MethodInfo* m = getResizedMethod())
                if (m->definition.isValid())
                    return m->definition.file;

            if (const MethodInfo* m = getConstructor())
                if (m->definition.isValid())
                    return m->definition.file;

            for (auto& m : methods)
                if (m.definition.isValid() && File (m.definition.file).hasFileExtension ("cpp;mm"))
                    return m.definition.file;

            for (auto& m : methods)
                if ((m.flags & MethodInfo::isConstructor) != 0 && m.definition.isValid())
                    return m.definition.file;

            for (auto& m : methods)
                if (m.definition.isValid() && File (m.definition.file).exists())
                    return m.definition.file;

            return {};
        }

        Array<File> getAllSourceFiles() const
        {
            Array<File> files;

            for (const MethodInfo& m : methods)
            {
                files.addIfNotAlreadyThere (m.declaration.file);
                files.addIfNotAlreadyThere (m.definition.file);
            }

            return files;
        }

        bool isDeclaredInFile (const File& file) const
        {
            return file == classDeclaration.file;
        }

        void mergeWith (const Class& other)
        {
            jassert (*this == other);

            if (other.classDeclaration.isValid())
                classDeclaration = other.classDeclaration;

            for (auto& m : other.members)
            {
                if (auto* existing = findMember (m.getName()))
                    existing->mergeWith (m);
                else
                    members.add (m);
            }
        }

        void nudgeAllCodeRanges (const String& file, int index, int delta)
        {
            for (MemberInfo& m : members)   m.nudgeAllCodeRanges (file, index, delta);
            for (MethodInfo& m : methods)   m.nudgeAllCodeRanges (file, index, delta);

            classDeclaration.nudge (file, index, delta);
        }

        void fileContentChanged (const String& file)
        {
            for (MemberInfo& m : members)   m.fileContentChanged (file);
            for (MethodInfo& m : methods)   m.fileContentChanged (file);

            classDeclaration.fileContentChanged (file);
        }

        Class (const ValueTree& v)
        {
            className = v[Ids::name];
            instantiationFlags.isAbstract = v[Ids::abstract];
            instantiationFlags.inAnonymousNamespace = v[Ids::anonymous];
            instantiationFlags.noDefaultConstructor = v[Ids::noDefConstructor];

            classDeclaration = v [Ids::classDecl].toString();

            for (int i = 0; i < v.getNumChildren(); ++i)
                members.add (MemberInfo (v.getChild(i)));
        }

        ValueTree toValueTree() const
        {
            ValueTree v (Ids::CLASS);
            v.setProperty (Ids::name, className, nullptr);
            v.setProperty (Ids::abstract, instantiationFlags.isAbstract, nullptr);
            v.setProperty (Ids::anonymous, instantiationFlags.inAnonymousNamespace, nullptr);
            v.setProperty (Ids::noDefConstructor, instantiationFlags.noDefaultConstructor, nullptr);
            classDeclaration.writeToValueTree (v, Ids::classDecl);

            for (const MemberInfo& m : members)
                v.appendChild (m.toValueTree(), nullptr);

            return v;
        }

        bool operator== (const Class& other) const noexcept  { return className == other.className; }
        bool operator!= (const Class& other) const noexcept  { return ! operator== (other); }
        bool operator<  (const Class& other) const noexcept  { return className < other.className; }

        const Array<MemberInfo>& getMembers() const     { return members; }

    private:
        String className;
        Array<MemberInfo> members;
        Array<MethodInfo> methods;
        SourceCodeRange classDeclaration;
        InstantiationFlags instantiationFlags;

        JUCE_LEAK_DETECTOR (Class)
        JUCE_DECLARE_WEAK_REFERENCEABLE (Class)
    };

    //==============================================================================
    struct Namespace
    {
        Namespace()  : name ("Global Namespace") {}
        Namespace (const String& n, const String& full)  : name (n), fullName (full) {}

        bool isEmpty() const noexcept
        {
            for (const auto& n : namespaces)
                if (! n.isEmpty())
                    return false;

            return components.size() == 0;
        }

        int getTotalClassesAndNamespaces() const
        {
            int total = components.size();

            for (const auto& n : namespaces)
                total += n.getTotalClassesAndNamespaces();

            return total;
        }

        void add (const Class& c, const String::CharPointerType& localName)
        {
            auto nextDoubleColon = CharacterFunctions::find (localName, CharPointer_ASCII ("::"));

            if (nextDoubleColon.isEmpty())
                merge (c);
            else
                getOrCreateNamespace (String (localName, nextDoubleColon))->add (c, nextDoubleColon + 2);
        }

        bool containsRecursively (const Class& c) const
        {
            if (components.contains (c))
                return true;

            for (const auto& n : namespaces)
                if (n.containsRecursively (c))
                    return true;

            return false;
        }

        const Class* findClass (const String& className) const
        {
            for (auto& c : components)
                if (c.getName() == className)
                    return &c;

            for (auto& n : namespaces)
                if (auto* c = n.findClass (className))
                    return c;

            return nullptr;
        }

        const MemberInfo* findClassMemberInfo (const String& className, const String& memberName) const
        {
            if (auto* classInfo = findClass (className))
                return classInfo->findMember (memberName);

            return nullptr;
        }

        void findClassesDeclaredInFile (Array<WeakReference<Class>>& results, const File& file)
        {
            for (int i = 0; i < components.size(); ++i)
            {
                auto& c = components.getReference (i);

                if (c.isDeclaredInFile (file))
                    results.add (&c);
            }

            for (int i = 0; i < namespaces.size(); ++i)
                namespaces.getReference (i).findClassesDeclaredInFile (results, file);
        }

        void merge (const Namespace& other)
        {
            if (components.size() == 0)
            {
                components = other.components;
            }
            else
            {
                for (const auto& c : other.components)
                    merge (c);
            }

            for (const auto& n : other.namespaces)
                getOrCreateNamespace (n.name)->merge (n);
        }

        void merge (const Class& c)
        {
            const int existing = components.indexOf (c);

            if (existing < 0)
                components.add (c);
            else
                components.getReference (existing).mergeWith (c);
        }

        Namespace* findNamespace (const String& targetName)
        {
            for (int i = 0; i < namespaces.size(); ++i)
            {
                auto& n = namespaces.getReference (i);

                if (n.name == targetName)
                    return &n;
            }

            return nullptr;
        }

        Namespace* createNamespace (const String& newName)
        {
            namespaces.add (Namespace (newName, fullName + "::" + newName));
            return findNamespace (newName);
        }

        Namespace* getOrCreateNamespace (const String& newName)
        {
            if (auto* existing = findNamespace (newName))
                return existing;

            return createNamespace (newName);
        }

        void addInstantiableClasses (SortedSet<Class>& classes) const
        {
            for (const auto& c : components)
                if (c.getInstantiationFlags().canBeInstantiated())
                    classes.add (c);

            for (const auto& n : namespaces)
                n.addInstantiableClasses (classes);
        }

        void swapWith (Namespace& other) noexcept
        {
            name.swapWith (other.name);
            components.swapWith (other.components);
            namespaces.swapWith (other.namespaces);
        }

        void nudgeAllCodeRanges (const String& file, int index, int delta)
        {
            for (int i = 0; i < components.size(); ++i)
                components.getReference (i).nudgeAllCodeRanges (file, index, delta);

            for (int i = 0; i < namespaces.size(); ++i)
                namespaces.getReference (i).nudgeAllCodeRanges (file, index, delta);
        }

        void fileContentChanged (const String& file)
        {
            for (int i = 0; i < components.size(); ++i)
                components.getReference (i).fileContentChanged (file);

            for (int i = 0; i < namespaces.size(); ++i)
                namespaces.getReference (i).fileContentChanged (file);
        }

        bool matches (const Namespace& other) const
        {
            if (name == other.name
                 && components == other.components
                 && namespaces.size() == other.namespaces.size())
            {
                for (int i = namespaces.size(); --i >= 0;)
                    if (! namespaces.getReference (i).matches (other.namespaces.getReference (i)))
                        return false;

                return true;
            }

            return false;
        }

        void getAllClassNames (StringArray& results, const InstantiationFlags& disallowedFlags) const
        {
            for (const auto& c : components)
                if (c.getInstantiationFlags().isDisallowed (disallowedFlags))
                    results.add (c.getName());

            for (const auto& n : namespaces)
                n.getAllClassNames (results, disallowedFlags);
        }

        ValueTree toValueTree() const
        {
            ValueTree v (Ids::CLASSLIST);

            v.setProperty (Ids::name, name, nullptr);

            for (const auto& c : components)    v.appendChild (c.toValueTree(), nullptr);
            for (const auto& n : namespaces)    v.appendChild (n.toValueTree(), nullptr);

            return v;
        }

        void loadFromValueTree (const ValueTree& v)
        {
            name = v[Ids::name];

            for (int i = 0; i < v.getNumChildren(); ++i)
            {
                const ValueTree c (v.getChild(i));

                if (c.hasType (Ids::CLASS))
                    components.add (Class (c));
                else if (c.hasType (Ids::CLASSLIST))
                    createNamespace (c[Ids::name])->loadFromValueTree (c);
            }
        }

        bool operator== (const Namespace& other) const noexcept  { return name == other.name; }
        bool operator!= (const Namespace& other) const noexcept  { return ! operator== (other); }
        bool operator<  (const Namespace& other) const noexcept  { return name < other.name; }

        String name, fullName;
        SortedSet<Class> components;
        SortedSet<Namespace> namespaces;

        JUCE_LEAK_DETECTOR (Namespace)
    };

    struct ClassList
    {
        ClassList() {}

        void clear()
        {
            Namespace newNamespace;
            globalNamespace.swapWith (newNamespace);
        }

        void registerComp (const Class& comp)
        {
            globalNamespace.add (comp, comp.getName().getCharPointer());
        }

        void merge (const ClassList& other)
        {
            globalNamespace.merge (other.globalNamespace);
        }

        void swapWith (ClassList& other) noexcept
        {
            globalNamespace.swapWith (other.globalNamespace);
        }

        //==============================================================================
        ValueTree toValueTree() const
        {
            return globalNamespace.toValueTree();
        }

        static ClassList fromValueTree (const ValueTree& v)
        {
            ClassList l;
            l.globalNamespace.loadFromValueTree (v);
            return l;
        }

        Namespace globalNamespace;

        bool operator== (const ClassList& other) const noexcept  { return globalNamespace.matches (other.globalNamespace); }
        bool operator!= (const ClassList& other) const noexcept  { return ! operator== (other); }

    private:
        JUCE_LEAK_DETECTOR (ClassList)
    };
};
