/*
 * This program source code file is part of KiCad, a free EDA CAD application.
 *
 * Copyright (C) 2018 Jean-Pierre Charras, jp.charras at wanadoo.fr
 * Copyright (C) 1992-2018 KiCad Developers, see AUTHORS.txt for contributors.
 *
 * This program 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 2
 * of the License, or (at your option) any later version.
 *
 * This program 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 this program; if not, you may find one here:
 * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
 * or you may search the http://www.gnu.org website for the version 2 license,
 * or you may write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
 */

/**
 * @file eeschema/dialogs/dialog_bom.cpp
 * @brief Dialog box for creating bom and other documents from generic netlist.
 */


#include <fctsys.h>
#include <pgm_base.h>
#include <kiface_i.h>
#include <confirm.h>
#include <gestfich.h>
#include <sch_edit_frame.h>
#include <wx/dynarray.h>

#include <netlist.h>
#include <netlist_exporter_generic.h>
#include <sch_sheet.h>
#include <invoke_sch_dialog.h>
#include <dialog_helpers.h>
#include <dialog_bom_base.h>
#include <html_messagebox.h>
#include <reporter.h>

#define BOM_PLUGINS_KEY wxT("bom_plugins")
#define BOM_PLUGIN_SELECTED_KEY wxT("bom_plugin_selected")

const char * s_bomHelpInfo =
#include <dialog_bom_help_html.h>
;

#include <dialog_bom_cfg_lexer.h>

using namespace T_BOMCFG_T;

/**
 * Structure BOM_PLUGIN
 * holds data of the BOM plugin.
 */
struct BOM_PLUGIN
{
    wxString Name;
    wxString Command;
    wxArrayString Options;
};

/**
 * Define wxArray of BOM_PLUGIN.
 */
WX_DECLARE_OBJARRAY( BOM_PLUGIN, BOM_PLUGIN_ARRAY );
#include <wx/arrimpl.cpp>
WX_DEFINE_OBJARRAY( BOM_PLUGIN_ARRAY )

/**
 * Class BOM_CFG_READER_PARSER
 * holds data and functions pertinent to parsing a S-expression file
 * for a WORKSHEET_LAYOUT.
 */
class BOM_CFG_READER_PARSER : public DIALOG_BOM_CFG_LEXER
{
    BOM_PLUGIN_ARRAY* m_pluginsList;

public:
    BOM_CFG_READER_PARSER( BOM_PLUGIN_ARRAY* aPlugins,
                           const char* aData, const wxString& aSource );
    void Parse();

private:
    void parsePlugin();
};

// PCB_PLOT_PARAMS_PARSER

BOM_CFG_READER_PARSER::BOM_CFG_READER_PARSER(  BOM_PLUGIN_ARRAY* aPlugins,
                                              const char* aLine,
                                              const wxString& aSource ) :
    DIALOG_BOM_CFG_LEXER( aLine, aSource )
{
    m_pluginsList = aPlugins;
}


void BOM_CFG_READER_PARSER::Parse()
{
    T token;

    while( ( token = NextTok() ) != T_RIGHT )
    {
        if( token == T_EOF)
           break;

        if( token == T_LEFT )
            token = NextTok();

        if( token == T_plugins )
            continue;

        switch( token )
        {
        case T_plugin:   // Defines a new plugin
            parsePlugin();
            break;

        default:
//            Unexpected( CurText() );
            break;
        }
    }
}

void BOM_CFG_READER_PARSER::parsePlugin()
{
    BOM_PLUGIN plugin;

    NeedSYMBOLorNUMBER();
    plugin.Name = FromUTF8();

    T token;
    while( ( token = NextTok() ) != T_RIGHT )
    {
        if( token == T_EOF)
           break;

        switch( token )
        {
        case T_LEFT:
            break;

        case T_cmd:
            NeedSYMBOLorNUMBER();
            plugin.Command = FromUTF8();
            NeedRIGHT();
            break;

        case T_opts:
            NeedSYMBOLorNUMBER();
            plugin.Options.Add( FromUTF8() );
            NeedRIGHT();
            break;

        default:
            Unexpected( CurText() );
            break;
        }
    }

    if( ! plugin.Name.IsEmpty() )
        m_pluginsList->Add( plugin );
}

