/*
 * This program source code file is part of KiCad, a free EDA CAD application.
 *
 * Copyright (C) 2019 Alexander Shuklin, jasuramme@gmail.com
 * Copyright (C) 2019-2021 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
 */


#include "dialog_board_statistics.h"
#include "base_units.h"
#include <confirm.h>
#include <pad.h>
#include <macros.h>
#include <wildcards_and_files_ext.h>
#include <wx/filedlg.h>

#define COL_LABEL 0
#define COL_AMOUNT 1

// Defines for components view
#define ROW_LABEL 0
#define COL_FRONT_SIDE 1
#define COL_BOTTOM_SIDE 2
#define COL_TOTAL 3

// Defines for board view
#define ROW_BOARD_WIDTH 0
#define ROW_BOARD_HEIGHT 1
#define ROW_BOARD_AREA 2


/**
 * The dialog last saved state.
 */
struct DIALOG_BOARD_STATISTICS_SAVED_STATE
{
    DIALOG_BOARD_STATISTICS_SAVED_STATE() :
            excludeNoPins( false ),
            subtractHoles( false ),
            saveReportInitialized(false)
    {
    }

    // Flags to remember last checkboxes state
    bool excludeNoPins;
    bool subtractHoles;

    // Variables to save last report file name and folder
    bool     saveReportInitialized; // true after the 3 next string are initialized
    wxString saveReportFolder;      // last report folder
    wxString saveReportName;        // last report filename
    wxString m_project;             // name of the project used to create the last report
                                    // used to reinit last state after a project change
};


static DIALOG_BOARD_STATISTICS_SAVED_STATE s_savedDialogState;

DIALOG_BOARD_STATISTICS::DIALOG_BOARD_STATISTICS( PCB_EDIT_FRAME* aParentFrame ) :
        DIALOG_BOARD_STATISTICS_BASE( aParentFrame ),
        m_boardWidth( 0 ),
        m_boardHeight( 0 ),
        m_boardArea( 0.0 ),
        m_hasOutline( false )
{
    m_parentFrame = aParentFrame;

    m_gridDrills->UseNativeColHeader();
    m_gridDrills->Connect( wxEVT_GRID_COL_SORT,
                           wxGridEventHandler( DIALOG_BOARD_STATISTICS::drillGridSort ),
                           nullptr, this );

    m_checkBoxExcludeComponentsNoPins->SetValue( s_savedDialogState.excludeNoPins );
    m_checkBoxSubtractHoles->SetValue( s_savedDialogState.subtractHoles );

    // Make labels for grids
    wxFont headingFont = KIUI::GetStatusFont( this );
    m_gridComponents->SetCellValue( ROW_LABEL, COL_FRONT_SIDE, _( "Front Side" ) );
    m_gridComponents->SetCellFont( ROW_LABEL, COL_FRONT_SIDE, headingFont );
    m_gridComponents->SetCellValue( ROW_LABEL, COL_BOTTOM_SIDE, _( "Back Side" ) );
    m_gridComponents->SetCellFont( ROW_LABEL, COL_BOTTOM_SIDE, headingFont );
    m_gridComponents->SetCellValue( ROW_LABEL, COL_TOTAL, _( "Total" ) );
    m_gridComponents->SetCellFont( ROW_LABEL, COL_TOTAL, headingFont );

    m_gridBoard->SetCellValue( 0, 0, _( "Width:" ) );
    m_gridBoard->SetCellAlignment( 0, 0, wxALIGN_LEFT, wxALIGN_CENTRE );
    m_gridBoard->SetCellValue( 1, 0, _( "Height:" ) );
    m_gridBoard->SetCellAlignment( 1, 0, wxALIGN_LEFT, wxALIGN_CENTRE );
    m_gridBoard->SetCellValue( 2, 0, _( "Area:" ) );
    m_gridBoard->SetCellAlignment( 2, 0, wxALIGN_LEFT, wxALIGN_CENTRE );

    wxGrid* grids[] = { m_gridComponents, m_gridPads, m_gridVias, m_gridBoard };

    for( auto& grid : grids )
    {
        // Remove wxgrid's selection boxes
        grid->SetCellHighlightPenWidth( 0 );
        grid->SetColMinimalAcceptableWidth( 80 );
        for( int i = 0; i < grid->GetNumberRows(); i++ )
            grid->SetCellAlignment( i, COL_LABEL, wxALIGN_LEFT, wxALIGN_CENTRE );
    }

    wxFileName fn = m_parentFrame->GetBoard()->GetFileName();

    if( !s_savedDialogState.saveReportInitialized
            || s_savedDialogState.m_project != Prj().GetProjectFullName() )
    {
        fn.SetName( fn.GetName() + "_report" );
        fn.SetExt( "txt" );
        s_savedDialogState.saveReportName = fn.GetFullName();
        s_savedDialogState.saveReportFolder = wxPathOnly( Prj().GetProjectFullName() );
        s_savedDialogState.m_project = Prj().GetProjectFullName();
        s_savedDialogState.saveReportInitialized = true;
    }

    // The wxStdDialogButtonSizer wxID_CANCLE button is in fact a close button
    // Nothing to cancel:
    m_sdbControlSizerCancel->SetLabel( _( "Close" ) );
}


