#include <fstream>
#include <vector>
#include <cassert>
#include <algorithm>
#include <filesystem>
#include <string>

bool read_binary_file( const std::string& filename_, std::vector<uint8_t>& content_ )
{
    std::ifstream file( filename_, std::ios::binary );
    if( !file.is_open() )
    {
        return false;
    }
    content_ = std::vector<uint8_t>( ( std::istreambuf_iterator<char>( file ) ), std::istreambuf_iterator<char>() );
    return true;
}

void write_binary_file( const std::string& file_path_, const void* const data_, size_t size_ )
{
    FILE* fp{};
    fopen_s( &fp, file_path_.c_str(), "wb+" );
    assert( fp );
    size_t written = fwrite( data_, 1, size_, fp );
    assert( written = size_ );
    fclose( fp );
}

std::string trim( const std::string& s )
{
    std::string::const_iterator it = s.begin();
    while( it != s.end() && isspace( *it ) )
        it++;

    std::string::const_reverse_iterator rit = s.rbegin();
    while( rit.base() != it && isspace( *rit ) )
        rit++;

    return std::string( it, rit.base() );
}

std::string hex_ascii_dump( const void* const buffer, const int size, const int width, const std::string& p_padding )
{
    uint8_t* const input = (uint8_t* const)buffer;

    if( size == 0 )
    {
        return "";
    }

    std::string tmp;
    std::string tmp_ascii;
    std::string tmp_hex;


    char HexStr[256][4];
    char Ascii[256];

    for( int i = 0; i < 256; i++ )
    {
        Ascii[i] = ( i >= ' ' && i <= '~' ) ? (char)i : '.';
        _snprintf_s( HexStr[i], sizeof( HexStr[i] ), "%02X ", i );
    }

    char Address[16];
    _snprintf_s( Address, sizeof( Address ), "%04X:  ", 0 );

    tmp_hex = Address;

    for( int i = 0; i < size; ++i )
    {
        uint8_t c = input[i];

        tmp_hex += HexStr[c];
        tmp_ascii += Ascii[c];

        bool nl = ( i > 0 ) && ( ( i + 1 ) % width == 0 );
        bool last = ( i == size - 1 );

        if( last )
        {
            size_t hex_size = 3 * width + 7;
            if( tmp_hex.size() < hex_size )
            {
                tmp_hex.resize( hex_size, 32 );
            }
        }

        if( nl || last )
        {
            tmp += p_padding + tmp_hex + tmp_ascii;
            if( !last )
            {
                tmp += "\n";
            }
            _snprintf_s( Address, sizeof( Address ), "%04X:  ", i + 1 );
            tmp_hex = Address;
            tmp_ascii.clear();
        }
    }

    return tmp;
}

struct title_t
{
    std::string name;
    std::vector<uint8_t> data;
};

