/*
 * file: idf_common.cpp
 *
 * This program source code file is part of KiCad, a free EDA CAD application.
 *
 * Copyright (C) 2013-2017 Cirilo Bernardo
 * Copyright (C) 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 <algorithm>
#include <list>
#include <string>
#include <iostream>
#include <sstream>
#include <iomanip>
#include <cerrno>
#include <cstdio>
#include <cmath>
#include <idf_common.h>
#include <idf_helpers.h>

using namespace IDF3;
using namespace std;


std::string source;
std::string message;

IDF_ERROR::IDF_ERROR( const char* aSourceFile, const char* aSourceMethod, int aSourceLine,
                      const std::string& aMessage ) noexcept
{
    ostringstream ostr;

    if( aSourceFile )
        ostr << "* " << aSourceFile << ":";
    else
        ostr << "* [BUG: No Source File]:";

    ostr << aSourceLine << ":";

    if( aSourceMethod )
        ostr << aSourceMethod << "(): ";
    else
        ostr << "[BUG: No Source Method]:\n* ";

    ostr << aMessage;
    message = ostr.str();
}


IDF_ERROR::~IDF_ERROR() noexcept
{
}


const char* IDF_ERROR::what() const noexcept
{
    return message.c_str();
}


IDF_NOTE::IDF_NOTE() :
        xpos( 0.0 ),
        ypos( 0.0 ),
        height( 0.0 ),
        length( 0.0 )
{
}


bool IDF_NOTE::readNote( std::istream& aBoardFile, IDF3::FILE_STATE& aBoardState,
                         IDF3::IDF_UNIT aBoardUnit )
{
    std::string iline;      // the input line
    bool isComment;         // true if a line just read in is a comment line
    std::streampos pos;
    int idx = 0;
    bool quoted = false;
    std::string token;

    // RECORD 2: X, Y, text Height, text Length, "TEXT"
    while( !FetchIDFLine( aBoardFile, iline, isComment, pos ) && aBoardFile.good() );

    if( ( !aBoardFile.good() && !aBoardFile.eof() ) || iline.empty() )
    {
        aBoardState = IDF3::FILE_INVALID;
        throw( IDF_ERROR( __FILE__, __FUNCTION__, __LINE__, "problems reading board notes" ) );
    }

    if( isComment )
    {
        aBoardState = IDF3::FILE_INVALID;
        throw( IDF_ERROR( __FILE__, __FUNCTION__, __LINE__,
                          "invalid IDFv3 file\n"
                          "* Violation of specification: comment within a section (NOTES)" ) );
    }

    idx = 0;
    GetIDFString( iline, token, quoted, idx );

    if( quoted )
    {
        aBoardState = IDF3::FILE_INVALID;
        throw( IDF_ERROR( __FILE__, __FUNCTION__, __LINE__,
                          "invalid IDFv3 file\n"
                          "* Violation of specification: X position in NOTES section must not be "
                          "in quotes" ) );
    }

    if( CompareToken( ".END_NOTES", token ) )
        return false;

    istringstream istr;
    istr.str( token );

    istr >> xpos;

    if( istr.fail() )
    {
        aBoardState = IDF3::FILE_INVALID;
        throw( IDF_ERROR( __FILE__, __FUNCTION__, __LINE__,
                          "invalid IDFv3 file\n"
                          "* Violation of specification: X position in NOTES section is not "
                          "numeric" ) );
    }

    if( !GetIDFString( iline, token, quoted, idx ) )
    {
        aBoardState = IDF3::FILE_INVALID;
        throw( IDF_ERROR( __FILE__, __FUNCTION__, __LINE__,
                          "invalid IDFv3 file\n"
                          "* Violation of specification: Y position in NOTES section is "
                          "missing" ) );
    }

    if( quoted )
    {
        aBoardState = IDF3::FILE_INVALID;
        throw( IDF_ERROR( __FILE__, __FUNCTION__, __LINE__,
                          "invalid IDFv3 file\n"
                          "* Violation of specification: Y position in NOTES section must not be "
                          "in quotes" ) );
    }

    istr.clear();
    istr.str( token );

    istr >> ypos;

    if( istr.fail() )
    {
        aBoardState = IDF3::FILE_INVALID;
        throw( IDF_ERROR( __FILE__, __FUNCTION__, __LINE__,
                          "invalid IDFv3 file\n"
                          "* Violation of specification: Y position in NOTES section is not "
                          "numeric" ) );
    }

    if( !GetIDFString( iline, token, quoted, idx ) )
    {
        aBoardState = IDF3::FILE_INVALID;
        throw( IDF_ERROR( __FILE__, __FUNCTION__, __LINE__,
                          "invalid IDFv3 file\n"
                          "* Violation of specification: text height in NOTES section is "
                          "missing" ) );
    }

    if( quoted )
    {
        aBoardState = IDF3::FILE_INVALID;
        throw( IDF_ERROR( __FILE__, __FUNCTION__, __LINE__,
                          "invalid IDFv3 file\n"
                          "* Violation of specification: text height in NOTES section must not "
                          "be in quotes" ) );
    }

    istr.clear();
    istr.str( token );

    istr >> height;

    if( istr.fail() )
    {
        aBoardState = IDF3::FILE_INVALID;
        throw( IDF_ERROR( __FILE__, __FUNCTION__, __LINE__,
                          "invalid IDFv3 file\n"
                          "* Violation of specification: text height in NOTES section is not "
                          "numeric" ) );
    }

    if( !GetIDFString( iline, token, quoted, idx ) )
    {
        aBoardState = IDF3::FILE_INVALID;
        throw( IDF_ERROR( __FILE__, __FUNCTION__, __LINE__,
                          "invalid IDFv3 file\n"
                          "* Violation of specification: text length in NOTES section is "
                          "missing" ) );
    }

    if( quoted )
    {
        aBoardState = IDF3::FILE_INVALID;
        throw( IDF_ERROR( __FILE__, __FUNCTION__, __LINE__,
                          "invalid IDFv3 file\n"
                          "* Violation of specification: text length in NOTES section must not "
                          "be in quotes" ) );
    }

    istr.clear();
    istr.str( token );

    istr >> length;

    if( istr.fail() )
    {
        aBoardState = IDF3::FILE_INVALID;
        throw( IDF_ERROR( __FILE__, __FUNCTION__, __LINE__,
                          "invalid IDFv3 file\n"
                          "* Violation of specification: text length in NOTES section is not "
                          "numeric" ) );
    }

    if( !GetIDFString( iline, token, quoted, idx ) )
    {
        aBoardState = IDF3::FILE_INVALID;
        throw( IDF_ERROR( __FILE__, __FUNCTION__, __LINE__,
                          "invalid IDFv3 file\n"
                          "* Violation of specification: text value in NOTES section is "
                          "missing" ) );
    }

    text = token;

    if( aBoardUnit == UNIT_THOU )
    {
        xpos *= IDF_THOU_TO_MM;
        ypos *= IDF_THOU_TO_MM;
        height *= IDF_THOU_TO_MM;
        length *= IDF_THOU_TO_MM;
    }

    return true;
}


bool IDF_NOTE::writeNote( std::ostream& aBoardFile, IDF3::IDF_UNIT aBoardUnit )
{
    if( aBoardUnit == UNIT_THOU )
    {
        aBoardFile << setiosflags( ios::fixed ) << setprecision( 1 ) << ( xpos / IDF_THOU_TO_MM )
                   << " " << ( ypos / IDF_THOU_TO_MM ) << " " << ( height / IDF_THOU_TO_MM ) << " "
                   << ( length / IDF_THOU_TO_MM ) << " ";
    }
    else
    {
        aBoardFile << setiosflags( ios::fixed ) << setprecision( 5 ) << xpos << " " << ypos << " "
                   << height << " " << length << " ";
    }

    aBoardFile << "\"" << text << "\"\n";

    return !aBoardFile.bad();
}


void IDF_NOTE::SetText( const std::string& aText )
{
    text = aText;
}


void IDF_NOTE::SetPosition( double aXpos, double aYpos )
{
    xpos = aXpos;
    ypos = aYpos;
}


void IDF_NOTE::SetSize( double aHeight, double aLength )
{
    height = aHeight;
    length = aLength;
}


const std::string& IDF_NOTE::GetText()
{
    return text;
}


void IDF_NOTE::GetPosition( double& aXpos, double& aYpos )
{
    aXpos = xpos;
    aYpos = ypos;
}


void IDF_NOTE::GetSize( double& aHeight, double& aLength )
{
    aHeight = height;
    aLength = length;
}


IDF_DRILL_DATA::IDF_DRILL_DATA() :
        dia( 0.0 ),
        x( 0.0 ),
        y( 0.0 ),
        plating( NPTH ),
        kref( NOREFDES ),
        khole( MTG ),
        owner( UNOWNED )
{
}


IDF_DRILL_DATA::IDF_DRILL_DATA( double aDrillDia, double aPosX, double aPosY,
                                IDF3::KEY_PLATING aPlating, const std::string& aRefDes,
                                const std::string& aHoleType, IDF3::KEY_OWNER aOwner )
{
    if( aDrillDia < 0.3 )
        dia = 0.3;
    else
        dia = aDrillDia;

    x = aPosX;
    y = aPosY;
    plating = aPlating;

    if( !aRefDes.compare( "BOARD" ) )
    {
        kref = BOARD;
    }
    else if( aRefDes.empty() || !aRefDes.compare( "NOREFDES" ) )
    {
        kref = NOREFDES;
    }
    else if( !aRefDes.compare( "PANEL" ) )
    {
        kref = PANEL;
    }
    else
    {
        kref = REFDES;
        refdes = aRefDes;
    }

    if( !aHoleType.compare( "PIN" ) )
    {
        khole = PIN;
    }
    else if( !aHoleType.compare( "VIA" ) )
    {
        khole = VIA;
    }
    else if( aHoleType.empty() || !aHoleType.compare( "MTG" ) )
    {
        khole = MTG;
    }
    else if( !aHoleType.compare( "TOOL" ) )
    {
        khole = TOOL;
    }
    else
    {
        khole = OTHER;
        holetype = aHoleType;
    }

    owner = aOwner;
}


bool IDF_DRILL_DATA::Matches( double aDrillDia, double aPosX, double aPosY ) const
{
    double ddia = aDrillDia - dia;
    IDF_POINT p1, p2;

    p1.x = x;
    p1.y = y;
    p2.x = aPosX;
    p2.y = aPosY;

    if( ddia > -0.00001 && ddia < 0.00001 && p1.Matches( p2, 0.00001 ) )
        return true;

    return false;
}


bool IDF_DRILL_DATA::read( std::istream& aBoardFile, IDF3::IDF_UNIT aBoardUnit,
                           IDF3::FILE_STATE aBoardState, IDF3::IDF_VERSION aIdfVersion )
{
    std::string iline;      // the input line
    bool isComment;         // true if a line just read in is a comment line
    std::streampos pos;
    int idx = 0;
    bool quoted = false;
    std::string token;

    // RECORD 2: DIA, X, Y, Plating Style, REFDES, HOLE TYPE, HOLE OWNER
    while( !FetchIDFLine( aBoardFile, iline, isComment, pos ) && aBoardFile.good() );

    if( ( !aBoardFile.good() && !aBoardFile.eof() ) || iline.empty() )
    {
        throw( IDF_ERROR( __FILE__, __FUNCTION__, __LINE__,
                          "problems reading board drilled holes" ) );
    }

    if( isComment )
    {
        throw( IDF_ERROR( __FILE__, __FUNCTION__, __LINE__,
                          "invalid IDF file\n"
                          "* Violation of specification: comment within a section (DRILLED "
                          "HOLES)" ) );
    }

    idx = 0;
    GetIDFString( iline, token, quoted, idx );

    if( quoted )
    {
        throw( IDF_ERROR( __FILE__, __FUNCTION__, __LINE__,
                          "invalid IDF file\n"
                          "* Violation of specification: drill diameter must not be in quotes" ) );
    }

    if( CompareToken( ".END_DRILLED_HOLES", token ) )
        return false;

    istringstream istr;
    istr.str( token );

    istr >> dia;

    if( istr.fail() )
    {
        throw( IDF_ERROR( __FILE__, __FUNCTION__, __LINE__,
                          "invalid IDF file\n"
                          "* Violation of specification: drill diameter is not numeric" ) );
    }

    if( ( aBoardUnit == UNIT_MM && dia < IDF_MIN_DIA_MM )
        || ( aBoardUnit == UNIT_THOU && dia < IDF_MIN_DIA_THOU )
        || ( aBoardUnit == UNIT_TNM && dia < IDF_MIN_DIA_TNM ) )
    {
        ostringstream ostr;
        ostr << "invalid IDF file\n";
        ostr << "* Invalid drill diameter (too small): '" << token << "'";

        throw( IDF_ERROR( __FILE__, __FUNCTION__, __LINE__, ostr.str() ) );
    }

    if( !GetIDFString( iline, token, quoted, idx ) )
    {
        throw( IDF_ERROR( __FILE__, __FUNCTION__, __LINE__,
                          "invalid IDF file\n"
                          "* Violation of specification: missing X position for drilled hole" ) );
    }

    if( quoted )
    {
        throw( IDF_ERROR( __FILE__, __FUNCTION__, __LINE__,
                          "invalid IDF file\n"
                          "* Violation of specification: X position in DRILLED HOLES section "
                          "must not be in quotes" ) );
    }

    istr.clear();
    istr.str( token );

    istr >> x;

    if( istr.fail() )
    {
        throw( IDF_ERROR( __FILE__, __FUNCTION__, __LINE__,
                          "invalid IDF file\n"
                          "* Violation of specification: X position in DRILLED HOLES section is "
                          "not numeric" ) );
    }

    if( !GetIDFString( iline, token, quoted, idx ) )
    {
        throw( IDF_ERROR( __FILE__, __FUNCTION__, __LINE__,
                          "invalid IDF file\n"
                          "* Violation of specification: missing Y position for drilled hole" ) );
    }

    if( quoted )
    {
        throw( IDF_ERROR( __FILE__, __FUNCTION__, __LINE__,
                          "invalid IDF file\n"
                          "* Violation of specification: Y position in DRILLED HOLES section "
                          "must not be in quotes" ) );
    }

    istr.clear();
    istr.str( token );

    istr >> y;

    if( istr.fail() )
    {
        throw( IDF_ERROR( __FILE__, __FUNCTION__, __LINE__,
                          "invalid IDF file\n"
                          "* Violation of specification: Y position in DRILLED HOLES section is "
                          "not numeric" ) );
    }

    if( aIdfVersion > IDF_V2 )
    {
        if( !GetIDFString( iline, token, quoted, idx ) )
        {
            throw( IDF_ERROR( __FILE__, __FUNCTION__, __LINE__,
                              "invalid IDFv3 file\n"
                              "* Violation of specification: missing PLATING for drilled hole" ) );
        }

        if( CompareToken( "PTH", token ) )
        {
            plating = IDF3::PTH;
        }
        else if( CompareToken( "NPTH", token ) )
        {
            plating = IDF3::NPTH;
        }
        else
        {
            ostringstream ostr;
            ostr << "invalid IDFv3 file\n";
            ostr << "* Violation of specification: invalid PLATING type ('" << token << "')";

            throw( IDF_ERROR( __FILE__, __FUNCTION__, __LINE__, ostr.str() ) );
        }
    }
    else
    {
        plating = IDF3::PTH;
    }

    if( !GetIDFString( iline, token, quoted, idx ) )
    {
        if( aIdfVersion > IDF_V2 )
        {
            throw( IDF_ERROR( __FILE__, __FUNCTION__, __LINE__,
                              "invalid IDFv3 file\n"
                              "* Violation of specification: missing REFDES for drilled hole" ) );
        }
        else
        {
            throw( IDF_ERROR( __FILE__, __FUNCTION__, __LINE__,
                              "invalid IDFv2 file\n"
                              "* Violation of specification: missing HOLE TYPE for drilled "
                              "hole" ) );
        }
    }

    std::string tok1 = token;

    if( !GetIDFString( iline, token, quoted, idx ) )
    {
        if( aIdfVersion > IDF_V2 )
        {
            throw( IDF_ERROR( __FILE__, __FUNCTION__, __LINE__,
                              "invalid IDFv3 file\n"
                              "* Violation of specification: missing HOLE TYPE for drilled "
                              "hole" ) );
        }
        else
        {
            throw( IDF_ERROR( __FILE__, __FUNCTION__, __LINE__,
                              "invalid IDFv2 file\n"
                              "* Violation of specification: missing REFDES for drilled hole" ) );
        }
    }

    std::string tok2 = token;

    if( aIdfVersion > IDF_V2 )
        token = tok1;

    if( CompareToken( "BOARD", token ) )
    {
        kref = IDF3::BOARD;
    }
    else if( CompareToken( "NOREFDES", token ) )
    {
        kref = IDF3::NOREFDES;
    }
    else if( CompareToken( "PANEL", token ) )
    {
        kref = IDF3::PANEL;
    }
    else
    {
        kref = IDF3::REFDES;
        refdes = token;
    }

    if( aIdfVersion > IDF_V2 )
        token = tok2;
    else
        token = tok1;

    if( CompareToken( "PIN", token ) )
    {
        khole = IDF3::PIN;
    }
    else if( CompareToken( "VIA", token ) )
    {
        khole = IDF3::VIA;
    }
    else if( CompareToken( "MTG", token ) )
    {
        khole = IDF3::MTG;
    }
    else if( CompareToken( "TOOL", token ) )
    {
        khole = IDF3::TOOL;
    }
    else
    {
        khole = IDF3::OTHER;
        holetype = token;
    }

    if( aIdfVersion > IDF_V2 )
    {
        if( !GetIDFString( iline, token, quoted, idx ) )
        {
            throw( IDF_ERROR( __FILE__, __FUNCTION__, __LINE__,
                              "invalid IDFv3 file\n"
                              "* Violation of specification: missing OWNER for drilled hole" ) );
        }

        if( !ParseOwner( token, owner ) )
        {
            ostringstream ostr;
            ostr << "invalid IDFv3 file\n";
            ostr << "* Violation of specification: invalid OWNER for drilled hole ('" << token
                 << "')";

            throw( IDF_ERROR( __FILE__, __FUNCTION__, __LINE__, ostr.str() ) );
        }
    }
    else
    {
        owner = IDF3::UNOWNED;
    }

    if( aBoardUnit == UNIT_THOU )
    {
        dia *= IDF_THOU_TO_MM;
        x *= IDF_THOU_TO_MM;
        y *= IDF_THOU_TO_MM;
    }
    else if( ( aIdfVersion == IDF_V2 ) && ( aBoardUnit == UNIT_TNM ) )
    {
        dia *= IDF_TNM_TO_MM;
        x *= IDF_TNM_TO_MM;
        y *= IDF_TNM_TO_MM;
    }
    else if( aBoardUnit != UNIT_MM )
    {
        ostringstream ostr;
        ostr << "\n* BUG: invalid UNIT type: " << aBoardUnit;

        throw( IDF_ERROR( __FILE__, __FUNCTION__, __LINE__, ostr.str() ) );
    }

    return true;
}


void IDF_DRILL_DATA::write( std::ostream& aBoardFile, IDF3::IDF_UNIT aBoardUnit )
{
    std::string holestr;
    std::string refstr;
    std::string ownstr;
    std::string pltstr;

    switch( khole )
    {
    case PIN:   holestr = "PIN";                  break;
    case VIA:   holestr = "VIA";                  break;
    case TOOL:  holestr = "TOOL";                 break;
    case OTHER: holestr = "\"" + holetype + "\""; break;
    default:    holestr = "MTG";                  break;
    }

    switch( kref )
    {
    case BOARD:  refstr = "BOARD";              break;
    case PANEL:  refstr = "PANEL";              break;
    case REFDES: refstr = "\"" + refdes + "\""; break;
    default:     refstr = "NOREFDES";           break;
    }

    if( plating == PTH )
        pltstr = "PTH";
    else
        pltstr = "NPTH";

    switch( owner )
    {
    case MCAD: ownstr = "MCAD";    break;
    case ECAD: ownstr = "ECAD";    break;
    default:   ownstr = "UNOWNED"; break;
    }

    if( aBoardUnit == UNIT_MM )
    {
        aBoardFile << std::setiosflags( std::ios::fixed ) << std::setprecision( 3 ) << dia << " "
                   << std::setprecision( 5 ) << x << " " << y << " " << pltstr.c_str() << " "
                   << refstr.c_str() << " " << holestr.c_str() << " " << ownstr.c_str() << "\n";
    }
    else
    {
        aBoardFile << std::setiosflags( std::ios::fixed ) << std::setprecision( 1 )
                   << ( dia / IDF_THOU_TO_MM ) << " " << std::setprecision( 1 )
                   << ( x / IDF_THOU_TO_MM ) << " " << ( y / IDF_THOU_TO_MM ) << " "
                   << pltstr.c_str() << " " << refstr.c_str() << " " << holestr.c_str() << " "
                   << ownstr.c_str() << "\n";
    }
}


double IDF_DRILL_DATA::GetDrillDia() const
{
    return dia;
}


double IDF_DRILL_DATA::GetDrillXPos() const
{
    return x;
}


double IDF_DRILL_DATA::GetDrillYPos() const
{
    return y;
}


IDF3::KEY_PLATING IDF_DRILL_DATA::GetDrillPlating()
{
    return plating;
}


const std::string& IDF_DRILL_DATA::GetDrillRefDes()
{
    switch( kref )
    {
    case BOARD:  refdes = "BOARD";    break;
    case PANEL:  refdes = "PANEL";    break;
    case REFDES:                      break;
    default:     refdes = "NOREFDES"; break;
    }

    return refdes;
}


const std::string& IDF_DRILL_DATA::GetDrillHoleType()
{
    switch( khole )
    {
    case PIN:   holetype = "PIN";  break;
    case VIA:   holetype = "VIA";  break;
    case TOOL:  holetype = "TOOL"; break;
    case OTHER:                    break;
    default:    holetype = "MTG";  break;
    }

    return holetype;
}


#ifdef DEBUG_IDF
void IDF3::PrintSeg( IDF_SEGMENT* aSegment )
{
    if( aSegment->IsCircle() )
    {
        fprintf( stdout, "printSeg(): CIRCLE: C(%.3f, %.3f) P(%.3f, %.3f) rad. %.3f\n",
                 aSegment->startPoint.x, aSegment->startPoint.y, aSegment->endPoint.x,
                 aSegment->endPoint.y, aSegment->radius );
        return;
    }

    if( aSegment->angle < -MIN_ANG || aSegment->angle > MIN_ANG )
    {
        fprintf( stdout, "printSeg(): ARC: p1(%.3f, %.3f) p2(%.3f, %.3f) ang. %.3f\n",
                 aSegment->startPoint.x, aSegment->startPoint.y, aSegment->endPoint.x,
                 aSegment->endPoint.y, aSegment->angle );
        return;
    }

    fprintf( stdout, "printSeg(): LINE: p1(%.3f, %.3f) p2(%.3f, %.3f)\n", aSegment->startPoint.x,
             aSegment->startPoint.y, aSegment->endPoint.x, aSegment->endPoint.y );
}
#endif


bool IDF_POINT::Matches( const IDF_POINT& aPoint, double aRadius ) const
{
    double dx = x - aPoint.x;
    double dy = y - aPoint.y;

    double d2 = dx * dx + dy * dy;

    if( d2 <= aRadius * aRadius )
        return true;

    return false;
}


double IDF_POINT::CalcDistance( const IDF_POINT& aPoint ) const
{
    double dx   = aPoint.x - x;
    double dy   = aPoint.y - y;
    double dist = sqrt( dx * dx + dy * dy );

    return dist;
}


double IDF3::CalcAngleRad( const IDF_POINT& aStartPoint, const IDF_POINT& aEndPoint )
{
    return atan2( aEndPoint.y - aStartPoint.y, aEndPoint.x - aStartPoint.x );
}


double IDF3::CalcAngleDeg( const IDF_POINT& aStartPoint, const IDF_POINT& aEndPoint )
{
    double ang = CalcAngleRad( aStartPoint, aEndPoint );

    // round to thousandths of a degree
    int iang = int (ang / M_PI * 1800000.0);

    ang = iang / 10000.0;

    return ang;
}


void IDF3::GetOutline( std::list<IDF_SEGMENT*>& aLines, IDF_OUTLINE& aOutline )
{
    aOutline.Clear();

    // NOTE: To tell if the point order is CCW or CW,
    // sum all:  (endPoint.X[n] - startPoint.X[n])*(endPoint[n] + startPoint.Y[n])
    // If the result is >0, the direction is CW, otherwise
    // it is CCW. Note that the result cannot be 0 unless
    // we have a bounded area of 0.

    // First we find the segment with the leftmost point
    std::list<IDF_SEGMENT*>::iterator bl    = aLines.begin();
    std::list<IDF_SEGMENT*>::iterator el    = aLines.end();
    std::list<IDF_SEGMENT*>::iterator idx   = bl++;       // iterator for the object with minX

    double minx = (*idx)->GetMinX();
    double curx;

    while( bl != el )
    {
        curx = (*bl)->GetMinX();

        if( curx < minx )
        {
            minx = curx;
            idx = bl;
        }

        ++bl;
    }

    aOutline.push( *idx );

#ifdef DEBUG_IDF
    PrintSeg( *idx );
#endif

    aLines.erase( idx );

    // If the item is a circle then we're done
    if( aOutline.front()->IsCircle() )
        return;

    // Assemble the loop
    bool complete = false;  // set if loop is complete
    bool matched;           // set if a segment's end point was matched

    while( !complete )
    {
        matched = false;
        bl  = aLines.begin();
        el  = aLines.end();

        while( bl != el && !matched )
        {
            if( (*bl)->MatchesStart( aOutline.back()->endPoint ) )
            {
                if( (*bl)->IsCircle() )
                {
                    // a circle on the perimeter is pathological but we just ignore it
                    ++bl;
                }
                else
                {
                    matched = true;

#ifdef DEBUG_IDF
                    PrintSeg( *bl );
#endif

                    aOutline.push( *bl );
                    bl = aLines.erase( bl );
                }

                continue;
            }

            ++bl;
        }

        if( !matched )
        {
            // attempt to match the end points
            bl  = aLines.begin();
            el  = aLines.end();

            while( bl != el && !matched )
            {
                if( (*bl)->MatchesEnd( aOutline.back()->endPoint ) )
                {
                    if( (*bl)->IsCircle() )
                    {
                        // a circle on the perimeter is pathological but we just ignore it
                        ++bl;
                    }
                    else
                    {
                        matched = true;
                        (*bl)->SwapEnds();

#ifdef DEBUG_IDF
                        printSeg( *bl );
#endif

                        aOutline.push( *bl );
                        bl = aLines.erase( bl );
                    }

                    continue;
                }

                ++bl;
            }
        }

        if( !matched )
        {
            // still no match - attempt to close the loop
            if( (aOutline.size() > 1) || ( aOutline.front()->angle < -MIN_ANG )
                || ( aOutline.front()->angle > MIN_ANG ) )
            {
                // close the loop
                IDF_SEGMENT* seg = new IDF_SEGMENT( aOutline.back()->endPoint,
                                                    aOutline.front()->startPoint );

                if( seg )
                {
                    complete = true;

#ifdef DEBUG_IDF
                    printSeg( seg );
#endif

                    aOutline.push( seg );
                    break;
                }
            }

            // the outline is bad; drop the segments
            aOutline.Clear();

            return;
        }

        // check if the loop is complete
        if( aOutline.front()->MatchesStart( aOutline.back()->endPoint ) )
        {
            complete = true;
            break;
        }
    }
}


IDF_SEGMENT::IDF_SEGMENT() :
        angle( 0.0 ),
        offsetAngle( 0.0 ),
        radius( 0.0 )
{
}


IDF_SEGMENT::IDF_SEGMENT( const IDF_POINT& aStartPoint, const IDF_POINT& aEndPoint ) :
        startPoint( aStartPoint ),
        endPoint( aEndPoint ),
        angle( 0.0 ),
        offsetAngle( 0.0),
        radius( 0.0 )
{
}


IDF_SEGMENT::IDF_SEGMENT( const IDF_POINT& aStartPoint, const IDF_POINT& aEndPoint, double aAngle,
                          bool aFromKicad ) :
        angle( 0.0 ),
        offsetAngle( 0.0 ),
        radius( 0.0 )
{
    double diff = abs( aAngle ) - 360.0;

    if( ( diff < MIN_ANG && diff > -MIN_ANG ) || ( aAngle < MIN_ANG && aAngle > -MIN_ANG )
        || ( !aFromKicad ) )
    {
        angle = 0.0;
        startPoint = aStartPoint;
        endPoint = aEndPoint;

        if( diff < MIN_ANG && diff > -MIN_ANG )
        {
            angle = 360.0;
            center = aStartPoint;
            offsetAngle = 0.0;
            radius = aStartPoint.CalcDistance( aEndPoint );
        }
        else if( aAngle > MIN_ANG || aAngle < -MIN_ANG )
        {
            angle = aAngle;
            CalcCenterAndRadius();
        }

        return;
    }

    // we need to convert from the KiCad arc convention
    angle = aAngle;

    center = aStartPoint;

    offsetAngle = IDF3::CalcAngleDeg( aStartPoint, aEndPoint );

    radius = aStartPoint.CalcDistance( aEndPoint );

    startPoint = aEndPoint;

    double ang = offsetAngle + aAngle;
    ang = (ang / 180.0) * M_PI;

    endPoint.x  = ( radius * cos( ang ) ) + center.x;
    endPoint.y  = ( radius * sin( ang ) ) + center.y;
}


bool IDF_SEGMENT::MatchesStart( const IDF_POINT& aPoint, double aRadius )
{
    return startPoint.Matches( aPoint, aRadius );
}


bool IDF_SEGMENT::MatchesEnd( const IDF_POINT& aPoint, double aRadius )
{
    return endPoint.Matches( aPoint, aRadius );
}


void IDF_SEGMENT::CalcCenterAndRadius()
{
    // NOTE:  this routine does not check if the points are the same
    // or too close to be sensible in a production setting.

    double offAng = IDF3::CalcAngleRad( startPoint, endPoint );
    double d      = startPoint.CalcDistance( endPoint ) / 2.0;
    double xm     = ( startPoint.x + endPoint.x ) * 0.5;
    double ym     = ( startPoint.y + endPoint.y ) * 0.5;

    radius = d / sin( angle * M_PI / 360.0 );

    if( radius < 0.0 )
        radius = -radius;

    // calculate the height of the triangle with base d and hypotenuse r
    double dh2 = radius * radius - d * d;

    // this should only ever happen due to rounding errors when r == d
    if( dh2 < 0 )
        dh2 = 0;

    double h = sqrt( dh2 );

    if( angle > 0.0 )
        offAng += M_PI_2;
    else
        offAng -= M_PI_2;

    if( angle < -180.0 )
        offAng += M_PI;
    else if( angle > 180 )
        offAng -= M_PI;

    center.x = h * cos( offAng ) + xm;
    center.y = h * sin( offAng ) + ym;

    offsetAngle = IDF3::CalcAngleDeg( center, startPoint );
}


bool IDF_SEGMENT::IsCircle()
{
    double diff = abs( angle ) - 360.0;

    if( ( diff < MIN_ANG ) && ( diff > -MIN_ANG ) )
        return true;

    return false;
}


double IDF_SEGMENT::GetMinX()
{
    if( angle == 0.0 )
        return std::min( startPoint.x, endPoint.x );

    // Calculate the leftmost point of the circle or arc

    // if only everything were this easy
    if( IsCircle() )
        return center.x - radius;

    // cases:
    // 1. CCW arc: if offset + included angle >= 180 deg then
    // MinX = center.x - radius, otherwise MinX is the
    // same as for the case of a line.
    // 2. CW arc: if offset + included angle <= -180 deg then
    // MinX = center.x - radius, otherwise MinX is the
    // same as for the case of a line.

    if( angle > 0 )
    {
        // CCW case
        if( ( offsetAngle + angle ) >= 180.0 )
            return center.x - radius;
        else
            return std::min( startPoint.x, endPoint.x );
    }

    // CW case
    if( ( offsetAngle + angle ) <= -180.0 )
        return center.x - radius;

    return std::min( startPoint.x, endPoint.x );
}


void IDF_SEGMENT::SwapEnds()
{
    if( IsCircle() )
    {
        // reverse the direction
        angle = -angle;
        return;
    }

    IDF_POINT tmp = startPoint;
    startPoint = endPoint;
    endPoint = tmp;

    if( ( angle < MIN_ANG ) && ( angle > -MIN_ANG ) )
        return;         // nothing more to do

    // change the direction of the arc
    angle = -angle;

    // calculate the new offset angle
    offsetAngle = IDF3::CalcAngleDeg( center, startPoint );
}


bool IDF_OUTLINE::IsCCW()
{
    // note: when outlines are not valid, 'false' is returned
    switch( outline.size() )
    {
    case 0:
        // no outline
        return false;
        break;

    case 1:
        // circles are always reported as CCW
        if( outline.front()->IsCircle() )
            return true;
        else
            return false;

        break;

    case 2:
        // we may have a closed outline consisting of:
        // 1. arc and line, winding depends on the arc
        // 2. 2 arcs, winding depends on larger arc
        {
            double a1 = outline.front()->angle;
            double a2 = outline.back()->angle;

            if( ( a1 < -MIN_ANG || a1 > MIN_ANG ) && ( a2 < -MIN_ANG || a2 > MIN_ANG ) )
            {
                // we have 2 arcs; the winding is determined by
                // the longer cord. although the angles are in
                // degrees, there is no need to convert to radians
                // to determine the longer cord.
                if( abs( a1 * outline.front()->radius ) >= abs( a2 * outline.back()->radius ) )
                {
                    // winding depends on a1
                    if( a1 < 0.0 )
                        return false;
                    else
                        return true;
                }
                else
                {
                    if( a2 < 0.0 )
                        return false;
                    else
                        return true;
                }
            }

            // we may have a line + arc (or 2 lines)
            if( a1 < -MIN_ANG )
                return false;

            if( a1 > MIN_ANG )
                return true;

            if( a2 < -MIN_ANG )
                return false;

            if( a2 > MIN_ANG )
                return true;

            // we have 2 lines (invalid outline)
            return false;
        }
        break;

    default:
        break;
    }

    double winding = dir + ( outline.front()->startPoint.x - outline.back()->endPoint.x )
                      * ( outline.front()->startPoint.y + outline.back()->endPoint.y );

    if( winding > 0.0 )
        return false;

    return true;
}


bool IDF_OUTLINE::IsCircle()
{
    if( outline.front()->IsCircle() )
        return true;

    return false;
}


bool IDF_OUTLINE::push( IDF_SEGMENT* item )
{
    if( !outline.empty() )
    {
        if( item->IsCircle() )
        {
            // not allowed
            ERROR_IDF << "INVALID GEOMETRY\n";
            cerr << "* a circle is being added to a non-empty outline\n";
            return false;
        }
        else
        {
            if( outline.back()->IsCircle() )
            {
                // we can't add lines to a circle
                ERROR_IDF << "INVALID GEOMETRY\n";
                cerr << "* a line is being added to a circular outline\n";
                return false;
            }
            else if( !item->MatchesStart( outline.back()->endPoint ) )
            {
                // startPoint[N] != endPoint[N -1]
                ERROR_IDF << "INVALID GEOMETRY\n";
                cerr << "* disjoint segments (current start point != last end point)\n";
                cerr << "* start point: " << item->startPoint.x << ", " << item->startPoint.y
                     << "\n";
                cerr << "* end point: " << outline.back()->endPoint.x << ", "
                     << outline.back()->endPoint.y << "\n";
                return false;
            }
        }
    }

    outline.push_back( item );

    double ang = outline.back()->angle;
    double oang = outline.back()->offsetAngle;
    double radius = outline.back()->radius;

    if( ang < -MIN_ANG || ang > MIN_ANG )
    {
        // arcs require special consideration since the winding depends on
        // the arc length; the arc length is adequately represented by
        // taking 2 cords from the endpoints to the midpoint of the arc.
        oang = ( oang + ang / 2.0 ) * M_PI / 180.0;
        double midx = outline.back()->center.x + radius * cos( oang );
        double midy = outline.back()->center.y + radius * sin( oang );

        dir += ( outline.back()->endPoint.x - midx ) * ( outline.back()->endPoint.y + midy );

        dir += ( midx - outline.back()->startPoint.x ) * ( midy + outline.back()->startPoint.y );
    }
    else
    {
        dir += ( outline.back()->endPoint.x - outline.back()->startPoint.x )
               * ( outline.back()->endPoint.y + outline.back()->startPoint.y );
    }

    return true;
}
