/*
 * This program source code file is part of KiCad, a free EDA CAD application.
 *
 * Copyright (C) 2016 Cirilo Bernardo <cirilo.bernardo@gmail.com>
 * Copyright (C) 2020-2022 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 "kicadpcb.h"

#include "kicadcurve.h"
#include "kicadfootprint.h"
#include "oce_utils.h"

#include <sexpr/sexpr.h>
#include <sexpr/sexpr_parser.h>

#include <wx/wxcrtvararg.h>

#include <memory>
#include <string>

#include <wx_filename.h>


KICADPCB::KICADPCB( const wxString& aPcbName )
{
    m_resolver.Set3DConfigDir( wxEmptyString );

    m_topSolderMask    = wxColour(  15, 102,  15 );
    m_bottomSolderMask = wxColour(  15, 102,  15 );
    m_topSilk          = wxColour( 240, 240, 240 );
    m_bottomSilk       = wxColour( 240, 240, 240 );
    m_copperFinish     = wxColour( 191, 155,  58 );

    m_thickness = 1.6;
    m_pcb_model = nullptr;
    m_minDistance = MIN_DISTANCE;
    m_useGridOrigin = false;
    m_useDrillOrigin = false;
    m_hasGridOrigin = false;
    m_hasDrillOrigin = false;
    m_pcbName = aPcbName;
}


KICADPCB::~KICADPCB()
{
    for( KICADFOOTPRINT* i : m_footprints )
        delete i;

    for( KICADCURVE* i : m_curves )
        delete i;

    delete m_pcb_model;

    return;
}


bool KICADPCB::ReadFile( const wxString& aFileName )
{
    wxFileName fname( aFileName );

    if( fname.GetExt() != "kicad_pcb" )
    {
        ReportMessage( wxString::Format( wxT( "expecting extension kicad_pcb, got %s\n" ),
                                         fname.GetExt() ) );
        return false;
    }

    if( !fname.FileExists() )
    {
        ReportMessage( wxString::Format( wxT( "No such file: %s\n" ), aFileName ) );
        return false;
    }

    fname.Normalize( FN_NORMALIZE_FLAGS | wxPATH_NORM_ENV_VARS );
    m_filename = fname.GetFullPath();

    try
    {
        SEXPR::PARSER parser;
        std::string infile( fname.GetFullPath().ToUTF8() );
        std::unique_ptr<SEXPR::SEXPR> data( parser.ParseFromFile( infile ) );

        if( !data )
        {
            ReportMessage( wxString::Format( wxT( "No data in file: %s\n" ), aFileName ) );
            return false;
        }

        if( !parsePCB( data.get() ) )
            return false;
    }
    catch( std::exception& e )
    {
        ReportMessage( wxString::Format( wxT( "error reading file: %s\n%s\n" ),
                                         aFileName,
                                         e.what() ) );
        return false;
    }
    catch( ... )
    {
        ReportMessage( wxString::Format( wxT( "unexpected exception while reading file: %s\n" ),
                                         aFileName ) );
        return false;
    }

    return true;
}


bool KICADPCB::WriteSTEP( const wxString& aFileName )
{
    if( m_pcb_model )
        return m_pcb_model->WriteSTEP( aFileName );

    return false;
}


#ifdef SUPPORTS_IGES
bool KICADPCB::WriteIGES( const wxString& aFileName )
{
    if( m_pcb_model )
        return m_pcb_model->WriteIGES( aFileName );

    return false;
}
#endif


bool KICADPCB::parsePCB( SEXPR::SEXPR* data )
{
    if( NULL == data )
        return false;

    if( data->IsList() )
    {
        size_t nc = data->GetNumberOfChildren();
        SEXPR::SEXPR* child = data->GetChild( 0 );
        std::string name = child->GetSymbol();

        bool result = true;

        for( size_t i = 1; i < nc && result; ++i )
        {
            child = data->GetChild( i );

            if( !child->IsList() )
            {
                ReportMessage( wxString::Format( wxT( "corrupt PCB file (line %d)\n" ),
                                                 child->GetLineNumber() ) );
                return false;
            }

            std::string symname( child->GetChild( 0 )->GetSymbol() );

            if( symname == "general" )
                result = result && parseGeneral( child );
            else if( symname == "setup" )
                result = result && parseSetup( child );
            else if( symname == "layers" )
                result = result && parseLayers( child );
            else if( symname == "module" )
                result = result && parseModule( child );
            else if( symname == "footprint" )
                result = result && parseModule( child );
            else if( symname == "gr_arc" )
                result = result && parseCurve( child, CURVE_ARC );
            else if( symname == "gr_line" )
                result = result && parseCurve( child, CURVE_LINE );
            else if( symname == "gr_rect" )
                result = result && parseRect( child );
            else if( symname == "gr_poly" )
                result = result && parsePolygon( child );
            else if( symname == "gr_circle" )
                result = result && parseCurve( child, CURVE_CIRCLE );
            else if( symname == "gr_curve" )
                result = result && parseCurve( child, CURVE_BEZIER );
        }

        return result;
    }

    ReportMessage( wxString::Format( wxT( "data is not a valid PCB file: %s\n" ), m_filename ) );
    return false;
}


bool KICADPCB::parseGeneral( SEXPR::SEXPR* data )
{
    size_t nc = data->GetNumberOfChildren();
    SEXPR::SEXPR* child = NULL;

    for( size_t i = 1; i < nc; ++i )
    {
        child = data->GetChild( i );

        if( !child->IsList() )
        {
            ReportMessage( wxString::Format( wxT( "corrupt PCB file (line %d)\n" ),
                                             child->GetLineNumber() ) );
            return false;
        }

        // at the moment only the thickness is of interest in the general section
        if( child->GetChild( 0 )->GetSymbol() != "thickness" )
            continue;

        m_thickness = child->GetChild( 1 )->GetDouble();
        return true;
    }

    ReportMessage( wxString::Format( wxT( "corrupt PCB file (line %d)\n"
                                          "no PCB thickness specified in general section\n" ),
                                     child->GetLineNumber() ) );
    return false;
}


bool KICADPCB::parseLayers( SEXPR::SEXPR* data )
{
    size_t nc = data->GetNumberOfChildren();
    SEXPR::SEXPR* child = NULL;

    // Read the layername and the corresponding layer id list:
    for( size_t i = 1; i < nc; ++i )
    {
        child = data->GetChild( i );

        if( !child->IsList() )
        {
            ReportMessage( wxString::Format( wxT( "corrupt PCB file (line %d)\n" ),
                                             child->GetLineNumber() ) );
            return false;
        }

        std::string ref;

        if( child->GetChild( 1 )->IsSymbol() )
            ref = child->GetChild( 1 )->GetSymbol();
        else
            ref = child->GetChild( 1 )->GetString();

        m_layersNames[ref] = child->GetChild( 0 )->GetInteger();
    }

    return true;
}


bool KICADPCB::parseStackupLayer( SEXPR::SEXPR* data )
{
    if( data->IsList() && data->GetNumberOfChildren() >= 3 )
    {
        size_t nc = data->GetNumberOfChildren();
        SEXPR::SEXPR* child = NULL;
        std::string   ref;
        std::string   value;

        for( size_t i = 1; i < nc; ++i )
        {
            child = data->GetChild( i );

            if( child->IsList() && child->GetChild( 0 )->GetSymbol() == "type" )
            {
                if( child->GetChild( 1 )->IsSymbol() )
                    ref = child->GetChild( 1 )->GetSymbol();
                else
                    ref = child->GetChild( 1 )->GetString();
            }
            else if( child->IsList() && child->GetChild( 0 )->GetSymbol() == "color" )
            {
                if( child->GetChild( 1 )->IsSymbol() )
                    value = child->GetChild( 1 )->GetSymbol();
                else
                    value = child->GetChild( 1 )->GetString();
            }
        }

        if( !value.empty() )
        {
            wxString colorName( value );
            wxColour color;

            if( colorName.StartsWith( wxT( "#" ) ) )
                color = wxColour( colorName.Left( 7 ) /* drop alpha component */ );
            else if( colorName == wxT( "Green" ) )
                color = wxColour(  20,  51,  36 );
            else if( colorName == wxT( "Light Green" ) )
                color = wxColour(  91, 168,  12);
            else if( colorName == wxT( "Saturated Green" ) )
                color = wxColour(  13, 104,  11 );
            else if( colorName == wxT( "Red" ) )
                color = wxColour( 181,  19,  21 );
            else if( colorName == wxT( "Light Red" ) )
                color = wxColour( 210,  40,  14 );
            else if( colorName == wxT( "Red/Orange" ) )
                color = wxColour( 239,  53,  41 );
            else if( colorName == wxT( "Blue" ) )
                color = wxColour(   2,  59, 162 );
            else if( colorName == wxT( "Light Blue 1" ) )
                color = wxColour(  54,  79, 116 );
            else if( colorName == wxT( "Light Blue 2" ) )
                color = wxColour(  61,  85, 130 );
            else if( colorName == wxT( "Green/Blue" ) )
                color = wxColour(  21,  70,  80 );
            else if( colorName == wxT( "Black" ) )
                color = wxColour(  11,  11,  11 );
            else if( colorName == wxT( "White" ) )
                color = wxColour( 245, 245, 245 );
            else if( colorName == wxT( "Purple" ) )
                color = wxColour(  32,   2,  53 );
            else if( colorName == wxT( "Light Purple" ) )
                color = wxColour( 119,  31,  91 );
            else if( colorName == wxT( "Yellow" ) )
                color = wxColour( 194,  195,  0 );

            if( ref == "Top Silk Screen" )
                m_topSilk = color;
            else if( ref == "Top Solder Mask" )
                m_topSolderMask = color;
            else if( ref == "Bottom Silk Screen" )
                m_bottomSilk = color;
            else if( ref == "Bottom Solder Mask" )
                m_bottomSolderMask = color;
        }
    }

    return true;
}