void DIALOG_BOARD_STATISTICS::refreshItemsTypes()
{
    m_componentsTypes.clear();

    // If you need some more types to be shown, simply add them to the
    // corresponding list
    m_componentsTypes.push_back( componentsType_t( FP_THROUGH_HOLE, _( "THT:" ) ) );
    m_componentsTypes.push_back( componentsType_t( FP_SMD, _( "SMD:" ) ) );

    m_padsTypes.clear();
    m_padsTypes.push_back( padsType_t( PAD_ATTRIB::PTH, _( "Through hole:" ) ) );
    m_padsTypes.push_back( padsType_t( PAD_ATTRIB::SMD, _( "SMD:" ) ) );
    m_padsTypes.push_back( padsType_t( PAD_ATTRIB::CONN, _( "Connector:" ) ) );
    m_padsTypes.push_back( padsType_t( PAD_ATTRIB::NPTH, _( "NPTH:" ) ) );

    m_viasTypes.clear();
    m_viasTypes.push_back( viasType_t( VIATYPE::THROUGH, _( "Through vias:" ) ) );
    m_viasTypes.push_back( viasType_t( VIATYPE::BLIND_BURIED, _( "Blind/buried:" ) ) );
    m_viasTypes.push_back( viasType_t( VIATYPE::MICROVIA, _( "Micro vias:" ) ) );

    // If there not enough rows in grids, append some
    int appendRows = m_componentsTypes.size() + 2 - m_gridComponents->GetNumberRows();

    if( appendRows > 0 )
        m_gridComponents->AppendRows( appendRows );

    appendRows = m_padsTypes.size() + 1 - m_gridPads->GetNumberRows();

    if( appendRows > 0 )
        m_gridPads->AppendRows( appendRows );

    appendRows = m_viasTypes.size() + 1 - m_gridVias->GetNumberRows();

    if( appendRows )
        m_gridVias->AppendRows( appendRows );
}


bool DIALOG_BOARD_STATISTICS::TransferDataToWindow()
{
    refreshItemsTypes();
    getDataFromPCB();
    updateWidets();
    Layout();
    drillsPanel->Layout();
    finishDialogSettings();
    return true;
}


