//
// LDASTools frameCPP - A library implementing the LIGO/Virgo frame
// specification
//
// Copyright (C) 2018 California Institute of Technology
//
// LDASTools frameCPP is free software; you may 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 (GPLv2) of the
// License or at your discretion, any later version.
//
// LDASTools frameCPP is distributed in the hope that it will be useful, but
// without any warranty or even the implied warranty of merchantability
// or fitness for a particular purpose. See the GNU General Public
// License (GPLv2) for more details.
//
// Neither the names of the California Institute of Technology (Caltech),
// The Massachusetts Institute of Technology (M.I.T), The Laser
// Interferometer Gravitational-Wave Observatory (LIGO), nor the names
// of its contributors may be used to endorse or promote products derived
// from this software without specific prior written permission.
//
// You should have received a copy of the licensing terms for this
// software included in the file LICENSE located in the top-level
// directory of this package. If you did not, you can view a copy at
// http://dcc.ligo.org/M1500244/LICENSE
//

#if HAVE_CONFIG_H
#include <framecpp_config.h>
#endif /* HAVE_CONFIG_H */

#include <stdlib.h>
#include <unistd.h>

#include <algorithm>
#include <iostream>
#include <iomanip>
#include <list>
#include <sstream>
#include <stdexcept>
#include <string>

#include "ldastoolsal/MemChecker.hh"
#include "ldastoolsal/fstream.hh"
#include "ldastoolsal/util.hh"
#include "ldastoolsal/types.hh"

#include "framecpp/FrameCPP.hh"
#include "framecpp/FrameCPPDebugManager.hh"
#include "framecpp/IFrameStream.hh"

#include <boost/program_options.hpp>

using std::filebuf;

using LDASTools::AL::cmp_nocase;
using LDASTools::AL::MemChecker;

using FrameCPP::Common::FrameBuffer;
typedef FrameCPP::Common::FrameSpec::Object Object;
typedef ::FrameCPP::Common::FrameSpec::ObjectInterface::object_type
    object_base_type;

namespace po = boost::program_options;

typedef FrameCPP::Common::IFrameStreamWrapper< 0,
                                               Object /* FrAdcData */,
                                               Object /* FrameH */,
                                               Object /* FrProcData */,
                                               Object /* FrSerData */,
                                               Object /* FrSimData */,
                                               Object /* FrRawData */,
                                               Object /* FrEvent */,
                                               Object /* FrSimEvent */ >
    IFrameStream;

class CommandLine;

enum object_type
{
    ADC,
    HEADER,
    LIST,
    PROC,
    ANY,
    SIM,
    SER,
    EVENT,
    SIM_EVENT
};

struct request_typearget_type
{
    enum
    {
        NAME,
        INDEX
    } type;
    std::string name;
    INT_4U      index;

    request_typearget_type( const std::string& Target )
        : type( NAME ), name( Target )
    {
        if ( Target.length( ) > 0 )
        {
            std::istringstream iss( Target );
            std::string        junk;
            iss >> index;
            if ( iss.good( ) )
            {
                iss >> junk;
                if ( junk.length( ) == 0 )
                {
                    type = INDEX;
                }
            }
        }
    }
};

object_type convert_to_type( const std::string& Type );

inline void
depart( int ExitCode )
{
    exit( ExitCode );
}

//-----------------------------------------------------------------------
/// \brief Class to handle command line options for this application
//-----------------------------------------------------------------------
class CommandLine
{
public:
    typedef std::pair< object_type, request_typearget_type > request_type;
    typedef std::list< request_type >      request_container_type;
    typedef std::pair< INT_4U, INT_4U >    range_info_type;
    typedef std::vector< range_info_type > range_type;

    CommandLine( int ArgC, char** ArgV );

    inline void
    Usage( int ExitValue ) const
    {
        std::cout << "Usage: " << m_program_name << " [options] <file> [<file> ...]\n";
        std::cout << m_visible_options << std::endl;
        depart( ExitValue );
    }

    inline bool
    empty( ) const
    {
        return m_input_files.empty();
    }

    inline std::string
    Pop( )
    {
        if (!m_input_files.empty()) {
            std::string file = m_input_files.front();
            m_input_files.pop_front();
            return file;
        }
        return "";
    }

    inline size_t
    size( ) const
    {
        return m_input_files.size();
    }

    inline bool
    BadOption( ) const
    {
        // With boost::program_options, bad options are handled during parsing
        // This method is kept for compatibility but always returns false
        return false;
    }