bool KICADPCB::parseStackup( SEXPR::SEXPR* data )
{
    size_t nc = data->GetNumberOfChildren();
    SEXPR::SEXPR* child = NULL;

    for( size_t i = 1; i < nc; ++i )
    {
        child = data->GetChild( i );

        if( !child->IsList() )
        {
            ReportMessage( wxString::Format( wxT( "corrupt PCB file (line %d)\n" ),
                                             child->GetLineNumber() ) );
            return false;
        }

        std::string ref;

        if( child->GetChild( 0 )->GetSymbol() == "layer" )
        {
            parseStackupLayer( child );
        }
        else if( child->GetChild( 0 )->GetSymbol() == "copper_finish" )
        {
            if( child->GetChild( 1 )->IsSymbol() )
                ref = child->GetChild( 1 )->GetSymbol();
            else
                ref = child->GetChild( 1 )->GetString();

            wxString finishName( ref );

            if( finishName.EndsWith( wxT( "OSP" ) ) )
            {
                m_copperFinish = wxColour( 184, 115, 50 );
            }
            else if( finishName.EndsWith( wxT( "IG" ) )
                  || finishName.EndsWith( wxT( "gold" ) ) )
            {
                m_copperFinish = wxColour( 178, 156, 0 );
            }
            else if( finishName.StartsWith( wxT( "HAL" ) )
                  || finishName.StartsWith( wxT( "HASL" ) )
                  || finishName.EndsWith( wxT( "tin" ) )
                  || finishName.EndsWith( wxT( "nickel" ) ) )
            {
                m_copperFinish = wxColour( 160, 160, 160 );
            }
            else if( finishName.EndsWith( wxT( "silver" ) ) )
            {
                m_copperFinish = wxColour( 213, 213, 213 );
            }
        }
    }

    return true;
}


