#include "config.h"

#include "ambdec.h"

#include <algorithm>
#include <cctype>
#include <cstddef>
#include <iterator>
#include <sstream>
#include <string>

#include "alfstream.h"
#include "core/logging.h"


namespace {

template<typename T, std::size_t N>
constexpr inline std::size_t size(const T(&)[N]) noexcept
{ return N; }

int readline(std::istream &f, std::string &output)
{
    while(f.good() && f.peek() == '\n')
        f.ignore();

    return std::getline(f, output) && !output.empty();
}

bool read_clipped_line(std::istream &f, std::string &buffer)
{
    while(readline(f, buffer))
    {
        std::size_t pos{0};
        while(pos < buffer.length() && std::isspace(buffer[pos]))
            pos++;
        buffer.erase(0, pos);

        std::size_t cmtpos{buffer.find_first_of('#')};
        if(cmtpos < buffer.length())
            buffer.resize(cmtpos);
        while(!buffer.empty() && std::isspace(buffer.back()))
            buffer.pop_back();

        if(!buffer.empty())
            return true;
    }
    return false;
}


std::string read_word(std::istream &f)
{
    std::string ret;
    f >> ret;
    return ret;
}

bool is_at_end(const std::string &buffer, std::size_t endpos)
{
    while(endpos < buffer.length() && std::isspace(buffer[endpos]))
        ++endpos;
    return !(endpos < buffer.length());
}


al::optional<std::string> load_ambdec_speakers(AmbDecConf::SpeakerConf *spkrs,
    const std::size_t num_speakers, std::istream &f, std::string &buffer)
{
    size_t cur_speaker{0};
    while(cur_speaker < num_speakers)
    {
        std::istringstream istr{buffer};

        std::string cmd{read_word(istr)};
        if(cmd.empty())
        {
            if(!read_clipped_line(f, buffer))
                return al::make_optional<std::string>("Unexpected end of file");
            continue;
        }

        if(cmd == "add_spkr")
        {
            AmbDecConf::SpeakerConf &spkr = spkrs[cur_speaker++];
            const size_t spkr_num{cur_speaker};

            istr >> spkr.Name;
            if(istr.fail()) WARN("Name not specified for speaker %zu\n", spkr_num);
            istr >> spkr.Distance;
            if(istr.fail()) WARN("Distance not specified for speaker %zu\n", spkr_num);
            istr >> spkr.Azimuth;
            if(istr.fail()) WARN("Azimuth not specified for speaker %zu\n", spkr_num);
            istr >> spkr.Elevation;
            if(istr.fail()) WARN("Elevation not specified for speaker %zu\n", spkr_num);
            istr >> spkr.Connection;
            if(istr.fail()) TRACE("Connection not specified for speaker %zu\n", spkr_num);
        }
        else
            return al::make_optional("Unexpected speakers command: "+cmd);

        istr.clear();
        const auto endpos = static_cast<std::size_t>(istr.tellg());
        if(!is_at_end(buffer, endpos))
            return al::make_optional("Extra junk on line: " + buffer.substr(endpos));
        buffer.clear();
    }

    return al::nullopt;
}

al::optional<std::string> load_ambdec_matrix(float (&gains)[MaxAmbiOrder+1],
    AmbDecConf::CoeffArray *matrix, const std::size_t maxrow, std::istream &f, std::string &buffer)
{
    bool gotgains{false};
    std::size_t cur{0u};
    while(cur < maxrow)
    {
        std::istringstream istr{buffer};

        std::string cmd{read_word(istr)};
        if(cmd.empty())
        {
            if(!read_clipped_line(f, buffer))
                return al::make_optional<std::string>("Unexpected end of file");
            continue;
        }

        if(cmd == "order_gain")
        {
            std::size_t curgain{0u};
            float value;
            while(istr.good())
            {
                istr >> value;
                if(istr.fail()) break;
                if(!istr.eof() && !std::isspace(istr.peek()))
                    return al::make_optional("Extra junk on gain "+std::to_string(curgain+1)+": "+
                        buffer.substr(static_cast<std::size_t>(istr.tellg())));
                if(curgain < size(gains))
                    gains[curgain++] = value;
            }
            std::fill(std::begin(gains)+curgain, std::end(gains), 0.0f);
            gotgains = true;
        }
        else if(cmd == "add_row")
        {
            AmbDecConf::CoeffArray &mtxrow = matrix[cur++];
            std::size_t curidx{0u};
            float value{};
            while(istr.good())
            {
                istr >> value;
                if(istr.fail()) break;
                if(!istr.eof() && !std::isspace(istr.peek()))
                    return al::make_optional("Extra junk on matrix element "+
                        std::to_string(curidx)+"x"+std::to_string(cur-1)+": "+
                        buffer.substr(static_cast<std::size_t>(istr.tellg())));
                if(curidx < mtxrow.size())
                    mtxrow[curidx++] = value;
            }
            std::fill(mtxrow.begin()+curidx, mtxrow.end(), 0.0f);
        }
        else
            return al::make_optional("Unexpected matrix command: "+cmd);

        istr.clear();
        const auto endpos = static_cast<std::size_t>(istr.tellg());
        if(!is_at_end(buffer, endpos))
            return al::make_optional("Extra junk on line: " + buffer.substr(endpos));
        buffer.clear();
    }

    if(!gotgains)
        return al::make_optional<std::string>("Matrix order_gain not specified");
    return al::nullopt;
}

} // namespace

