/*
 * This program source code file is part of KiCad, a free EDA CAD application.
 *
 * Copyright The 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 3 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, see <http://www.gnu.org/licenses/>.
 */

#include "lib_table_grid_tricks.h"
#include "confirm.h"
#include "lib_table_grid_data_model.h"
#include <libraries/library_manager.h>
#include <wx/clipbrd.h>
#include <wx/log.h>
#include <wx/msgdlg.h>
#include <lib_id.h>


LIB_TABLE_GRID_TRICKS::LIB_TABLE_GRID_TRICKS( WX_GRID* aGrid ) :
        GRID_TRICKS( aGrid )
{
    m_grid->Disconnect( wxEVT_CHAR_HOOK );
    m_grid->Connect( wxEVT_CHAR_HOOK, wxCharEventHandler( LIB_TABLE_GRID_TRICKS::onCharHook ), nullptr, this );
}


LIB_TABLE_GRID_TRICKS::LIB_TABLE_GRID_TRICKS( WX_GRID* aGrid,
        std::function<void( wxCommandEvent& )> aAddHandler ) :
        GRID_TRICKS( aGrid, aAddHandler )
{
    m_grid->Disconnect( wxEVT_CHAR_HOOK );
    m_grid->Connect( wxEVT_CHAR_HOOK, wxCharEventHandler( LIB_TABLE_GRID_TRICKS::onCharHook ), nullptr, this );
}


void LIB_TABLE_GRID_TRICKS::onGridCellLeftClick( wxGridEvent& aEvent )
{
    if( aEvent.GetCol() == COL_STATUS )
    {
        // Status column button action depends on row:
        // Normal rows should have no button, so they are a no-op
        // Errored rows should have the warning button, so we show their error
        // Configurable libraries will have the options button, so we launch the config
        // Chained tables will have the open button, so we request the table be opened
        LIB_TABLE_GRID_DATA_MODEL* table = static_cast<LIB_TABLE_GRID_DATA_MODEL*>( m_grid->GetTable() );
        const LIBRARY_TABLE_ROW&   row = table->at( aEvent.GetRow() );
        LIBRARY_MANAGER_ADAPTER*   adapter = table->Adapter();

        wxString title = row.Type() == "Table"
                                ? wxString::Format( _( "Error loading library table '%s'" ), row.Nickname() )
                                : wxString::Format( _( "Error loading library '%s'" ), row.Nickname() );

        if( !row.IsOk() )
        {
            DisplayErrorMessage( m_grid, title, row.ErrorDescription() );
        }
        else if( std::optional<LIBRARY_ERROR> e = adapter->LibraryError( row.Nickname() ) )
        {
            DisplayErrorMessage( m_grid, title, e->message );
        }
        else if( adapter->SupportsConfigurationDialog( row.Nickname() ) )
        {
            adapter->ShowConfigurationDialog( row.Nickname(), wxGetTopLevelParent( m_grid ) );
        }
        else if( row.Type() == LIBRARY_TABLE_ROW::TABLE_TYPE_NAME )
        {
            openTable( row );
        }

        aEvent.Skip();
    }
    else
    {
        GRID_TRICKS::onGridCellLeftClick( aEvent );
    }
}


