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

   This file is part of the JUCE library - "Jules' Utility Class Extensions"
   Copyright 2004-9 by Raw Material Software Ltd.

  ------------------------------------------------------------------------------

   JUCE can be redistributed and/or modified under the terms of the GNU General
   Public License (Version 2), as published by the Free Software Foundation.
   A copy of the license is included in the JUCE distribution, or can be found
   online at www.gnu.org/licenses.

   JUCE is distributed in the hope that it will be useful, but WITHOUT ANY
   WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
   A PARTICULAR PURPOSE.  See the GNU General Public License for more details.

  ------------------------------------------------------------------------------

   To release a closed-source product which uses JUCE, commercial licenses are
   available: visit www.rawmaterialsoftware.com/juce for more information.

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

#ifndef __JUCER_PROJECTSAVER_JUCEHEADER__
#define __JUCER_PROJECTSAVER_JUCEHEADER__


//==============================================================================
class ProjectSaver
{
public:
    ProjectSaver (Project& project_, const File& projectFile_)
        : project (project_), projectFile (projectFile_), resourceFile (project_)
    {
    }

    const String save()
    {
        const File oldFile (project.getFile());
        project.setFile (projectFile);

        switch (project.getJuceLinkageMode())
        {
        case Project::notLinkedToJuce:
            hasAppHeaderFile = ! project.isLibrary();
            hasAppConfigFile = false;
            numJuceSourceFiles = 0;
            break;

        case Project::useAmalgamatedJuce:
        case Project::useAmalgamatedJuceViaSingleTemplate:
            hasAppHeaderFile = true;
            hasAppConfigFile = true;
            numJuceSourceFiles = 1;
            break;

        case Project::useAmalgamatedJuceViaMultipleTemplates:
            hasAppHeaderFile = true;
            hasAppConfigFile = true;
            numJuceSourceFiles = project.getNumSeparateAmalgamatedFiles();
            break;

        case Project::useLinkedJuce:
            hasAppHeaderFile = true;
            hasAppConfigFile = true;
            numJuceSourceFiles = 0;
            break;

        default:
            jassertfalse;
            break;
        }

        hasResources = (resourceFile.getNumFiles() > 0);

        writeMainProjectFile();

        if (errors.size() == 0)
            writeJuceSourceWrappers();

        if (errors.size() == 0)
            writeProjects();

        if (errors.size() > 0)
            project.setFile (oldFile);

        return errors[0];
    }

private:
    Project& project;
    const File& projectFile;
    ResourceFile resourceFile;
    StringArray errors;

    File appConfigFile, juceHeaderFile, binaryDataCpp, pluginCharacteristicsFile;
    bool hasAppHeaderFile, hasAppConfigFile, hasResources;
    int numJuceSourceFiles;

    void writeMainProjectFile()
    {
        ScopedPointer <XmlElement> xml (project.getProjectRoot().createXml());
        jassert (xml != 0);

        if (xml != 0)
        {
            #if JUCE_DEBUG
            {
                MemoryOutputStream mo;
                project.getProjectRoot().writeToStream (mo);

                MemoryInputStream mi (mo.getData(), mo.getDataSize(), false);
                ValueTree v = ValueTree::readFromStream (mi);
                ScopedPointer <XmlElement> xml2 (v.createXml());

                // This bit just tests that ValueTree save/load works reliably.. Let me know if this asserts for you!
                jassert (xml->isEquivalentTo (xml2, true));
            }
            #endif

            MemoryOutputStream mo;
            xml->writeToStream (mo, String::empty);

            if (! overwriteFileWithNewDataIfDifferent (projectFile, mo))
                errors.add ("Couldn't write to the target file!");
        }
    }

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

