/*
 *  The Mana Client
 *  Copyright (C) 2004-2009  The Mana World Development Team
 *  Copyright (C) 2009-2012  The Mana Developers
 *
 *  This file is part of The Mana Client.
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation; either version 2 of the License, or
 *  any later version.
 *
 *  This program 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.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

#include "gui/setup_video.h"

#include "client.h"
#include "configuration.h"
#include "game.h"
#include "graphics.h"
#include "localplayer.h"
#include "particle.h"

#include "gui/okdialog.h"

#include "gui/widgets/checkbox.h"
#include "gui/widgets/dropdown.h"
#include "gui/widgets/label.h"
#include "gui/widgets/layout.h"
#include "gui/widgets/slider.h"
#include "gui/widgets/spacer.h"

#include "utils/gettext.h"
#include "utils/stringutils.h"

#include <guichan/key.hpp>
#include <guichan/listmodel.hpp>

#include <SDL.h>

#include <numeric>
#include <string>
#include <vector>

/**
 * A list model for a given list of strings.
 *
 * \ingroup Interface
 */
class StringListModel : public gcn::ListModel
{
public:
    StringListModel(std::vector<std::string> strings)
        : mStrings(std::move(strings))
    {}

    int getNumberOfElements() override
    {
        return mStrings.size();
    }

    std::string getElementAt(int i) override
    {
        return mStrings[i];
    }

private:
    const std::vector<std::string> mStrings;
};

/**
 * The list model for mode list.
 *
 * \ingroup Interface
 */
class ResolutionListModel : public gcn::ListModel
{
public:
    ResolutionListModel()
    {
        mDisplayModes = Client::getVideo().displayModes();

        // Add a dummy mode for "current window size"
        mDisplayModes.insert(mDisplayModes.begin(), DisplayMode());
    }

    int getNumberOfElements() override
    {
        return mDisplayModes.size();
    }

    std::string getElementAt(int i) override
    {
        if (i == 0)
            return _("Custom");

        const auto &mode = getModeAt(i);
        auto result = toString(mode.width) + "x" + toString(mode.height);

        // Append the aspect ratio
        const int gcd = std::gcd(mode.width, mode.height);
        int aspectWidth = mode.width / gcd;
        int aspectHeight = mode.height / gcd;
        if (aspectWidth == 8 && aspectHeight == 5)
        {
            aspectWidth = 16;
            aspectHeight = 10;
        }
        if (aspectWidth == 7 && aspectHeight == 3)
        {
            aspectWidth = 21;
            aspectHeight = 9;
        }
        if (aspectWidth <= 32)
            result += "  (" + toString(aspectWidth) + ":" + toString(aspectHeight) + ")";

        return result;
    }

    const DisplayMode &getModeAt(int i) const
    {
        return mDisplayModes.at(i);
    }

    /**
     * Returns the index corresponding to the given video resolution
     * or 0 ("Custom") if not found.
     */
    int getIndexOf(int width, int height) const
    {
        for (unsigned i = 1; i < mDisplayModes.size(); i++) {
            const auto &mode = mDisplayModes[i];
            if (mode.width == width && mode.height == height)
                return i;
        }

        return 0;
    }

private:
    std::vector<DisplayMode> mDisplayModes;
};

/**
 * The list model for choosing the scale.
 *
 * \ingroup Interface
 */
class ScaleListModel : public gcn::ListModel
{
public:
    ScaleListModel(const VideoSettings &videoSettings)
        : mVideoSettings(videoSettings)
    {}

    void setVideoSettings(const VideoSettings &videoSettings)
    {
        mVideoSettings = videoSettings;
    }

    int getNumberOfElements() override
    {
        return mVideoSettings.maxScale() + 1;
    }

    std::string getElementAt(int i) override
    {
        if (i == 0)
            return strprintf(_("Auto (%dx)"), mVideoSettings.autoScale());

        return strprintf(_("%dx"), i);
    }

private:
    VideoSettings mVideoSettings;
};


static const char *overlayDetailToString(int detail)
{
    if (detail == -1)
        detail = config.overlayDetail;

    switch (detail)
    {
        case 0: return _("off");
        case 1: return _("low");
        case 2: return _("high");
    }
    return "";
}

static const char *particleDetailToString(int detail)
{
    if (detail == -1)
        detail = 3 - config.particleEmitterSkip;

    switch (detail)
    {
        case 0: return _("low");
        case 1: return _("medium");
        case 2: return _("high");
        case 3: return _("max");
    }
    return "";
}