void LIB_TABLE_GRID_TRICKS::showPopupMenu( wxMenu& menu, wxGridEvent& aEvent )
{
    // Ensure selection parameters are up to date
    getSelectedArea();

    LIB_TABLE_GRID_DATA_MODEL* tbl = static_cast<LIB_TABLE_GRID_DATA_MODEL*>( m_grid->GetTable() );
    const LIBRARY_TABLE_ROW&   firstRow = tbl->at( m_sel_row_start );

    bool showSettings = false;
    bool showOpen = false;
    if( LIBRARY_MANAGER_ADAPTER* adapter = tbl->Adapter() )
    {
        wxString nickname = tbl->GetValue( m_sel_row_start, COL_NICKNAME );

        if( m_sel_row_count == 1 && adapter->SupportsConfigurationDialog( nickname ) )
        {
            showSettings = true;
            menu.Append( LIB_TABLE_GRID_TRICKS_LIBRARY_SETTINGS, _( "Edit Settings..." ) );
        }
    }

    if( firstRow.Type() == LIBRARY_TABLE_ROW::TABLE_TYPE_NAME )
    {
        showOpen = true;
        menu.Append( LIB_TABLE_GRID_TRICKS_OPEN_TABLE, _( "Open Library Table" ) );
    }

    if( showSettings || showOpen )
        menu.AppendSeparator();

    bool showActivate = false;
    bool showDeactivate = false;
    bool showSetVisible = false;
    bool showUnsetVisible = false;
    bool showOptions = false;

    for( int row = m_sel_row_start; row < m_sel_row_start + m_sel_row_count; ++row )
    {
        if( tbl->GetValueAsBool( row, 0 ) )
            showDeactivate = true;
        else
            showActivate = true;

        if( tbl->GetValueAsBool( row, 1 ) )
            showUnsetVisible = true;
        else
            showSetVisible = true;

        if( showActivate && showDeactivate && showSetVisible && showUnsetVisible )
            break;
    }

    if( showActivate )
        menu.Append( LIB_TABLE_GRID_TRICKS_ACTIVATE_SELECTED, _( "Activate Selected" ) );

    if( showDeactivate )
        menu.Append( LIB_TABLE_GRID_TRICKS_DEACTIVATE_SELECTED, _( "Deactivate Selected" ) );

    if( supportsVisibilityColumn() )
    {
        if( showSetVisible )
            menu.Append( LIB_TABLE_GRID_TRICKS_SET_VISIBLE, _( "Set Visible Flag" ) );

        if( showUnsetVisible )
            menu.Append( LIB_TABLE_GRID_TRICKS_UNSET_VISIBLE, _( "Unset Visible Flag" ) );
    }

    if( m_sel_row_count == 1 && firstRow.Type() != LIBRARY_TABLE_ROW::TABLE_TYPE_NAME )
    {
        showOptions = true;
        menu.Append( LIB_TABLE_GRID_TRICKS_OPTIONS_EDITOR, _( "Edit Options..." ),
                     _( "Edit options for this library entry" ) );
    }

    if( showActivate || showDeactivate || showSetVisible || showUnsetVisible || showOptions )
        menu.AppendSeparator();

    GRID_TRICKS::showPopupMenu( menu, aEvent );
}


void LIB_TABLE_GRID_TRICKS::doPopupSelection( wxCommandEvent& event )
{
    int menu_id = event.GetId();
    LIB_TABLE_GRID_DATA_MODEL* tbl = (LIB_TABLE_GRID_DATA_MODEL*) m_grid->GetTable();

    if( menu_id == LIB_TABLE_GRID_TRICKS_OPTIONS_EDITOR )
    {
        optionsEditor( m_grid->GetGridCursorRow() );
    }
    else if( menu_id == LIB_TABLE_GRID_TRICKS_ACTIVATE_SELECTED
            || menu_id == LIB_TABLE_GRID_TRICKS_DEACTIVATE_SELECTED )
    {
        bool selected_state = menu_id == LIB_TABLE_GRID_TRICKS_ACTIVATE_SELECTED;

        for( int row = m_sel_row_start; row < m_sel_row_start + m_sel_row_count; ++row )
            tbl->SetValueAsBool( row, 0, selected_state );

        // Ensure the new state (on/off) of the widgets is immediately shown:
        m_grid->Refresh();
    }
    else if( menu_id == LIB_TABLE_GRID_TRICKS_SET_VISIBLE
            || menu_id == LIB_TABLE_GRID_TRICKS_UNSET_VISIBLE )
    {
        bool selected_state = menu_id == LIB_TABLE_GRID_TRICKS_SET_VISIBLE;

        for( int row = m_sel_row_start; row < m_sel_row_start + m_sel_row_count; ++row )
            tbl->SetValueAsBool( row, 1, selected_state );

        // Ensure the new state (on/off) of the widgets is immediately shown:
        m_grid->Refresh();
    }
    else if( menu_id == LIB_TABLE_GRID_TRICKS_LIBRARY_SETTINGS )
    {
        LIBRARY_MANAGER_ADAPTER* adapter = tbl->Adapter();
        LIBRARY_TABLE_ROW&       row = tbl->At( m_sel_row_start );

        adapter->ShowConfigurationDialog( row.Nickname(), wxGetTopLevelParent( m_grid ) );
    }
    else if( menu_id == LIB_TABLE_GRID_TRICKS_OPEN_TABLE )
    {
        openTable( tbl->At( m_sel_row_start ) );
    }
    else
    {
        GRID_TRICKS::doPopupSelection( event );
    }
}


