/*
 * This program source code file is part of KiCad, a free EDA CAD application.
 *
 * Copyright (C) 2017 Jean-Pierre Charras, jp.charras at wanadoo.fr
 * Copyright (C) 2012 SoftPLC Corporation, Dick Hollenbeck <dick@softplc.com>
 * Copyright (C) 1992-2017 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 pcbnew/pcbplot.cpp
 */

#include <fctsys.h>
#include <class_plotter.h>
#include <confirm.h>
#include <wxPcbStruct.h>
#include <pcbplot.h>
#include <base_units.h>
#include <reporter.h>
#include <class_board.h>
#include <pcbnew.h>
#include <plotcontroller.h>
#include <pcb_plot_params.h>
#include <wx/ffile.h>
#include <dialog_plot.h>
#include <macros.h>
#include <build_version.h>


const wxString GetGerberProtelExtension( LAYER_NUM aLayer )
{
    if( IsCopperLayer( aLayer ) )
    {
        if( aLayer == F_Cu )
            return wxT( "gtl" );
        else if( aLayer == B_Cu )
            return wxT( "gbl" );
        else
        {
            return wxString::Format( wxT( "g%d" ), aLayer+1 );
        }
    }
    else
    {
        switch( aLayer )
        {
        case B_Adhes:       return wxT( "gba" );
        case F_Adhes:       return wxT( "gta" );

        case B_Paste:       return wxT( "gbp" );
        case F_Paste:       return wxT( "gtp" );

        case B_SilkS:       return wxT( "gbo" );
        case F_SilkS:       return wxT( "gto" );

        case B_Mask:        return wxT( "gbs" );
        case F_Mask:        return wxT( "gts" );

        case Edge_Cuts:     return wxT( "gm1" );

        case Dwgs_User:
        case Cmts_User:
        case Eco1_User:
        case Eco2_User:
        default:            return wxT( "gbr" );
        }
    }
}


const wxString GetGerberFileFunctionAttribute( const BOARD *aBoard, LAYER_NUM aLayer )
{
    wxString attrib;

    switch( aLayer )
    {
    case F_Adhes:
        attrib = "Glue,Top";
        break;

    case B_Adhes:
        attrib = "Glue,Bot";
        break;

    case F_SilkS:
        attrib = "Legend,Top";
        break;

    case B_SilkS:
        attrib = "Legend,Bot";
        break;

    case F_Mask:
        attrib = "Soldermask,Top";
        break;

    case B_Mask:
        attrib = "Soldermask,Bot";
        break;

    case F_Paste:
        attrib = "Paste,Top";
        break;

    case B_Paste:
        attrib = "Paste,Bot";
        break;

    case Edge_Cuts:
        // Board outline.
        // Can be "Profile,NP" (Not Plated: usual) or "Profile,P"
        // This last is the exception (Plated)
        attrib = "Profile,NP";
        break;

    case Dwgs_User:
        attrib = "Drawing";
        break;

    case Cmts_User:
        attrib = "Other,Comment";
        break;

    case Eco1_User:
        attrib = "Other,ECO1";
        break;

    case Eco2_User:
        attrib = "Other,ECO2";
        break;

    case B_Fab:
        attrib = "Other,Fab,Bot";
        break;

    case F_Fab:
        attrib = "Other,Fab,Top";
        break;

    case B_Cu:
        attrib.Printf( wxT( "Copper,L%d,Bot" ), aBoard->GetCopperLayerCount() );
        break;

    case F_Cu:
        attrib = "Copper,L1,Top";
        break;

    default:
        if( IsCopperLayer( aLayer ) )
            attrib.Printf( wxT( "Copper,L%d,Inr" ), aLayer+1 );
        else
            attrib.Printf( wxT( "Other,User" ), aLayer+1 );
        break;
    }

    // Add the signal type of the layer, if relevant
    if( IsCopperLayer( aLayer ) )
    {
        LAYER_T type = aBoard->GetLayerType( ToLAYER_ID( aLayer ) );

        switch( type )
        {
        case LT_SIGNAL:
            attrib += ",Signal";
            break;
        case LT_POWER:
            attrib += ",Plane";
            break;
        case LT_MIXED:
            attrib += ",Mixed";
            break;
        default:
            break;   // do nothing (but avoid a warning for unhandled LAYER_T values from GCC)
        }
    }

    wxString fileFct;
    fileFct.Printf( "%%TF.FileFunction,%s*%%", GetChars( attrib ) );

    return fileFct;
}