    inline void
    Range( INT_4U First, INT_4U Last )
    {
        m_ranges.push_back( range_info_type( First, Last ) );
    }

    inline const range_type&
    Ranges( ) const
    {
        return m_ranges;
    }

    inline void
    RangesPop( )
    {
        m_ranges.pop_back( );
    }

    inline const request_container_type&
    Requests( ) const
    {
        return m_requests;
    }

    inline bool
    ScientificNotation( ) const
    {
        return m_scientific_notation;
    }

    inline void
    ScientificNotation( bool Value )
    {
        m_scientific_notation = Value;
    }

    inline bool
    SilentData( ) const
    {
        return m_silent_data;
    }

    inline void
    SilentData( bool Value )
    {
        m_silent_data = Value;
    }

    inline const std::string&
    ProgramName( ) const
    {
        return m_program_name;
    }

private:
    std::string m_program_name;
    po::options_description m_visible_options;
    po::options_description m_all_options;
    po::variables_map m_vm;
    
    bool      m_scientific_notation;
    bool      m_silent_data;

    request_container_type m_requests;
    range_type             m_ranges;
    std::list<std::string> m_input_files;
};

CommandLine::CommandLine( int ArgC, char** ArgV )
    : m_program_name( ArgV[0] ),
      m_visible_options( "Options" ),
      m_all_options( "All options" ),
      m_scientific_notation( false ),
      m_silent_data( false )
{
    try {
        // Define visible options (shown in --help)
        m_visible_options.add_options()
            ("help,h", "Show this help message")
            ("adc", po::value<std::vector<std::string>>()->composing(), 
             "Display the specified FrADC channel")
            ("dump", po::value<std::vector<std::string>>()->composing(),
             "Dump the contents of the specified channel (format: [(adc|event|proc|ser|sim|simevent)%]channel-name)")
            ("frame-range", po::value<std::vector<std::string>>()->composing(),
             "Specify the range of frames to use in queries (format: first[,last])")
            ("header", "Display the frame header information")
            ("list", po::value<std::vector<std::string>>()->composing(),
             "Display the channel names of the specified type (adc|event|proc|ser|sim|simevent|any)")
            ("proc", po::value<std::vector<std::string>>()->composing(),
             "Display the specified FrProc channel")
            ("scientific-notation", "Display real and complex elements in scientific notation")
            ("silent-data", "Suppress data output")
        ;

        // Add debug options using FrameCPPDebugManager
        FrameCPP::Utilities::FrameCPPDebugManager::add_debug_options(m_visible_options, m_vm);

        // Hidden options (input files)
        po::options_description hidden_options("Hidden options");
        hidden_options.add_options()
            ("input-files", po::value<std::vector<std::string>>(), "Input files")
        ;

        // All options (visible + hidden)
        m_all_options.add(m_visible_options).add(hidden_options);

        // Positional options (input files)
        po::positional_options_description pos_options;
        pos_options.add("input-files", -1);

        // Parse command line
        po::store(po::command_line_parser(ArgC, ArgV)
                  .options(m_all_options)
                  .positional(pos_options)
                  .run(), m_vm);
        po::notify(m_vm);

        // Handle help
        if (m_vm.count("help")) {
            Usage(0);
        }

        // Configure debug manager from parsed options
        FrameCPP::Utilities::FrameCPPDebugManager::configure_from_options(m_vm);

        // Process options
        if (m_vm.count("scientific-notation")) {
            m_scientific_notation = true;
        }
        if (m_vm.count("silent-data")) {
            m_silent_data = true;
        }

        // Process input files
        if (m_vm.count("input-files")) {
            std::vector<std::string> files = m_vm["input-files"].as<std::vector<std::string>>();
            for (const auto& file : files) {
                m_input_files.push_back(file);
            }
        }

        // Process ADC requests
        if (m_vm.count("adc")) {
            std::vector<std::string> channels = m_vm["adc"].as<std::vector<std::string>>();
            for (const auto& channel : channels) {
                m_requests.push_back(request_type(ADC, request_typearget_type(channel)));
            }
        }

        // Process dump requests
        if (m_vm.count("dump")) {
            std::vector<std::string> dumps = m_vm["dump"].as<std::vector<std::string>>();
            for (const auto& dump : dumps) {
                std::string name(dump);
                std::string prefix;
                std::string::size_type pos = name.find_first_of("%");
                
                if (pos != std::string::npos) {
                    prefix = name.substr(0, pos);
                    ++pos;
                    name.replace(0, pos, "");
                }
                m_requests.push_back(request_type(convert_to_type(prefix), 
                                                 request_typearget_type(name)));
            }
        }

        // Process frame range requests
        if (m_vm.count("frame-range")) {
            std::vector<std::string> ranges = m_vm["frame-range"].as<std::vector<std::string>>();
            for (const auto& range_str : ranges) {
                std::istringstream iss(range_str);
                INT_4U first, last;
                char c;
                
                iss >> first;
                if (iss.eof()) {
                    last = first;
                } else {
                    iss >> c >> last;
                }
                m_ranges.push_back(range_info_type(first, ++last));
            }
        }

        // Process header request
        if (m_vm.count("header")) {
            m_requests.push_back(request_type(HEADER, request_typearget_type("")));
        }

        // Process list requests
        if (m_vm.count("list")) {
            std::vector<std::string> lists = m_vm["list"].as<std::vector<std::string>>();
            for (const auto& list : lists) {
                m_requests.push_back(request_type(LIST, request_typearget_type(list)));
            }
        }

        // Process proc requests
        if (m_vm.count("proc")) {
            std::vector<std::string> channels = m_vm["proc"].as<std::vector<std::string>>();
            for (const auto& channel : channels) {
                m_requests.push_back(request_type(PROC, request_typearget_type(channel)));
            }
        }
        
    } catch (const po::error& e) {
        std::cerr << "Error parsing command line: " << e.what() << std::endl;
        Usage(1);
    } catch (const std::exception& e) {
        std::cerr << "Error: " << e.what() << std::endl;
        Usage(1);
    }
}