std::vector<title_t> parse_vce_data( std::vector<uint8_t>& content )
{
    // using names names from
    // https://moddingwiki.shikadi.net/wiki/Kris%27_Music_System_Voice_Format

    uint8_t* current = content.data();

    constexpr size_t STPK_PASSES_MASK = 0x7F;

    uint32_t lenFile = *(uint32_t*)current;
    current += sizeof( lenFile );
    printf( "  lenFile: %u (lenFile & STPK_PASSES_MASK = %u)\n", lenFile,
            uint8_t( lenFile & STPK_PASSES_MASK ) ); // always shorte - maybe without header length?

    assert( lenFile <= content.size() );

    uint16_t numInstruments = *(uint16_t*)current;
    current += sizeof( numInstruments );
    printf( "  numInstruments: %u\n", numInstruments );

    const size_t titles_table_size = numInstruments * 4;
    const size_t off_insts_table_size = numInstruments * sizeof( uint32_t );
    const size_t header_size = sizeof( lenFile ) + sizeof( numInstruments ) + titles_table_size + off_insts_table_size;
    printf( "-->header-size: %u\n", header_size );

    std::vector<std::string> titles( numInstruments );
    printf( "  title\n" );
    for( size_t i = 0; i < numInstruments; ++i )
    {
        const std::string title = { current, current + 4 };
        titles[i] = title;
        printf( "    [%u] = %s\n", i, title.c_str() );
        current += 4;
    }

    bool offsets_advancing = true;
    std::vector<uint32_t> offInsts( numInstruments );
    {
        int last_ofs = -1;
        printf( "  offInst\n" );
        for( size_t i = 0; i < numInstruments; ++i )
        {
            uint32_t offInst = *(uint32_t*)current;

            //---
            // advancing offsets?
            bool advanced = ( (int)offInst > last_ofs );
            if( offsets_advancing )
            {
                if( !advanced )
                {
                    offsets_advancing = false;
                }
            }
            last_ofs = offInst;
            //---

            offInsts[i] = offInst;

            current += sizeof( offInst );
            printf( "    [%u]: %u\n", i, offInst );
        }
    }
    //assert( offInsts[0] == 0 ); // not always true

    printf( "offsets always advancing: %s\n", offsets_advancing ? "yes" : "no" );

    uint8_t* header_end = current;
    uint8_t* data_end = 0;

    std::vector<std::vector<uint8_t>> insts_data( numInstruments );
    printf( "  Instr\n" );
    for( size_t i = 0; i < numInstruments; ++i )
    {
        uint8_t* data_begin = header_end + offInsts[i];

        uint8_t* data_curr = data_begin;

        uint16_t lenInst = *(uint16_t*)data_curr;
        data_curr += sizeof( uint16_t );
        printf( "    [%u] lenInst: %u (0x%x)\n", i, lenInst, lenInst );

        uint16_t unknown = *(uint16_t*)data_curr;
        data_curr += sizeof( uint16_t );
        assert( unknown = 1 );

        data_end = data_begin + lenInst;

        std::vector<uint8_t> data{ data_begin, data_end };
        insts_data[i] = data;
    }

    // always the sames size?
    size_t first_size = insts_data[0].size();
    for( auto& data : insts_data )
    {
        assert( data.size() == first_size );
    }

    //------------

    // find gaps in data

    auto sorted_offsets = offInsts;
    std::sort( sorted_offsets.begin(), sorted_offsets.end() );
    {
        uint16_t last_ofs = sorted_offsets[0];
        for( size_t i = 1; i < sorted_offsets.size(); ++i )
        {
            const uint16_t expected_offset = last_ofs + first_size;
            const uint16_t given_offset = sorted_offsets[i];
            printf( "expected-offset %u\n", expected_offset );
            printf( "given-offset %u\n", given_offset );
            assert( expected_offset <= given_offset );

            if( expected_offset < given_offset )
            {
                const size_t gap_size = given_offset - expected_offset;
                uint8_t* gap_begin = header_end + expected_offset;
                uint8_t* gap_end = gap_begin + gap_size;
                std::vector<uint8_t> gap{ gap_begin, gap_end };
                printf( "GAP(size: %u)\n%s\n", gap_size, hex_ascii_dump( gap.data(), gap.size(), 32, "  " ).c_str() );
            }

            last_ofs = sorted_offsets[i];
            printf( "\n" );
        }
    }

    int brk = 1;

    //------------

    // data behind instrument data? filled with a unique value, mostly 0xFF or 0x77
    // maybe padding or something
    uint8_t* rest_begin = header_end + sorted_offsets.back() + first_size;
    uint8_t* rest_end = content.data() + content.size();
    size_t rest_size = rest_end - rest_begin;

    if( rest_size > 0 )
    {
        const std::vector<uint8_t> rest{ rest_begin, rest_end };
        const uint8_t first = rest[0];
        bool always_same = true;
        for( const auto& v : rest )
        {
            if( v != first )
            {
                always_same = false;
                break;
            }
        }

        if( always_same )
        {
            printf( "  Rest-Data: %u bytes filled with value: 0x%02X\n", rest_size, first );
        }
        else
        {
            printf( "  Rest-Data (size: %u)\n%s\n", rest.size(),
                    hex_ascii_dump( rest.data(), rest.size(), 32, "  " ).c_str() );
        }
    }

    printf( "Instrument data\n" );

    // show instrument info - nearly no information on the wiki page
    for( size_t i = 0; i < insts_data.size(); ++i )
    {
        auto& data = insts_data[i];
        const size_t width = 32;
        //const size_t width = data.size();
        printf( "[%u]\n%s\n", i, hex_ascii_dump( data.data(), data.size(), width, "  " ).c_str() );
    }

    std::vector<title_t> titles_data( insts_data.size() );
    for( size_t i = 0; i < insts_data.size(); ++i )
    {
        titles_data[i].name = titles[i];
        titles_data[i].data = insts_data[i];
    }

    return titles_data;
}