int KICADPCB::GetLayerId( std::string& aLayerName )
{
    int lid = -1;
    auto item = m_layersNames.find( aLayerName );

    if( item != m_layersNames.end() )
        lid = item->second;

    return lid;
}


bool KICADPCB::parseSetup( SEXPR::SEXPR* data )
{
    size_t nc = data->GetNumberOfChildren();
    SEXPR::SEXPR* child = NULL;

    for( size_t i = 1; i < nc; ++i )
    {
        child = data->GetChild( i );

        if( !child->IsList() )
        {
            ReportMessage( wxString::Format( wxT( "corrupt PCB file (line %d)\n" ),
                                             child->GetLineNumber() ) );
            return false;
        }

        // at the moment only the Grid and Drill origins are of interest in the setup section
        if( child->GetChild( 0 )->GetSymbol() == "grid_origin" )
        {
            if( child->GetNumberOfChildren() != 3 )
            {
                ReportMessage( wxString::Format( wxT( "corrupt PCB file (line %d): grid_origin has "
                                                      "%d children (expected: 3)\n" ),
                                                 child->GetLineNumber(),
                                                 child->GetNumberOfChildren() ) );
                return false;
            }

            m_gridOrigin.x = child->GetChild( 1 )->GetDouble();
            m_gridOrigin.y = child->GetChild( 2 )->GetDouble();
            m_hasGridOrigin = true;
        }
        else if( child->GetChild( 0 )->GetSymbol() == "aux_axis_origin" )
        {
            if( child->GetNumberOfChildren() != 3 )
            {
                ReportMessage( wxString::Format( wxT( "corrupt PCB file (line %d): aux_axis_origin "
                                                      "has %d children (expected: 3)\n" ),
                                                 child->GetLineNumber(),
                                                 child->GetNumberOfChildren() ) );
                return false;
            }

            m_drillOrigin.x = child->GetChild( 1 )->GetDouble();
            m_drillOrigin.y = child->GetChild( 2 )->GetDouble();
            m_hasDrillOrigin = true;
        }
        else if( child->GetChild( 0 )->GetSymbol() == "stackup" )
        {
            parseStackup( child );
        }
    }

    return true;
}


