//
// LDASTools AL - A library collection to provide an abstraction layer
//
// Copyright (C) 2018 California Institute of Technology
//
// LDASTools AL 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 AL 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
//

#include <ldastoolsal_config.h>

#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>

#include <iostream>
#include <map>
#include <sstream>
#include <stdexcept>

#include "ldastoolsal/MemChecker.hh"
#include "ldastoolsal/mutexlock.hh"
#include "ldastoolsal/regex.hh"
#include "ldastoolsal/regexmatch.hh"
#include "ldastoolsal/Singleton.hh"

#include "ldastoolsal/FileType.hh"
#include "ldastoolsal/System.hh"

using LDASTools::AL::MemChecker;
using LDASTools::AL::MutexLock;
using LDASTools::System::ErrnoMessage;

namespace
{
    class file_pattern_cache
    {
        SINGLETON_TS_DECL( file_pattern_cache );

    public:
        enum
        {
            REGEX_BINARY = 0,
            REGEX_BZIP,
            REGEX_ELF,
            REGEX_EPS,
            REGEX_FRAME,
            REGEX_GIF,
            REGEX_GZIP,
            REGEX_HTML,
            REGEX_HTML_404,
            REGEX_HTML_ERROR,
            REGEX_ILWD,
            REGEX_ILWD_FRAME,
            REGEX_PGP,
            REGEX_SCRIPT,
            REGEX_XML,
            REGEX_XML_DOC,
            //-----------------------------------------------------------------------
            // Set the size of the array
            //-----------------------------------------------------------------------
            REGEX_MAX_SIZE
        };

        ~file_pattern_cache( );

        static const Regex&
        Lookup( int Id )
        {
            return Instance( ).lookup( Id );
        }

    private:
        const Regex&
        lookup( int Id )
        {
            return *( m_cache[ Id ] );
        }

        typedef std::vector< Regex* > file_regex_registry_type;

        //-------------------------------------------------------------------
        /// \brief Baton to control access
        //-------------------------------------------------------------------
        MutexLock::baton_type m_baton;

        file_regex_registry_type m_cache;
    };

    SINGLETON_TS_INST( file_pattern_cache );
    //---------------------------------------------------------------------
    /// Initialize anything that is specific to the class.
    //---------------------------------------------------------------------
    file_pattern_cache::file_pattern_cache( )
    {
        m_cache.resize( REGEX_MAX_SIZE, (Regex*)NULL );

        m_cache[ REGEX_BINARY ] =
            new Regex( "[^[:print:][:space:]]", REG_EXTENDED );
        m_cache[ REGEX_BZIP ] = new Regex( "^BZh91AY\\&SY" );
        m_cache[ REGEX_ELF ] = new Regex( "^[\x7F]ELF", REG_EXTENDED );
        m_cache[ REGEX_EPS ] = new Regex( "[[:space:]]EPSF[-]" );
        m_cache[ REGEX_FRAME ] = new Regex( "^IGWD" );
        m_cache[ REGEX_GIF ] = new Regex( "^GIF" );
        m_cache[ REGEX_GZIP ] = new Regex( "^\x1f\x8b" );
        m_cache[ REGEX_HTML ] = new Regex( "([<]html[>]|[<]a[[:space:]]+href=)",
                                           REG_EXTENDED | REG_ICASE );
        m_cache[ REGEX_HTML_404 ] = new Regex(
            "404[[:space:]]+Not[[:space:]]+Found", REG_EXTENDED | REG_ICASE );
        m_cache[ REGEX_HTML_ERROR ] = new Regex(
            "Error[[:space:]]+[(][[:digit:]]{3}[)]", REG_EXTENDED | REG_ICASE );
        m_cache[ REGEX_ILWD ] =
            new Regex( "[<][?]ilwd[?][>]", REG_EXTENDED | REG_ICASE );
        m_cache[ REGEX_ILWD_FRAME ] = new Regex( "LIGO::Frame" );
        m_cache[ REGEX_PGP ] = new Regex( "BEGIN PGP MESSAGE" );
        m_cache[ REGEX_SCRIPT ] =
            new Regex( "^[#][!][[:space:]]*([^[:space:]]+)", REG_EXTENDED );
        m_cache[ REGEX_XML ] = new Regex( "[<][?]xml", REG_ICASE );
        m_cache[ REGEX_XML_DOC ] =
            new Regex( "[<]!DOCTYPE[[:space:]]+([^[:space:]]+)",
                       REG_EXTENDED | REG_ICASE );

        MemChecker::Append(
            singleton_suicide,
            "<anonymous>::file_pattern_cache::singleton_suicide",
            10 );
    }