void DIALOG_BOARD_STATISTICS::getDataFromPCB()
{
    BOARD* board = m_parentFrame->GetBoard();

    // Get footprints and pads count
    for( FOOTPRINT* footprint : board->Footprints() )
    {
        // Do not proceed footprints with no pads if checkbox checked
        if( m_checkBoxExcludeComponentsNoPins->GetValue() && ! footprint->Pads().size() )
            continue;

        // Go through components types list
        for( auto& type : m_componentsTypes )
        {
            if(( footprint->GetAttributes() & type.attribute ) > 0 )
            {
                if( footprint->IsFlipped() )
                    type.backSideQty++;
                else
                    type.frontSideQty++;
                break;
            }
        }

        for( PAD* pad : footprint->Pads() )
        {
            // Go through pads types list
            for( auto& type : m_padsTypes )
            {
                if( pad->GetAttribute() == type.attribute )
                {
                    type.qty++;
                    break;
                }
            }

            if( pad->GetDrillSize().x > 0 && pad->GetDrillSize().y > 0 )
            {
                PCB_LAYER_ID top, bottom;

                if( pad->GetLayerSet().CuStack().empty() )
                {
                    // The pad is not on any copper layer
                    top = UNDEFINED_LAYER;
                    bottom = UNDEFINED_LAYER;
                }
                else
                {
                    top = pad->GetLayerSet().CuStack().front();
                    bottom = pad->GetLayerSet().CuStack().back();
                }

                drillType_t drill( pad->GetDrillSize().x, pad->GetDrillSize().y,
                                   pad->GetDrillShape(), pad->GetAttribute() != PAD_ATTRIB::NPTH,
                                   true, top, bottom );

                auto it = m_drillTypes.begin();

                for( ; it != m_drillTypes.end(); ++it )
                {
                    if( *it == drill )
                    {
                        it->qty++;
                        break;
                    }
                }

                if( it == m_drillTypes.end() )
                {
                    drill.qty = 1;
                    m_drillTypes.push_back( drill );
                    m_gridDrills->InsertRows();
                }
            }
        }
    }

    // Get via counts
    for( PCB_TRACK* track : board->Tracks() )
    {
        if( PCB_VIA* via = dyn_cast<PCB_VIA*>( track ) )
        {
            for( auto& type : m_viasTypes )
            {
                if( via->GetViaType() == type.attribute )
                {
                    type.qty++;
                    break;
                }
            }

            drillType_t drill( via->GetDrillValue(), via->GetDrillValue(), PAD_DRILL_SHAPE_CIRCLE,
                               true, false, via->TopLayer(), via->BottomLayer() );

            auto it = m_drillTypes.begin();

            for( ; it != m_drillTypes.end(); ++it )
            {
                if( *it == drill )
                {
                    it->qty++;
                    break;
                }
            }

            if( it == m_drillTypes.end() )
            {
                drill.qty = 1;
                m_drillTypes.push_back( drill );
                m_gridDrills->InsertRows();
            }
        }
    }

    sort( m_drillTypes.begin(), m_drillTypes.end(),
          drillType_t::COMPARE( drillType_t::COL_COUNT, false ) );

    bool           boundingBoxCreated = false; //flag if bounding box initialized
    BOX2I          bbox;
    SHAPE_POLY_SET polySet;
    m_hasOutline = board->GetBoardPolygonOutlines( polySet );

    // If board has no Edge Cuts lines, board->GetBoardPolygonOutlines will
    // return small rectangle, so we double check that
    bool edgeCutsExists = false;

    for( BOARD_ITEM* drawing : board->Drawings() )
    {
        if( drawing->GetLayer() == Edge_Cuts )
        {
            edgeCutsExists = true;
            break;
        }
    }

    if( !edgeCutsExists )
        m_hasOutline = false;

    if( m_hasOutline )
    {
        m_boardArea = 0.0;

        for( int i = 0; i < polySet.OutlineCount(); i++ )
        {
            SHAPE_LINE_CHAIN& outline = polySet.Outline( i );
            m_boardArea += outline.Area();

            // If checkbox "subtract holes" is checked
            if( m_checkBoxSubtractHoles->GetValue() )
            {
                for( int j = 0; j < polySet.HoleCount( i ); j++ )
                    m_boardArea -= polySet.Hole( i, j ).Area();
            }

            if( boundingBoxCreated )
            {
                bbox.Merge( outline.BBox() );
            }
            else
            {
                bbox = outline.BBox();
                boundingBoxCreated = true;
            }
        }

        m_boardWidth = bbox.GetWidth();
        m_boardHeight = bbox.GetHeight();
    }
}