    void writeAppConfig (OutputStream& out)
    {
        writeJucerComment (out);
        out << "    If you want to change any of these values, use the Jucer to do so, rather than" << newLine
            << "    editing this file directly!" << newLine
            << newLine
            << "    Any commented-out settings will fall back to using the default values that" << newLine
            << "    they are given in juce_Config.h" << newLine
            << newLine
            << "*/" << newLine << newLine;

        bool notActive = project.getJuceLinkageMode() == Project::useLinkedJuce
                            || project.getJuceLinkageMode() == Project::notLinkedToJuce;
        if (notActive)
            out << "/* NOTE: These configs aren't available when you're linking to the juce library statically!" << newLine
                << "         If you need to set a configuration that differs from the default, you'll need" << newLine
                << "         to include the amalgamated Juce files." << newLine << newLine;

        OwnedArray <Project::JuceConfigFlag> flags;
        project.getJuceConfigFlags (flags);

        for (int i = 0; i < flags.size(); ++i)
        {
            const Project::JuceConfigFlag* const f = flags[i];
            int value = (int) f->value.getValue();

            if (value < 1 || value > 2)
                out << "//#define  ";
            else
                out << "#define    ";

            out << f->symbol;

            if (value == 1)
                out << " 1";
            else if (value == 2)
                out << " 0";

            out << newLine;
        }

        if (notActive)
            out << newLine << "*/" << newLine;
    }

    void writeSourceWrapper (OutputStream& out, int fileNumber)
    {
        writeJucerComment (out);
        out << "    This file pulls in all the Juce source code, and builds it using the settings" << newLine
            << "    defined in " << appConfigFile.getFileName() << "." << newLine
            << newLine
            << "    If you want to change the method by which Juce is linked into your app, use the" << newLine
            << "    Jucer to change it, rather than trying to edit this file directly." << newLine
            << newLine
            << "*/"
            << newLine << newLine
            << createIncludeStatement (appConfigFile, appConfigFile) << newLine;

        if (fileNumber == 0)
            writeInclude (out, project.isUsingFullyAmalgamatedFile() ? "juce_amalgamated.cpp"
                                                                     : "amalgamation/juce_amalgamated_template.cpp");
        else
            writeInclude (out, "amalgamation/juce_amalgamated" + String (fileNumber) + ".cpp");
    }

    void writeAppHeader (OutputStream& out)
    {
        writeJucerComment (out);
        out << "    This is the header file that your files should include in order to get all the" << newLine
            << "    Juce library headers. You should NOT include juce.h or juce_amalgamated.h directly in" << newLine
            << "    your own source files, because that wouldn't pick up the correct Juce configuration" << newLine
            << "    options for your app." << newLine
            << newLine
            << "*/" << newLine << newLine;

        String headerGuard ("__APPHEADERFILE_" + String::toHexString (juceHeaderFile.hashCode()).toUpperCase() + "__");
        out << "#ifndef " << headerGuard << newLine
            << "#define " << headerGuard << newLine << newLine;

        if (hasAppConfigFile)
            out << createIncludeStatement (appConfigFile, appConfigFile) << newLine;

        if (project.getJuceLinkageMode() != Project::notLinkedToJuce)
        {
            writeInclude (out, (project.isUsingSingleTemplateFile() || project.isUsingMultipleTemplateFiles())
                                   ? "juce_amalgamated.h"  // could use "amalgamation/juce_amalgamated_template.h", but it's slower..
                                   : (project.isUsingFullyAmalgamatedFile()
                                        ? "juce_amalgamated.h"
                                        : "juce.h"));
        }

        if (binaryDataCpp.exists())
            out << createIncludeStatement (binaryDataCpp.withFileExtension (".h"), appConfigFile) << newLine;

        out << newLine
            << "namespace ProjectInfo" << newLine
            << "{" << newLine
            << "    const char* const  projectName    = " << replaceCEscapeChars (project.getProjectName().toString()).quoted() << ";" << newLine
            << "    const char* const  versionString  = " << replaceCEscapeChars (project.getVersion().toString()).quoted() << ";" << newLine
            << "    const int          versionNumber  = " << createVersionCode (project.getVersion().toString()) << ";" << newLine
            << "}" << newLine
            << newLine
            << "#endif   // " << headerGuard << newLine;
    }