Setup_Video::Setup_Video():
    mVideoSettings(Client::getVideo().settings()),
    mCustomCursorEnabled(config.customCursor),
    mParticleEffectsEnabled(config.particleEffects),
    mFps(config.fpsLimit),
    mSDLTransparencyDisabled(config.disableTransparency),
    mWindowModeListModel(new StringListModel({ _("Windowed"), _("Windowed Fullscreen"), _("Fullscreen") })),
    mResolutionListModel(new ResolutionListModel),
    mScaleListModel(new ScaleListModel(mVideoSettings)),
    mWindowModeDropDown(new DropDown(mWindowModeListModel.get())),
    mResolutionDropDown(new DropDown(mResolutionListModel.get())),
    mScaleDropDown(new DropDown(mScaleListModel.get())),
    mVSyncCheckBox(new CheckBox(_("VSync"), mVideoSettings.vsync)),
    mOpenGLCheckBox(new CheckBox(_("OpenGL (Legacy)"), mVideoSettings.openGL)),
    mCustomCursorCheckBox(new CheckBox(_("Custom cursor"), mCustomCursorEnabled)),
    mParticleEffectsCheckBox(new CheckBox(_("Particle effects"), mParticleEffectsEnabled)),
    mFpsCheckBox(new CheckBox(_("FPS limit:"))),
    mFpsSlider(new Slider(10, 120)),
    mFpsLabel(new Label),
    mOverlayDetail(config.overlayDetail),
    mOverlayDetailSlider(new Slider(0, 2)),
    mOverlayDetailField(new Label),
    mParticleDetail(3 - config.particleEmitterSkip),
    mParticleDetailSlider(new Slider(0, 3)),
    mParticleDetailField(new Label),
    mDisableSDLTransparencyCheckBox(
                          new CheckBox(_("Disable transparency (Low CPU mode)"),
                                       mSDLTransparencyDisabled))
{
    setName(_("Video"));

    auto overlayDetailLabel = new Label(_("Ambient FX:"));
    auto particleDetailLabel = new Label(_("Particle detail:"));

#ifndef USE_OPENGL
    mOpenGLCheckBox->setEnabled(false);
#endif

    mFpsLabel->setCaption(mFps > 0 ? toString(mFps) : _("None"));
    mFpsLabel->setWidth(60);
    mFpsSlider->setValue(mFps);
    mFpsSlider->setEnabled(mFps > 0);
    mFpsCheckBox->setSelected(mFps > 0);

    overlayDetailLabel->setAlignment(Graphics::RIGHT);
    particleDetailLabel->setAlignment(Graphics::RIGHT);

    // If the openGL Mode is enabled, disabling the transaprency
    // is irrelevant.
    mDisableSDLTransparencyCheckBox->setEnabled(!mVideoSettings.openGL);

    // Pre-select the current video mode.
    mWindowModeDropDown->setSelected(static_cast<int>(mVideoSettings.windowMode));
    mResolutionDropDown->setSelected(mResolutionListModel->getIndexOf(mVideoSettings.width,
                                                                      mVideoSettings.height));
    mResolutionDropDown->setEnabled(mVideoSettings.windowMode != WindowMode::WindowedFullscreen);
    mScaleDropDown->setSelected(mVideoSettings.userScale);

    // Set actions
    mWindowModeDropDown->setActionEventId("windowmode");
    mResolutionDropDown->setActionEventId("resolution");
    mCustomCursorCheckBox->setActionEventId("customcursor");
    mParticleEffectsCheckBox->setActionEventId("particleeffects");
    mDisableSDLTransparencyCheckBox->setActionEventId("disableTransparency");
    mFpsCheckBox->setActionEventId("fpslimitcheckbox");
    mFpsSlider->setActionEventId("fpslimitslider");
    mOverlayDetailSlider->setActionEventId("overlaydetailslider");
    mOverlayDetailField->setActionEventId("overlaydetailfield");
    mOpenGLCheckBox->setActionEventId("opengl");
    mParticleDetailSlider->setActionEventId("particledetailslider");
    mParticleDetailField->setActionEventId("particledetailfield");

    // Set listeners
    mWindowModeDropDown->addActionListener(this);
    mResolutionDropDown->addActionListener(this);
    mCustomCursorCheckBox->addActionListener(this);
    mOpenGLCheckBox->addActionListener(this);
    mParticleEffectsCheckBox->addActionListener(this);
    mDisableSDLTransparencyCheckBox->addActionListener(this);
    mFpsCheckBox->addActionListener(this);
    mFpsSlider->addActionListener(this);
    mOverlayDetailSlider->addActionListener(this);
    mOverlayDetailField->addKeyListener(this);
    mParticleDetailSlider->addActionListener(this);
    mParticleDetailField->addKeyListener(this);

    mOverlayDetailField->setCaption(overlayDetailToString(mOverlayDetail));
    mOverlayDetailSlider->setValue(mOverlayDetail);

    mParticleDetailField->setCaption(particleDetailToString(mParticleDetail));
    mParticleDetailSlider->setValue(mParticleDetail);

    // Do the layout
    ContainerPlacer place = getPlacer(0, 0);
    place.getCell().setHAlign(LayoutCell::FILL);

    place(0, 0, new Label(_("Window mode:")));
    place(1, 0, mWindowModeDropDown, 2).setPadding(2);
    place(0, 1, new Label(_("Resolution:")));
    place(1, 1, mResolutionDropDown, 2).setPadding(2);
    place(0, 2, new Label(_("Scale:")));
    place(1, 2, mScaleDropDown, 2).setPadding(2);
    place(0, 3, mVSyncCheckBox, 4);
    place(0, 4, mOpenGLCheckBox, 4);

    place = getPlacer(0, 1);
    place.getCell().setHAlign(LayoutCell::FILL);

    place(0, 0, new Spacer(), 4);
    place(0, 1, mCustomCursorCheckBox, 4);
    place(0, 2, mDisableSDLTransparencyCheckBox, 4);

    place(0, 3, mFpsCheckBox);
    place(1, 3, mFpsSlider, 2).setVAlign(LayoutCell::CENTER);
    place(3, 3, mFpsLabel);

    place(0, 4, mParticleEffectsCheckBox, 4);

    place(0, 5, particleDetailLabel);
    place(1, 5, mParticleDetailSlider, 2).setVAlign(LayoutCell::CENTER);
    place(3, 5, mParticleDetailField);

    place(0, 6, overlayDetailLabel);
    place(1, 6, mOverlayDetailSlider, 2).setVAlign(LayoutCell::CENTER);
    place(3, 6, mOverlayDetailField);
}