void DIALOG_BOARD_STATISTICS::updateWidets()
{
    int totalPads  = 0;
    int currentRow = 0;

    for( const auto& type : m_padsTypes )
    {
        m_gridPads->SetCellValue( currentRow, COL_LABEL, type.title );
        m_gridPads->SetCellValue( currentRow, COL_AMOUNT,
                                  wxString::Format( wxT( "%i " ), type.qty ) );
        totalPads += type.qty;
        currentRow++;
    }

    m_gridPads->SetCellValue( currentRow, COL_LABEL, _( "Total:" ) );
    m_gridPads->SetCellValue( currentRow, COL_AMOUNT, wxString::Format( "%i ", totalPads ) );

    int totalVias = 0;
    currentRow = 0;

    for( const auto& type : m_viasTypes )
    {
        m_gridVias->SetCellValue( currentRow, COL_LABEL, type.title );
        m_gridVias->SetCellValue( currentRow, COL_AMOUNT, wxString::Format( "%i ", type.qty ) );
        totalVias += type.qty;
        currentRow++;
    }

    m_gridVias->SetCellValue( currentRow, COL_LABEL, _( "Total:" ) );
    m_gridVias->SetCellValue( currentRow, COL_AMOUNT, wxString::Format( "%i ", totalVias ) );


    int totalFront = 0;
    int totalBack  = 0;

    // We don't use row 0, as there labels are
    currentRow = 1;

    for( const auto& type : m_componentsTypes )
    {
        m_gridComponents->SetCellValue( currentRow, COL_LABEL, type.title );
        m_gridComponents->SetCellValue(
                currentRow, COL_FRONT_SIDE, wxString::Format( "%i ", type.frontSideQty ) );
        m_gridComponents->SetCellValue(
                currentRow, COL_BOTTOM_SIDE, wxString::Format( "%i ", type.backSideQty ) );
        m_gridComponents->SetCellValue( currentRow, 3,
                wxString::Format( wxT( "%i " ), type.frontSideQty + type.backSideQty ) );
        totalFront += type.frontSideQty;
        totalBack  += type.backSideQty;
        currentRow++;
    }

    m_gridComponents->SetCellValue( currentRow, COL_LABEL, _( "Total:" ) );
    m_gridComponents->SetCellValue( currentRow, COL_FRONT_SIDE,
                                    wxString::Format( "%i ", totalFront ) );
    m_gridComponents->SetCellValue( currentRow, COL_BOTTOM_SIDE,
                                    wxString::Format( "%i ", totalBack ) );
    m_gridComponents->SetCellValue( currentRow, COL_TOTAL,
                                    wxString::Format( "%i ", totalFront + totalBack ) );

    if( m_hasOutline )
    {
        m_gridBoard->SetCellValue( ROW_BOARD_WIDTH, COL_AMOUNT,
                                   MessageTextFromValue( GetUserUnits(), m_boardWidth ) + " " );
        m_gridBoard->SetCellValue( ROW_BOARD_HEIGHT, COL_AMOUNT,
                                   MessageTextFromValue( GetUserUnits(), m_boardHeight ) + " " );
        m_gridBoard->SetCellValue( ROW_BOARD_AREA, COL_AMOUNT,
                                   MessageTextFromValue( GetUserUnits(), m_boardArea, true,
                                                         EDA_DATA_TYPE::AREA ) );
    }
    else
    {
        m_gridBoard->SetCellValue( ROW_BOARD_WIDTH, COL_AMOUNT, _( "unknown" ) );
        m_gridBoard->SetCellValue( ROW_BOARD_HEIGHT, COL_AMOUNT, _( "unknown" ) );
        m_gridBoard->SetCellValue( ROW_BOARD_AREA, COL_AMOUNT, _( "unknown" ) );
    }

    updateDrillGrid();

    m_gridComponents->AutoSize();
    m_gridPads->AutoSize();
    m_gridBoard->AutoSize();
    m_gridVias->AutoSize();
    m_gridDrills->AutoSize();

    adjustDrillGridColumns();
}


