/*
 * This program source code file is part of KiCad, a free EDA CAD application.
 *
 * Copyright (C) 2018 CERN
 * @author Jon Evans <jon@craftyjon.com>
 *
 * 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 <wx/tokenzr.h>

#include <invoke_sch_dialog.h>
#include <sch_sheet_path.h>

#include "dialog_bus_manager.h"


BEGIN_EVENT_TABLE( DIALOG_BUS_MANAGER, DIALOG_SHIM )
    EVT_BUTTON( wxID_OK, DIALOG_BUS_MANAGER::OnOkClick )
    EVT_BUTTON( wxID_CANCEL, DIALOG_BUS_MANAGER::OnCancelClick )
END_EVENT_TABLE()


DIALOG_BUS_MANAGER::DIALOG_BUS_MANAGER( SCH_EDIT_FRAME* aParent )
        : DIALOG_SHIM( aParent, wxID_ANY, _( "Bus Definitions" ),
                       wxDefaultPosition, wxSize( 640, 480 ),
                       wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER ),
          m_parent( aParent )
{
    auto sizer = new wxBoxSizer( wxVERTICAL );
    auto buttons = new wxStdDialogButtonSizer();

    buttons->AddButton( new wxButton( this, wxID_OK ) );
    buttons->AddButton( new wxButton( this, wxID_CANCEL ) );
    buttons->Realize();

    auto top_container = new wxBoxSizer( wxHORIZONTAL );
    auto left_pane = new wxBoxSizer( wxVERTICAL );
    auto right_pane = new wxBoxSizer( wxVERTICAL );

    // Left pane: alias list
    auto lbl_aliases = new wxStaticText( this, wxID_ANY, _( "Bus Aliases" ),
                                         wxDefaultPosition, wxDefaultSize,
                                         wxALIGN_LEFT );

    m_bus_list_view = new wxListView( this, wxID_ANY, wxDefaultPosition,
                                      wxSize( 300, 300 ), wxLC_ALIGN_LEFT |
                                      wxLC_NO_HEADER | wxLC_REPORT |
                                      wxLC_SINGLE_SEL );
    m_bus_list_view->InsertColumn( 0, "" );

    auto lbl_alias_edit = new wxStaticText( this, wxID_ANY, _( "Alias Name" ),
                                            wxDefaultPosition, wxDefaultSize,
                                            wxALIGN_LEFT );

    m_bus_edit = new wxTextCtrl( this, wxID_ANY, wxEmptyString,
            wxDefaultPosition, wxDefaultSize, wxTE_PROCESS_ENTER );

    auto left_button_sizer = new wxBoxSizer( wxHORIZONTAL );

    m_btn_add_bus = new wxButton( this, wxID_ANY, _( "Add" ) );
    m_btn_rename_bus = new wxButton( this, wxID_ANY, _( "Rename" ) );
    m_btn_remove_bus = new wxButton( this, wxID_ANY, _( "Remove" ) );

    left_button_sizer->Add( m_btn_add_bus );
    left_button_sizer->Add( m_btn_rename_bus );
    left_button_sizer->Add( m_btn_remove_bus );

    left_pane->Add( lbl_aliases, 0, wxEXPAND | wxALL, 5 );
    left_pane->Add( m_bus_list_view, 1, wxEXPAND | wxALL, 5 );
    left_pane->Add( lbl_alias_edit, 0, wxEXPAND | wxALL, 5 );
    left_pane->Add( m_bus_edit, 0, wxEXPAND | wxALL, 5 );
    left_pane->Add( left_button_sizer, 0, wxEXPAND | wxALL, 5 );

    // Right pane: signal list
    auto lbl_signals = new wxStaticText( this, wxID_ANY, _( "Alias Members" ),
                                         wxDefaultPosition, wxDefaultSize,
                                         wxALIGN_LEFT );

    m_signal_list_view = new wxListView( this, wxID_ANY, wxDefaultPosition,
                                         wxSize( 300, 300 ), wxLC_ALIGN_LEFT |
                                         wxLC_NO_HEADER | wxLC_REPORT |
                                         wxLC_SINGLE_SEL );
    m_signal_list_view->InsertColumn( 0, "" );

    auto lbl_signal_edit = new wxStaticText( this, wxID_ANY, _( "Member Name" ),
                                             wxDefaultPosition, wxDefaultSize,
                                             wxALIGN_LEFT );

    m_signal_edit = new wxTextCtrl( this, wxID_ANY, wxEmptyString,
            wxDefaultPosition, wxDefaultSize, wxTE_PROCESS_ENTER );

    auto right_button_sizer = new wxBoxSizer( wxHORIZONTAL );

    m_btn_add_signal = new wxButton( this, wxID_ANY, _( "Add" ) );
    m_btn_rename_signal = new wxButton( this, wxID_ANY, _( "Rename" ) );
    m_btn_remove_signal = new wxButton( this, wxID_ANY, _( "Remove" ) );

    right_button_sizer->Add( m_btn_add_signal );
    right_button_sizer->Add( m_btn_rename_signal );
    right_button_sizer->Add( m_btn_remove_signal );

    right_pane->Add( lbl_signals, 0, wxEXPAND | wxALL, 5 );
    right_pane->Add( m_signal_list_view, 1, wxEXPAND | wxALL, 5 );
    right_pane->Add( lbl_signal_edit, 0, wxEXPAND | wxALL, 5 );
    right_pane->Add( m_signal_edit, 0, wxEXPAND | wxALL, 5 );
    right_pane->Add( right_button_sizer, 0, wxEXPAND | wxALL, 5 );

    top_container->Add( left_pane, 1, wxEXPAND );
    top_container->Add( right_pane, 1, wxEXPAND );

    sizer->Add( top_container, 1, wxEXPAND | wxALL, 5 );
    sizer->Add( buttons, 0, wxEXPAND | wxBOTTOM, 10 );
    SetSizer( sizer );

    // Setup validators

    wxTextValidator validator;
    validator.SetStyle( wxFILTER_EXCLUDE_CHAR_LIST );
    validator.SetCharExcludes( "\r\n\t " );
    m_bus_edit->SetValidator( validator );

    // Allow spaces in the signal edit, so that you can type in a list of
    // signals in the box and it can automatically split them when you add.
    validator.SetCharExcludes( "\r\n\t" );
    m_signal_edit->SetValidator( validator );

    // Setup events

    Bind( wxEVT_INIT_DIALOG, &DIALOG_BUS_MANAGER::OnInitDialog, this );
    m_bus_list_view->Connect( wxEVT_COMMAND_LIST_ITEM_DESELECTED,
            wxListEventHandler( DIALOG_BUS_MANAGER::OnSelectBus ), NULL, this );
    m_bus_list_view->Connect( wxEVT_COMMAND_LIST_ITEM_SELECTED,
            wxListEventHandler( DIALOG_BUS_MANAGER::OnSelectBus ), NULL, this );
    m_signal_list_view->Connect( wxEVT_COMMAND_LIST_ITEM_DESELECTED,
            wxListEventHandler( DIALOG_BUS_MANAGER::OnSelectSignal ), NULL, this );
    m_signal_list_view->Connect( wxEVT_COMMAND_LIST_ITEM_SELECTED,
            wxListEventHandler( DIALOG_BUS_MANAGER::OnSelectSignal ), NULL, this );

    m_btn_add_bus->Connect( wxEVT_COMMAND_BUTTON_CLICKED,
            wxCommandEventHandler( DIALOG_BUS_MANAGER::OnAddBus ), NULL, this );
    m_btn_rename_bus->Connect( wxEVT_COMMAND_BUTTON_CLICKED,
            wxCommandEventHandler( DIALOG_BUS_MANAGER::OnRenameBus ), NULL, this );
    m_btn_remove_bus->Connect( wxEVT_COMMAND_BUTTON_CLICKED,
            wxCommandEventHandler( DIALOG_BUS_MANAGER::OnRemoveBus ), NULL, this );
    m_signal_edit->Connect( wxEVT_TEXT_ENTER,
            wxCommandEventHandler( DIALOG_BUS_MANAGER::OnAddSignal ), NULL, this );

    m_btn_add_signal->Connect( wxEVT_COMMAND_BUTTON_CLICKED,
            wxCommandEventHandler( DIALOG_BUS_MANAGER::OnAddSignal ), NULL, this );
    m_btn_rename_signal->Connect( wxEVT_COMMAND_BUTTON_CLICKED,
            wxCommandEventHandler( DIALOG_BUS_MANAGER::OnRenameSignal ), NULL, this );
    m_btn_remove_signal->Connect( wxEVT_COMMAND_BUTTON_CLICKED,
            wxCommandEventHandler( DIALOG_BUS_MANAGER::OnRemoveSignal ), NULL, this );
    m_bus_edit->Connect( wxEVT_TEXT_ENTER,
            wxCommandEventHandler( DIALOG_BUS_MANAGER::OnAddBus ), NULL, this );

    // Set initial UI state

    m_btn_rename_bus->Disable();
    m_btn_remove_bus->Disable();

    m_btn_add_signal->Disable();
    m_btn_rename_signal->Disable();
    m_btn_remove_signal->Disable();

    m_bus_edit->SetHint( _( "Bus Alias Name" ) );
    m_signal_edit->SetHint( _( "Net or Bus Name" ) );

    FinishDialogSettings();
}


void DIALOG_BUS_MANAGER::OnInitDialog( wxInitDialogEvent& aEvent )
{
    TransferDataToWindow();
}


bool DIALOG_BUS_MANAGER::TransferDataToWindow()
{
    m_aliases.clear();

    SCH_SHEET_LIST aSheets( g_RootSheet );
    std::vector< std::shared_ptr< BUS_ALIAS > > original_aliases;

    // collect aliases from each open sheet
    for( unsigned i = 0; i < aSheets.size(); i++ )
    {
        auto sheet_aliases = aSheets[i].LastScreen()->GetBusAliases();
        original_aliases.insert( original_aliases.end(), sheet_aliases.begin(),
                                 sheet_aliases.end() );
    }

    original_aliases.erase( std::unique( original_aliases.begin(),
                                         original_aliases.end() ),
                            original_aliases.end() );

    // clone into a temporary working set
    int idx = 0;
    for( const auto& alias : original_aliases )
    {
        m_aliases.push_back( alias->Clone() );
        auto text = getAliasDisplayText( alias );
        m_bus_list_view->InsertItem( idx, text );
        m_bus_list_view->SetItemPtrData( idx,  wxUIntPtr( m_aliases[idx].get() ) );
        idx++;
    }

    m_bus_list_view->SetColumnWidth( 0, -1 );

    return true;
}


void DIALOG_BUS_MANAGER::OnOkClick( wxCommandEvent& aEvent )
{
    if( TransferDataFromWindow() )
    {
        ( ( SCH_EDIT_FRAME* )GetParent() )->OnModify();
        EndModal( wxID_OK );
    }
}


void DIALOG_BUS_MANAGER::OnCancelClick( wxCommandEvent& aEvent )
{
    EndModal( wxID_CANCEL );
}


bool DIALOG_BUS_MANAGER::TransferDataFromWindow()
{
    // Since we have a clone of all the data, and it is from potentially
    // multiple screens, the way this works is to rebuild each screen's aliases.
    // A list of screens is stored here so that the screen's alias list is only
    // cleared once.

    std::unordered_set< SCH_SCREEN* > cleared_list;

    for( const auto& alias : m_aliases )
    {
        auto screen = alias->GetParent();

        if( cleared_list.count( screen ) == 0 )
        {
            screen->ClearBusAliases();
            cleared_list.insert( screen );
        }

        screen->AddBusAlias( alias );
    }

    return true;
}


void DIALOG_BUS_MANAGER::OnSelectBus( wxListEvent& event )
{
    if( event.GetEventType() == wxEVT_COMMAND_LIST_ITEM_SELECTED )
    {
        auto alias = m_aliases[ event.GetIndex() ];

        if( m_active_alias != alias )
        {
            m_active_alias = alias;

            m_bus_edit->ChangeValue( alias->GetName() );

            m_btn_add_bus->Disable();
            m_btn_rename_bus->Enable();
            m_btn_remove_bus->Enable();

            auto members = alias->Members();

            // TODO(JE) Clear() seems to be clearing the hint, contrary to
            // the wx documentation.
            m_signal_edit->Clear();
            m_signal_list_view->DeleteAllItems();

            for( unsigned i = 0; i < members.size(); i++ )
            {
                m_signal_list_view->InsertItem( i, members[i] );
            }

            m_signal_list_view->SetColumnWidth( 0, -1 );

            m_btn_add_signal->Enable();
            m_btn_rename_signal->Disable();
            m_btn_remove_signal->Disable();
        }
    }
    else
    {
        m_active_alias = NULL;
        m_bus_edit->Clear();
        m_signal_edit->Clear();
        m_signal_list_view->DeleteAllItems();

        m_btn_add_bus->Enable();
        m_btn_rename_bus->Disable();
        m_btn_remove_bus->Disable();

        m_btn_add_signal->Disable();
        m_btn_rename_signal->Disable();
        m_btn_remove_signal->Disable();
    }
}


void DIALOG_BUS_MANAGER::OnSelectSignal( wxListEvent& event )
{
    if( event.GetEventType() == wxEVT_COMMAND_LIST_ITEM_SELECTED )
    {
        m_signal_edit->ChangeValue( event.GetText() );
        m_btn_rename_signal->Enable();
        m_btn_remove_signal->Enable();
    }
    else
    {
        m_signal_edit->Clear();
        m_btn_rename_signal->Disable();
        m_btn_remove_signal->Disable();
    }
}


void DIALOG_BUS_MANAGER::OnAddBus( wxCommandEvent& aEvent )
{
    // If there is an active alias, then check that the user actually
    // changed the text in the edit box (we can't have duplicate aliases)

    auto new_name = m_bus_edit->GetValue();

    if( new_name.Length() == 0 )
    {
        return;
    }

    for( const auto& alias : m_aliases )
    {
        if( alias->GetName() == new_name )
        {
            // TODO(JE) display error?
            return;
        }
    }

    if( !m_active_alias ||
        ( m_active_alias && m_active_alias->GetName().Cmp( new_name ) ) )
    {
        // The values are different; create a new alias
        auto alias = std::make_shared<BUS_ALIAS>();
        alias->SetName( new_name );

        // New aliases get stored on the currently visible sheet
        alias->SetParent( static_cast<SCH_EDIT_FRAME*>( GetParent() )->GetScreen() );
        auto text = getAliasDisplayText( alias );

        m_aliases.push_back( alias );
        long idx = m_bus_list_view->InsertItem( m_aliases.size() - 1, text );
        m_bus_list_view->SetColumnWidth( 0, -1 );
        m_bus_list_view->Select( idx );
        m_bus_edit->Clear();
    }
    else
    {
        // TODO(JE) Check about desired result here.
        // Maybe warn the user?  Or just do nothing
    }
}


void DIALOG_BUS_MANAGER::OnRenameBus( wxCommandEvent& aEvent )
{
    // We should only get here if there is an active alias
    wxASSERT( m_active_alias );

    m_active_alias->SetName( m_bus_edit->GetValue() );
    long idx = m_bus_list_view->FindItem( -1, wxUIntPtr( m_active_alias.get() ) );

    wxASSERT( idx >= 0 );

    m_bus_list_view->SetItemText( idx, getAliasDisplayText( m_active_alias ) );
}


void DIALOG_BUS_MANAGER::OnRemoveBus( wxCommandEvent& aEvent )
{
    // We should only get here if there is an active alias
    wxASSERT( m_active_alias );
    long i = m_bus_list_view->GetFirstSelected();
    wxASSERT(  m_active_alias == m_aliases[ i ] );

    m_bus_list_view->DeleteItem( i );
    m_aliases.erase( m_aliases.begin() + i );
    m_bus_edit->Clear();

    m_active_alias = NULL;

    auto evt = wxListEvent( wxEVT_COMMAND_LIST_ITEM_DESELECTED );
    OnSelectBus( evt );
}


void DIALOG_BUS_MANAGER::OnAddSignal( wxCommandEvent& aEvent )
{
    auto name_list = m_signal_edit->GetValue();

    if( !m_active_alias || name_list.Length() == 0 )
    {
        return;
    }

    // String collecting net names that were not added to the bus
    wxString notAdded;

    // Parse a space-separated list and add each one
    wxStringTokenizer tok( name_list, " " );
    while( tok.HasMoreTokens() )
    {
        auto name = tok.GetNextToken();

        if( !m_active_alias->Contains( name ) )
        {
            m_active_alias->AddMember( name );

            long idx = m_signal_list_view->InsertItem(
                    m_active_alias->GetMemberCount() - 1, name );

            m_signal_list_view->SetColumnWidth( 0, -1 );
            m_signal_list_view->Select( idx );
        }
        else
        {
            // Some of the requested net names were not added to the list, so keep them for editing
            notAdded = notAdded.IsEmpty() ? name : notAdded + " " + name;
        }
    }

    m_signal_edit->SetValue( notAdded );
    m_signal_edit->SetInsertionPointEnd();
}


void DIALOG_BUS_MANAGER::OnRenameSignal( wxCommandEvent& aEvent )
{
    // We should only get here if there is an active alias
    wxASSERT( m_active_alias );

    auto new_name = m_signal_edit->GetValue();
    long idx = m_signal_list_view->GetFirstSelected();

    wxASSERT( idx >= 0 );

    auto old_name = m_active_alias->Members()[ idx ];

    // User could have typed a space here, so check first
    if( new_name.Find( " " ) != wxNOT_FOUND )
    {
        // TODO(JE) error feedback
        m_signal_edit->ChangeValue( old_name );
        return;
    }

    m_active_alias->Members()[ idx ] = new_name;
    m_signal_list_view->SetItemText( idx, new_name );
    m_signal_list_view->SetColumnWidth( 0, -1 );
}


void DIALOG_BUS_MANAGER::OnRemoveSignal( wxCommandEvent& aEvent )
{
    // We should only get here if there is an active alias
    wxASSERT( m_active_alias );

    long idx = m_signal_list_view->GetFirstSelected();

    wxASSERT( idx >= 0 );

    m_active_alias->Members().erase( m_active_alias->Members().begin() + idx );

    m_signal_list_view->DeleteItem( idx );
    m_signal_edit->Clear();
    m_btn_rename_signal->Disable();
    m_btn_remove_signal->Disable();
}


wxString DIALOG_BUS_MANAGER::getAliasDisplayText( std::shared_ptr< BUS_ALIAS > aAlias )
{
    wxString name = aAlias->GetName();
    wxFileName sheet_name( aAlias->GetParent()->GetFileName() );

    name += _T( " (" ) + sheet_name.GetFullName() + _T( ")" );

    return name;
}


// see invoke_sch_dialog.h
void InvokeDialogBusManager( SCH_EDIT_FRAME* aCaller )
{
    DIALOG_BUS_MANAGER dlg( aCaller );
    dlg.ShowModal();
}