bool KICADPCB::parseModule( SEXPR::SEXPR* data )
{
    KICADFOOTPRINT* footprint = new KICADFOOTPRINT( this );

    if( !footprint->Read( data ) )
    {
        delete footprint;
        return false;
    }

    m_footprints.push_back( footprint );
    return true;
}


bool KICADPCB::parseRect( SEXPR::SEXPR* data )
{
    KICADCURVE* rect = new KICADCURVE();

    if( !rect->Read( data, CURVE_LINE ) )
    {
        delete rect;
        return false;
    }

    // reject any curves not on the Edge.Cuts layer
    if( rect->GetLayer() != LAYER_EDGE )
    {
        delete rect;
        return true;
    }

    KICADCURVE* top = new KICADCURVE( *rect );
    KICADCURVE* right = new KICADCURVE( *rect );
    KICADCURVE* bottom = new KICADCURVE( *rect );
    KICADCURVE* left = new KICADCURVE( *rect );
    delete rect;

    top->m_end.y = right->m_start.y;
    m_curves.push_back( top );

    right->m_start.x = bottom->m_end.x;
    m_curves.push_back( right );

    bottom->m_start.y = left->m_end.y;
    m_curves.push_back( bottom );

    left->m_end.x = top->m_start.x;
    m_curves.push_back( left );

    return true;
}