void DIALOG_BOARD_STATISTICS::updateDrillGrid()
{
    BOARD* board = m_parentFrame->GetBoard();
    int    currentRow = 0;

    for( const auto& type : m_drillTypes )
    {
        wxString shapeStr;
        wxString startLayerStr;
        wxString stopLayerStr;

        switch( type.shape )
        {
        case PAD_DRILL_SHAPE_CIRCLE:
            shapeStr = _( "Round" );
            break;
        case PAD_DRILL_SHAPE_OBLONG:
            shapeStr = _( "Slot" );
            break;
        default:
            shapeStr = _( "???" );
            break;
        }

        if( type.startLayer == UNDEFINED_LAYER )
            startLayerStr = _( "N/A" );
        else
            startLayerStr = board->GetLayerName( type.startLayer );

        if( type.stopLayer == UNDEFINED_LAYER )
            stopLayerStr = _( "N/A" );
        else
            stopLayerStr = board->GetLayerName( type.stopLayer );

        m_gridDrills->SetCellValue(
                currentRow, drillType_t::COL_COUNT, wxString::Format( "%i", type.qty ) );
        m_gridDrills->SetCellValue( currentRow, drillType_t::COL_SHAPE, shapeStr );
        m_gridDrills->SetCellValue( currentRow, drillType_t::COL_X_SIZE,
                MessageTextFromValue( GetUserUnits(), type.xSize ) );
        m_gridDrills->SetCellValue( currentRow, drillType_t::COL_Y_SIZE,
                MessageTextFromValue( GetUserUnits(), type.ySize ) );
        m_gridDrills->SetCellValue(
                currentRow, drillType_t::COL_PLATED, type.isPlated ? _( "PTH" ) : _( "NPTH" ) );
        m_gridDrills->SetCellValue(
                currentRow, drillType_t::COL_VIA_PAD, type.isPad ? _( "Pad" ) : _( "Via" ) );
        m_gridDrills->SetCellValue( currentRow, drillType_t::COL_START_LAYER, startLayerStr );
        m_gridDrills->SetCellValue( currentRow, drillType_t::COL_STOP_LAYER, stopLayerStr );

        currentRow++;
    }
}


void DIALOG_BOARD_STATISTICS::printGridToStringAsTable( wxGrid* aGrid, wxString& aStr,
                                                        bool aUseRowLabels, bool aUseColLabels,
                                                        bool aUseFirstColAsLabel )
{
    std::vector<int> widths( aGrid->GetNumberCols(), 0 );
    int              rowLabelsWidth = 0;

    // Determine column widths.

    if( aUseColLabels )
    {
        for( int col = 0; col < aGrid->GetNumberCols(); col++ )
            widths[col] = aGrid->GetColLabelValue( col ).length();
    }

    for( int row = 0; row < aGrid->GetNumberRows(); row++ )
    {
        rowLabelsWidth = std::max<int>( rowLabelsWidth, aGrid->GetRowLabelValue( row ).length() );

        for( int col = 0; col < aGrid->GetNumberCols(); col++ )
            widths[col] = std::max<int>( widths[col], aGrid->GetCellValue( row, col ).length() );
    }

    // Print the cells.

    wxString tmp;

    // Print column labels.

    aStr << "|";

    if( aUseRowLabels )
    {
        aStr.Append( ' ', rowLabelsWidth );
        aStr << " |";
    }

    for( int col = 0; col < aGrid->GetNumberCols(); col++ )
    {
        if( aUseColLabels )
            tmp.Printf( " %*s |", widths[col], aGrid->GetColLabelValue( col ) );
        else
            tmp.Printf( " %*s |", widths[col], aGrid->GetCellValue( 0, col ) );

        aStr << tmp;
    }

    aStr << "\n";

    // Print column label horizontal separators.

    aStr << "|";

    if( aUseRowLabels )
    {
        aStr.Append( '-', rowLabelsWidth );
        aStr << "-|";
    }

    for( int col = 0; col < aGrid->GetNumberCols(); col++ )
    {
        aStr << "-";
        aStr.Append( '-', widths[col] );
        aStr << "-|";
    }

    aStr << "\n";

    // Print regular cells.

    int firstRow = 0, firstCol = 0;

    if( !aUseColLabels )
        firstRow = 1;

    if( !aUseRowLabels && aUseFirstColAsLabel )
        firstCol = 1;

    for( int row = firstRow; row < aGrid->GetNumberRows(); row++ )
    {
        if( aUseRowLabels )
            tmp.Printf( "|%-*s |", rowLabelsWidth, aGrid->GetRowLabelValue( row ) );
        else if( aUseFirstColAsLabel )
            tmp.Printf( "|%-*s  |", widths[0], aGrid->GetCellValue( row, 0 ) );
        else
            tmp.Printf( "|" );

        aStr << tmp;

        for( int col = firstCol; col < aGrid->GetNumberCols(); col++ )
        {
            tmp.Printf( " %*s |", widths[col], aGrid->GetCellValue( row, col ) );
            aStr << tmp;
        }

        aStr << "\n";
    }
}


