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

   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
{
    Array<Drawable*> asArray (const Icons& icons)
    {
        Array<Drawable*> result;

        if (icons.small != nullptr)
            result.add (icons.small.get());

        if (icons.big != nullptr)
            result.add (icons.big.get());

        return result;
    }

    namespace mac
    {
        static Image fixIconImageSize (Drawable& image)
        {
            const int validSizes[] = { 16, 32, 64, 128, 256, 512, 1024 };

            auto w = image.getWidth();
            auto h = image.getHeight();

            int bestSize = 16;

            for (int size : validSizes)
            {
                if (w == h && w == size)
                {
                    bestSize = w;
                    break;
                }

                if (jmax (w, h) > size)
                    bestSize = size;
            }

            return rescaleImageForIcon (image, bestSize);
        }

        static void writeIconData (MemoryOutputStream& out, const Image& image, const char* type)
        {
            MemoryOutputStream pngData;
            PNGImageFormat pngFormat;
            pngFormat.writeImageToStream (image, pngData);

            out.write (type, 4);
            out.writeIntBigEndian (8 + (int) pngData.getDataSize());
            out << pngData;
        }
    } // namespace mac

    static void writeMacIcon (const Icons& icons, OutputStream& out)
    {
        MemoryOutputStream data;
        auto smallest = std::numeric_limits<int>::max();
        Drawable* smallestImage = nullptr;

        const auto images = asArray (icons);

        for (int i = 0; i < images.size(); ++i)
        {
            auto image = mac::fixIconImageSize (*images[i]);
            jassert (image.getWidth() == image.getHeight());

            if (image.getWidth() < smallest)
            {
                smallest = image.getWidth();
                smallestImage = images[i];
            }

            switch (image.getWidth())
            {
                case 16:   mac::writeIconData (data, image, "icp4"); break;
                case 32:   mac::writeIconData (data, image, "icp5"); break;
                case 64:   mac::writeIconData (data, image, "icp6"); break;
                case 128:  mac::writeIconData (data, image, "ic07"); break;
                case 256:  mac::writeIconData (data, image, "ic08"); break;
                case 512:  mac::writeIconData (data, image, "ic09"); break;
                case 1024: mac::writeIconData (data, image, "ic10"); break;
                default:   break;
            }
        }

        jassert (data.getDataSize() > 0); // no suitable sized images?

        // If you only supply a 1024 image, the file doesn't work on 10.8, so we need
        // to force a smaller one in there too..
        if (smallest > 512 && smallestImage != nullptr)
            mac::writeIconData (data, rescaleImageForIcon (*smallestImage, 512), "ic09");

        out.write ("icns", 4);
        out.writeIntBigEndian ((int) data.getDataSize() + 8);
        out << data;
    }

    Image getBestIconForSize (const Icons& icons,
                              int size,
                              bool returnNullIfNothingBigEnough)
    {
        auto* const im = [&]() -> Drawable*
        {
            if ((icons.small != nullptr) != (icons.big != nullptr))
                return icons.small != nullptr ? icons.small.get() : icons.big.get();

            if (icons.small != nullptr && icons.big != nullptr)
            {
                if (icons.small->getWidth() >= size && icons.big->getWidth() >= size)
                    return icons.small->getWidth() < icons.big->getWidth() ? icons.small.get() : icons.big.get();

                if (icons.small->getWidth() >= size)
                    return icons.small.get();

                if (icons.big->getWidth() >= size)
                    return icons.big.get();
            }

            return nullptr;
        }();

        if (im == nullptr)
            return {};

        if (returnNullIfNothingBigEnough && im->getWidth() < size && im->getHeight() < size)
            return {};

        return rescaleImageForIcon (*im, size);
    }

    namespace win
    {
        static void writeBMPImage (const Image& image, const int w, const int h, MemoryOutputStream& out)
        {
            int maskStride = (w / 8 + 3) & ~3;

            out.writeInt (40); // bitmapinfoheader size
            out.writeInt (w);
            out.writeInt (h * 2);
            out.writeShort (1); // planes
            out.writeShort (32); // bits
            out.writeInt (0); // compression
            out.writeInt ((h * w * 4) + (h * maskStride)); // size image
            out.writeInt (0); // x pixels per meter
            out.writeInt (0); // y pixels per meter
            out.writeInt (0); // clr used
            out.writeInt (0); // clr important

            Image::BitmapData bitmap (image, Image::BitmapData::readOnly);
            int alphaThreshold = 5;

            int y;
            for (y = h; --y >= 0;)
            {
                for (int x = 0; x < w; ++x)
                {
                    auto pixel = bitmap.getPixelColour (x, y);

                    if (pixel.getAlpha() <= alphaThreshold)
                    {
                        out.writeInt (0);
                    }
                    else
                    {
                        out.writeByte ((char) pixel.getBlue());
                        out.writeByte ((char) pixel.getGreen());
                        out.writeByte ((char) pixel.getRed());
                        out.writeByte ((char) pixel.getAlpha());
                    }
                }
            }

            for (y = h; --y >= 0;)
            {
                int mask = 0, count = 0;

                for (int x = 0; x < w; ++x)
                {
                    auto pixel = bitmap.getPixelColour (x, y);

                    mask <<= 1;
                    if (pixel.getAlpha() <= alphaThreshold)
                        mask |= 1;

                    if (++count == 8)
                    {
                        out.writeByte ((char) mask);
                        count = 0;
                        mask = 0;
                    }
                }

                if (mask != 0)
                    out.writeByte ((char) mask);

                for (int i = maskStride - w / 8; --i >= 0;)
                    out.writeByte (0);
            }
        }

        static void writeIcon (const Array<Image>& images, OutputStream& out)
        {
            out.writeShort (0); // reserved
            out.writeShort (1); // .ico tag
            out.writeShort ((short) images.size());

            MemoryOutputStream dataBlock;

            int imageDirEntrySize = 16;
            int dataBlockStart = 6 + images.size() * imageDirEntrySize;

            for (int i = 0; i < images.size(); ++i)
            {
                auto oldDataSize = dataBlock.getDataSize();

                auto& image = images.getReference (i);
                auto w = image.getWidth();
                auto h = image.getHeight();

                if (w >= 256 || h >= 256)
                {
                    PNGImageFormat pngFormat;
                    pngFormat.writeImageToStream (image, dataBlock);
                }
                else
                {
                    writeBMPImage (image, w, h, dataBlock);
                }

                out.writeByte ((char) w);
                out.writeByte ((char) h);
                out.writeByte (0);
                out.writeByte (0);
                out.writeShort (1); // colour planes
                out.writeShort (32); // bits per pixel
                out.writeInt ((int) (dataBlock.getDataSize() - oldDataSize));
                out.writeInt (dataBlockStart + (int) oldDataSize);
            }

            jassert (out.getPosition() == dataBlockStart);
            out << dataBlock;
        }
    } // namespace win

    static void writeWinIcon (const Icons& icons, OutputStream& os)
    {
        Array<Image> images;
        int sizes[] = { 16, 32, 48, 256 };

        for (int size : sizes)
        {
            auto im = getBestIconForSize (icons, size, true);

            if (im.isValid())
                images.add (im);
        }

        if (images.size() > 0)
            win::writeIcon (images, os);
    }

    void writeMacIcon (const Icons& icons, const File& file)
    {
        writeStreamToFile (file, [&] (MemoryOutputStream& mo) { writeMacIcon (icons, mo); });
    }

    void writeWinIcon (const Icons& icons, const File& file)
    {
        writeStreamToFile (file, [&] (MemoryOutputStream& mo) { writeWinIcon (icons, mo); });
    }

    Image rescaleImageForIcon (Drawable& d, const int size)
    {
        if (auto* drawableImage = dynamic_cast<DrawableImage*> (&d))
        {
            auto im = SoftwareImageType().convert (drawableImage->getImage());

            if (im.getWidth() == size && im.getHeight() == size)
                return im;

            // (scale it down in stages for better resampling)
            while (im.getWidth() > 2 * size && im.getHeight() > 2 * size)
                im = im.rescaled (im.getWidth() / 2,
                                  im.getHeight() / 2);

            Image newIm (Image::ARGB, size, size, true, SoftwareImageType());
            Graphics g (newIm);
            g.drawImageWithin (im, 0, 0, size, size,
                               RectanglePlacement::centred | RectanglePlacement::onlyReduceInSize, false);
            return newIm;
        }

        Image im (Image::ARGB, size, size, true, SoftwareImageType());
        Graphics g (im);
        d.drawWithin (g, im.getBounds().toFloat(), RectanglePlacement::centred, 1.0f);
        return im;
    }

    struct AppIconType
    {
        const char* idiom;
        const char* sizeString;
        const char* filename;
        const char* scale;
        int size;
    };

    static const AppIconType iOSAppIconTypes[]
    {
        { "iphone",          "20x20",     "Icon-Notification-20@2x.png",       "2x", 40   },
        { "iphone",          "20x20",     "Icon-Notification-20@3x.png",       "3x", 60   },
        { "iphone",          "29x29",     "Icon-29.png",                       "1x", 29   },
        { "iphone",          "29x29",     "Icon-29@2x.png",                    "2x", 58   },
        { "iphone",          "29x29",     "Icon-29@3x.png",                    "3x", 87   },
        { "iphone",          "40x40",     "Icon-Spotlight-40@2x.png",          "2x", 80   },
        { "iphone",          "40x40",     "Icon-Spotlight-40@3x.png",          "3x", 120  },
        { "iphone",          "60x60",     "Icon-60@2x.png",                    "2x", 120  },
        { "iphone",          "60x60",     "Icon-@3x.png",                      "3x", 180  },
        { "ipad",            "20x20",     "Icon-Notifications-20.png",         "1x", 20   },
        { "ipad",            "20x20",     "Icon-Notifications-20@2x.png",      "2x", 40   },
        { "ipad",            "29x29",     "Icon-Small-1.png",                  "1x", 29   },
        { "ipad",            "29x29",     "Icon-Small@2x-1.png",               "2x", 58   },
        { "ipad",            "40x40",     "Icon-Spotlight-40.png",             "1x", 40   },
        { "ipad",            "40x40",     "Icon-Spotlight-40@2x-1.png",        "2x", 80   },
        { "ipad",            "76x76",     "Icon-76.png",                       "1x", 76   },
        { "ipad",            "76x76",     "Icon-76@2x.png",                    "2x", 152  },
        { "ipad",            "83.5x83.5", "Icon-83.5@2x.png",                  "2x", 167  },
        { "ios-marketing",   "1024x1024", "Icon-AppStore-1024.png",            "1x", 1024 }
    };

    static void createiOSIconFiles (const Icons& icons, File appIconSet)
    {
        auto* imageToUse = icons.big != nullptr ? icons.big.get()
                                                : icons.small.get();

        if (imageToUse != nullptr)
        {
            for (auto& type : iOSAppIconTypes)
            {
                auto image = rescaleImageForIcon (*imageToUse, type.size);

                if (image.hasAlphaChannel())
                {
                    Image background (Image::RGB, image.getWidth(), image.getHeight(), false);
                    Graphics g (background);
                    g.fillAll (Colours::white);

                    g.drawImageWithin (image, 0, 0, image.getWidth(), image.getHeight(),
                                       RectanglePlacement::centred | RectanglePlacement::onlyReduceInSize);

                    image = background;
                }

                MemoryOutputStream pngData;
                PNGImageFormat pngFormat;
                pngFormat.writeImageToStream (image, pngData);

                overwriteFileIfDifferentOrThrow (appIconSet.getChildFile (type.filename), pngData);
            }
        }
    }

    static String getiOSAssetContents (var images)
    {
        DynamicObject::Ptr v (new DynamicObject());

        var info (new DynamicObject());
        info.getDynamicObject()->setProperty ("version", 1);
        info.getDynamicObject()->setProperty ("author", "xcode");

        v->setProperty ("images", images);
        v->setProperty ("info", info);

        return JSON::toString (var (v.get()));
    }

    //==============================================================================
    static String getiOSAppIconContents()
    {
        var images;

        for (auto& type : iOSAppIconTypes)
        {
            DynamicObject::Ptr d (new DynamicObject());
            d->setProperty ("idiom",    type.idiom);
            d->setProperty ("size",     type.sizeString);
            d->setProperty ("filename", type.filename);
            d->setProperty ("scale",    type.scale);
            images.append (var (d.get()));
        }

        return getiOSAssetContents (images);
    }

    struct ImageType
    {
        const char* orientation;
        const char* idiom;
        const char* subtype;
        const char* extent;
        const char* scale;
        const char* filename;
        int width;
        int height;
    };

    static const ImageType iOSLaunchImageTypes[]
    {
        { "portrait", "iphone", nullptr,      "full-screen", "2x", "LaunchImage-iphone-2x.png",         640, 960 },
        { "portrait", "iphone", "retina4",    "full-screen", "2x", "LaunchImage-iphone-retina4.png",    640, 1136 },
        { "portrait", "ipad",   nullptr,      "full-screen", "1x", "LaunchImage-ipad-portrait-1x.png",  768, 1024 },
        { "landscape","ipad",   nullptr,      "full-screen", "1x", "LaunchImage-ipad-landscape-1x.png", 1024, 768 },
        { "portrait", "ipad",   nullptr,      "full-screen", "2x", "LaunchImage-ipad-portrait-2x.png",  1536, 2048 },
        { "landscape","ipad",   nullptr,      "full-screen", "2x", "LaunchImage-ipad-landscape-2x.png", 2048, 1536 }
    };

    static void createiOSLaunchImageFiles (const File& launchImageSet)
    {
        for (auto& type : iOSLaunchImageTypes)
        {
            Image image (Image::ARGB, type.width, type.height, true); // (empty black image)
            image.clear (image.getBounds(), Colours::black);

            MemoryOutputStream pngData;
            PNGImageFormat pngFormat;
            pngFormat.writeImageToStream (image, pngData);
            build_tools::overwriteFileIfDifferentOrThrow (launchImageSet.getChildFile (type.filename), pngData);
        }
    }

    static String getiOSLaunchImageContents()
    {
        var images;

        for (auto& type : iOSLaunchImageTypes)
        {
            DynamicObject::Ptr d (new DynamicObject());
            d->setProperty ("orientation", type.orientation);
            d->setProperty ("idiom", type.idiom);
            d->setProperty ("extent",  type.extent);
            d->setProperty ("minimum-system-version", "7.0");
            d->setProperty ("scale", type.scale);
            d->setProperty ("filename", type.filename);

            if (type.subtype != nullptr)
                d->setProperty ("subtype", type.subtype);

            images.append (var (d.get()));
        }

        return getiOSAssetContents (images);
    }

    RelativePath createXcassetsFolderFromIcons (const Icons& icons,
                                                const File& targetFolder,
                                                String projectFilenameRootString)
    {
        const auto assets      = targetFolder.getChildFile (projectFilenameRootString)
                                             .getChildFile ("Images.xcassets");
        const auto iconSet     = assets.getChildFile ("AppIcon.appiconset");
        const auto launchImage = assets.getChildFile ("LaunchImage.launchimage");

        overwriteFileIfDifferentOrThrow (iconSet.getChildFile ("Contents.json"), getiOSAppIconContents());
        createiOSIconFiles (icons, iconSet);

        overwriteFileIfDifferentOrThrow (launchImage.getChildFile ("Contents.json"), getiOSLaunchImageContents());
        createiOSLaunchImageFiles (launchImage);

        return { assets, targetFolder, RelativePath::buildTargetFolder };
    }
}
}