    void writeInclude (OutputStream& out, const String& pathFromJuceFolder)
    {
        StringArray paths, guards;

        for (int i = project.getNumExporters(); --i >= 0;)
        {
            ScopedPointer <ProjectExporter> exporter (project.createExporter (i));

            if (exporter != 0)
            {
                const RelativePath juceFromProject (exporter->getJuceFolder().toString(), RelativePath::projectFolder);
                const RelativePath fileFromProject (juceFromProject.getChildFile (pathFromJuceFolder));
                const RelativePath fileFromHere (fileFromProject.rebased (project.getFile().getParentDirectory(),
                                                                          juceHeaderFile.getParentDirectory(), RelativePath::unknown));

                paths.add (fileFromHere.toUnixStyle());
                guards.add (exporter->getOSTestMacro());
            }
        }

        StringArray uniquePaths (paths);
        uniquePaths.removeDuplicates (false);

        if (uniquePaths.size() == 1)
        {
            out << "#include " << paths[0].quoted() << newLine;
        }
        else
        {
            int i = paths.size();
            for (; --i >= 0;)
            {
                for (int j = i; --j >= 0;)
                {
                    if (paths[i] == paths[j] && guards[i] == guards[j])
                    {
                        paths.remove (i);
                        guards.remove (i);
                    }
                }
            }

            for (i = 0; i < paths.size(); ++i)
            {
                out << (i == 0 ? "#if " : "#elif ") << guards[i] << newLine
                    << " #include " << paths[i].quoted() << newLine;
            }

            out << "#endif" << newLine;
        }
    }

    static int countMaxPluginChannels (const String& configString, bool isInput)
    {
        StringArray configs;
        configs.addTokens (configString, ", {}", String::empty);
        configs.trim();
        configs.removeEmptyStrings();
        jassert ((configs.size() & 1) == 0);  // looks like a syntax error in the configs?

        int maxVal = 0;
        for (int i = (isInput ? 0 : 1); i < configs.size(); i += 2)
            maxVal = jmax (maxVal, configs[i].getIntValue());

        return maxVal;
    }

    static const String createVersionCode (const String& version)
    {
        StringArray configs;
        configs.addTokens (version, ",.", String::empty);
        configs.trim();
        configs.removeEmptyStrings();

        int value = (configs[0].getIntValue() << 16) + (configs[1].getIntValue() << 8) + configs[2].getIntValue();

        if (configs.size() >= 4)
            value = (value << 8) + configs[3].getIntValue();

        return "0x" + String::toHexString (value);
    }