static const wxString GetGerberFilePolarityAttribute( LAYER_NUM aLayer )
{
    /* build the string %TF.FilePolarity,Positive*%
     * or  %TF.FilePolarity,Negative*%
     * an emply string for layers which do not use a polarity
     *
     * The value of the .FilePolarity specifies whether the image represents the
     * presence or absence of material.
     * This attribute can only be used when the file represents a pattern in a material layer,
     * e.g. copper, solder mask, legend.
     * Together with.FileFunction it defines the role of that image in
     * the layer structure of the PCB.
     * Note that the .FilePolarity attribute does not change the image -
     * no attribute does.
     * It changes the interpretation of the image.
     * For example, in a copper layer in positive polarity a round flash generates a copper pad.
     * In a copper layer in negative polarity it generates a clearance.
     * Solder mask images usually represent solder mask openings and are then negative.
     * This may be counter-intuitive.
     */
    int polarity = 0;

    switch( aLayer )
    {
    case F_Adhes:
    case B_Adhes:
    case F_SilkS:
    case B_SilkS:
    case F_Paste:
    case B_Paste:
        polarity = 1;
        break;

    case F_Mask:
    case B_Mask:
        polarity = -1;
        break;

    default:
        if( IsCopperLayer( aLayer ) )
            polarity = 1;
        break;
    }

    wxString filePolarity;

    if( polarity == 1 )
        filePolarity = "%TF.FilePolarity,Positive*%";
    if( polarity == -1 )
        filePolarity = "%TF.FilePolarity,Negative*%";

    return filePolarity;
}

/* Add some X2 attributes to the file header, as defined in the
 * Gerber file format specification J4 and "Revision 2015.06"
 */

// A helper function to convert a X2 attribute string to a X1 structured comment:
static wxString& makeStringCompatX1( wxString& aText, bool aUseX1CompatibilityMode )
{
    if( aUseX1CompatibilityMode )
    {
        aText.Replace( "%", "" );
        aText.Prepend( "G04 #@! " );
    }

    return aText;
}