// The main dialog frame to run scripts to build bom
class DIALOG_BOM : public DIALOG_BOM_BASE
{
private:
    SCH_EDIT_FRAME*   m_parent;
    BOM_PLUGIN_ARRAY  m_plugins;
    wxConfigBase*     m_config;         // to store the "plugins"

public:
    // Constructor and destructor
    DIALOG_BOM( SCH_EDIT_FRAME* parent );
    ~DIALOG_BOM();

private:
    void OnPluginSelected( wxCommandEvent& event ) override;
    void OnRunPlugin( wxCommandEvent& event ) override;
    void OnCancelClick( wxCommandEvent& event ) override;
    void OnHelp( wxCommandEvent& event ) override;
    void OnAddPlugin( wxCommandEvent& event ) override;
    void OnRemovePlugin( wxCommandEvent& event ) override;
    void OnEditPlugin( wxCommandEvent& event ) override;
    void OnCommandLineEdited( wxCommandEvent& event ) override;
    void OnNameEdited( wxCommandEvent& event ) override;
    void OnShowConsoleChanged( wxCommandEvent& event ) override;

    void pluginInit();
    void installPluginsList();

    /**
     * @return the Plugin filename from a command line
     * @param aCommand = the command line
     */
    wxString getPluginFileName( const wxString& aCommand );

    /**
     * display (when exists) the text found between the keyword "@package"
     * (compatible with doxygen comments)
     * and the end of comment block (""" in python", --> in xml)
     */
    void displayPluginInfo( FILE * aFile, const wxString& aFilename );

    /**
     * Browse plugin files, and set m_CommandStringCtrl field
     * @return a command line ro run the plugin
     */
    wxString choosePlugin();
};

// Create and show DIALOG_BOM.
int InvokeDialogCreateBOM( SCH_EDIT_FRAME* aCaller )
{
    DIALOG_BOM dlg( aCaller );
    return dlg.ShowModal();
}

DIALOG_BOM::DIALOG_BOM( SCH_EDIT_FRAME* parent ) :
    DIALOG_BOM_BASE( parent )
{
    m_parent = parent;
    m_config = Kiface().KifaceSettings();
    installPluginsList();

#ifndef __WINDOWS__
    m_checkBoxShowConsole->Show( false );
#endif

    // Now all widgets have the size fixed, call FinishDialogSettings
    FinishDialogSettings();
}

DIALOG_BOM::~DIALOG_BOM()
{
    // Save the plugin descriptions in config.
    // the config stores only one string.
    // plugins are saved inside a S expr:
    // ( plugins
    //    ( plugin "plugin name 1" (cmd "command line 1") )
    //    ( plugin "plugin name 2" (cmd "command line 2") (opts "option1") (opts "option2") )
    //     ....
    // )

    STRING_FORMATTER writer;
    writer.Print( 0, "(plugins" );

    for( unsigned ii = 0; ii < m_plugins.GetCount(); ii++ )
    {
        writer.Print( 1, "(plugin %s (cmd %s)",
                      writer.Quotew( m_plugins.Item( ii ).Name ).c_str(),
                      writer.Quotew( m_plugins.Item( ii ).Command ).c_str() );

        for( unsigned jj = 0; jj < m_plugins.Item( ii ).Options.GetCount(); jj++ )
        {
            writer.Print( 1, "(opts %s)",
                          writer.Quotew( m_plugins.Item( ii ).Options.Item( jj ) ).c_str() );
        }

        writer.Print( 0, ")" );
    }

    writer.Print( 0, ")" );

    wxString list( FROM_UTF8( writer.GetString().c_str() ) );

    m_config->Write( BOM_PLUGINS_KEY, list );

    wxString active_plugin_name = m_lbPlugins->GetStringSelection( );
    m_config->Write( BOM_PLUGIN_SELECTED_KEY, active_plugin_name );

}

/* Read the initialized plugins in config and fill the list
 * of names
 */