    void writePluginCharacteristics (OutputStream& out)
    {
        String headerGuard ("__PLUGINCHARACTERISTICS_" + String::toHexString (pluginCharacteristicsFile.hashCode()).toUpperCase() + "__");

        writeJucerComment (out);
        out << "    This header file contains configuration options for the plug-in. If you need to change any of" << newLine
            << "    these, it'd be wise to do so using the Jucer, rather than editing this file directly..." << newLine
            << newLine
            << "*/" << newLine
            << newLine
            << "#ifndef " << headerGuard << newLine
            << "#define " << headerGuard << newLine
            << newLine
            << "#define JucePlugin_Build_VST    " << ((bool) project.shouldBuildVST().getValue() ? 1 : 0) << newLine
            << "#define JucePlugin_Build_AU     " << ((bool) project.shouldBuildAU().getValue() ? 1 : 0) << newLine
            << "#define JucePlugin_Build_RTAS   " << ((bool) project.shouldBuildRTAS().getValue() ? 1 : 0) << newLine
            << newLine
            << "#define JucePlugin_Name                 " << project.getPluginName().toString().quoted() << newLine
            << "#define JucePlugin_Desc                 " << project.getPluginDesc().toString().quoted() << newLine
            << "#define JucePlugin_Manufacturer         " << project.getPluginManufacturer().toString().quoted() << newLine
            << "#define JucePlugin_ManufacturerCode     '" << project.getPluginManufacturerCode().toString().trim().substring (0, 4) << "'" << newLine
            << "#define JucePlugin_PluginCode           '" << project.getPluginCode().toString().trim().substring (0, 4) << "'" << newLine
            << "#define JucePlugin_MaxNumInputChannels  " << countMaxPluginChannels (project.getPluginChannelConfigs().toString(), true) << newLine
            << "#define JucePlugin_MaxNumOutputChannels " << countMaxPluginChannels (project.getPluginChannelConfigs().toString(), false) << newLine
            << "#define JucePlugin_PreferredChannelConfigurations   " << project.getPluginChannelConfigs().toString() << newLine
            << "#define JucePlugin_IsSynth              " << ((bool) project.getPluginIsSynth().getValue() ? 1 : 0) << newLine
            << "#define JucePlugin_WantsMidiInput       " << ((bool) project.getPluginWantsMidiInput().getValue() ? 1 : 0) << newLine
            << "#define JucePlugin_ProducesMidiOutput   " << ((bool) project.getPluginProducesMidiOut().getValue() ? 1 : 0) << newLine
            << "#define JucePlugin_SilenceInProducesSilenceOut  " << ((bool) project.getPluginSilenceInProducesSilenceOut().getValue() ? 1 : 0) << newLine
            << "#define JucePlugin_TailLengthSeconds    " << (double) project.getPluginTailLengthSeconds().getValue() << newLine
            << "#define JucePlugin_EditorRequiresKeyboardFocus  " << ((bool) project.getPluginEditorNeedsKeyFocus().getValue() ? 1 : 0) << newLine
            << "#define JucePlugin_VersionCode          " << createVersionCode (project.getVersion().toString()) << newLine
            << "#define JucePlugin_VersionString        " << project.getVersion().toString().quoted() << newLine
            << "#define JucePlugin_VSTUniqueID          JucePlugin_PluginCode" << newLine
            << "#define JucePlugin_VSTCategory          " << ((bool) project.getPluginIsSynth().getValue() ? "kPlugCategSynth" : "kPlugCategEffect") << newLine
            << "#define JucePlugin_AUMainType           " << ((bool) project.getPluginIsSynth().getValue() ? "kAudioUnitType_MusicDevice" : "kAudioUnitType_Effect") << newLine
            << "#define JucePlugin_AUSubType            JucePlugin_PluginCode" << newLine
            << "#define JucePlugin_AUExportPrefix       " << project.getPluginAUExportPrefix().toString() << newLine
            << "#define JucePlugin_AUExportPrefixQuoted " << project.getPluginAUExportPrefix().toString().quoted() << newLine
            << "#define JucePlugin_AUManufacturerCode   JucePlugin_ManufacturerCode" << newLine
            << "#define JucePlugin_CFBundleIdentifier   " << project.getBundleIdentifier().toString() << newLine
            << "#define JucePlugin_AUCocoaViewClassName " << project.getPluginAUCocoaViewClassName().toString() << newLine
            << "#define JucePlugin_RTASCategory         " << ((bool) project.getPluginIsSynth().getValue() ? "ePlugInCategory_SWGenerators" : "ePlugInCategory_None") << newLine
            << "#define JucePlugin_RTASManufacturerCode JucePlugin_ManufacturerCode" << newLine
            << "#define JucePlugin_RTASProductId        JucePlugin_PluginCode" << newLine
            << "#define JUCE_USE_VSTSDK_2_4             1" << newLine
            << newLine
            << "#endif   // " << headerGuard << newLine;
    }

    bool replaceFileIfDifferent (const File& f, const MemoryOutputStream& newData)
    {
        if (! overwriteFileWithNewDataIfDifferent (f, newData))
        {
            errors.add ("Can't write to file: " + f.getFullPathName());
            return false;
        }

        return true;
    }