void LIB_TABLE_GRID_TRICKS::onCharHook( wxKeyEvent& ev )
{
    if( ev.GetModifiers() == wxMOD_CONTROL && ev.GetKeyCode() == 'V' && m_grid->IsCellEditControlShown() )
    {
        wxLogNull doNotLog;

        if( wxTheClipboard->Open() )
        {
            if( wxTheClipboard->IsSupported( wxDF_TEXT ) || wxTheClipboard->IsSupported( wxDF_UNICODETEXT ) )
            {
                wxTextDataObject data;
                wxTheClipboard->GetData( data );

                wxString text = data.GetText();

                if( !text.Contains( '\t' ) && text.Contains( ',' ) )
                    text.Replace( ',', '\t' );

                if( text.Contains( '\t' ) || text.Contains( '\n' ) || text.Contains( '\r' ) )
                {
                    m_grid->CancelPendingChanges();
                    int row = m_grid->GetGridCursorRow();

                    // Check if the current row already has data (has a nickname)
                    wxGridTableBase* table = m_grid->GetTable();
                    if( table && row >= 0 && row < table->GetNumberRows() )
                    {
                        // Check if the row has a nickname (indicating it has existing data)
                        wxString nickname = table->GetValue( row, COL_NICKNAME );
                        if( !nickname.IsEmpty() )
                        {
                            // Row already has data, don't allow pasting over it
                            wxTheClipboard->Close();
                            wxBell(); // Provide audio feedback
                            return;
                        }
                    }

                    m_grid->ClearSelection();
                    m_grid->SelectRow( row );
                    m_grid->SetGridCursor( row, 0 );
                    getSelectedArea();
                    paste_text( text );
                    wxTheClipboard->Close();
                    m_grid->ForceRefresh();
                    return;
                }
            }

            wxTheClipboard->Close();
        }
    }

    GRID_TRICKS::onCharHook( ev );
}


/*
 * Handle specialized clipboard text, either s-expr syntax starting with a lib table preamble
 * (such as "(fp_lib_table"), or spreadsheet formatted text.
 */
void LIB_TABLE_GRID_TRICKS::paste_text( const wxString& cb_text )
{
    LIB_TABLE_GRID_DATA_MODEL* tbl = static_cast<LIB_TABLE_GRID_DATA_MODEL*>( m_grid->GetTable() );

    if( size_t ndx = cb_text.find( getTablePreamble() ); ndx != std::string::npos )
    {
        // paste the LIB_TABLE_ROWs of s-expr, starting at column 0 regardless of current cursor column.

        if( LIBRARY_TABLE tempTable( true, cb_text, tbl->Table().Scope() ); tempTable.IsOk() )
        {
            std::ranges::copy( tempTable.Rows(),
                               std::inserter( tbl->Table().Rows(), tbl->Table().Rows().begin() ) );

            if( tbl->GetView() )
            {
                wxGridTableMessage msg( tbl, wxGRIDTABLE_NOTIFY_ROWS_INSERTED, 0, 0 );
                tbl->GetView()->ProcessTableMessage( msg );
            }
        }
        else
        {
            DisplayError( wxGetTopLevelParent( m_grid ), tempTable.ErrorDescription() );
        }
    }
    else
    {
        wxString text = cb_text;

        if( !text.Contains( '\t' ) && text.Contains( ',' ) )
            text.Replace( ',', '\t' );

        if( text.Contains( '\t' ) )
        {
            int row = m_grid->GetGridCursorRow();
            m_grid->ClearSelection();
            m_grid->SelectRow( row );
            m_grid->SetGridCursor( row, 0 );
            getSelectedArea();
        }

        GRID_TRICKS::paste_text( text );

        m_grid->AutoSizeColumns( false );
    }

    m_grid->AutoSizeColumns( false );
}