void BuildGerberX2Header( const BOARD *aBoard, wxArrayString& aHeader )
{
    wxString text;

    // Creates the TF,.GenerationSoftware. Format is:
    // %TF,.GenerationSoftware,<vendor>,<application name>[,<application version>]*%
    text.Printf( wxT( "%%TF.GenerationSoftware,KiCad,Pcbnew,%s*%%" ), GetBuildVersion() );
    aHeader.Add( text );

    // creates the TF.CreationDate ext:
    // The attribute value must conform to the full version of the ISO 8601
    // date and time format, including time and time zone. Note that this is
    // the date the Gerber file was effectively created,
    // not the time the project of PCB was started
    wxDateTime date( wxDateTime::GetTimeNow() );
    // Date format: see http://www.cplusplus.com/reference/ctime/strftime
    wxString msg = date.Format( wxT( "%z" ) );  // Extract the time zone offset
    // The time zone offset format is + (or -) mm or hhmm  (mm = number of minutes, hh = number of hours)
    // we want +(or -) hh:mm
    if( msg.Len() > 3 )
        msg.insert( 3, ":", 1 ),
    text.Printf( wxT( "%%TF.CreationDate,%s%s*%%" ), GetChars( date.FormatISOCombined() ), GetChars( msg ) );
    aHeader.Add( text );

    // Creates the TF,.ProjectId. Format is (from Gerber file format doc):
    // %TF.ProjectId,<project id>,<project GUID>,<revision id>*%
    // <project id> is the name of the project, restricted to basic ASCII symbols only,
    // and comma not accepted
    // All illegal chars will be replaced by underscore
    // <project GUID> is a 32 hexadecimal digits string which is an unique id of a project.
    // This is a random 128-bit number expressed in 32 hexadecimal digits.
    // See en.wikipedia.org/wiki/GUID for more information
    // However Kicad does not handle such a project GUID, so it is built from the board name
    // Rem: <project id> accepts only ASCII 7 code (only basic ASCII codes are allowed in gerber files).
    wxFileName fn = aBoard->GetFileName();
    msg = fn.GetFullName();
    wxString guid;

    // Build a 32 digits GUID from the board name:
    for( unsigned ii = 0; ii < msg.Len(); ii++ )
    {
        int cc1 = int( msg[ii] ) & 0x0F;
        int cc2 = ( int( msg[ii] ) >> 4) & 0x0F;
        guid << wxString::Format( wxT( "%X%X" ), cc2, cc1 );

        if( guid.Len() >= 32 )
            break;
    }

    // guid has 32 digits, so add missing digits
    int cnt = 32 - guid.Len();

    if( cnt > 0 )
        guid.Append( '0', cnt );

    // build the <project id> string: this is the board short filename (without ext)
    // and all non ASCII chars and comma are replaced by '_'
    msg = fn.GetName();
    msg.Replace( wxT( "," ), wxT( "_" ) );

    // build the <rec> string. All non ASCII chars and comma are replaced by '_'
    wxString rev = ((BOARD*)aBoard)->GetTitleBlock().GetRevision();
    rev.Replace( wxT( "," ), wxT( "_" ) );

    if( rev.IsEmpty() )
        rev = wxT( "rev?" );

    text.Printf( wxT( "%%TF.ProjectId,%s,%s,%s*%%" ), msg.ToAscii(), GetChars( guid ), rev.ToAscii() );
    aHeader.Add( text );

    // Add the TF.SameCoordinates, that specify all gerber files uses the same
    // origin and orientation, and the registration between files is OK.
    // The parameter of TF.SameCoordinates is a string that is common
    // to all files using the same registration and has no special meaning:
    // this is just a key
    // Because there is no mirroring/rotation in Kicad, only the plot offset origin
    // can create incorrect registration.
    // So we create a key from plot offset options.
    // and therefore for a given board, all Gerber files having the same key have the same
    // plot origin and use the same registration
    //
    // Currently the key is "Original" when using absolute Pcbnew coordinates,
    // and te PY ans PY position od auxiliary axis, when using it.
    // Please, if absolute Pcbnew coordinates, one day, are set by user, change the way
    // the key is built to ensure file only using the *same* axis have the same key.
    wxString registration_id = "Original";
    wxPoint auxOrigin = aBoard->GetAuxOrigin();

    if( aBoard->GetPlotOptions().GetUseAuxOrigin() && auxOrigin.x && auxOrigin.y )
        registration_id.Printf( "PX%xPY%x", auxOrigin.x, auxOrigin.y );

    text.Printf( "%%TF.SameCoordinates,%s*%%", registration_id.GetData() );
    aHeader.Add( text );
}