    file_pattern_cache::~file_pattern_cache( )
    {
        for ( file_regex_registry_type::const_iterator cur = m_cache.begin( ),
                                                       last = m_cache.end( );
              cur != last;
              ++cur )
        {
            delete *cur;
        }
        m_cache.erase( m_cache.begin( ), m_cache.end( ) );
    }

#define BinaryRegex                                                            \
    ( file_pattern_cache::Lookup( file_pattern_cache::REGEX_BINARY ) )
#define BZipRegex                                                              \
    ( file_pattern_cache::Lookup( file_pattern_cache::REGEX_BZIP ) )
#define ElfRegex ( file_pattern_cache::Lookup( file_pattern_cache::REGEX_ELF ) )
#define EPSRegex ( file_pattern_cache::Lookup( file_pattern_cache::REGEX_EPS ) )
#define FrameRegex                                                             \
    ( file_pattern_cache::Lookup( file_pattern_cache::REGEX_FRAME ) )
#define GIFRegex ( file_pattern_cache::Lookup( file_pattern_cache::REGEX_GIF ) )
#define GZipRegex                                                              \
    ( file_pattern_cache::Lookup( file_pattern_cache::REGEX_GZIP ) )
#define HTMLRegex                                                              \
    ( file_pattern_cache::Lookup( file_pattern_cache::REGEX_HTML ) )
#define HTML404Regex                                                           \
    ( file_pattern_cache::Lookup( file_pattern_cache::REGEX_HTML_404 ) )
#define HTMLErrorRegex                                                         \
    ( file_pattern_cache::Lookup( file_pattern_cache::REGEX_HTML_ERROR ) )
#define ILWDRegex                                                              \
    ( file_pattern_cache::Lookup( file_pattern_cache::REGEX_ILWD ) )
#define ILWDFrameRegex                                                         \
    ( file_pattern_cache::Lookup( file_pattern_cache::REGEX_ILWD_FRAME ) )
#define PGPRegex ( file_pattern_cache::Lookup( file_pattern_cache::REGEX_PGP ) )
#define ScriptRegex                                                            \
    ( file_pattern_cache::Lookup( file_pattern_cache::REGEX_SCRIPT ) )
#define XMLRegex ( file_pattern_cache::Lookup( file_pattern_cache::REGEX_XML ) )
#define XMLDocRegex                                                            \
    ( file_pattern_cache::Lookup( file_pattern_cache::REGEX_XML_DOC ) )

#if NEEDED
    static bool file_regex_registry_init( );

    static bool initialized = file_regex_registry_init( );