void DIALOG_BOM::installPluginsList()
{
    wxString list, active_plugin_name;
    m_config->Read( BOM_PLUGINS_KEY, &list );
    m_config->Read( BOM_PLUGIN_SELECTED_KEY, &active_plugin_name );

    if( !list.IsEmpty() )
    {
        BOM_CFG_READER_PARSER cfg_parser( &m_plugins, TO_UTF8( list ),
                                          wxT( "plugins" ) );
        try
        {
            cfg_parser.Parse();
        }
        catch( const IO_ERROR& )
        {
//            wxLogMessage( ioe.What() );
        }
    }

    // Populate list box
    for( unsigned ii = 0; ii < m_plugins.GetCount(); ii++ )
    {
        m_lbPlugins->Append( m_plugins.Item( ii ).Name );

        if( active_plugin_name == m_plugins.Item( ii ).Name )
            m_lbPlugins->SetSelection( ii );
    }

    pluginInit();
}

void DIALOG_BOM::OnPluginSelected( wxCommandEvent& event )
{
    pluginInit();
}

#include <wx/ffile.h>
void DIALOG_BOM::pluginInit()
{
    int ii = m_lbPlugins->GetSelection();

    if( ii < 0 )
    {
        m_textCtrlName->SetValue( wxEmptyString );
        m_textCtrlCommand->SetValue( wxEmptyString );
        return;
    }

    m_textCtrlName->SetValue( m_plugins.Item( ii ).Name );
    m_textCtrlCommand->SetValue( m_plugins.Item( ii ).Command );

#ifdef __WINDOWS__
    if( m_plugins.Item( ii ).Options.Index( wxT( "show_console" ) ) == wxNOT_FOUND )
        m_checkBoxShowConsole->SetValue( false );
    else
        m_checkBoxShowConsole->SetValue( true );
#endif

    wxString pluginName = getPluginFileName( m_textCtrlCommand->GetValue() );

    if( pluginName.IsEmpty() )
        return;

    FILE* pluginFile = wxFopen( pluginName, "rt" );

    if( pluginFile == NULL )
    {
        wxString msg;
        msg.Printf( _( "Failed to open file \"%s\"" ), GetChars( pluginName ) );
        DisplayError( this, msg );
        return;
    }

    displayPluginInfo( pluginFile, pluginName );
}


void DIALOG_BOM::displayPluginInfo( FILE * aFile, const wxString& aFilename )
{
    m_Messages->Clear();

    // display (when exists) the text found between the keyword "@package"
    // (compatible with doxygen comments)
    // and the end of comment block (""" in python", --> in xml)

    wxString data;
    wxFFile fdata( aFile );        // dtor will close the file

    if( !fdata.ReadAll( &data ) )
        return;

    wxString header( wxT( "@package" ) );
    wxString endsection( wxT( "-->" ) );        // For xml

    wxFileName fn( aFilename );

    if( fn.GetExt().IsSameAs( wxT("py"), false ) )
        endsection = wxT( "\"\"\"" );
    else if( !fn.GetExt().IsSameAs( wxT("xsl"), false ) )
        // If this is not a python file, we know nothing about file
        // and the info cannot be found
        return;

    // Extract substring between @package and """
    int strstart = data.Find( header );

    if( strstart == wxNOT_FOUND )
        return;

    strstart += header.Length();
    int strend = data.find( endsection, strstart );

    if( strend == wxNOT_FOUND)
        return;

    // Remove empty line if any
    while( data[strstart] < ' ' )
            strstart++;

    m_Messages->SetValue( data.SubString( strstart, strend-1 ) );
}

/**
 * Function RunPlugin
 * run the plugin command line
 */
void DIALOG_BOM::OnRunPlugin( wxCommandEvent& event )
{
    wxFileName  fn;

    // Calculate the xml netlist filename
    fn = g_RootSheet->GetScreen()->GetFileName();

    fn.SetPath( wxPathOnly( Prj().GetProjectFullName() ) );

    fn.ClearExt();
    wxString fullfilename = fn.GetFullPath();
    m_parent->ClearMsgPanel();

    wxString reportmsg;
    WX_STRING_REPORTER reporter( &reportmsg );
    m_parent->SetNetListerCommand( m_textCtrlCommand->GetValue() );

#ifdef __WINDOWS__
    if( m_checkBoxShowConsole->IsChecked() )
        m_parent->SetExecFlags( wxEXEC_SHOW_CONSOLE );
#endif

    m_parent->CreateNetlist( -1, fullfilename, 0, &reporter, false );

    m_Messages->SetValue( reportmsg );
}


void DIALOG_BOM::OnCancelClick( wxCommandEvent& event )
{
    EndModal( wxID_CANCEL );
}