AmbDecConf::~AmbDecConf() = default;


al::optional<std::string> AmbDecConf::load(const char *fname) noexcept
{
    al::ifstream f{fname};
    if(!f.is_open())
        return al::make_optional<std::string>("Failed to open file");

    bool speakers_loaded{false};
    bool matrix_loaded{false};
    bool lfmatrix_loaded{false};
    std::string buffer;
    while(read_clipped_line(f, buffer))
    {
        std::istringstream istr{buffer};

        std::string command{read_word(istr)};
        if(command.empty())
            return al::make_optional("Malformed line: "+buffer);

        if(command == "/description")
            readline(istr, Description);
        else if(command == "/version")
        {
            istr >> Version;
            if(!istr.eof() && !std::isspace(istr.peek()))
                return al::make_optional("Extra junk after version: " +
                    buffer.substr(static_cast<std::size_t>(istr.tellg())));
            if(Version != 3)
                return al::make_optional("Unsupported version: "+std::to_string(Version));
        }
        else if(command == "/dec/chan_mask")
        {
            if(ChanMask)
                return al::make_optional<std::string>("Duplicate chan_mask definition");

            istr >> std::hex >> ChanMask >> std::dec;
            if(!istr.eof() && !std::isspace(istr.peek()))
                return al::make_optional("Extra junk after mask: " +
                    buffer.substr(static_cast<std::size_t>(istr.tellg())));

            if(!ChanMask)
                return al::make_optional("Invalid chan_mask: "+std::to_string(ChanMask));
        }
        else if(command == "/dec/freq_bands")
        {
            if(FreqBands)
                return al::make_optional<std::string>("Duplicate freq_bands");

            istr >> FreqBands;
            if(!istr.eof() && !std::isspace(istr.peek()))
                return al::make_optional("Extra junk after freq_bands: " +
                    buffer.substr(static_cast<std::size_t>(istr.tellg())));

            if(FreqBands != 1 && FreqBands != 2)
                return al::make_optional("Invalid freq_bands: "+std::to_string(FreqBands));
        }
        else if(command == "/dec/speakers")
        {
            if(NumSpeakers)
                return al::make_optional<std::string>("Duplicate speakers");

            istr >> NumSpeakers;
            if(!istr.eof() && !std::isspace(istr.peek()))
                return al::make_optional("Extra junk after speakers: " +
                    buffer.substr(static_cast<std::size_t>(istr.tellg())));

            if(!NumSpeakers)
                return al::make_optional("Invalid speakers: "+std::to_string(NumSpeakers));
            Speakers = std::make_unique<SpeakerConf[]>(NumSpeakers);
        }
        else if(command == "/dec/coeff_scale")
        {
            std::string scale = read_word(istr);
            if(scale == "n3d") CoeffScale = AmbDecScale::N3D;
            else if(scale == "sn3d") CoeffScale = AmbDecScale::SN3D;
            else if(scale == "fuma") CoeffScale = AmbDecScale::FuMa;
            else
                return al::make_optional("Unexpected coeff_scale: "+scale);
        }
        else if(command == "/opt/xover_freq")
        {
            istr >> XOverFreq;
            if(!istr.eof() && !std::isspace(istr.peek()))
                return al::make_optional("Extra junk after xover_freq: " +
                    buffer.substr(static_cast<std::size_t>(istr.tellg())));
        }
        else if(command == "/opt/xover_ratio")
        {
            istr >> XOverRatio;
            if(!istr.eof() && !std::isspace(istr.peek()))
                return al::make_optional("Extra junk after xover_ratio: " +
                    buffer.substr(static_cast<std::size_t>(istr.tellg())));
        }
        else if(command == "/opt/input_scale" || command == "/opt/nfeff_comp" ||
                command == "/opt/delay_comp" || command == "/opt/level_comp")
        {
            /* Unused */
            read_word(istr);
        }
        else if(command == "/speakers/{")
        {
            if(!NumSpeakers)
                return al::make_optional<std::string>("Speakers defined without a count");

            const auto endpos = static_cast<std::size_t>(istr.tellg());
            if(!is_at_end(buffer, endpos))
                return al::make_optional("Extra junk on line: " + buffer.substr(endpos));
            buffer.clear();

            if(auto err = load_ambdec_speakers(Speakers.get(), NumSpeakers, f, buffer))
                return err;
            speakers_loaded = true;

            if(!read_clipped_line(f, buffer))
                return al::make_optional<std::string>("Unexpected end of file");
            std::istringstream istr2{buffer};
            std::string endmark{read_word(istr2)};
            if(endmark != "/}")
                return al::make_optional("Expected /} after speaker definitions, got "+endmark);
            istr.swap(istr2);
        }
        else if(command == "/lfmatrix/{" || command == "/hfmatrix/{" || command == "/matrix/{")
        {
            if(!NumSpeakers)
                return al::make_optional<std::string>("Matrix defined without a count");
            const auto endpos = static_cast<std::size_t>(istr.tellg());
            if(!is_at_end(buffer, endpos))
                return al::make_optional("Extra junk on line: " + buffer.substr(endpos));
            buffer.clear();

            if(!Matrix)
            {
                Matrix = std::make_unique<CoeffArray[]>(NumSpeakers * FreqBands);
                LFMatrix = Matrix.get();
                HFMatrix = LFMatrix + NumSpeakers*(FreqBands-1);
            }

            if(FreqBands == 1)
            {
                if(command != "/matrix/{")
                    return al::make_optional(
                        "Unexpected \""+command+"\" type for a single-band decoder");
                if(auto err = load_ambdec_matrix(HFOrderGain, HFMatrix, NumSpeakers, f, buffer))
                    return err;
                matrix_loaded = true;
            }
            else
            {
                if(command == "/lfmatrix/{")
                {
                    if(auto err=load_ambdec_matrix(LFOrderGain, LFMatrix, NumSpeakers, f, buffer))
                        return err;
                    lfmatrix_loaded = true;
                }
                else if(command == "/hfmatrix/{")
                {
                    if(auto err=load_ambdec_matrix(HFOrderGain, HFMatrix, NumSpeakers, f, buffer))
                        return err;
                    matrix_loaded = true;
                }
                else
                    return al::make_optional(
                        "Unexpected \""+command+"\" type for a dual-band decoder");
            }

            if(!read_clipped_line(f, buffer))
                return al::make_optional<std::string>("Unexpected end of file");
            std::istringstream istr2{buffer};
            std::string endmark{read_word(istr2)};
            if(endmark != "/}")
                return al::make_optional("Expected /} after matrix definitions, got "+endmark);
            istr.swap(istr2);
        }
        else if(command == "/end")
        {
            const auto endpos = static_cast<std::size_t>(istr.tellg());
            if(!is_at_end(buffer, endpos))
                return al::make_optional("Extra junk on end: " + buffer.substr(endpos));

            if(!speakers_loaded || !matrix_loaded || (FreqBands == 2 && !lfmatrix_loaded))
                return al::make_optional<std::string>("No decoder defined");

            return al::nullopt;
        }
        else
            return al::make_optional("Unexpected command: " + command);

        istr.clear();
        const auto endpos = static_cast<std::size_t>(istr.tellg());
        if(!is_at_end(buffer, endpos))
            return al::make_optional("Extra junk on line: " + buffer.substr(endpos));
        buffer.clear();
    }
    return al::make_optional<std::string>("Unexpected end of file");
}