Setup_Video::~Setup_Video() = default;

void Setup_Video::apply()
{
    // Video mode changes
    auto &video = Client::getVideo();
    mVideoSettings = video.settings();

    mVideoSettings.windowMode = static_cast<WindowMode>(mWindowModeDropDown->getSelected());

    if (mResolutionDropDown->getSelected() > 0)
    {
        const auto &mode = mResolutionListModel->getModeAt(mResolutionDropDown->getSelected());
        mVideoSettings.width = mode.width;
        mVideoSettings.height = mode.height;
    }

    mVideoSettings.userScale = std::max(0, mScaleDropDown->getSelected());
    mVideoSettings.vsync = mVSyncCheckBox->isSelected();

    if (video.apply(mVideoSettings))
    {
        config.windowMode = mVideoSettings.windowMode;
        config.scale = mVideoSettings.userScale;
        config.vsync = mVideoSettings.vsync;
        config.screenWidth = mVideoSettings.width;
        config.screenHeight = mVideoSettings.height;

        Client::instance()->checkGraphicsSize();
    }
    else
    {
        new OkDialog(_("Error"), _("Failed to change video mode."));
    }

    // OpenGL change
    if (mOpenGLCheckBox->isSelected() != mVideoSettings.openGL)
    {
        config.opengl = mOpenGLCheckBox->isSelected();

        // OpenGL can currently only be changed by restarting, notify user.
        if (mOpenGLCheckBox->isSelected())
        {
            new OkDialog(_("Changing to OpenGL"),
                         _("Applying change to OpenGL requires restart.\n\n"
                           "In case OpenGL messes up your game graphics, "
                           "restart the game with the command line option "
                           "\"--no-opengl\"."));
        }
        else
        {
            new OkDialog(_("Deactivating OpenGL"),
                         _("Applying change to OpenGL requires restart."));
        }
    }
    // If LowCPU is enabled from a disabled state we warn the user
    else if (mDisableSDLTransparencyCheckBox->isSelected())
    {
        if (!config.disableTransparency)
        {
            new OkDialog(_("Transparency disabled"),
                         _("You must restart to apply changes."));
        }
    }
    else
    {
        if (config.disableTransparency)
        {
            new OkDialog(_("Transparency enabled"),
                         _("You must restart to apply changes."));
        }
    }
    config.disableTransparency = mDisableSDLTransparencyCheckBox->isSelected();

    mFps = mFpsCheckBox->isSelected() ? (int) mFpsSlider->getValue() : 0;
    mFpsSlider->setEnabled(mFps > 0);

    // FPS change
    config.fpsLimit = mFps;

    // We sync old and new values at apply time
    mVideoSettings.windowMode = config.windowMode;
    mVideoSettings.vsync = config.vsync;
    mVideoSettings.openGL = config.opengl;
    mCustomCursorEnabled = config.customCursor;
    mParticleEffectsEnabled = config.particleEffects;
    mOverlayDetail = config.overlayDetail;
    mSDLTransparencyDisabled = config.disableTransparency;
}