int main( int argc, char* argv[] )
{
    printf( "vce_dump 0.3\n" );

    std::string filepath;
    std::string opt;
    std::string folder;
    if( argc == 2 || argc == 4 )
    {
        filepath = argv[1];
        if( argc == 4 )
        {
            opt = argv[2];
            folder = argv[3];
        }
    }
    else
    {
        printf( "vce_dump VCEFile [e|i FOLDER]\n" );
        printf( "vce_dump TEST.VCE - shows vce content\n" );
        printf( "vce_dump TEST.VCE e d:\temp\test_vce - exports TEST.VCE into the folder\n" );
        printf( "vce_dump TEST.VCE i d:\temp\test_vce - imports folder content into a new TEST.VCE\n" );
        return 1;
    }

    if( argc == 2 || argc == 4 )
    {
        if( opt == "" || opt == "e" )
        {
            std::vector<uint8_t> content;
            if( read_binary_file( filepath, content ) )
            {
                printf( "file: %s, size: %u bytes\n", filepath.c_str(), content.size() );

                std::vector<title_t> titles_data = parse_vce_data( content );

                if( opt == "e" )
                {
                    std::filesystem::create_directory( folder );

                    const std::string order_filepath = folder + "//order.txt";
                    FILE* order_fp{};
                    fopen_s( &order_fp, order_filepath.c_str(), "w+" );

                    for( const auto& title : titles_data )
                    {
                        const std::string filepath = folder + "//" + title.name + ".bin";
                        write_binary_file( filepath, title.data.data(), title.data.size() );

                        fprintf( order_fp, "%s\n", title.name.c_str() );
                    }

                    fclose( order_fp );
                }
            }
            else
            {
                printf( "can't read: %s\n", filepath.c_str() );
            }
        }
        else if( opt == "i" )
        {
            std::vector<std::string> ordering;
            {
                const std::string order_filepath = folder + "//order.txt";
                std::ifstream order_file( order_filepath );
                if( !order_file )
                {
                    printf( "order.txt not available!\n" );
                    return 2;
                }

                std::string line;
                while( std::getline( order_file, line ) )
                {
                    line = trim( line );
                    if( line.empty() )
                    {
                        continue;
                    }

                    if( line.size() > 4 )
                    {
                        printf( "title needs to be 4 chars\n" );
                        return 3;
                    }

                    ordering.push_back( line );
                }
            }

            std::vector<std::vector<uint8_t>> datas;
            for( const auto& title : ordering )
            {
                const std::string data_filepath = folder + "//" + title + ".bin";

                std::vector<uint8_t> data;
                if( !read_binary_file( data_filepath, data ) )
                {
                    printf( "file: %s missing\n", data_filepath.c_str() );
                    return 3;
                }

                datas.push_back( data );
            }

            // write VCE file

            const size_t title_table_size = ordering.size() * sizeof( char ) * 4;
            const size_t offset_table_size = ordering.size() * sizeof( uint32_t );
            const size_t header_size = sizeof( uint32_t ) + sizeof( uint16_t ) + title_table_size + offset_table_size;

            size_t data_size = 0;
            for( const auto& data : datas )
            {
                data_size += data.size();
            }

            const size_t filesize = header_size + data_size;

            std::vector<uint8_t> content( filesize );

            uint8_t* current = content.data();

            ( *(uint32_t*)current ) = filesize;
            current += sizeof( uint32_t );

            ( *(uint16_t*)current ) = ordering.size();
            current += sizeof( uint16_t );

            for( auto title : ordering )
            {
                ::memcpy( current, title.c_str(), 4 );
                current += 4;
            }

            uint32_t offset = 0;
            for( const auto& data : datas )
            {
                ( *(uint32_t*)current ) = offset;
                offset += data.size();
                current += sizeof( uint32_t );
            }

            for( const auto& data : datas )
            {
                ::memcpy( current, data.data(), data.size() );
                current += data.size();
            }

            write_binary_file( filepath, content.data(), content.size() );
        }
    }

    return 0;
}