    static bool
    file_regex_registry_init( )
    {

        file_regex_registry.resize( REGEX_MAX_SIZE );

        initialized = true;

#define CREATE2( A, B )                                                        \
    static Regex A##_( B );                                                    \
    file_regex_registry[ A ] = &A##_

#define CREATE( A, B, C )                                                      \
    static Regex A##_( B, C );                                                 \
    file_regex_registry[ A ] = &A##_

        CREATE( REGEX_BINARY, "[^[:print:][:space:]]", REG_EXTENDED );
        CREATE2( REGEX_BZIP, "^BZh91AY\\&SY" );
        CREATE( REGEX_ELF, "^[\x7F]ELF", REG_EXTENDED );
        CREATE2( REGEX_EPS, "[[:space:]]EPSF[-]" );
        CREATE2( REGEX_FRAME, "^IGWD" );
        CREATE2( REGEX_GIF, "^GIF" );
        CREATE2( REGEX_GZIP, "^\x1f\x8b" );
        CREATE( REGEX_HTML,
                "([<]html[>]|[<]a[[:space:]]+href=)",
                REG_EXTENDED | REG_ICASE );
        CREATE( REGEX_HTML_404,
                "404[[:space:]]+Not[[:space:]]+Found",
                REG_EXTENDED | REG_ICASE );
        CREATE( REGEX_HTML_ERROR,
                "Error[[:space:]]+[(][[:digit:]]{3}[)]",
                REG_EXTENDED | REG_ICASE );
        CREATE( REGEX_ILWD, "[<][?]ilwd[?][>]", REG_EXTENDED | REG_ICASE );
        CREATE2( REGEX_ILWD_FRAME, "LIGO::Frame" );
        CREATE2( REGEX_PGP, "BEGIN PGP MESSAGE" );
        CREATE(
            REGEX_SCRIPT, "^[#][!][[:space:]]*([^[:space:]]+)", REG_EXTENDED );
        CREATE( REGEX_XML, "[<][?]xml", REG_ICASE );
        CREATE( REGEX_XML_DOC,
                "[<]!DOCTYPE[[:space:]]+([^[:space:]]+)",
                REG_EXTENDED | REG_ICASE );

#undef CREATE
#undef CREATE2

        return true;
    }
#endif /* NEEDED */
} // namespace