bool LIB_TABLE_GRID_TRICKS::handleDoubleClick( wxGridEvent& aEvent )
{
    if( aEvent.GetCol() == COL_OPTIONS )
    {
        optionsEditor( aEvent.GetRow() );
        return true;
    }

    return false;
}


void LIB_TABLE_GRID_TRICKS::AppendRowHandler( WX_GRID* aGrid )
{
    aGrid->OnAddRow(
            [&]() -> std::pair<int, int>
            {
                    aGrid->AppendRows( 1 );
                return { aGrid->GetNumberRows() - 1, COL_NICKNAME };
            } );
}


void LIB_TABLE_GRID_TRICKS::DeleteRowHandler( WX_GRID* aGrid )
{
    if( !aGrid->CommitPendingChanges() )
        return;

    wxGridUpdateLocker noUpdates( aGrid );

    int curRow = aGrid->GetGridCursorRow();
    int curCol = aGrid->GetGridCursorCol();

    // In a wxGrid, collect rows that have a selected cell, or are selected
    // It is not so easy: it depends on the way the selection was made.
    // Here, we collect rows selected by clicking on a row label, and rows that contain
    // previously-selected cells.
    // If no candidate, just delete the row with the grid cursor.
    wxArrayInt selectedRows	= aGrid->GetSelectedRows();
    wxGridCellCoordsArray cells = aGrid->GetSelectedCells();
    wxGridCellCoordsArray blockTopLeft = aGrid->GetSelectionBlockTopLeft();
    wxGridCellCoordsArray blockBotRight = aGrid->GetSelectionBlockBottomRight();

    // Add all row having cell selected to list:
    for( unsigned ii = 0; ii < cells.GetCount(); ii++ )
        selectedRows.Add( cells[ii].GetRow() );

    // Handle block selection
    if( !blockTopLeft.IsEmpty() && !blockBotRight.IsEmpty() )
    {
        for( int i = blockTopLeft[0].GetRow(); i <= blockBotRight[0].GetRow(); ++i )
            selectedRows.Add( i );
    }

    // Use the row having the grid cursor only if we have no candidate:
    if( selectedRows.size() == 0 && aGrid->GetGridCursorRow() >= 0 )
        selectedRows.Add( aGrid->GetGridCursorRow() );

    if( selectedRows.size() == 0 )
    {
        wxBell();
        return;
    }

    std::sort( selectedRows.begin(), selectedRows.end() );

    // Remove selected rows (note: a row can be stored more than once in list)
    int last_row = -1;

    // Needed to avoid a wxWidgets alert if the row to delete is the last row
    // at least on wxMSW 3.2
    aGrid->ClearSelection();

    for( int ii = selectedRows.GetCount()-1; ii >= 0; ii-- )
    {
        int row = selectedRows[ii];

        if( row != last_row )
        {
            last_row = row;
            aGrid->DeleteRows( row, 1 );
        }
    }

    if( aGrid->GetNumberRows() > 0 && curRow >= 0 )
        aGrid->SetGridCursor( std::min( curRow, aGrid->GetNumberRows() - 1 ), curCol );
}


void LIB_TABLE_GRID_TRICKS::MoveUpHandler( WX_GRID* aGrid )
{
    aGrid->OnMoveRowUp(
            [&]( int row )
            {
                LIB_TABLE_GRID_DATA_MODEL* tbl = static_cast<LIB_TABLE_GRID_DATA_MODEL*>( aGrid->GetTable() );
                int curRow = aGrid->GetGridCursorRow();

                std::deque<LIBRARY_TABLE_ROW>& rows = tbl->Table().Rows();

                auto current = rows.begin() + curRow;
                auto prev    = rows.begin() + curRow - 1;

                std::iter_swap( current, prev );

                // Update the wxGrid
                wxGridTableMessage msg( tbl, wxGRIDTABLE_NOTIFY_ROWS_INSERTED, row - 1, 0 );
                tbl->GetView()->ProcessTableMessage( msg );
            } );
}