/**
 * Function OnRemovePlugin
 * Remove a plugin from the list
 */
void DIALOG_BOM::OnRemovePlugin( wxCommandEvent& event )
{
    int ii = m_lbPlugins->GetSelection();

    if( ii < 0 )
        return;

    m_lbPlugins->Delete( ii );

    m_plugins.RemoveAt( ii );

    // Select the next item, if exists
    if( (int)m_lbPlugins->GetCount() >= ii )
        ii =  m_lbPlugins->GetCount() - 1;

    if( ii >= 0 )
        m_lbPlugins->SetSelection( ii );

    pluginInit();
}

/**
 * Function OnAddPlugin
 * Add a new panel for a new netlist plugin
 */
void DIALOG_BOM::OnAddPlugin( wxCommandEvent& event )
{
    wxString cmdLine = choosePlugin();
    BOM_PLUGIN newPlugin;

    if( cmdLine.IsEmpty() )
        return;

    // Creates a new plugin entry
    wxFileName fn( getPluginFileName( cmdLine ) );

    wxString defaultName = fn.GetName();
    wxString name = wxGetTextFromUser( _("Plugin name in plugin list") ,
                                       _("Plugin name"), defaultName );

    if( name.IsEmpty() )
        return;

    // Verify if it does not exists
    for( unsigned ii = 0; ii < m_plugins.GetCount(); ii++ )
    {
        if( name == m_plugins.Item( ii ).Name )
        {
            wxMessageBox( _("This name already exists. Abort") );
            return;
        }
    }

    // Append the new plugin
    newPlugin.Name = name;
    newPlugin.Command = wxEmptyString;
    m_plugins.Add( newPlugin );
    m_lbPlugins->SetSelection( m_lbPlugins->GetCount() - 1 );
    m_lbPlugins->Append( name );
    m_lbPlugins->SetSelection( m_lbPlugins->GetCount() - 1 );
    m_textCtrlCommand->SetValue( cmdLine );

    pluginInit();
}

/*
 * Browse plugin files, and set m_CommandStringCtrl field
 */
wxString DIALOG_BOM::choosePlugin()
{
    wxString mask = wxT( "*" );
#ifndef __WXMAC__
    wxString path = Pgm().GetExecutablePath();
#else
    wxString path = GetOSXKicadDataDir() + wxT( "/plugins" );
#endif

    wxString fullFileName = EDA_FILE_SELECTOR( _( "Plugin files:" ),
                                               path,
                                               wxEmptyString,
                                               wxEmptyString,
                                               mask,
                                               this,
                                               wxFD_OPEN,
                                               true );
    if( fullFileName.IsEmpty() )
        return wxEmptyString;

    // Creates a default command line,
    // suitable to run the external tool xslproc or python
    // The default command line depending on plugin extension, currently
    // "xsl" or "exe" or "py" or "pyw"
    wxString    cmdLine;
    wxFileName  fn( fullFileName );
    wxString    ext = fn.GetExt();

    // Python requires on Windows the path of the script ends by '/' instead of '\'
    // otherwise import does not find modules in the same folder as the python script
    // We cannot change all '\' to '/' in command line because it causes issues with files
    // that are stored on network resources and pointed to using the Windows
    // Universal Naming Convention format. (full filename starting by \\server\)
    //
    // I hope changing the last separator only to '/' will work.
#ifdef __WINDOWS__
    if( ext == wxT("py" ) || ext == wxT("pyw" ) )
    {
        wxString sc_path = fn.GetPathWithSep();
        sc_path.RemoveLast();
        fullFileName = sc_path +'/' + fn.GetFullName();;
    }
#endif

    if( ext == "xsl" )
        cmdLine.Printf( "xsltproc -o \"%%O\" \"%s\" \"%%I\"", GetChars( fullFileName ) );
    else if( ext == "exe" )
        cmdLine.Printf( "\"%s\" < \"%%I\" > \"%%O\"", GetChars( fullFileName ) );
    else if( ext == "py" )
        cmdLine.Printf( "python \"%s\" \"%%I\" \"%%O\"", GetChars( fullFileName ) );
    else if( ext == "pyw" )
#ifdef __WINDOWS__
        cmdLine.Printf( "pythonw \"%s\" \"%%I\" \"%%O\"", GetChars( fullFileName ) );
#else
        cmdLine.Printf( "python \"%s\" \"%%I\" \"%%O\"", GetChars( fullFileName ) );
#endif
    else
        cmdLine.Printf( "\"%s\"", GetChars( fullFileName ) );

    return cmdLine;
}