void DIALOG_BOARD_STATISTICS::adjustDrillGridColumns()
{
    int newTotalWidth = m_gridDrills->GetClientSize().GetWidth();
    int curTotalWidth = 0;

    // Find the total current width
    for( int i = 0; i < m_gridDrills->GetNumberCols(); i++ )
    {
        if( i != drillType_t::COL_START_LAYER && i != drillType_t::COL_STOP_LAYER )
            curTotalWidth += m_gridDrills->GetColSize( i );
    }

    // Resize the last two columns to fill all available space

    int remainingWidth = newTotalWidth - curTotalWidth;

    m_gridDrills->SetColSize( drillType_t::COL_START_LAYER, remainingWidth / 2 );
    m_gridDrills->SetColSize( drillType_t::COL_STOP_LAYER, remainingWidth - remainingWidth / 2 );

    m_gridDrills->Refresh();
}


void DIALOG_BOARD_STATISTICS::checkboxClicked( wxCommandEvent& aEvent )
{
    s_savedDialogState.excludeNoPins = m_checkBoxExcludeComponentsNoPins->GetValue();
    s_savedDialogState.subtractHoles = m_checkBoxSubtractHoles->GetValue();
    refreshItemsTypes();
    getDataFromPCB();
    updateWidets();
    Layout();
    drillsPanel->Layout();
}