void LIB_TABLE_GRID_TRICKS::MoveDownHandler( WX_GRID* aGrid )
{
    aGrid->OnMoveRowDown(
            [&]( int row )
            {
                LIB_TABLE_GRID_DATA_MODEL* tbl = static_cast<LIB_TABLE_GRID_DATA_MODEL*>( aGrid->GetTable() );
                int curRow = aGrid->GetGridCursorRow();
                std::deque<LIBRARY_TABLE_ROW>& rows = tbl->Table().Rows();

                auto current = rows.begin() + curRow;
                auto next    = rows.begin() + curRow + 1;

                std::iter_swap( current, next );

                // Update the wxGrid
                wxGridTableMessage msg( tbl, wxGRIDTABLE_NOTIFY_ROWS_INSERTED, row, 0 );
                tbl->GetView()->ProcessTableMessage( msg );
            } );
}


bool LIB_TABLE_GRID_TRICKS::VerifyTable( WX_GRID* aGrid, bool aSupportsVisibilityColumn,
                                         std::function<void( int aRow, int aCol )> aErrorHandler )
{
    wxWindow*                  topLevelParent = wxGetTopLevelParent( aGrid );
    LIB_TABLE_GRID_DATA_MODEL* model = static_cast<LIB_TABLE_GRID_DATA_MODEL*>( aGrid->GetTable() );
    wxString                   msg;

    for( int r = 0; r < model->GetNumberRows(); )
    {
        wxString nick = model->GetValue( r, COL_NICKNAME ).Trim( false ).Trim();
        wxString uri  = model->GetValue( r, COL_URI ).Trim( false ).Trim();
        unsigned illegalCh = 0;

        if( !uri )
        {
            // Silently nuke rows that have no libraray URI
            model->DeleteRows( r, 1 );
        }
        else if( !nick || ( illegalCh = LIB_ID::FindIllegalLibraryNameChar( nick ) ) )
        {
            if( !nick )
                msg = _( "Library must have a nickname." );
            else
                msg = wxString::Format( _( "Illegal character '%c' in nickname '%s'." ), illegalCh, nick );

            aErrorHandler( r, COL_NICKNAME );

            KICAD_MESSAGE_DIALOG errdlg( topLevelParent, msg, _( "Library Nickname Error" ) );
            errdlg.ShowModal();
            return false;
        }
        else
        {
            // set the trimmed values back into the table so they get saved to disk.
            model->SetValue( r, COL_NICKNAME, nick );
            model->SetValue( r, COL_URI, uri );

            if( !aSupportsVisibilityColumn )
            {
            // Make sure to not save an inappropriate hidden flag
                model->SetValue( r, COL_VISIBLE, wxS( "1" ) );
            }

            ++r;        // this row was OK.
        }
    }

    // check for duplicate nickNames
    for( int r1 = 0; r1 < model->GetNumberRows() - 1; ++r1 )
    {
        wxString nick1 = model->GetValue( r1, COL_NICKNAME );

        for( int r2 = r1 + 1; r2 < model->GetNumberRows(); ++r2 )
        {
            wxString nick2 = model->GetValue( r2, COL_NICKNAME );

            if( nick1 == nick2 )
            {
                msg = wxString::Format( _( "Multiple libraries cannot share the same nickname ('%s')." ), nick1 );

                // go to the lower of the two rows, it is technically the duplicate:
                aErrorHandler( r2, 1 );

                KICAD_MESSAGE_DIALOG errdlg( topLevelParent, msg, _( "Library Nickname Error" ) );
                errdlg.ShowModal();
                return false;
            }
        }
    }

    return true;
}