namespace
{
    template < class T >
    void
    list_names( const T& DataSet, bool Alphabetical )
    {
        typename T::const_iterator begin( DataSet.begin( ) );
        typename T::const_iterator end( DataSet.end( ) );

        std::list< std::string > names;

        for ( typename T::const_iterator o( begin ); o != end; o++ )
        {
            names.push_back( ( *o ).first );
        }
        if ( Alphabetical )
        {
            names.sort( );
        }
        for ( std::list< std::string >::const_iterator i( names.begin( ) );
              i != names.end( );
              i++ )
        {
            std::cout << ( *i ) << std::endl;
        }
    }

} // namespace

template < int >
void dump( Object* Obj, const CommandLine& Options );
template < int >
void dump_toc_list( const Object* TOC, object_type Type );

#include "FrameQuery.tcc"
#include "DumpObjects6.cc"
#include "DumpObjects7.tcc"
#include "DumpObjects8.tcc"
#include "DumpObjects9.tcc"

namespace
{
    void
    dump_object( INT_4U Version, Object* Obj, const CommandLine& Options )
    {
        switch ( Version )
        {
        case 9:
          dump< 9 >( Obj, Options );
          break;
        case 8:
            dump< 8 >( Obj, Options );
            break;
        case 7:
            dump< 7 >( Obj, Options );
            break;
        case 6:
            dump< 6 >( Obj, Options );
            break;
        default:
        {
            std::ostringstream msg;

            msg << "Version " << Version
                << " of the frame specification is currently unsupported by "
                   "this application";
            throw std::runtime_error( msg.str( ) );
        }
        }
    }

    void
    toc_list( INT_4U Version, const Object* TOC, object_type Type )
    {
        switch ( Version )
        {
        case 8:
            dump_toc_list< 8 >( TOC, Type );
            break;
        case 7:
            dump_toc_list< 7 >( TOC, Type );
            break;
        case 6:
            dump_toc_list< 6 >( TOC, Type );
            break;
        default:
        {
            std::ostringstream msg;

            msg << "Version " << Version
                << " of the frame specification is currently unsupported by "
                   "this application";
            throw std::runtime_error( msg.str( ) );
        }
        }
    }

} // namespace

object_type
convert_to_type( const std::string& Type )
{
    object_type retval = ANY;

    if ( cmp_nocase( Type, "adc" ) == 0 )
    {
        retval = ADC;
    }
    else if ( cmp_nocase( Type, "event" ) == 0 )
    {
        retval = EVENT;
    }
    else if ( cmp_nocase( Type, "proc" ) == 0 )
    {
        retval = PROC;
    }
    else if ( cmp_nocase( Type, "ser" ) == 0 )
    {
        retval = SER;
    }
    else if ( cmp_nocase( Type, "sim" ) == 0 )
    {
        retval = SIM;
    }
    else if ( cmp_nocase( Type, "simevent" ) == 0 )
    {
        retval = SIM_EVENT;
    }
    return retval;
}