void DIALOG_BOARD_STATISTICS::saveReportClicked( wxCommandEvent& aEvent )
{
    FILE*    outFile;
    wxString msg;
    wxString boardName;

    wxFileName fn = m_parentFrame->GetBoard()->GetFileName();
    boardName = fn.GetName();
    wxFileDialog saveFileDialog( this, _( "Save Report File" ),
                                 s_savedDialogState.saveReportFolder,
                                 s_savedDialogState.saveReportName,
                                 TextFileWildcard(),
                                 wxFD_SAVE | wxFD_OVERWRITE_PROMPT );

    if( saveFileDialog.ShowModal() == wxID_CANCEL )
        return;

    s_savedDialogState.saveReportFolder = wxPathOnly( saveFileDialog.GetPath() );
    s_savedDialogState.saveReportName = saveFileDialog.GetFilename();

    outFile = wxFopen( saveFileDialog.GetPath(), "wt" );

    if( outFile == nullptr )
    {
        msg.Printf( _( "Failed to create file '%s'." ), saveFileDialog.GetPath() );
        DisplayErrorMessage( this, msg );
        return;
    }

    msg << _( "PCB statistics report\n=====================" ) << "\n";
    msg << wxS( "- " ) << _( "Date" ) << wxS( ": " ) << wxDateTime::Now().Format() << "\n";
    msg << wxS( "- " ) << _( "Project" ) << wxS( ": " )<< Prj().GetProjectName() << "\n";
    msg << wxS( "- " ) << _( "Board name" ) << wxS( ": " )<< boardName << "\n";

    msg << "\n";
    msg << _( "Board" ) << "\n-----\n";

    if( m_hasOutline )
    {
        msg << wxS( "- " ) << _( "Width" ) << wxS( ": " ) <<
               MessageTextFromValue( GetUserUnits(), m_boardWidth ) << "\n";
        msg << wxS( "- " ) << _( "Height" ) << wxS( ": " ) <<
               MessageTextFromValue( GetUserUnits(), m_boardHeight ) << "\n";
        msg << wxS( "- " ) << _( "Area" ) + wxS( ": " ) <<
               MessageTextFromValue( GetUserUnits(), m_boardArea, true, EDA_DATA_TYPE::AREA );
        msg << "\n";
    }
    else
    {
        msg << wxS( "- " ) << _( "Width" ) << wxS( ": " ) << _( "unknown" ) << "\n";
        msg << wxS( "- " ) << _( "Height" ) << wxS( ": " ) << _( "unknown" ) << "\n";
        msg << wxS( "- " ) << _( "Area" ) << wxS( ": " ) << _( "unknown" ) << "\n";
    }

    msg << "\n";
    msg << _( "Pads" ) << "\n----\n";

    for( auto& type : m_padsTypes )
        msg << "- " << type.title << " " << type.qty << "\n";

    msg << "\n";
    msg << _( "Vias" ) << "\n----\n";

    for( auto& type : m_viasTypes )
        msg << "- " << type.title << " " << type.qty << "\n";

    // We will save data about components in the table.
    // We have to calculate column widths
    std::vector<int>      widths;
    std::vector<wxString> labels{ "", _( "Front Side" ), _( "Back Side" ), _( "Total" ) };
    wxString tmp;

    widths.reserve( labels.size() );

    for( const auto& label : labels )
        widths.push_back( label.size() );

    int frontTotal = 0;
    int backTotal = 0;

    for( const auto& type : m_componentsTypes )
    {
        // Get maximum width for left label column
        widths[0] = std::max<int>( type.title.size(), widths[0] );
        frontTotal += type.frontSideQty;
        backTotal += type.backSideQty;
    }

    // Get maximum width for other columns
    tmp.Printf( "%i", frontTotal );
    widths[1] = std::max<int>( tmp.size(), widths[1] );
    tmp.Printf( "%i", backTotal );
    widths[2] = std::max<int>( tmp.size(), widths[2] );
    tmp.Printf( "%i", frontTotal + backTotal );
    widths[3] = std::max<int>( tmp.size(), widths[3] );

    //Write components amount to file
    msg << "\n";
    msg << _( "Components" ) << "\n----------\n";
    msg << "\n";

    printGridToStringAsTable( m_gridComponents, msg, false, false, true );

    msg << "\n";
    msg << _( "Drill holes" ) << "\n-----------\n";
    msg << "\n";

    printGridToStringAsTable( m_gridDrills, msg, false, true, false );

    if( fprintf( outFile, "%s", TO_UTF8( msg ) ) < 0 )
    {
        msg.Printf( _( "Error writing file '%s'." ), saveFileDialog.GetPath() );
        DisplayErrorMessage( this, msg );
    }

    fclose( outFile );
}


void DIALOG_BOARD_STATISTICS::drillGridSize( wxSizeEvent& aEvent )
{
    aEvent.Skip();
    adjustDrillGridColumns();
}

void DIALOG_BOARD_STATISTICS::drillGridSort( wxGridEvent& aEvent )
{
    drillType_t::COL_ID colId = static_cast<drillType_t::COL_ID>( aEvent.GetCol() );
    bool ascending =
            !( m_gridDrills->IsSortingBy( colId ) && m_gridDrills->IsSortOrderAscending() );

    sort( m_drillTypes.begin(), m_drillTypes.end(), drillType_t::COMPARE( colId, ascending ) );

    updateDrillGrid();
}


DIALOG_BOARD_STATISTICS::~DIALOG_BOARD_STATISTICS()
{
}
