/*Copyright (C) 2010-2023 Duncan Deveaux

This file is part of Hikou no mizu.

Hikou no mizu 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 3 of the License, or
(at your option) any later version.

Hikou no mizu 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 Hikou no mizu.  If not, see <http://www.gnu.org/licenses/>.
*/

#include <iostream>
#include <sstream>
#include <vector>
#include <cmath>
#include <cstdlib>
#include <pugixml.hpp>

/// Hikou no mizu's ConfReader, see ../src
#include "Configuration/ConfReader.hpp"

/// Struct to contain a <animation> tag, and its associated <hits> tag if any
struct AnimNode
{
    AnimNode(const pugi::xml_node &animElement,
             const pugi::xml_node &hitsElement) :
    anim(animElement), hits(hitsElement) {}

    pugi::xml_node anim, hits;
};

std::vector<AnimNode> parseXML(pugi::xml_document&, const std::string&);
void preprocessAnimNode(pugi::xml_node&);
void updateAnimNode(pugi::xml_node&, int, int, int, int, double, double);
void updateHitsNode(pugi::xml_node&, int, int, double, double);
std::string roundAsString(double number);

/// descbuild: Converts a single abstract character animation description tag
/// into a game-readable format (absolute values for frame positions and sizes)
int main(int argc, char **argv)
{
    if (argc <= 2)
    {
        std::cerr << "Usage: " << argv[0]
                  << " [animation_metadata_file]"
                  << " [dest_xml_description_file]" << std::endl;
        return EXIT_FAILURE;
    }

    const std::string animInfoPath(argv[1]);
    const char * const xmlDescriptionPath = argv[2];

    //Read the metadata of the generated animation
    ConfReader animInfoReader;
    if (!animInfoReader.load(animInfoPath))
    {
        std::cerr << "Error while loading the file: "
                  << animInfoPath << std::endl;
        return EXIT_FAILURE;
    }

    std::string fullAnimName;
    int framesAmount, frameWidth, frameHeight, tiles;
    double widthCorrection, heightCorrection;

    const bool readingSuccess =
        animInfoReader.getItem("animation_name", fullAnimName) &&
        animInfoReader.getItem("frames_amount", framesAmount) &&
        animInfoReader.getItem("frame_width", frameWidth) &&
        animInfoReader.getItem("frame_height", frameHeight) &&
        animInfoReader.getItem("tiles_per_column", tiles) &&
        animInfoReader.getItem("correction_width", widthCorrection) &&
        animInfoReader.getItem("correction_height", heightCorrection);

    if (!readingSuccess)
    {
        std::cerr << "Error while parsing the file: "
                  << animInfoPath << std::endl;
        return EXIT_FAILURE;
    }
    else if (framesAmount <= 0 || frameWidth <= 0 || frameHeight <= 0 ||
             tiles <= 0 || widthCorrection <= 0 || heightCorrection <= 0)
    {
        std::cerr << "Malformed file, at least one value is not "
                  << "strictly positive: " << animInfoPath << std::endl;
        return EXIT_FAILURE;
    }

    //Extract character and animation name (without the .png extension)
    const std::string::size_type delimiterPos = fullAnimName.find('/');
    if (delimiterPos == std::string::npos)
    {
        std::cerr << "Error while parsing the animation name: "
                  << fullAnimName << std::endl;
        return EXIT_FAILURE;
    }

    const std::string animName = fullAnimName.substr(delimiterPos+1,
                                        fullAnimName.length()-delimiterPos-5);

    //Open the xml animations' description file for filling in
    pugi::xml_document xmlFile;
    if (!xmlFile.load_file(xmlDescriptionPath,
                           pugi::parse_default | pugi::parse_declaration))
    {
        std::cerr << "Error while loading the file: "
                  << xmlDescriptionPath << std::endl;
        return EXIT_FAILURE;
    }

    //Look for the matching <animation> tag
    std::vector<AnimNode> animNodes = parseXML(xmlFile, animName);
    if (animNodes.empty())
    {
        std::cerr << "Could not find an animation with name: "
                  << animName << std::endl;
        return EXIT_FAILURE;
    }

    //Update the tags
    std::vector<AnimNode>::iterator it;
    for (it = animNodes.begin(); it != animNodes.end(); it++)
    {
        preprocessAnimNode(it->anim); //Unfold <frameSet> elements
        updateAnimNode(it->anim, framesAmount, frameWidth, frameHeight, tiles,
                       widthCorrection, heightCorrection);
        if (it->hits)
            updateHitsNode(it->hits, frameWidth, frameHeight,
                           widthCorrection, heightCorrection);
    }

    //Save the updated file and exit
    xmlFile.save_file(xmlDescriptionPath, "" /*no indentation*/,
                      pugi::format_default, pugi::encoding_utf8);

    return EXIT_SUCCESS;
}