int
main( int ArgC, char** ArgV ) try
{
    MemChecker::Trigger gc_trigger( true );
    
    // Initialize debug level manager with frameCPP-specific configuration FIRST
    // This must happen before any frame processing to ensure loggers get correct debug levels
    if (!FrameCPP::Utilities::FrameCPPDebugManager::initialize(ArgC, ArgV, "framecpp_dump_toc")) {
        return 0; // Help was requested and shown
    }
    
    CommandLine         cl( ArgC, ArgV );

    int exit_status = 0;

    FrameCPP::Initialize( );

    if ( ( cl.empty( ) ) || ( cl.BadOption( ) ) )
    {
        cl.Usage( 1 );
    }
    while ( cl.empty( ) == false )
    {
        try
        {
            //-----------------------------------------------------------------
            // Create new instance
            //-----------------------------------------------------------------
            FrameBuffer< filebuf >* ifb(
                new FrameBuffer< filebuf >( std::ios::in ) );
            ifb->open( cl.Pop( ).c_str( ), std::ios::in | std::ios::binary );
            IFrameStream frame_stream( ifb );

            //-----------------------------------------------------------------
            // Give some basic information
            //-----------------------------------------------------------------
            std::cout << "Frame Version: " << frame_stream.Version( );
            try
            {
                const FrameCPP::Common::FrHeader& hdr(
                    frame_stream.GetFrHeader( ) );

                std::cout << " Frame Library: " << hdr.GetFrameLibraryName( )
                          << " Library Revision: " << hdr.GetLibraryRevision( )
                          << " Originator: " << hdr.GetOriginator( );
            }
            catch ( ... )
            {
                // Some information does not exist
            }
            std::cout << std::endl;
            //-----------------------------------------------------------------
            // Start with providing the user requested information.
            //-----------------------------------------------------------------
            if ( cl.Requests( ).size( ) == 0 )
            {
                try
                {
                    if ( cl.Ranges( ).size( ) == 0 )
                    {
                        //-------------------------------------------------------------
                        // Read
                        //-------------------------------------------------------------
                        object_base_type frame_base(
                            frame_stream.ReadFrame( ) );

                        try
                        {
                            while ( frame_base )
                            {
                                dump_object( frame_stream.Version( ),
                                             frame_base.get( ),
                                             cl );
                                frame_base = frame_stream.ReadFrame( );
                            }
                        }
                        catch ( const std::range_error& Error )
                        {
                            //---------------------------------------------------------
                            // This is how we expect to exit.
                            //---------------------------------------------------------
                        }
                    }
                    else
                    {
                        //-------------------------------------------------------------
                        // Read specific frames from the frame file
                        //-------------------------------------------------------------
                        object_base_type frame_base(
                            frame_stream.ReadFrame( ) );

                        for ( CommandLine::range_type::const_iterator
                                  current_range = cl.Ranges( ).begin( ),
                                  end_range = cl.Ranges( ).end( );
                              current_range != end_range;
                              current_range++ )
                        {
                            for ( INT_4U
                                      current_frame( ( *current_range ).first ),
                                  end_frame( ( *current_range ).second );
                                  current_frame < end_frame;
                                  current_frame++ )
                            {
                                std::cout << "Frame Offset: " << current_frame
                                          << std::endl;

                                frame_base =
                                    frame_stream.ReadFrameN( current_frame );

                                if ( frame_base )
                                {
                                    dump_object( frame_stream.Version( ),
                                                 frame_base.get( ),
                                                 cl );
                                }
                            }
                        }
                    }
                }
                catch ( const std::exception& e )
                {
                    std::cerr << "Caught exception while processing file: "
                              << e.what( ) << std::endl;
                }
            }
            else
            {
                bool patched_range( false );
                if ( cl.Ranges( ).size( ) == 0 )
                {
                    cl.Range( 0, frame_stream.GetNumberOfFrames( ) );
                    patched_range = true;
                }
                object_base_type object;
                int              action;

                for ( CommandLine::range_type::const_iterator
                          current_range = cl.Ranges( ).begin( ),
                          end_range = cl.Ranges( ).end( );
                      current_range != end_range;
                      ++current_range )
                {
                    for ( INT_4U current_frame( ( *current_range ).first ),
                          end_frame( ( *current_range ).second );
                          current_frame < end_frame;
                          ++current_frame )
                    {
                        std::cout << "Frame Offset: " << current_frame
                                  << std::endl;
                        for ( CommandLine::request_container_type::
                                  const_iterator r( cl.Requests( ).begin( ) );
                              r != cl.Requests( ).end( );
                              r++ )
                        {
                            int                                 outputs = 0;
                            CommandLine::request_container_type dump_objects;

                            object.reset( );
                            action = ( *r ).first;
                            if ( action == ANY )
                            {
                                dump_objects.push_back(
                                    CommandLine::request_type(
                                        ADC, ( *r ).second ) );
                                dump_objects.push_back(
                                    CommandLine::request_type(
                                        EVENT, ( *r ).second ) );
                                dump_objects.push_back(
                                    CommandLine::request_type(
                                        PROC, ( *r ).second ) );
                                dump_objects.push_back(
                                    CommandLine::request_type(
                                        SER, ( *r ).second ) );
                                dump_objects.push_back(
                                    CommandLine::request_type(
                                        SIM, ( *r ).second ) );
                                dump_objects.push_back(
                                    CommandLine::request_type(
                                        SIM_EVENT, ( *r ).second ) );
                            }
                            else
                            {
                                dump_objects.push_back( *r );
                            }
                            for ( std::list< CommandLine::request_type >::
                                      const_iterator d( dump_objects.begin( ) );
                                  d != dump_objects.end( );
                                  d++ )
                            {
                                switch ( ( *d ).first )
                                {
#define READ_OBJECT_BY_NAME( FUNC )                                            \
    if ( ( *d ).second.type == request_typearget_type::NAME )                  \
    {                                                                          \
        object = frame_stream.FUNC( current_frame, ( *d ).second.name );       \
    }

#define READ_OBJECT( FUNC )                                                    \
    READ_OBJECT_BY_NAME( FUNC )                                                \
    else if ( ( *d ).second.type == request_typearget_type::INDEX )            \
    {                                                                          \
        object = frame_stream.FUNC( current_frame, ( *d ).second.index );      \
    }

                                case ADC:
                                    READ_OBJECT( ReadFrAdcData );
                                    break;
                                case EVENT:
                                    READ_OBJECT_BY_NAME( ReadFrEvent );
                                    break;
                                case PROC:
                                    READ_OBJECT( ReadFrProcData );
                                    break;
                                case SER:
                                    READ_OBJECT_BY_NAME( ReadFrSerData );
                                    break;
                                case SIM:
                                    READ_OBJECT_BY_NAME( ReadFrSimData );
                                    break;
                                case SIM_EVENT:
                                    READ_OBJECT_BY_NAME( ReadFrSimEvent );
                                    break;

#undef READ_OBJECT
#undef READ_OBJECT_BY_NAME

                                case HEADER:
                                {
                                    using FrameCPP::Common::FrameH;

                                    object = frame_stream.ReadFrameH(
                                        current_frame,
                                        FrameH::DETECT_SIM |
                                            FrameH::DETECT_PROC |
                                            FrameH::HISTORY );
                                }
                                break;
                                case LIST:
                                {
                                    ++outputs;
                                    const Object* toc( frame_stream.GetTOC( ) );
                                    object_type   type =
                                        convert_to_type( ( *d ).second.name );

                                    toc_list(
                                        frame_stream.Version( ), toc, type );
                                }
                                break;
                                default:
                                    break;
                                }
                                if ( object.get( ) )
                                {
                                    dump_object( frame_stream.Version( ),
                                                 object.get( ),
                                                 cl );
                                    ++outputs;
                                }
                            }
                            if ( outputs == 0 )
                            {
                                std::cerr << "WARNING: Unknown object named: "
                                          << ( *r ).second.name << std::endl;
                            }
                        }
                    }
                }
                if ( patched_range )
                {
                    // Remove the element that was added
                    cl.RangesPop( );
                }
            }
        }
        catch ( const std::exception& exc )
        {
            std::cerr << exc.what( ) << std::endl;
            exit_status = 1;
        }
    } // for ( int x = 0; x < ArgC; x++ )

    depart( exit_status );
}
catch ( std::exception& e )
{
    std::cerr << "ABORT: Caught exception: " << e.what( ) << std::endl;
    depart( 1 );
}
catch ( ... )
{
    std::cerr << "ABORT: Caught unknown exception: " << std::endl;
    depart( 1 );
}