    void writeJuceSourceWrappers()
    {
        const File wrapperFolder (project.getWrapperFolder());

        appConfigFile = wrapperFolder.getChildFile (project.getAppConfigFilename());
        pluginCharacteristicsFile = wrapperFolder.getChildFile (project.getPluginCharacteristicsFilename());

        juceHeaderFile = project.getAppIncludeFile();
        binaryDataCpp = wrapperFolder.getChildFile ("BinaryData.cpp");

        if (resourceFile.getNumFiles() > 0)
        {
            if (! wrapperFolder.createDirectory())
            {
                errors.add ("Couldn't create folder: " + wrapperFolder.getFullPathName());
                return;
            }

            //resourceFile.setJuceHeaderToInclude (juceHeaderFile);
            resourceFile.setClassName ("BinaryData");

            if (! resourceFile.write (binaryDataCpp))
                errors.add ("Can't create binary resources file: " + binaryDataCpp.getFullPathName());
        }
        else
        {
            binaryDataCpp.deleteFile();
            binaryDataCpp.withFileExtension ("h").deleteFile();
        }

        if (project.isLibrary())
            return;

        if (! wrapperFolder.createDirectory())
        {
            errors.add ("Couldn't create folder: " + wrapperFolder.getFullPathName());
            return;
        }

        if (hasAppConfigFile)
        {
            MemoryOutputStream mem;
            writeAppConfig (mem);
            replaceFileIfDifferent (appConfigFile, mem);
        }
        else
        {
            appConfigFile.deleteFile();
        }

        if (project.isAudioPlugin())
        {
            MemoryOutputStream mem;
            writePluginCharacteristics (mem);
            replaceFileIfDifferent (pluginCharacteristicsFile, mem);
        }

        for (int i = 0; i <= project.getNumSeparateAmalgamatedFiles(); ++i)
        {
            const File sourceWrapperCpp (getSourceWrapperCpp (i));
            const File sourceWrapperMM (sourceWrapperCpp.withFileExtension (".mm"));

            if (numJuceSourceFiles > 0
                 && ((i == 0 && numJuceSourceFiles == 1) || (i != 0 && numJuceSourceFiles > 1)))
            {
                MemoryOutputStream mem;
                writeSourceWrapper (mem, i);
                replaceFileIfDifferent (sourceWrapperCpp, mem);
                replaceFileIfDifferent (sourceWrapperMM, mem);
            }
            else
            {
                sourceWrapperMM.deleteFile();
                sourceWrapperCpp.deleteFile();
            }
        }

        if (hasAppHeaderFile)
        {
            MemoryOutputStream mem;
            writeAppHeader (mem);
            replaceFileIfDifferent (juceHeaderFile, mem);
        }
        else
        {
            juceHeaderFile.deleteFile();
        }
    }

    void writeProjects()
    {
        for (int i = project.getNumExporters(); --i >= 0;)
        {
            ScopedPointer <ProjectExporter> exporter (project.createExporter (i));
            std::cout << "Writing files for: " << exporter->getName() << std::endl;

            const File targetFolder (exporter->getTargetFolder());

            if (targetFolder.createDirectory())
            {
                if (hasAppConfigFile)
                    exporter->juceWrapperFiles.add (RelativePath (appConfigFile, targetFolder, RelativePath::buildTargetFolder));

                if (hasAppHeaderFile)
                    exporter->juceWrapperFiles.add (RelativePath (juceHeaderFile, targetFolder, RelativePath::buildTargetFolder));

                if (hasResources)
                {
                    exporter->juceWrapperFiles.add (RelativePath (binaryDataCpp, targetFolder, RelativePath::buildTargetFolder));
                    exporter->juceWrapperFiles.add (RelativePath (binaryDataCpp, targetFolder, RelativePath::buildTargetFolder)
                                                        .withFileExtension (".h"));
                }

                if (numJuceSourceFiles > 0)
                {
                    for (int i = 0; i <= project.getNumSeparateAmalgamatedFiles(); ++i)
                    {
                        const File sourceWrapperCpp (getSourceWrapperCpp (i));
                        const File sourceWrapperMM (sourceWrapperCpp.withFileExtension (".mm"));

                        if ((i == 0 && numJuceSourceFiles == 1) || (i != 0 && numJuceSourceFiles > 1))
                        {
                            if (exporter->usesMMFiles())
                                exporter->juceWrapperFiles.add (RelativePath (sourceWrapperMM, targetFolder, RelativePath::buildTargetFolder));
                            else
                                exporter->juceWrapperFiles.add (RelativePath (sourceWrapperCpp, targetFolder, RelativePath::buildTargetFolder));
                        }
                    }
                }

                if (project.isAudioPlugin())
                    exporter->juceWrapperFiles.add (RelativePath (pluginCharacteristicsFile, targetFolder, RelativePath::buildTargetFolder));

                String error = exporter->create();

                if (error.isNotEmpty())
                    errors.add (error);
            }
            else
            {
                errors.add ("Can't create folder: " + exporter->getTargetFolder().getFullPathName());
            }
        }
    }

    const File getSourceWrapperCpp (int fileIndex) const
    {
        return project.getWrapperFolder().getChildFile (project.getJuceSourceFilenameRoot() + (fileIndex != 0 ? String (fileIndex) : String::empty))
                                         .withFileExtension (".cpp");
    }
};


#endif