/// Parses the input 'xmlFile' looking for
/// the description of the requested animation
std::vector<AnimNode> parseXML(pugi::xml_document &xmlFile,
                               const std::string &animName)
{
    std::vector<AnimNode> animNodes;

    pugi::xml_node stateNode;
    for (stateNode = xmlFile.child("main").child("data").first_child();
         stateNode;
         stateNode = stateNode.next_sibling())
    {
        //Main animation?
        pugi::xml_node animNode = stateNode.child("animation");
        if (std::string(animNode.attribute("path").value()) == animName)
            //A main animation: No associated <hits> tag
            animNodes.emplace_back(animNode, pugi::xml_node());
        else
        {
            //Attacks?
            pugi::xml_node attackNode;
            for (attackNode = stateNode.child("attacks").first_child();
                 attackNode;
                 attackNode = attackNode.next_sibling())
            {
                pugi::xml_node attackAnimNode = attackNode.child("animation");
                if (std::string(attackAnimNode.attribute("path").value()) == animName)
                    animNodes.emplace_back(attackAnimNode, attackNode.child("hits"));
            }
        }
    }

    return animNodes;
}

/// Preprocesses a <animation> tag to unfold the <frameSet>
/// children into sets of <frame> children
void preprocessAnimNode(pugi::xml_node &animNode)
{
    pugi::xml_node frameSetNode = animNode.child("frameSet");
    while (frameSetNode)
    {
        //Extract the frame range and body coordinates of the <frameSet> element
        const std::string rangeStr(frameSetNode.attribute("id_range").value());

        const char *relBodyX = frameSetNode.attribute("bodyX_rel").value(),
                   *relBodyY = frameSetNode.attribute("bodyY_rel").value(),
                   *relBodyWidth = frameSetNode.attribute("bodyWidth_rel").value(),
                   *relBodyHeight = frameSetNode.attribute("bodyHeight_rel").value();

        const std::string::size_type delimiterPos = rangeStr.find(':');
        if (delimiterPos != std::string::npos)
        {
            int startFrame, endFrame;
            std::istringstream(rangeStr.substr(0, delimiterPos)) >> startFrame;
            std::istringstream(rangeStr.substr(delimiterPos + 1,
                rangeStr.length() - delimiterPos - 1)) >> endFrame;

            //Unfold the <frameSet> element
            for (int frameId = ((startFrame <= endFrame) ? startFrame : endFrame);
                frameId <= ((startFrame <= endFrame) ? endFrame : startFrame);
                frameId++)
            {
                //Insert the <frame> element before or after the <frameSet>
                //element depending on the wanted order
                pugi::xml_node frame;
                if (startFrame <= endFrame)
                    frame = animNode.insert_child_before("frame", frameSetNode);
                else frame = animNode.insert_child_after("frame", frameSetNode);

                frame.append_attribute("id").set_value(frameId);
                frame.append_attribute("bodyX_rel").set_value(relBodyX);
                frame.append_attribute("bodyY_rel").set_value(relBodyY);
                frame.append_attribute("bodyWidth_rel").set_value(relBodyWidth);
                frame.append_attribute("bodyHeight_rel").set_value(relBodyHeight);
            }
        }

        animNode.remove_child(frameSetNode);
        frameSetNode = animNode.child("frameSet");
    }
}