void AddGerberX2Header( PLOTTER * aPlotter,
            const BOARD *aBoard, bool aUseX1CompatibilityMode )
{
    wxString text;

    // Creates the TF,.GenerationSoftware. Format is:
    // %TF,.GenerationSoftware,<vendor>,<application name>[,<application version>]*%
    text.Printf( wxT( "%%TF.GenerationSoftware,KiCad,Pcbnew,%s*%%" ), GetBuildVersion() );
    aPlotter->AddLineToHeader( makeStringCompatX1( text, aUseX1CompatibilityMode ) );

    // creates the TF.CreationDate ext:
    // The attribute value must conform to the full version of the ISO 8601
    // date and time format, including time and time zone. Note that this is
    // the date the Gerber file was effectively created,
    // not the time the project of PCB was started
    wxDateTime date( wxDateTime::GetTimeNow() );
    // Date format: see http://www.cplusplus.com/reference/ctime/strftime
    wxString msg = date.Format( wxT( "%z" ) );  // Extract the time zone offset
    // The time zone offset format is + (or -) mm or hhmm  (mm = number of minutes, hh = number of hours)
    // we want +(or -) hh:mm
    if( msg.Len() > 3 )
        msg.insert( 3, ":", 1 ),
    text.Printf( wxT( "%%TF.CreationDate,%s%s*%%" ), GetChars( date.FormatISOCombined() ), GetChars( msg ) );
    aPlotter->AddLineToHeader( makeStringCompatX1( text, aUseX1CompatibilityMode ) );

    // Creates the TF,.ProjectId. Format is (from Gerber file format doc):
    // %TF.ProjectId,<project id>,<project GUID>,<revision id>*%
    // <project id> is the name of the project, restricted to basic ASCII symbols only,
    // and comma not accepted
    // All illegal chars will be replaced by underscore
    // <project GUID> is a 32 hexadecimal digits string which is an unique id of a project.
    // This is a random 128-bit number expressed in 32 hexadecimal digits.
    // See en.wikipedia.org/wiki/GUID for more information
    // However Kicad does not handle such a project GUID, so it is built from the board name
    // Rem: <project id> accepts only ASCII 7 code (only basic ASCII codes are allowed in gerber files).
    wxFileName fn = aBoard->GetFileName();
    msg = fn.GetFullName();
    wxString guid;

    // Build a 32 digits GUID from the board name:
    for( unsigned ii = 0; ii < msg.Len(); ii++ )
    {
        int cc1 = int( msg[ii] ) & 0x0F;
        int cc2 = ( int( msg[ii] ) >> 4) & 0x0F;
        guid << wxString::Format( wxT( "%X%X" ), cc2, cc1 );

        if( guid.Len() >= 32 )
            break;
    }

    // guid has 32 digits, so add missing digits
    int cnt = 32 - guid.Len();

    if( cnt > 0 )
        guid.Append( '0', cnt );

    // build the <project id> string: this is the board short filename (without ext)
    // and all non ASCII chars and comma are replaced by '_'
    msg = fn.GetName();
    msg.Replace( wxT( "," ), wxT( "_" ) );

    // build the <rec> string. All non ASCII chars and comma are replaced by '_'
    wxString rev = ((BOARD*)aBoard)->GetTitleBlock().GetRevision();
    rev.Replace( wxT( "," ), wxT( "_" ) );

    if( rev.IsEmpty() )
        rev = wxT( "rev?" );

    text.Printf( wxT( "%%TF.ProjectId,%s,%s,%s*%%" ), msg.ToAscii(), GetChars( guid ), rev.ToAscii() );
    aPlotter->AddLineToHeader( makeStringCompatX1( text, aUseX1CompatibilityMode ) );

    // Add the TF.SameCoordinates, that specify all gerber files uses the same
    // origin and orientation, and the registration between files is OK.
    // The parameter of TF.SameCoordinates is a string that is common
    // to all files using the same registration and has no special meaning:
    // this is just a key
    // Because there is no mirroring/rotation in Kicad, only the plot offset origin
    // can create incorrect registration.
    // So we create a key from plot offset options.
    // and therefore for a given board, all Gerber files having the same key have the same
    // plot origin and use the same registration
    //
    // Currently the key is "Original" when using absolute Pcbnew coordinates,
    // and te PY ans PY position od auxiliary axis, when using it.
    // Please, if absolute Pcbnew coordinates, one day, are set by user, change the way
    // the key is built to ensure file only using the *same* axis have the same key.
    wxString registration_id = "Original";
    wxPoint auxOrigin = aBoard->GetAuxOrigin();

    if( aBoard->GetPlotOptions().GetUseAuxOrigin() && auxOrigin.x && auxOrigin.y )
        registration_id.Printf( "PX%xPY%x", auxOrigin.x, auxOrigin.y );

    text.Printf( "%%TF.SameCoordinates,%s*%%", registration_id.GetData() );
    aPlotter->AddLineToHeader( makeStringCompatX1( text, aUseX1CompatibilityMode ) );
}


void AddGerberX2Attribute( PLOTTER * aPlotter,
            const BOARD *aBoard, LAYER_NUM aLayer, bool aUseX1CompatibilityMode )
{
    AddGerberX2Header( aPlotter, aBoard, aUseX1CompatibilityMode );

    wxString text;

    // Add the TF.FileFunction
    text = GetGerberFileFunctionAttribute( aBoard, aLayer );
    aPlotter->AddLineToHeader( makeStringCompatX1( text, aUseX1CompatibilityMode ) );

    // Add the TF.FilePolarity (for layers which support that)
    text = GetGerberFilePolarityAttribute( aLayer );

    if( !text.IsEmpty() )
        aPlotter->AddLineToHeader( makeStringCompatX1( text, aUseX1CompatibilityMode ) );
}