namespace LDASTools
{
    namespace AL
    {
        FileType::FileType( const std::string& Filename )
        {
            //-------------------------------------------------------------------
            // Patterns
            //-------------------------------------------------------------------
            static char JPegTag[] = { '\xFF', '\xD8', '\xFF', '\xE0', '\x00',
                                      '\x10', 'J',    'F',    'I',    'F' };
            static char PDFTag[] = { '%', '!', 'P', 'D', 'F', '-' };
            static char PNGTag[] = { '\x89', 'P', 'N', 'G' };
            static char PSTag[] = { '%', '!', 'P', 'S', '-' };
            static char TiffTag[] = { 'M', 'M', '\0', '*' };

            //-------------------------------------------------------------------
            // :TODO: Check for existance
            //-------------------------------------------------------------------
            bool        is_link = false;
            struct stat file_info;
            int         err = lstat( Filename.c_str( ), &file_info );

            if ( err != 0 )
            {
                std::ostringstream msg;

                msg << "FileType error when accessing file: " << Filename
                    << " - " << strerror( errno );
                throw std::runtime_error( msg.str( ) );
            }
            if ( file_info.st_mode == S_IFLNK )
            {
                is_link = true;
                err = stat( Filename.c_str( ), &file_info );

                if ( err != 0 )
                {
                    std::ostringstream msg;

                    msg << "FileType error when accessing symbolic link file: "
                        << Filename << " - " << strerror( errno );
                    throw std::runtime_error( msg.str( ) );
                }
            }
            switch ( file_info.st_mode & S_IFMT )
            {
            case S_IFIFO:
                m_type_desc += "fifio special";
                break;
            case S_IFCHR:
                m_type_desc += "character special";
                break;
            case S_IFDIR:
                m_type_desc += "directory";
                break;
            case S_IFBLK:
                m_type_desc += "block special";
                break;
            case S_IFREG:
                if ( file_info.st_size == 0 )
                {
                    m_type_desc += "empty";
                }
                else
                {
                    int fd = open( Filename.c_str( ), O_RDONLY );
                    if ( fd < 0 )
                    {
                        m_type_desc += "unknown";
                    }
                    else // if ( fd < 0 )
                    {
                        char       buffer[ 1025 ];
                        RegexMatch compare( 1 );
                        ssize_t len = read( fd, buffer, sizeof( buffer ) - 1 );
                        if ( len == ( ssize_t )( -1 ) )
                        {
                            m_type_desc += "unknown (";
                            m_type_desc += ErrnoMessage( );
                            m_type_desc += " )";
                        }
                        else
                        {
                            buffer[ len ] = '\0';
                            bool           binary = false;
                            unsigned char* ptr = (unsigned char*)buffer;
                            for ( ssize_t x = 0;
                                  ( binary == false ) && ( x < len );
                                  ++x, ++ptr )
                            {
                                if ( !( isspace( *ptr ) || isprint( *ptr ) ) )
                                {
                                    binary = true;
                                }
                            } // for

                            //-----------------------------------------------------------
                            // Differentiate between binary and text
                            //-----------------------------------------------------------
                            if ( binary )
                            {
                                m_type_desc = "binary";
                            }
                            else
                            {
                                m_type_desc = "text";
                            }
                            //-----------------------------------------------------------
                            //
                            //-----------------------------------------------------------
                            if ( compare.match( ScriptRegex, buffer ) )
                            {
                                m_type_desc += " ";
                                m_type_desc += compare.getSubString( 0 );
                            }
                            else if ( binary &&
                                      compare.match( ElfRegex, buffer ) )
                            {
                                m_type_desc += " elf";
                            }
                            else if ( binary &&
                                      compare.match( BZipRegex, buffer ) )
                            {
                                m_type_desc += " bzip";
                            }
                            else if ( binary &&
                                      compare.match( GZipRegex, buffer ) )
                            {
                                m_type_desc += " gzip";
                            }
                            else if ( binary &&
                                      compare.match( GIFRegex, buffer ) )
                            {
                                m_type_desc += " gif";
                            }
                            else if ( binary &&
                                      ( strncmp( PNGTag,
                                                 buffer,
                                                 sizeof( PNGTag ) ) == 0 ) )
                            {
                                m_type_desc += " png";
                            }
                            else if ( binary &&
                                      ( strncmp( JPegTag,
                                                 buffer,
                                                 sizeof( JPegTag ) ) == 0 ) )
                            {
                                m_type_desc += " jpeg";
                            }
                            else if ( binary &&
                                      ( strncmp( TiffTag,
                                                 buffer,
                                                 sizeof( TiffTag ) ) == 0 ) )
                            {
                                m_type_desc += " tiff";
                            }
                            else if ( binary &&
                                      ( strncmp( PDFTag,
                                                 buffer,
                                                 sizeof( PDFTag ) ) == 0 ) )
                            {
                                m_type_desc += " pdf";
                            }
                            else if ( !binary &&
                                      compare.match( HTMLRegex, buffer ) )
                            {
                                m_type_desc += " html";
                                if ( compare.match( HTML404Regex, buffer ) )
                                {
                                    m_type_desc += " 404";
                                }
                                else if ( compare.match( HTMLErrorRegex,
                                                         buffer ) )
                                {
                                    m_type_desc += " ";
                                    m_type_desc += compare.getSubString( 0 );
                                }
                            }
                            else if ( strncmp( PSTag,
                                               buffer,
                                               sizeof( PSTag ) ) == 0 )
                            {
                                m_type_desc += " ps";
                                if ( compare.match( EPSRegex, buffer ) )
                                {
                                    m_type_desc += " eps";
                                }
                            }
                            else if ( compare.match( XMLRegex, buffer ) )
                            {
                                m_type_desc += " xml";
                                if ( compare.match( XMLDocRegex, buffer ) )
                                {
                                    m_type_desc += " ";
                                    m_type_desc += compare.getSubString( 0 );
                                }
                            }
                            else if ( compare.match( PGPRegex, buffer ) )
                            {
                                m_type_desc += " pgp";
                            }
                            else if ( compare.match( FrameRegex, buffer ) )
                            {
                                m_type_desc += " frame";
                            }
                            else if ( compare.match( ILWDRegex, buffer ) )
                            {
                                m_type_desc += " ilwd";
                                if ( compare.match( ILWDFrameRegex, buffer ) )
                                {
                                    m_type_desc += " ligo frame";
                                }
                            }
                            else
                            {
                                m_type_desc += " unknown";
                            }
                        } // if ( len < 0 )
                        close( fd );
                    }
                }
                break;
            }
            if ( is_link )
            {
                m_type_desc += " link";
            }
        }
    } // namespace AL
} // namespace LDASTools