/// Updates a <animation> tag from relative values
/// to absolute, game-readable values
void updateAnimNode(pugi::xml_node &animNode, int framesAmount,
                    int frameWidth, int frameHeight, int tiles,
                    double widthCorrection, double heightCorrection)
{
    //The number of horizontal/vertical pixels between each frame
    constexpr int framesGap = 4;

    //Compute the position of each frame in the spritesheet
    std::vector<std::pair<int, int>> frames;
    for (int i = 0; i < framesAmount; i++)
    {
        const int colIx = i%tiles, rowIx = static_cast<int>(i/tiles); //tiles>0
        const int frameX = colIx * (frameWidth + framesGap),
                  frameY = rowIx * (frameHeight + framesGap);

        frames.push_back(std::make_pair(frameX, frameY));
    }

    pugi::xml_node frameNode;
    for (frameNode = animNode.child("frame");
         frameNode;
         frameNode = frameNode.next_sibling("frame"))
    {
        //Query abstract frame description attributes
        const int frameId = frameNode.attribute("id").as_int(-1);

        const double
            relBodyX = frameNode.attribute("bodyX_rel").as_double(0.0)
                * widthCorrection,
            relBodyY = frameNode.attribute("bodyY_rel").as_double(0.0)
                * heightCorrection,
            relBodyWidth = frameNode.attribute("bodyWidth_rel").as_double(1.0)
                * widthCorrection,
            relBodyHeight = frameNode.attribute("bodyHeight_rel").as_double(1.0)
                * heightCorrection;

        //Add static frame data
        if (frameId >= 0 && frameId < framesAmount)
        {
            //Frame absolute position and size
            const unsigned int uFrameId = static_cast<unsigned int>(frameId);
            frameNode.append_attribute("x").set_value(frames[uFrameId].first);
            frameNode.append_attribute("y").set_value(frames[uFrameId].second);
            frameNode.append_attribute("width").set_value(frameWidth);
            frameNode.append_attribute("height").set_value(frameHeight);

            //Absolute values for body x, y, width and height
            frameNode.append_attribute("bodyX").set_value(roundAsString(
                relBodyX * static_cast<double>(frameWidth)).c_str());
            frameNode.append_attribute("bodyY").set_value(roundAsString(
                relBodyY * static_cast<double>(frameHeight)).c_str());
            frameNode.append_attribute("bodyWidth").set_value(roundAsString(
                relBodyWidth * static_cast<double>(frameWidth)).c_str());
            frameNode.append_attribute("bodyHeight").set_value(roundAsString(
                relBodyHeight * static_cast<double>(frameHeight)).c_str());

            //Remove abstract frame description attributes
            frameNode.remove_attribute("id");
            frameNode.remove_attribute("bodyX_rel");
            frameNode.remove_attribute("bodyY_rel");
            frameNode.remove_attribute("bodyWidth_rel");
            frameNode.remove_attribute("bodyHeight_rel");
        }
    }
}

/// Updates a <hits> tag from relative values to absolute, game-readable values
void updateHitsNode(pugi::xml_node &hitsNode, int frameWidth, int frameHeight,
                    double widthCorrection, double heightCorrection)
{
    pugi::xml_node hitNode;
    for (hitNode = hitsNode.child("hit");
         hitNode;
         hitNode = hitNode.next_sibling("hit"))
    {
        const double
            relX = hitNode.attribute("x_rel").as_double(1.0) * widthCorrection,
            relY = hitNode.attribute("y_rel").as_double(0.5) * heightCorrection,
            relWidth = hitNode.attribute("width_rel").as_double(0.1)
                * widthCorrection,
            relHeight = hitNode.attribute("height_rel").as_double(0.1)
                * heightCorrection;

        hitNode.append_attribute("x").set_value(roundAsString(
            relX * static_cast<double>(frameWidth)).c_str());
        hitNode.append_attribute("y").set_value(roundAsString(
            relY * static_cast<double>(frameHeight)).c_str());
        hitNode.append_attribute("width").set_value(roundAsString(
            relWidth * static_cast<double>(frameWidth)).c_str());
        hitNode.append_attribute("height").set_value(roundAsString(
            relHeight * static_cast<double>(frameHeight)).c_str());

        hitNode.remove_attribute("x_rel");
        hitNode.remove_attribute("y_rel");
        hitNode.remove_attribute("width_rel");
        hitNode.remove_attribute("height_rel");
    }
}

/// Rounds 'number' to 0.1 precision and returns the result as a string
std::string roundAsString(double number)
{
    std::ostringstream ss;
    ss << round(number * 10.0) / 10.0;

    return ss.str();
}