void Setup_Video::cancel()
{
    // Set back to the current video mode.
    mVideoSettings = Client::getVideo().settings();
    mWindowModeDropDown->setSelected(static_cast<int>(mVideoSettings.windowMode));
    mResolutionDropDown->setSelected(mResolutionListModel->getIndexOf(mVideoSettings.width,
                                                                      mVideoSettings.height));
    mScaleDropDown->setSelected(mVideoSettings.userScale);
    mVSyncCheckBox->setSelected(mVideoSettings.vsync);
    mOpenGLCheckBox->setSelected(mVideoSettings.openGL);
    mCustomCursorCheckBox->setSelected(mCustomCursorEnabled);
    mParticleEffectsCheckBox->setSelected(mParticleEffectsEnabled);
    mFpsCheckBox->setSelected(mFps > 0);
    mFpsSlider->setValue(mFps);
    mFpsSlider->setEnabled(mFps > 0);
    mOverlayDetailSlider->setValue(mOverlayDetail);
    mParticleDetailSlider->setValue(mParticleDetail);
    std::string text = mFpsCheckBox->isSelected() ? toString(mFps) : _("None");
    mFpsLabel->setCaption(text);
    mDisableSDLTransparencyCheckBox->setSelected(mSDLTransparencyDisabled);
    mDisableSDLTransparencyCheckBox->setEnabled(!mVideoSettings.openGL);

    config.windowMode = mVideoSettings.windowMode;

    setConfigValue(&Config::customCursor, mCustomCursorEnabled);
    config.particleEffects = mParticleEffectsEnabled;
    config.opengl = mVideoSettings.openGL;
    config.disableTransparency = mSDLTransparencyDisabled;
}

void Setup_Video::action(const gcn::ActionEvent &event)
{
    const std::string &id = event.getId();

    if (id == "windowmode" || id == "resolution")
    {
        auto windowMode = static_cast<WindowMode>(mWindowModeDropDown->getSelected());

        // When the window mode is "windowed fullscreen" we should select the
        // desktop resolution and disable the option to change it
        if (windowMode == WindowMode::WindowedFullscreen)
        {
            const auto &desktop = Client::getVideo().desktopDisplayMode();
            mResolutionDropDown->setSelected(
                        mResolutionListModel->getIndexOf(desktop.width, desktop.height));
            mResolutionDropDown->setEnabled(false);
        }
        else
        {
            mResolutionDropDown->setEnabled(true);
        }

        refreshScaleList();
    }
    else if (id == "resolution")
    {
        refreshScaleList();
    }
    else if (id == "customcursor")
    {
        setConfigValue(&Config::customCursor, mCustomCursorCheckBox->isSelected());
    }
    else if (id == "particleeffects")
    {
        config.particleEffects = mParticleEffectsCheckBox->isSelected();
        Particle::enabled = mParticleEffectsCheckBox->isSelected();

        if (Game::instance())
        {
            new OkDialog(_("Particle Effect Settings Changed."),
                         _("Changes will take effect on map change."));
        }
    }
    else if (id == "overlaydetailslider")
    {
        int val = (int) mOverlayDetailSlider->getValue();
        mOverlayDetailField->setCaption(overlayDetailToString(val));
        config.overlayDetail = val;
    }
    else if (id == "particledetailslider")
    {
        int val = (int) mParticleDetailSlider->getValue();
        mParticleDetailField->setCaption(particleDetailToString(val));
        config.particleEmitterSkip = 3 - val;
        Particle::emitterSkip = 4 - val;
    }
    else if (id == "fpslimitcheckbox" || id == "fpslimitslider")
    {
        int fps = (int) mFpsSlider->getValue();
        fps = fps > 0 ? fps : mFpsSlider->getScaleStart();
        mFps = mFpsCheckBox->isSelected() ? fps : 0;
        const std::string text = mFps > 0 ? toString(mFps) : _("None");

        mFpsLabel->setCaption(text);
        mFpsSlider->setValue(mFps);
        mFpsSlider->setEnabled(mFps > 0);
    }
    else if (id == "opengl" || id == "disableTransparency")
    {
        // Disable transparency disabling when in OpenGL.
        if (mOpenGLCheckBox->isSelected())
        {
            mDisableSDLTransparencyCheckBox->setSelected(false);
            mDisableSDLTransparencyCheckBox->setEnabled(false);
        }
        else
        {
            mDisableSDLTransparencyCheckBox->setEnabled(true);
        }
    }
}

void Setup_Video::refreshScaleList()
{
    if (mResolutionDropDown->getSelected() > 0)
    {
        const auto &mode = mResolutionListModel->getModeAt(mResolutionDropDown->getSelected());
        mVideoSettings.width = mode.width;
        mVideoSettings.height = mode.height;
    }
    else
    {
        auto &videoSettings = Client::getVideo().settings();
        mVideoSettings.width = videoSettings.width;
        mVideoSettings.height = videoSettings.height;
    }

    mScaleListModel->setVideoSettings(mVideoSettings);
    mScaleDropDown->adjustHeight();
    mScaleDropDown->setSelected(mVideoSettings.userScale);
}