void BuildPlotFileName( wxFileName* aFilename, const wxString& aOutputDir,
                        const wxString& aSuffix, const wxString& aExtension )
{
    // aFilename contains the base filename only (without path and extension)
    // when calling this function.
    // It is expected to be a valid filename (this is usually the board filename)
    aFilename->SetPath( aOutputDir );

    // Set the file extension
    aFilename->SetExt( aExtension );

    // remove leading and trailing spaces if any from the suffix, if
    // something survives add it to the name;
    // also the suffix can contain some not allowed chars in filename (/ \ . :),
    // so change them to underscore
    // Remember it can be called from a python script, so the illegal chars
    // have to be filtered here.
    wxString suffix = aSuffix;
    suffix.Trim( true );
    suffix.Trim( false );

    wxString badchars = wxFileName::GetForbiddenChars(wxPATH_DOS);
    badchars.Append( '%' );

    for( unsigned ii = 0; ii < badchars.Len(); ii++ )
        suffix.Replace( badchars[ii], wxT("_") );

    if( !suffix.IsEmpty() )
        aFilename->SetName( aFilename->GetName() + wxT( "-" ) + suffix );
}


PLOT_CONTROLLER::PLOT_CONTROLLER( BOARD *aBoard )
{
    m_plotter = NULL;
    m_board = aBoard;
    m_plotLayer = UNDEFINED_LAYER;
}


PLOT_CONTROLLER::~PLOT_CONTROLLER()
{
    ClosePlot();
}


/* IMPORTANT THING TO KNOW: the locale during plots *MUST* be kept as
 * C/POSIX using a LOCALE_IO object on the stack. This even when
 * opening/closing the plotfile, since some drivers do I/O even then */

void PLOT_CONTROLLER::ClosePlot()
{
    LOCALE_IO toggle;

    if( m_plotter )
    {
        m_plotter->EndPlot();
        delete m_plotter;
        m_plotter = NULL;
    }
}


bool PLOT_CONTROLLER::OpenPlotfile( const wxString &aSuffix,
                                    PlotFormat     aFormat,
                                    const wxString &aSheetDesc )
{
    LOCALE_IO toggle;

    /* Save the current format: sadly some plot routines depends on this
       but the main reason is that the StartPlot method uses it to
       dispatch the plotter creation */
    GetPlotOptions().SetFormat( aFormat );

    // Ensure that the previous plot is closed
    ClosePlot();

    // Now compute the full filename for the output and start the plot
    // (after ensuring the output directory is OK)
    wxString outputDirName = GetPlotOptions().GetOutputDirectory() ;
    wxFileName outputDir = wxFileName::DirName( outputDirName );
    wxString boardFilename = m_board->GetFileName();

    if( EnsureFileDirectoryExists( &outputDir, boardFilename ) )
    {
        // outputDir contains now the full path of plot files
        m_plotFile = boardFilename;
        m_plotFile.SetPath( outputDir.GetPath() );
        wxString fileExt = GetDefaultPlotExtension( aFormat );

        // Gerber format can use specific file ext, depending on layers
        // (now not a good practice, because the official file ext is .gbr)
        if( GetPlotOptions().GetFormat() == PLOT_FORMAT_GERBER &&
            GetPlotOptions().GetUseGerberProtelExtensions() )
            fileExt = GetGerberProtelExtension( GetLayer() );

        // Build plot filenames from the board name and layer names:
        BuildPlotFileName( &m_plotFile, outputDir.GetPath(), aSuffix, fileExt );

        m_plotter = StartPlotBoard( m_board, &GetPlotOptions(), ToLAYER_ID( GetLayer() ),
                                    m_plotFile.GetFullPath(), aSheetDesc );
    }

    return( m_plotter != NULL );
}


bool PLOT_CONTROLLER::PlotLayer()
{
    LOCALE_IO toggle;

    // No plot open, nothing to do...
    if( !m_plotter )
        return false;

    // Fully delegated to the parent
    PlotOneBoardLayer( m_board, m_plotter, ToLAYER_ID( GetLayer() ), GetPlotOptions() );

    return true;
}


void PLOT_CONTROLLER::SetColorMode( bool aColorMode )
{
    if( !m_plotter )
        return;

    m_plotter->SetColorMode( aColorMode );
}


bool PLOT_CONTROLLER::GetColorMode()
{
    if( !m_plotter )
        return false;

    return m_plotter->GetColorMode();
}