wxString DIALOG_BOM::getPluginFileName(  const wxString& aCommand )
{
    wxString pluginName;

    // Try to find the plugin name.
    // This is possible if the name ends by .py or .pyw or .xsl or .exe
    int pos = -1;

    if( (pos = aCommand.Find( wxT(".pyw") )) != wxNOT_FOUND )
        pos += 3;
    else if( (pos = aCommand.Find( wxT(".py") )) != wxNOT_FOUND )
        pos += 2;
    else if( (pos = aCommand.Find( wxT(".xsl") )) != wxNOT_FOUND )
        pos += 3;
    else if( (pos = aCommand.Find( wxT(".exe") )) != wxNOT_FOUND )
        pos += 3;

    // the end of plugin name is at position pos.
    if( pos > 0 )
    {
        // Be sure this is the end of the name: the next char is " or space
        int eos = aCommand[pos+1];

        if( eos == ' '|| eos == '\"' )
        {
            // search for the starting point of the name
            int jj = pos-1;
            while( jj >= 0 )
                if( aCommand[jj] != eos )
                    jj--;
                else
                    break;

            // extract the name
            if( jj >= 0 )
            {
                eos = aCommand[jj];

                if( eos == ' '|| eos == '\"' )  // do not include delimiters
                    jj++;

                pluginName = aCommand.SubString( jj, pos );
            }
        }
    }

    // Using a format like %P is possible in  plugin name, so expand it
    wxString prj_dir = Prj().GetProjectPath();

    if( prj_dir.EndsWith( '/' ) || prj_dir.EndsWith( '\\' ) )
        prj_dir.RemoveLast();

    pluginName.Replace( wxT( "%P" ), prj_dir.GetData(), true );

    return pluginName;
}

void DIALOG_BOM::OnEditPlugin( wxCommandEvent& event )
{
    wxString    pluginName = getPluginFileName( m_textCtrlCommand->GetValue() );

    if( pluginName.Length() <= 2 )      // if name != ""
    {
        wxMessageBox( _("Plugin file name not found. Cannot edit plugin file") );
        return;
    }

    AddDelimiterString( pluginName );
    wxString    editorname = Pgm().GetEditorName();

    if( !editorname.IsEmpty() )
        ExecuteFile( this, editorname, pluginName );
    else
        wxMessageBox( _("No text editor selected in KiCad. Please choose it") );
}

void DIALOG_BOM::OnHelp( wxCommandEvent& event )
{
    HTML_MESSAGE_BOX help_Dlg( this, _("Bom Generation Help") );
    help_Dlg.SetDialogSizeInDU( 500, 350 );

    wxString msg = FROM_UTF8(s_bomHelpInfo);
    help_Dlg.m_htmlWindow->AppendToPage( msg );
    help_Dlg.ShowModal();
}

void DIALOG_BOM::OnCommandLineEdited( wxCommandEvent& event )
{
    int ii = m_lbPlugins->GetSelection();

    if( ii < 0 )
        return;

    m_plugins.Item( ii ).Command = m_textCtrlCommand->GetValue();
}

void DIALOG_BOM::OnNameEdited( wxCommandEvent& event )
{
    int ii = m_lbPlugins->GetSelection();

    if( ii < 0 )
        return;

    m_plugins.Item( ii ).Name = m_textCtrlName->GetValue();
    m_lbPlugins->SetString( ii, m_plugins.Item( ii ).Name );
}

void DIALOG_BOM::OnShowConsoleChanged( wxCommandEvent& event )
{
#ifdef __WINDOWS__
    int ii = m_lbPlugins->GetSelection();

    if( ii < 0 )
        return;

    if( m_checkBoxShowConsole->IsChecked() )
    {
        if( m_plugins.Item( ii ).Options.Index( wxT( "show_console" ) ) == wxNOT_FOUND )
            m_plugins.Item( ii ).Options.Add( wxT( "show_console" ) );
    }
    else
    {
        m_plugins.Item( ii ).Options.Remove( wxT( "show_console" ) );
    }
#endif
}