bool KICADPCB::parsePolygon( SEXPR::SEXPR* data )
{
    std::unique_ptr<KICADCURVE> poly = std::make_unique<KICADCURVE>();

    if( !poly->Read( data, CURVE_POLYGON ) )
        return false;

    // reject any curves not on the Edge.Cuts layer
    if( poly->GetLayer() != LAYER_EDGE )
        return true;

    auto pts = poly->m_poly;

    for( std::size_t ii = 1; ii < pts.size(); ++ii )
    {
        KICADCURVE* seg = new KICADCURVE();
        seg->m_form = CURVE_LINE;
        seg->m_layer = poly->GetLayer();
        seg->m_start = pts[ii - 1];
        seg->m_end = pts[ii];
        m_curves.push_back( seg );
    }

    KICADCURVE* seg = new KICADCURVE();
    seg->m_form = CURVE_LINE;
    seg->m_layer = poly->GetLayer();
    seg->m_start = pts.back();
    seg->m_end = pts.front();
    m_curves.push_back( seg );

    return true;
}


bool KICADPCB::parseCurve( SEXPR::SEXPR* data, CURVE_TYPE aCurveType )
{
    KICADCURVE* curve = new KICADCURVE();

    if( !curve->Read( data, aCurveType ) )
    {
        delete curve;
        return false;
    }

    // reject any curves not on the Edge.Cuts layer
    if( curve->GetLayer() != LAYER_EDGE )
    {
        delete curve;
        return true;
    }

    m_curves.push_back( curve );
    return true;
}


bool KICADPCB::ComposePCB( bool aComposeVirtual, bool aSubstituteModels )
{
    if( m_pcb_model )
        return true;

    if( m_footprints.empty() && m_curves.empty() )
    {
        ReportMessage( wxT( "Error: no PCB data (no footprint, no outline) to render\n" ) );
        return false;
    }

    DOUBLET origin;

    // Determine the coordinate system reference:
    // Precedence of reference point is Drill Origin > Grid Origin > User Offset
    if( m_useDrillOrigin && m_hasDrillOrigin )
        origin = m_drillOrigin;
    else if( m_useGridOrigin && m_hasDrillOrigin )
        origin = m_gridOrigin;
    else
        origin = m_origin;

    m_pcb_model = new PCBMODEL( m_pcbName );

    // TODO: Handle when top & bottom soldermask colours are different...
    m_pcb_model->SetBoardColor( m_topSolderMask.Red() / 255.0,
                                m_topSolderMask.Green() / 255.0,
                                m_topSolderMask.Blue() / 255.0 );

    m_pcb_model->SetPCBThickness( m_thickness );
    m_pcb_model->SetMinDistance( m_minDistance );

    for( KICADCURVE* i : m_curves )
    {
        if( CURVE_NONE == i->m_form || LAYER_EDGE != i->m_layer )
            continue;

        // adjust the coordinate system
        // Note: we negate the Y coordinates due to the fact in Pcbnew the Y axis
        // is from top to bottom.
        KICADCURVE lcurve = *i;
        lcurve.m_start.y = -( lcurve.m_start.y - origin.y );
        lcurve.m_end.y = -( lcurve.m_end.y - origin.y );
        lcurve.m_start.x -= origin.x;
        lcurve.m_end.x -= origin.x;
        // used in bezier curves:
        lcurve.m_bezierctrl1.y = -( lcurve.m_bezierctrl1.y - origin.y );
        lcurve.m_bezierctrl1.x -= origin.x;
        lcurve.m_bezierctrl2.y = -( lcurve.m_bezierctrl2.y - origin.y );
        lcurve.m_bezierctrl2.x -= origin.x;

        if( CURVE_ARC == lcurve.m_form )
            lcurve.m_angle = -lcurve.m_angle;

        m_pcb_model->AddOutlineSegment( &lcurve );
    }

    for( KICADFOOTPRINT* i : m_footprints )
        i->ComposePCB( m_pcb_model, &m_resolver, origin, aComposeVirtual, aSubstituteModels );

    ReportMessage( wxT( "Create PCB solid model\n" ) );

    if( !m_pcb_model->CreatePCB() )
    {
        ReportMessage( wxT( "could not create PCB solid model\n" ) );
        delete m_pcb_model;
        m_pcb_model = NULL;
        return false;
    }

    return true;
}
