/*
 * This program source code file is part of KiCad, a free EDA CAD application.
 *
 * Copyright (C) 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, see <http://www.gnu.org/licenses/>.
 */

#include <wx/log.h>

#include <lib_shape.h>
#include <lib_field.h>
#include <lib_pin.h>
#include <lib_text.h>
#include <macros.h>
#include <richio.h>
#include <string_utils.h>
#include <template_fieldnames.h>
#include <trace_helpers.h>
#include <wildcards_and_files_ext.h>

#include "sch_legacy_lib_plugin_cache.h"
#include "sch_legacy_plugin_helpers.h"


#define LIB_VERSION_MAJOR 2  ///< Legacy symbol library major version.
#define LIB_VERSION_MINOR 4  ///< Legacy symbol library minor version.

#define LIB_VERSION( major, minor ) ( major * 100 + minor )

/** Legacy symbol library (.lib) file header. */
#define LIBFILE_IDENT     "EESchema-LIBRARY Version"

/** Legacy symbol library document (.dcm) file header. */
#define DOCFILE_IDENT     "EESchema-DOCLIB  Version 2.0"

/**
 * Library versions 2.4 and lower use the old separate library (.lib) and
 * document (.dcm) files.  Symbol libraries after 2.4 merged the library
 * and document files into a single library file.  This macro checks if the
 * library version supports the old format.
 */
#define USE_OLD_DOC_FILE_FORMAT( major, minor )                      \
    ( LIB_VERSION( major, minor ) <= LIB_VERSION( 2, 4 ) )


SCH_LEGACY_PLUGIN_CACHE::SCH_LEGACY_PLUGIN_CACHE( const wxString& aFullPathAndFileName ) :
    SCH_LIB_PLUGIN_CACHE( aFullPathAndFileName )
{
    m_versionMajor = -1;
    m_versionMinor = -1;
}


void SCH_LEGACY_PLUGIN_CACHE::Load()
{
    if( !m_libFileName.FileExists() )
    {
        THROW_IO_ERROR( wxString::Format( _( "Library file '%s' not found." ),
                                          m_libFileName.GetFullPath() ) );
    }

    wxCHECK_RET( m_libFileName.IsAbsolute(),
                 wxString::Format( "Cannot use relative file paths in legacy plugin to "
                                   "open library '%s'.", m_libFileName.GetFullPath() ) );

    wxLogTrace( traceSchLegacyPlugin, "Loading legacy symbol file '%s'",
                m_libFileName.GetFullPath() );

    FILE_LINE_READER reader( m_libFileName.GetFullPath() );

    if( !reader.ReadLine() )
        THROW_IO_ERROR( _( "Unexpected end of file." ) );

    const char* line = reader.Line();

    if( !strCompare( "EESchema-LIBRARY Version", line, &line ) )
    {
        // Old .sym files (which are libraries with only one symbol, used to store and reuse shapes)
        // EESchema-LIB Version x.x SYMBOL. They are valid files.
        if( !strCompare( "EESchema-LIB Version", line, &line ) )
            SCH_PARSE_ERROR( "file is not a valid symbol or symbol library file", reader, line );
    }

    m_versionMajor = parseInt( reader, line, &line );

    if( *line != '.' )
        SCH_PARSE_ERROR( "invalid file version formatting in header", reader, line );

    line++;

    m_versionMinor = parseInt( reader, line, &line );

    if( m_versionMajor < 1 || m_versionMinor < 0 || m_versionMinor > 99 )
        SCH_PARSE_ERROR( "invalid file version in header", reader, line );

    // Check if this is a symbol library which is the same as a symbol library but without
    // any alias, documentation, footprint filters, etc.
    if( strCompare( "SYMBOL", line, &line ) )
    {
        // Symbol files add date and time stamp info to the header.
        m_libType = SCH_LIB_TYPE::LT_SYMBOL;

        /// @todo Probably should check for a valid date and time stamp even though it's not used.
    }
    else
    {
        m_libType = SCH_LIB_TYPE::LT_EESCHEMA;
    }

    while( reader.ReadLine() )
    {
        line = reader.Line();

        if( *line == '#' || isspace( *line ) )  // Skip comments and blank lines.
            continue;

        // Headers where only supported in older library file formats.
        if( m_libType == SCH_LIB_TYPE::LT_EESCHEMA && strCompare( "$HEADER", line ) )
            loadHeader( reader );

        if( strCompare( "DEF", line ) )
        {
            // Read one DEF/ENDDEF symbol entry from library:
            LIB_SYMBOL* symbol = LoadPart( reader, m_versionMajor, m_versionMinor, &m_symbols );

            m_symbols[ symbol->GetName() ] = symbol;
        }
    }

    SCH_LEGACY_PLUGIN_CACHE::IncrementModifyHash();

    // Remember the file modification time of library file when the
    // cache snapshot was made, so that in a networked environment we will
    // reload the cache as needed.
    m_fileModTime = GetLibModificationTime();

    if( USE_OLD_DOC_FILE_FORMAT( m_versionMajor, m_versionMinor ) )
        loadDocs();
}


void SCH_LEGACY_PLUGIN_CACHE::loadDocs()
{
    const char* line;
    wxString    text;
    wxString    aliasName;
    wxFileName  fn = m_libFileName;
    LIB_SYMBOL* symbol = nullptr;;

    fn.SetExt( LegacySymbolDocumentFileExtension );

    // Not all libraries will have a document file.
    if( !fn.FileExists() )
        return;

    if( !fn.IsFileReadable() )
    {
        THROW_IO_ERROR( wxString::Format( _( "Insufficient permissions to read library '%s'." ),
                                          fn.GetFullPath() ) );
    }

    FILE_LINE_READER reader( fn.GetFullPath() );

    line = reader.ReadLine();

    if( !line )
        THROW_IO_ERROR( _( "symbol document library file is empty" ) );

    if( !strCompare( DOCFILE_IDENT, line, &line ) )
    {
        SCH_PARSE_ERROR( "invalid document library file version formatting in header",
                         reader, line );
    }

    while( reader.ReadLine() )
    {
        line = reader.Line();

        if( *line == '#' )    // Comment line.
            continue;

        if( !strCompare( "$CMP", line, &line ) != 0 )
            SCH_PARSE_ERROR( "$CMP command expected", reader, line );

        aliasName = wxString::FromUTF8( line );
        aliasName.Trim();

        LIB_SYMBOL_MAP::iterator it = m_symbols.find( aliasName );

        if( it == m_symbols.end() )
            wxLogWarning( "Symbol '%s' not found in library:\n\n"
                          "'%s'\n\nat line %d offset %d", aliasName, fn.GetFullPath(),
                          reader.LineNumber(), (int) (line - reader.Line() ) );
        else
            symbol = it->second;

        // Read the current alias associated doc.
        // if the alias does not exist, just skip the description
        // (Can happen if a .dcm is not synchronized with the corresponding .lib file)
        while( reader.ReadLine() )
        {
            line = reader.Line();

            if( !line )
                SCH_PARSE_ERROR( "unexpected end of file", reader, line );

            if( strCompare( "$ENDCMP", line, &line ) )
                break;

            text = FROM_UTF8( line + 2 );
            // Remove spaces at eol, and eol chars:
            text = text.Trim();

            switch( line[0] )
            {
            case 'D':
                if( symbol )
                    symbol->SetDescription( text );
                break;

            case 'K':
                if( symbol )
                    symbol->SetKeyWords( text );
                break;

            case 'F':
                if( symbol )
                    symbol->GetFieldById( DATASHEET_FIELD )->SetText( text );
                break;

            case 0:
            case '\n':
            case '\r':
            case '#':
                // Empty line or commment
                break;

            default:
                SCH_PARSE_ERROR( "expected token in symbol definition", reader, line );
            }
        }
    }
}


void SCH_LEGACY_PLUGIN_CACHE::loadHeader( FILE_LINE_READER& aReader )
{
    const char* line = aReader.Line();

    wxASSERT( strCompare( "$HEADER", line, &line ) );

    while( aReader.ReadLine() )
    {
        line = (char*) aReader;

        // The time stamp saved in old library files is not used or saved in the latest
        // library file version.
        if( strCompare( "TimeStamp", line, &line ) )
            continue;
        else if( strCompare( "$ENDHEADER", line, &line ) )
            return;
    }

    SCH_PARSE_ERROR( "$ENDHEADER not found", aReader, line );
}


LIB_SYMBOL* SCH_LEGACY_PLUGIN_CACHE::LoadPart( LINE_READER& aReader, int aMajorVersion,
                                               int aMinorVersion, LIB_SYMBOL_MAP* aMap )
{
    const char* line = aReader.Line();

    while( *line == '#' )
        aReader.ReadLine();

    if( !strCompare( "DEF", line, &line ) )
        SCH_PARSE_ERROR( "invalid symbol definition", aReader, line );

    long num;
    size_t pos = 4;                               // "DEF" plus the first space.
    wxString utf8Line = wxString::FromUTF8( line );
    wxStringTokenizer tokens( utf8Line, " \r\n\t" );

    if( tokens.CountTokens() < 8 )
        SCH_PARSE_ERROR( "invalid symbol definition", aReader, line );

    // Read DEF line:
    std::unique_ptr<LIB_SYMBOL> symbol = std::make_unique<LIB_SYMBOL>( wxEmptyString );

    wxString name, prefix, tmp;

    name = tokens.GetNextToken();

    // This fixes a dubious decision to escape LIB_ID characters. Escaped LIB_IDs broke rescue
    // library look up.  Legacy LIB_IDs should not be escaped.
    if( name != UnescapeString( name ) )
        name = UnescapeString( name );

    pos += name.size() + 1;

    prefix = tokens.GetNextToken();
    pos += prefix.size() + 1;

    tmp = tokens.GetNextToken();
    pos += tmp.size() + 1;                        // NumOfPins, unused.

    tmp = tokens.GetNextToken();                  // Pin name offset.

    if( !tmp.ToLong( &num ) )
    {
        THROW_PARSE_ERROR( "invalid pin offset", aReader.GetSource(), aReader.Line(),
                           aReader.LineNumber(), pos );
    }

    pos += tmp.size() + 1;
    symbol->SetPinNameOffset( schIUScale.MilsToIU( (int)num ) );

    tmp = tokens.GetNextToken();                  // Show pin numbers.

    if( !( tmp == "Y" || tmp == "N") )
        THROW_PARSE_ERROR( "expected Y or N", aReader.GetSource(), aReader.Line(),
                           aReader.LineNumber(), pos );

    pos += tmp.size() + 1;
    symbol->SetShowPinNumbers( ( tmp == "N" ) ? false : true );

    tmp = tokens.GetNextToken();                  // Show pin names.

    if( !( tmp == "Y" || tmp == "N") )
    {
        THROW_PARSE_ERROR( "expected Y or N", aReader.GetSource(), aReader.Line(),
                           aReader.LineNumber(), pos );
    }

    pos += tmp.size() + 1;
    symbol->SetShowPinNames( ( tmp == "N" ) ? false : true );

    tmp = tokens.GetNextToken();                  // Number of units.

    if( !tmp.ToLong( &num ) )
    {
        THROW_PARSE_ERROR( "invalid unit count", aReader.GetSource(), aReader.Line(),
                           aReader.LineNumber(), pos );
    }

    pos += tmp.size() + 1;
    symbol->SetUnitCount( (int)num );

    // Ensure m_unitCount is >= 1.  Could be read as 0 in old libraries.
    if( symbol->GetUnitCount() < 1 )
        symbol->SetUnitCount( 1 );

    // Copy symbol name and prefix.

    // The root alias is added to the alias list by SetName() which is called by SetText().
    if( name.IsEmpty() )
    {
        symbol->SetName( "~" );
    }
    else if( name[0] != '~' )
    {
        symbol->SetName( name );
    }
    else
    {
        symbol->SetName( name.Right( name.Length() - 1 ) );
        symbol->GetValueField().SetVisible( false );
    }

    // Don't set the library alias, this is determined by the symbol library table.
    symbol->SetLibId( LIB_ID( wxEmptyString, symbol->GetName() ) );

    LIB_FIELD& reference = symbol->GetReferenceField();

    if( prefix == "~" )
    {
        reference.Empty();
        reference.SetVisible( false );
    }
    else
    {
        reference.SetText( prefix );
    }

    // In version 2.2 and earlier, this parameter was a '0' which was just a place holder.
    // The was no concept of interchangeable multiple unit symbols.
    if( LIB_VERSION( aMajorVersion, aMinorVersion ) > 0
     && LIB_VERSION( aMajorVersion, aMinorVersion ) <= LIB_VERSION( 2, 2 ) )
    {
        // Nothing needs to be set since the default setting for symbols with multiple
        // units were never interchangeable.  Just parse the 0 an move on.
        tmp = tokens.GetNextToken();
        pos += tmp.size() + 1;
    }
    else
    {
        tmp = tokens.GetNextToken();

        if( tmp == "L" )
            symbol->LockUnits( true );
        else if( tmp == "F" || tmp == "0" )
            symbol->LockUnits( false );
        else
            THROW_PARSE_ERROR( "expected L, F, or 0", aReader.GetSource(), aReader.Line(),
                               aReader.LineNumber(), pos );

        pos += tmp.size() + 1;
    }

    // There is the optional power symbol flag.
    if( tokens.HasMoreTokens() )
    {
        tmp = tokens.GetNextToken();

        if( tmp == "P" )
            symbol->SetPower();
        else if( tmp == "N" )
            symbol->SetNormal();
        else
            THROW_PARSE_ERROR( "expected P or N", aReader.GetSource(), aReader.Line(),
                               aReader.LineNumber(), pos );
    }

    line = aReader.ReadLine();

    // Read lines until "ENDDEF" is found.
    while( line )
    {
        if( *line == '#' )                                  // Comment
            ;
        else if( strCompare( "Ti", line, &line ) )          // Modification date is ignored.
            continue;
        else if( strCompare( "ALIAS", line, &line ) )       // Aliases
            loadAliases( symbol, aReader, aMap );
        else if( *line == 'F' )                             // Fields
            loadField( symbol, aReader );
        else if( strCompare( "DRAW", line, &line ) )        // Drawing objects.
            loadDrawEntries( symbol, aReader, aMajorVersion, aMinorVersion );
        else if( strCompare( "$FPLIST", line, &line ) )     // Footprint filter list
            loadFootprintFilters( symbol, aReader );
        else if( strCompare( "ENDDEF", line, &line ) )      // End of symbol description
        {
            return symbol.release();
        }

        line = aReader.ReadLine();
    }

    SCH_PARSE_ERROR( "missing ENDDEF", aReader, line );
}


void SCH_LEGACY_PLUGIN_CACHE::loadAliases( std::unique_ptr<LIB_SYMBOL>& aSymbol,
                                           LINE_READER&                 aReader,
                                           LIB_SYMBOL_MAP*              aMap )
{
    wxString newAliasName;
    const char* line = aReader.Line();

    wxCHECK_RET( strCompare( "ALIAS", line, &line ), "Invalid ALIAS section" );

    wxString utf8Line = wxString::FromUTF8( line );
    wxStringTokenizer tokens( utf8Line, " \r\n\t" );

    // Parse the ALIAS list.
    while( tokens.HasMoreTokens() )
    {
        newAliasName = tokens.GetNextToken();

        if( aMap )
        {
            LIB_SYMBOL* newSymbol = new LIB_SYMBOL( newAliasName );

            // Inherit the parent mandatory field attributes.
            for( int id = 0; id < MANDATORY_FIELDS; ++id )
            {
                LIB_FIELD* field = newSymbol->GetFieldById( id );

                // the MANDATORY_FIELDS are exactly that in RAM.
                wxASSERT( field );

                LIB_FIELD* parentField = aSymbol->GetFieldById( id );

                wxASSERT( parentField );

                *field = *parentField;

                if( id == VALUE_FIELD )
                    field->SetText( newAliasName );

                field->SetParent( newSymbol );
            }

            newSymbol->SetParent( aSymbol.get() );

            // This will prevent duplicate aliases.
            (*aMap)[ newSymbol->GetName() ] = newSymbol;
        }
    }
}


void SCH_LEGACY_PLUGIN_CACHE::loadField( std::unique_ptr<LIB_SYMBOL>& aSymbol,
                                         LINE_READER&                 aReader )
{
    const char* line = aReader.Line();

    wxCHECK_RET( *line == 'F', "Invalid field line" );

    wxString    text;
    int         id;

    if( sscanf( line + 1, "%d", &id ) != 1 || id < 0 )
        SCH_PARSE_ERROR( "invalid field ID", aReader, line + 1 );

    LIB_FIELD* field;

    if( id >= 0 && id < MANDATORY_FIELDS )
    {
        field = aSymbol->GetFieldById( id );

        // this will fire only if somebody broke a constructor or editor.
        // MANDATORY_FIELDS are always present in ram resident symbols, no
        // exceptions, and they always have their names set, even fixed fields.
        wxASSERT( field );
    }
    else
    {
        field = new LIB_FIELD( aSymbol.get(), id );
        aSymbol->AddDrawItem( field, false );
    }

    // Skip to the first double quote.
    while( *line != '"' && *line != 0 )
        line++;

    if( *line == 0 )
        SCH_PARSE_ERROR( _( "unexpected end of line" ), aReader, line );

    parseQuotedString( text, aReader, line, &line, true );

    // The value field needs to be "special" escaped.  The other fields are
    // escaped normally and don't need special handling
    if( id == VALUE_FIELD )
        text = EscapeString( text, CTX_QUOTED_STR );

    // Doctor the *.lib file field which has a "~" in blank fields.  New saves will
    // not save like this.
    if( text.size() == 1 && text[0] == '~' )
        field->SetText( wxEmptyString );
    else
        field->SetText( ConvertToNewOverbarNotation( text ) );

    VECTOR2I pos;

    pos.x = schIUScale.MilsToIU( parseInt( aReader, line, &line ) );
    pos.y = schIUScale.MilsToIU( parseInt( aReader, line, &line ) );
    field->SetPosition( pos );

    wxSize textSize;

    textSize.x = textSize.y = schIUScale.MilsToIU( parseInt( aReader, line, &line ) );
    field->SetTextSize( textSize );

    char textOrient = parseChar( aReader, line, &line );

    if( textOrient == 'H' )
        field->SetTextAngle( ANGLE_HORIZONTAL );
    else if( textOrient == 'V' )
        field->SetTextAngle( ANGLE_VERTICAL );
    else
        SCH_PARSE_ERROR( "invalid field text orientation parameter", aReader, line );

    char textVisible = parseChar( aReader, line, &line );

    if( textVisible == 'V' )
        field->SetVisible( true );
    else if ( textVisible == 'I' )
        field->SetVisible( false );
    else
        SCH_PARSE_ERROR( "invalid field text visibility parameter", aReader, line );

    // It may be technically correct to use the library version to determine if the field text
    // attributes are present.  If anyone knows if that is valid and what version that would be,
    // please change this to test the library version rather than an EOL or the quoted string
    // of the field name.
    if( *line != 0 && *line != '"' )
    {
        char textHJustify = parseChar( aReader, line, &line );

        if( textHJustify == 'C' )
            field->SetHorizJustify( GR_TEXT_H_ALIGN_CENTER );
        else if( textHJustify == 'L' )
            field->SetHorizJustify( GR_TEXT_H_ALIGN_LEFT );
        else if( textHJustify == 'R' )
            field->SetHorizJustify( GR_TEXT_H_ALIGN_RIGHT );
        else
            SCH_PARSE_ERROR( "invalid field text horizontal justification", aReader, line );

        wxString attributes;

        parseUnquotedString( attributes, aReader, line, &line );

        size_t attrSize = attributes.size();

        if( !(attrSize == 3 || attrSize == 1 ) )
            SCH_PARSE_ERROR( "invalid field text attributes size", aReader, line );

        switch( (wxChar) attributes[0] )
        {
        case 'C': field->SetVertJustify( GR_TEXT_V_ALIGN_CENTER ); break;
        case 'B': field->SetVertJustify( GR_TEXT_V_ALIGN_BOTTOM ); break;
        case 'T': field->SetVertJustify( GR_TEXT_V_ALIGN_TOP );    break;
        default:  SCH_PARSE_ERROR( "invalid field text vertical justification", aReader, line );
        }

        if( attrSize == 3 )
        {
            wxChar attr_1 = attributes[1];
            wxChar attr_2 = attributes[2];

            if( attr_1 == 'I' )        // Italic
                field->SetItalic( true );
            else if( attr_1 != 'N' )   // No italics is default, check for error.
                SCH_PARSE_ERROR( "invalid field text italic parameter", aReader, line );

            if ( attr_2 == 'B' )       // Bold
                field->SetBold( true );
            else if( attr_2 != 'N' )   // No bold is default, check for error.
                SCH_PARSE_ERROR( "invalid field text bold parameter", aReader, line );
        }
    }

    // Fields in RAM must always have names.
    if( id >= 0 && id < MANDATORY_FIELDS )
    {
        // Fields in RAM must always have names, because we are trying to get
        // less dependent on field ids and more dependent on names.
        // Plus assumptions are made in the field editors.
        field->m_name = TEMPLATE_FIELDNAME::GetDefaultFieldName( id );

        // Ensure the VALUE field = the symbol name (can be not the case
        // with malformed libraries: edited by hand, or converted from other tools)
        if( id == VALUE_FIELD )
            field->SetText( aSymbol->GetName() );
    }
    else
    {
        parseQuotedString( field->m_name, aReader, line, &line, true );  // Optional.
    }
}


void SCH_LEGACY_PLUGIN_CACHE::loadDrawEntries( std::unique_ptr<LIB_SYMBOL>& aSymbol,
                                               LINE_READER&                 aReader,
                                               int                          aMajorVersion,
                                               int                          aMinorVersion )
{
    const char* line = aReader.Line();

    wxCHECK_RET( strCompare( "DRAW", line, &line ), "Invalid DRAW section" );

    line = aReader.ReadLine();

    while( line )
    {
        if( strCompare( "ENDDRAW", line, &line ) )
        {
            aSymbol->GetDrawItems().sort();
            return;
        }

        switch( line[0] )
        {
        case 'A':    // Arc
            aSymbol->AddDrawItem( loadArc( aSymbol, aReader ), false );
            break;

        case 'C':    // Circle
            aSymbol->AddDrawItem( loadCircle( aSymbol, aReader ), false );
            break;

        case 'T':    // Text
            aSymbol->AddDrawItem( loadText( aSymbol, aReader, aMajorVersion, aMinorVersion ),
                                  false );
            break;

        case 'S':    // Square
            aSymbol->AddDrawItem( loadRect( aSymbol, aReader ), false );
            break;

        case 'X':    // Pin Description
            aSymbol->AddDrawItem( loadPin( aSymbol, aReader ), false );
            break;

        case 'P':    // Polyline
            aSymbol->AddDrawItem( loadPolyLine( aSymbol, aReader ), false );
            break;

        case 'B':    // Bezier Curves
            aSymbol->AddDrawItem( loadBezier( aSymbol, aReader ), false );
            break;

        case '#':    // Comment
        case '\n':   // Empty line
        case '\r':
        case 0:
            break;

        default:
            SCH_PARSE_ERROR( "undefined DRAW entry", aReader, line );
        }

        line = aReader.ReadLine();
    }

    SCH_PARSE_ERROR( "File ended prematurely loading symbol draw element.", aReader, line );
}


FILL_T SCH_LEGACY_PLUGIN_CACHE::parseFillMode( LINE_READER& aReader, const char* aLine,
                                               const char** aOutput )
{
    switch ( parseChar( aReader, aLine, aOutput ) )
    {
    case 'F': return FILL_T::FILLED_SHAPE;
    case 'f': return FILL_T::FILLED_WITH_BG_BODYCOLOR;
    case 'N': return FILL_T::NO_FILL;
    default:  SCH_PARSE_ERROR( "invalid fill type, expected f, F, or N", aReader, aLine );
    }

    // This will never be reached but quiets the compiler warnings
    return FILL_T::NO_FILL;
}


LIB_SHAPE* SCH_LEGACY_PLUGIN_CACHE::loadArc( std::unique_ptr<LIB_SYMBOL>& aSymbol,
                                             LINE_READER&                 aReader )
{
    const char* line = aReader.Line();

    wxCHECK_MSG( strCompare( "A", line, &line ), nullptr, "Invalid arc definition" );

    LIB_SHAPE* arc = new LIB_SHAPE( aSymbol.get(), SHAPE_T::ARC );

    VECTOR2I center;

    center.x = schIUScale.MilsToIU( parseInt( aReader, line, &line ) );
    center.y = schIUScale.MilsToIU( parseInt( aReader, line, &line ) );

    arc->SetPosition( center );

    (void) schIUScale.MilsToIU( parseInt( aReader, line, &line ) );

    EDA_ANGLE angle1( parseInt( aReader, line, &line ), TENTHS_OF_A_DEGREE_T );
    EDA_ANGLE angle2( parseInt( aReader, line, &line ), TENTHS_OF_A_DEGREE_T );

    arc->SetUnit( parseInt( aReader, line, &line ) );
    arc->SetConvert( parseInt( aReader, line, &line ) );

    STROKE_PARAMS stroke( schIUScale.MilsToIU( parseInt( aReader, line, &line ) ), PLOT_DASH_TYPE::SOLID );

    arc->SetStroke( stroke );

    // Old libraries (version <= 2.2) do not have always this FILL MODE param when fill mode
    // is no fill (default mode).
    if( *line != 0 )
        arc->SetFillMode( parseFillMode( aReader, line, &line ) );

    // Actual Coordinates of arc ends are read from file
    if( *line != 0 )
    {
        VECTOR2I arcStart, arcEnd;

        arcStart.x = schIUScale.MilsToIU( parseInt( aReader, line, &line ) );
        arcStart.y = schIUScale.MilsToIU( parseInt( aReader, line, &line ) );
        arcEnd.x = schIUScale.MilsToIU( parseInt( aReader, line, &line ) );
        arcEnd.y = schIUScale.MilsToIU( parseInt( aReader, line, &line ) );

        arc->SetStart( arcStart );
        arc->SetEnd( arcEnd );
    }
    // Actual Coordinates of arc ends are not read from file (old library), calculate them
    else
    {
        arc->SetArcAngleAndEnd( angle2 - angle1, true );
    }

    /*
     * Current file format stores start-mid-end and so doesn't care about winding. We
     * store start-end with an implied winding internally though.
     * This issue is only for 180 deg arcs, because 180 deg are a limit to handle arcs in
     * legacy libs.
     *
     * So a workaround is to slightly change the arc angle to
     * avoid 180 deg arc after correction
     */
    EDA_ANGLE arc_angle = arc->GetArcAngle();

    if( arc_angle == ANGLE_180 )
    {
        VECTOR2I new_center = CalcArcCenter( arc->GetStart(), arc->GetEnd(),
                              EDA_ANGLE( 179.5, DEGREES_T ) );
        arc->SetCenter( new_center );
    }

    // In legacy libraries, an arc angle is always <= 180.0 degrees
    // So if the created arc is > 180 degrees, swap arc ends to have a < 180 deg arc.
    if( arc->GetArcAngle() > ANGLE_180 )
    {
        VECTOR2I new_end = arc->GetStart();
        VECTOR2I new_start = arc->GetEnd();
        arc->SetStart( new_start );
        arc->SetEnd( new_end );
    }

    return arc;
}


LIB_SHAPE* SCH_LEGACY_PLUGIN_CACHE::loadCircle( std::unique_ptr<LIB_SYMBOL>& aSymbol,
                                                LINE_READER&                 aReader )
{
    const char* line = aReader.Line();

    wxCHECK_MSG( strCompare( "C", line, &line ), nullptr, "Invalid circle definition" );

    LIB_SHAPE* circle = new LIB_SHAPE( aSymbol.get(), SHAPE_T::CIRCLE );

    VECTOR2I center;

    center.x = schIUScale.MilsToIU( parseInt( aReader, line, &line ) );
    center.y = schIUScale.MilsToIU( parseInt( aReader, line, &line ) );

    int radius = schIUScale.MilsToIU( parseInt( aReader, line, &line ) );

    circle->SetStart( center );
    circle->SetEnd( VECTOR2I( center.x + radius, center.y ) );
    circle->SetUnit( parseInt( aReader, line, &line ) );
    circle->SetConvert( parseInt( aReader, line, &line ) );

    STROKE_PARAMS stroke( schIUScale.MilsToIU( parseInt( aReader, line, &line ) ), PLOT_DASH_TYPE::SOLID );

    circle->SetStroke( stroke );

    if( *line != 0 )
        circle->SetFillMode( parseFillMode( aReader, line, &line ) );

    return circle;
}


LIB_TEXT* SCH_LEGACY_PLUGIN_CACHE::loadText( std::unique_ptr<LIB_SYMBOL>& aSymbol,
                                             LINE_READER&                 aReader,
                                             int                          aMajorVersion,
                                             int                          aMinorVersion )
{
    const char* line = aReader.Line();

    wxCHECK_MSG( strCompare( "T", line, &line ), nullptr, "Invalid LIB_TEXT definition" );

    LIB_TEXT* text = new LIB_TEXT( aSymbol.get() );
    double    angleInTenths = parseInt( aReader, line, &line );

    text->SetTextAngle( EDA_ANGLE( angleInTenths, TENTHS_OF_A_DEGREE_T ) );

    VECTOR2I center;

    center.x = schIUScale.MilsToIU( parseInt( aReader, line, &line ) );
    center.y = schIUScale.MilsToIU( parseInt( aReader, line, &line ) );
    text->SetPosition( center );

    wxSize size;

    size.x = size.y = schIUScale.MilsToIU( parseInt( aReader, line, &line ) );
    text->SetTextSize( size );
    text->SetVisible( !parseInt( aReader, line, &line ) );
    text->SetUnit( parseInt( aReader, line, &line ) );
    text->SetConvert( parseInt( aReader, line, &line ) );

    wxString str;

    // If quoted string loading fails, load as not quoted string.
    if( *line == '"' )
    {
        parseQuotedString( str, aReader, line, &line );

        str = ConvertToNewOverbarNotation( str );
    }
    else
    {
        parseUnquotedString( str, aReader, line, &line );

        // In old libs, "spaces" are replaced by '~' in unquoted strings:
        str.Replace( "~", " " );
    }

    if( !str.IsEmpty() )
    {
        // convert two apostrophes back to double quote
        str.Replace( "''", "\"" );
    }

    text->SetText( str );

    // Here things are murky and not well defined.  At some point it appears the format
    // was changed to add text properties.  However rather than add the token to the end of
    // the text definition, it was added after the string and no mention if the file
    // verion was bumped or not so this code make break on very old symbol libraries.
    //
    // Update: apparently even in the latest version this can be different so added a test
    //         for end of line before checking for the text properties.
    if( LIB_VERSION( aMajorVersion, aMinorVersion ) > 0
             && LIB_VERSION( aMajorVersion, aMinorVersion ) > LIB_VERSION( 2, 0 )
             && !is_eol( *line ) )
    {
        if( strCompare( "Italic", line, &line ) )
            text->SetItalic( true );
        else if( !strCompare( "Normal", line, &line ) )
            SCH_PARSE_ERROR( "invalid text stype, expected 'Normal' or 'Italic'", aReader, line );

        if( parseInt( aReader, line, &line ) > 0 )
            text->SetBold( true );

        // Some old libaries version > 2.0 do not have these options for text justification:
        if( !is_eol( *line ) )
        {
            switch( parseChar( aReader, line, &line ) )
            {
            case 'L': text->SetHorizJustify( GR_TEXT_H_ALIGN_LEFT );   break;
            case 'C': text->SetHorizJustify( GR_TEXT_H_ALIGN_CENTER ); break;
            case 'R': text->SetHorizJustify( GR_TEXT_H_ALIGN_RIGHT );  break;
            default: SCH_PARSE_ERROR( "invalid horizontal text justication; expected L, C, or R",
                                      aReader, line );
            }

            switch( parseChar( aReader, line, &line ) )
            {
            case 'T': text->SetVertJustify( GR_TEXT_V_ALIGN_TOP );    break;
            case 'C': text->SetVertJustify( GR_TEXT_V_ALIGN_CENTER ); break;
            case 'B': text->SetVertJustify( GR_TEXT_V_ALIGN_BOTTOM ); break;
            default: SCH_PARSE_ERROR( "invalid vertical text justication; expected T, C, or B",
                                      aReader, line );
            }
        }
    }

    return text;
}


LIB_SHAPE* SCH_LEGACY_PLUGIN_CACHE::loadRect( std::unique_ptr<LIB_SYMBOL>& aSymbol,
                                              LINE_READER&                 aReader )
{
    const char* line = aReader.Line();

    wxCHECK_MSG( strCompare( "S", line, &line ), nullptr, "Invalid rectangle definition" );

    LIB_SHAPE* rectangle = new LIB_SHAPE( aSymbol.get(), SHAPE_T::RECT );

    VECTOR2I pos;

    pos.x = schIUScale.MilsToIU( parseInt( aReader, line, &line ) );
    pos.y = schIUScale.MilsToIU( parseInt( aReader, line, &line ) );
    rectangle->SetPosition( pos );

    VECTOR2I end;

    end.x = schIUScale.MilsToIU( parseInt( aReader, line, &line ) );
    end.y = schIUScale.MilsToIU( parseInt( aReader, line, &line ) );
    rectangle->SetEnd( end );

    rectangle->SetUnit( parseInt( aReader, line, &line ) );
    rectangle->SetConvert( parseInt( aReader, line, &line ) );

    STROKE_PARAMS stroke( schIUScale.MilsToIU( parseInt( aReader, line, &line ) ), PLOT_DASH_TYPE::SOLID );

    rectangle->SetStroke( stroke );


    if( *line != 0 )
        rectangle->SetFillMode( parseFillMode( aReader, line, &line ) );

    return rectangle;
}


LIB_PIN* SCH_LEGACY_PLUGIN_CACHE::loadPin( std::unique_ptr<LIB_SYMBOL>& aSymbol,
                                           LINE_READER&                 aReader )
{
    const char* line = aReader.Line();

    wxCHECK_MSG( strCompare( "X", line, &line ), nullptr, "Invalid LIB_PIN definition" );

    wxString name;
    wxString number;

    size_t pos = 2;                               // "X" plus ' ' space character.
    wxString tmp;
    wxString utf8Line = wxString::FromUTF8( line );
    wxStringTokenizer tokens( utf8Line, " \r\n\t" );

    if( tokens.CountTokens() < 11 )
        SCH_PARSE_ERROR( "invalid pin definition", aReader, line );

    tmp = tokens.GetNextToken();
    name = tmp;
    pos += tmp.size() + 1;

    tmp = tokens.GetNextToken();
    number = tmp ;
    pos += tmp.size() + 1;

    long num;
    VECTOR2I position;

    tmp = tokens.GetNextToken();

    if( !tmp.ToLong( &num ) )
    {
        THROW_PARSE_ERROR( "invalid pin X coordinate", aReader.GetSource(), aReader.Line(),
                           aReader.LineNumber(), pos );
    }

    pos += tmp.size() + 1;
    position.x = schIUScale.MilsToIU( (int) num );

    tmp = tokens.GetNextToken();

    if( !tmp.ToLong( &num ) )
    {
        THROW_PARSE_ERROR( "invalid pin Y coordinate", aReader.GetSource(), aReader.Line(),
                           aReader.LineNumber(), pos );
    }

    pos += tmp.size() + 1;
    position.y = schIUScale.MilsToIU( (int) num );

    tmp = tokens.GetNextToken();

    if( !tmp.ToLong( &num ) )
    {
        THROW_PARSE_ERROR( "invalid pin length", aReader.GetSource(), aReader.Line(),
                           aReader.LineNumber(), pos );
    }

    pos += tmp.size() + 1;
    int length = schIUScale.MilsToIU( (int) num );


    tmp = tokens.GetNextToken();

    if( tmp.size() > 1 )
    {
        THROW_PARSE_ERROR( "invalid pin orientation", aReader.GetSource(), aReader.Line(),
                           aReader.LineNumber(), pos );
    }

    pos += tmp.size() + 1;
    int orientation = tmp[0];

    tmp = tokens.GetNextToken();

    if( !tmp.ToLong( &num ) )
    {
        THROW_PARSE_ERROR( "invalid pin number text size", aReader.GetSource(), aReader.Line(),
                           aReader.LineNumber(), pos );
    }

    pos += tmp.size() + 1;
    int numberTextSize = schIUScale.MilsToIU( (int) num );

    tmp = tokens.GetNextToken();

    if( !tmp.ToLong( &num ) )
    {
        THROW_PARSE_ERROR( "invalid pin name text size", aReader.GetSource(), aReader.Line(),
                           aReader.LineNumber(), pos );
    }

    pos += tmp.size() + 1;
    int nameTextSize = schIUScale.MilsToIU( (int) num );

    tmp = tokens.GetNextToken();

    if( !tmp.ToLong( &num ) )
    {
        THROW_PARSE_ERROR( "invalid pin unit", aReader.GetSource(), aReader.Line(),
                           aReader.LineNumber(), pos );
    }

    pos += tmp.size() + 1;
    int unit = (int) num;

    tmp = tokens.GetNextToken();

    if( !tmp.ToLong( &num ) )
    {
        THROW_PARSE_ERROR( "invalid pin alternate body type", aReader.GetSource(), aReader.Line(),
                           aReader.LineNumber(), pos );
    }

    pos += tmp.size() + 1;
    int convert = (int) num;

    tmp = tokens.GetNextToken();

    if( tmp.size() != 1 )
    {
        THROW_PARSE_ERROR( "invalid pin type", aReader.GetSource(), aReader.Line(),
                           aReader.LineNumber(), pos );
    }

    pos += tmp.size() + 1;
    char type = tmp[0];
    ELECTRICAL_PINTYPE pinType;

    switch( type )
    {
    case 'I': pinType = ELECTRICAL_PINTYPE::PT_INPUT;         break;
    case 'O': pinType = ELECTRICAL_PINTYPE::PT_OUTPUT;        break;
    case 'B': pinType = ELECTRICAL_PINTYPE::PT_BIDI;          break;
    case 'T': pinType = ELECTRICAL_PINTYPE::PT_TRISTATE;      break;
    case 'P': pinType = ELECTRICAL_PINTYPE::PT_PASSIVE;       break;
    case 'U': pinType = ELECTRICAL_PINTYPE::PT_UNSPECIFIED;   break;
    case 'W': pinType = ELECTRICAL_PINTYPE::PT_POWER_IN;      break;
    case 'w': pinType = ELECTRICAL_PINTYPE::PT_POWER_OUT;     break;
    case 'C': pinType = ELECTRICAL_PINTYPE::PT_OPENCOLLECTOR; break;
    case 'E': pinType = ELECTRICAL_PINTYPE::PT_OPENEMITTER;   break;
    case 'N': pinType = ELECTRICAL_PINTYPE::PT_NC;            break;
    default:
        THROW_PARSE_ERROR( "unknown pin type", aReader.GetSource(), aReader.Line(),
                           aReader.LineNumber(), pos );
    }


    LIB_PIN* pin = new LIB_PIN( aSymbol.get(),
                                ConvertToNewOverbarNotation( name ),
                                ConvertToNewOverbarNotation( number ),
                                orientation,
                                pinType,
                                length,
                                nameTextSize,
                                numberTextSize,
                                convert,
                                position,
                                unit );

    // Optional
    if( tokens.HasMoreTokens() )       /* Special Symbol defined */
    {
        tmp = tokens.GetNextToken();

        enum
        {
            INVERTED        = 1 << 0,
            CLOCK           = 1 << 1,
            LOWLEVEL_IN     = 1 << 2,
            LOWLEVEL_OUT    = 1 << 3,
            FALLING_EDGE    = 1 << 4,
            NONLOGIC        = 1 << 5
        };

        int flags = 0;

        for( int j = tmp.size(); j > 0; )
        {
            switch( tmp[--j].GetValue() )
            {
            case '~': break;
            case 'N': pin->SetVisible( false ); break;
            case 'I': flags |= INVERTED;        break;
            case 'C': flags |= CLOCK;           break;
            case 'L': flags |= LOWLEVEL_IN;     break;
            case 'V': flags |= LOWLEVEL_OUT;    break;
            case 'F': flags |= FALLING_EDGE;    break;
            case 'X': flags |= NONLOGIC;        break;
            default: THROW_PARSE_ERROR( "invalid pin attribut", aReader.GetSource(),
                                        aReader.Line(), aReader.LineNumber(), pos );
            }

            pos += 1;
        }

        switch( flags )
        {
        case 0:                   pin->SetShape( GRAPHIC_PINSHAPE::LINE );               break;
        case INVERTED:            pin->SetShape( GRAPHIC_PINSHAPE::INVERTED );           break;
        case CLOCK:               pin->SetShape( GRAPHIC_PINSHAPE::CLOCK );              break;
        case INVERTED | CLOCK:    pin->SetShape( GRAPHIC_PINSHAPE::INVERTED_CLOCK );     break;
        case LOWLEVEL_IN:         pin->SetShape( GRAPHIC_PINSHAPE::INPUT_LOW );          break;
        case LOWLEVEL_IN | CLOCK: pin->SetShape( GRAPHIC_PINSHAPE::CLOCK_LOW );          break;
        case LOWLEVEL_OUT:        pin->SetShape( GRAPHIC_PINSHAPE::OUTPUT_LOW );         break;
        case FALLING_EDGE:        pin->SetShape( GRAPHIC_PINSHAPE::FALLING_EDGE_CLOCK ); break;
        case NONLOGIC:            pin->SetShape( GRAPHIC_PINSHAPE::NONLOGIC );           break;
        default:
            SCH_PARSE_ERROR( "pin attributes do not define a valid pin shape", aReader, line );
        }
    }

    return pin;
}


LIB_SHAPE* SCH_LEGACY_PLUGIN_CACHE::loadPolyLine( std::unique_ptr<LIB_SYMBOL>& aSymbol,
                                                   LINE_READER&                 aReader )
{
    const char* line = aReader.Line();

    wxCHECK_MSG( strCompare( "P", line, &line ), nullptr, "Invalid poly definition" );

    LIB_SHAPE* polyLine = new LIB_SHAPE( aSymbol.get(), SHAPE_T::POLY );

    int points = parseInt( aReader, line, &line );
    polyLine->SetUnit( parseInt( aReader, line, &line ) );
    polyLine->SetConvert( parseInt( aReader, line, &line ) );

    STROKE_PARAMS stroke( schIUScale.MilsToIU( parseInt( aReader, line, &line ) ), PLOT_DASH_TYPE::SOLID );

    polyLine->SetStroke( stroke );

    VECTOR2I pt;

    for( int i = 0; i < points; i++ )
    {
        pt.x = schIUScale.MilsToIU( parseInt( aReader, line, &line ) );
        pt.y = schIUScale.MilsToIU( parseInt( aReader, line, &line ) );
        polyLine->AddPoint( pt );
    }

    if( *line != 0 )
        polyLine->SetFillMode( parseFillMode( aReader, line, &line ) );

    return polyLine;
}


LIB_SHAPE* SCH_LEGACY_PLUGIN_CACHE::loadBezier( std::unique_ptr<LIB_SYMBOL>& aSymbol,
                                                LINE_READER&                 aReader )
{
    const char* line = aReader.Line();

    wxCHECK_MSG( strCompare( "B", line, &line ), nullptr, "Invalid Bezier definition" );

    int points = parseInt( aReader, line, &line );

    wxCHECK_MSG( points == 4, NULL, "Invalid Bezier curve definition" );

    LIB_SHAPE* bezier = new LIB_SHAPE( aSymbol.get(), SHAPE_T::BEZIER );

    bezier->SetUnit( parseInt( aReader, line, &line ) );
    bezier->SetConvert( parseInt( aReader, line, &line ) );

    STROKE_PARAMS stroke ( schIUScale.MilsToIU( parseInt( aReader, line, &line ) ), PLOT_DASH_TYPE::SOLID );

    bezier->SetStroke( stroke );

    VECTOR2I pt;

    pt.x = schIUScale.MilsToIU( parseInt( aReader, line, &line ) );
    pt.y = schIUScale.MilsToIU( parseInt( aReader, line, &line ) );
    bezier->SetStart( pt );

    pt.x = schIUScale.MilsToIU( parseInt( aReader, line, &line ) );
    pt.y = schIUScale.MilsToIU( parseInt( aReader, line, &line ) );
    bezier->SetBezierC1( pt );

    pt.x = schIUScale.MilsToIU( parseInt( aReader, line, &line ) );
    pt.y = schIUScale.MilsToIU( parseInt( aReader, line, &line ) );
    bezier->SetBezierC2( pt );

    pt.x = schIUScale.MilsToIU( parseInt( aReader, line, &line ) );
    pt.y = schIUScale.MilsToIU( parseInt( aReader, line, &line ) );
    bezier->SetEnd( pt );

    bezier->RebuildBezierToSegmentsPointsList( bezier->GetWidth() );

    if( *line != 0 )
        bezier->SetFillMode( parseFillMode( aReader, line, &line ) );

    return bezier;
}


void SCH_LEGACY_PLUGIN_CACHE::loadFootprintFilters( std::unique_ptr<LIB_SYMBOL>& aSymbol,
                                                    LINE_READER&                 aReader )
{
    const char* line = aReader.Line();

    wxCHECK_RET( strCompare( "$FPLIST", line, &line ), "Invalid footprint filter list" );

    line = aReader.ReadLine();

    wxArrayString footprintFilters;

    while( line )
    {
        if( strCompare( "$ENDFPLIST", line, &line ) )
        {
            aSymbol->SetFPFilters( footprintFilters );
            return;
        }

        wxString footprint;

        parseUnquotedString( footprint, aReader, line, &line );
        footprintFilters.Add( footprint );
        line = aReader.ReadLine();
    }

    SCH_PARSE_ERROR( "File ended prematurely while loading footprint filters.", aReader, line );
}


void SCH_LEGACY_PLUGIN_CACHE::Save( const std::optional<bool>& aOpt )
{
    wxCHECK( aOpt, /* void */ );

    bool doSaveDocFile = *aOpt;

    if( !m_isModified )
        return;

    // Write through symlinks, don't replace them
    wxFileName fn = GetRealFile();

    auto formatter = std::make_unique<FILE_OUTPUTFORMATTER>( fn.GetFullPath() );
    formatter->Print( 0, "%s %d.%d\n", LIBFILE_IDENT, LIB_VERSION_MAJOR, LIB_VERSION_MINOR );
    formatter->Print( 0, "#encoding utf-8\n");

    for( LIB_SYMBOL_MAP::iterator it = m_symbols.begin();  it != m_symbols.end();  it++ )
    {
        if( !it->second->IsRoot() )
            continue;

        SaveSymbol( it->second, *formatter.get(), &m_symbols );
    }

    formatter->Print( 0, "#\n#End Library\n" );
    formatter.reset();

    m_fileModTime = fn.GetModificationTime();
    m_isModified = false;

    if( doSaveDocFile )
        saveDocFile();
}


void SCH_LEGACY_PLUGIN_CACHE::SaveSymbol( LIB_SYMBOL* aSymbol, OUTPUTFORMATTER& aFormatter,
                                          LIB_SYMBOL_MAP* aMap )
{
    /*
     * NB:
     * Some of the rescue code still uses the legacy format as an intermediary, so we have
     * to keep this code.
     */

    wxCHECK_RET( aSymbol && aSymbol->IsRoot(), "Invalid LIB_SYMBOL pointer." );

    // LIB_ALIAS objects are deprecated but we still need to gather up the derived symbols
    // and save their names for the old file format.
    wxArrayString aliasNames;

    if( aMap )
    {
        for( auto& entry : *aMap )
        {
            LIB_SYMBOL* symbol = entry.second;

            if( symbol->IsAlias() && symbol->GetParent().lock() == aSymbol->SharedPtr() )
                aliasNames.Add( symbol->GetName() );
        }
    }

    LIB_FIELD&  value = aSymbol->GetValueField();

    // First line: it s a comment (symbol name for readers)
    aFormatter.Print( 0, "#\n# %s\n#\n", TO_UTF8( value.GetText() ) );

    // Save data
    aFormatter.Print( 0, "DEF" );
    aFormatter.Print( 0, " %s", TO_UTF8( value.GetText() ) );

    LIB_FIELD& reference = aSymbol->GetReferenceField();

    if( !reference.GetText().IsEmpty() )
        aFormatter.Print( 0, " %s", TO_UTF8( reference.GetText() ) );
    else
        aFormatter.Print( 0, " ~" );

    aFormatter.Print( 0, " %d %d %c %c %d %c %c\n",
                      0, schIUScale.IUToMils( aSymbol->GetPinNameOffset() ),
                      aSymbol->ShowPinNumbers() ? 'Y' : 'N',
                      aSymbol->ShowPinNames() ? 'Y' : 'N',
                      aSymbol->GetUnitCount(), aSymbol->UnitsLocked() ? 'L' : 'F',
                      aSymbol->IsPower() ? 'P' : 'N' );

    timestamp_t dateModified = aSymbol->GetLastModDate();

    if( dateModified != 0 )
    {
        int sec  = dateModified & 63;
        int min  = ( dateModified >> 6 ) & 63;
        int hour = ( dateModified >> 12 ) & 31;
        int day  = ( dateModified >> 17 ) & 31;
        int mon  = ( dateModified >> 22 ) & 15;
        int year = ( dateModified >> 26 ) + 1990;

        aFormatter.Print( 0, "Ti %d/%d/%d %d:%d:%d\n", year, mon, day, hour, min, sec );
    }

    std::vector<LIB_FIELD*> fields;
    aSymbol->GetFields( fields );

    // Mandatory fields:
    // may have their own save policy so there is a separate loop for them.
    // Empty fields are saved, because the user may have set visibility,
    // size and orientation
    for( int i = 0; i < MANDATORY_FIELDS; ++i )
        saveField( fields[i], aFormatter );

    // User defined fields:
    // may have their own save policy so there is a separate loop for them.
    int fieldId = MANDATORY_FIELDS;     // really wish this would go away.

    for( unsigned i = MANDATORY_FIELDS; i < fields.size(); ++i )
    {
        // There is no need to save empty fields, i.e. no reason to preserve field
        // names now that fields names come in dynamically through the template
        // fieldnames.
        if( !fields[i]->GetText().IsEmpty() )
        {
            fields[i]->SetId( fieldId++ );
            saveField( fields[i], aFormatter );
        }
    }

    // Save the alias list: a line starting by "ALIAS".
    if( !aliasNames.IsEmpty() )
    {
        aFormatter.Print( 0, "ALIAS" );

        for( unsigned i = 0; i < aliasNames.GetCount(); i++ )
            aFormatter.Print( 0, " %s", TO_UTF8( aliasNames[i] ) );

        aFormatter.Print( 0, "\n" );
    }

    wxArrayString footprints = aSymbol->GetFPFilters();

    // Write the footprint filter list
    if( footprints.GetCount() != 0 )
    {
        aFormatter.Print( 0, "$FPLIST\n" );

        for( unsigned i = 0; i < footprints.GetCount(); i++ )
            aFormatter.Print( 0, " %s\n", TO_UTF8( footprints[i] ) );

        aFormatter.Print( 0, "$ENDFPLIST\n" );
    }

    // Save graphics items (including pins)
    if( !aSymbol->GetDrawItems().empty() )
    {
        // Sort the draw items in order to editing a file editing by hand.
        aSymbol->GetDrawItems().sort();

        aFormatter.Print( 0, "DRAW\n" );

        for( LIB_ITEM& item : aSymbol->GetDrawItems() )
        {
            switch( item.Type() )
            {
            default:
            case LIB_FIELD_T:     /* Fields have already been saved above. */  break;
            case LIB_PIN_T:       savePin( (LIB_PIN* ) &item, aFormatter );    break;
            case LIB_TEXT_T:      saveText( ( LIB_TEXT* ) &item, aFormatter ); break;
            case LIB_SHAPE_T:
            {
                LIB_SHAPE& shape = static_cast<LIB_SHAPE&>( item );

                switch( shape.GetShape() )
                {
                case SHAPE_T::ARC:    saveArc( &shape, aFormatter );           break;
                case SHAPE_T::BEZIER: saveBezier( &shape, aFormatter );        break;
                case SHAPE_T::CIRCLE: saveCircle( &shape, aFormatter );        break;
                case SHAPE_T::POLY:   savePolyLine( &shape, aFormatter );      break;
                case SHAPE_T::RECT:   saveRectangle( &shape, aFormatter );     break;
                default:                                                       break;
                }
            }
            }
        }

        aFormatter.Print( 0, "ENDDRAW\n" );
    }

    aFormatter.Print( 0, "ENDDEF\n" );
}


void SCH_LEGACY_PLUGIN_CACHE::saveArc( LIB_SHAPE* aArc, OUTPUTFORMATTER& aFormatter )
{
    wxCHECK_RET( aArc && aArc->GetShape() == SHAPE_T::ARC, "Invalid ARC object." );

    EDA_ANGLE startAngle, endAngle;

    aArc->CalcArcAngles( startAngle, endAngle );
    startAngle.Normalize180();
    endAngle.Normalize180();

    aFormatter.Print( 0, "A %d %d %d %d %d %d %d %d %c %d %d %d %d\n",
                      schIUScale.IUToMils( aArc->GetPosition().x ),
                      schIUScale.IUToMils( aArc->GetPosition().y ),
                      schIUScale.IUToMils( aArc->GetRadius() ),
                      startAngle.AsTenthsOfADegree(),
                      endAngle.AsTenthsOfADegree(),
                      aArc->GetUnit(),
                      aArc->GetConvert(),
                      schIUScale.IUToMils( aArc->GetWidth() ),
                      fill_tab[ static_cast<int>( aArc->GetFillMode() ) - 1 ],
                      schIUScale.IUToMils( aArc->GetStart().x ),
                      schIUScale.IUToMils( aArc->GetStart().y ),
                      schIUScale.IUToMils( aArc->GetEnd().x ),
                      schIUScale.IUToMils( aArc->GetEnd().y ) );
}


void SCH_LEGACY_PLUGIN_CACHE::saveBezier( LIB_SHAPE* aBezier, OUTPUTFORMATTER& aFormatter )
{
    wxCHECK_RET( aBezier && aBezier->GetShape() == SHAPE_T::BEZIER, "Invalid BEZIER object." );

    aFormatter.Print( 0, "B 4 %d %d %d",
                      aBezier->GetUnit(),
                      aBezier->GetConvert(),
                      schIUScale.IUToMils( aBezier->GetWidth() ) );

    aFormatter.Print( 0, " %d %d %d %d %d %d %d %d",
                      schIUScale.IUToMils( aBezier->GetStart().x ),
                      schIUScale.IUToMils( aBezier->GetStart().y ),
                      schIUScale.IUToMils( aBezier->GetBezierC1().x ),
                      schIUScale.IUToMils( aBezier->GetBezierC1().y ),
                      schIUScale.IUToMils( aBezier->GetBezierC2().x ),
                      schIUScale.IUToMils( aBezier->GetBezierC2().y ),
                      schIUScale.IUToMils( aBezier->GetEnd().x ),
                      schIUScale.IUToMils( aBezier->GetEnd().y ) );

    aFormatter.Print( 0, " %c\n", fill_tab[ static_cast<int>( aBezier->GetFillMode() ) - 1 ] );
}


void SCH_LEGACY_PLUGIN_CACHE::saveCircle( LIB_SHAPE* aCircle, OUTPUTFORMATTER& aFormatter )
{
    wxCHECK_RET( aCircle && aCircle->GetShape() == SHAPE_T::CIRCLE, "Invalid CIRCLE object." );

    aFormatter.Print( 0, "C %d %d %d %d %d %d %c\n",
                      schIUScale.IUToMils( aCircle->GetPosition().x ),
                      schIUScale.IUToMils( aCircle->GetPosition().y ),
                      schIUScale.IUToMils( aCircle->GetRadius() ),
                      aCircle->GetUnit(),
                      aCircle->GetConvert(),
                      schIUScale.IUToMils( aCircle->GetWidth() ),
                      fill_tab[ static_cast<int>( aCircle->GetFillMode() ) - 1 ] );
}


void SCH_LEGACY_PLUGIN_CACHE::saveField( const LIB_FIELD* aField, OUTPUTFORMATTER& aFormatter )
{
    wxCHECK_RET( aField && aField->Type() == LIB_FIELD_T, "Invalid LIB_FIELD object." );

    int      hjustify, vjustify;
    int      id = aField->GetId();
    wxString text = aField->GetText();

    hjustify = 'C';

    if( aField->GetHorizJustify() == GR_TEXT_H_ALIGN_LEFT )
        hjustify = 'L';
    else if( aField->GetHorizJustify() == GR_TEXT_H_ALIGN_RIGHT )
        hjustify = 'R';

    vjustify = 'C';

    if( aField->GetVertJustify() == GR_TEXT_V_ALIGN_BOTTOM )
        vjustify = 'B';
    else if( aField->GetVertJustify() == GR_TEXT_V_ALIGN_TOP )
        vjustify = 'T';

    aFormatter.Print( 0, "F%d %s %d %d %d %c %c %c %c%c%c",
                      id,
                      EscapedUTF8( text ).c_str(),       // wraps in quotes
                      schIUScale.IUToMils( aField->GetTextPos().x ),
                      schIUScale.IUToMils( aField->GetTextPos().y ),
                      schIUScale.IUToMils( aField->GetTextWidth() ),
                      aField->GetTextAngle().IsHorizontal() ? 'H' : 'V',
                      aField->IsVisible() ? 'V' : 'I',
                      hjustify, vjustify,
                      aField->IsItalic() ? 'I' : 'N',
                      aField->IsBold() ? 'B' : 'N' );

    /* Save field name, if necessary
     * Field name is saved only if it is not the default name.
     * Just because default name depends on the language and can change from
     * a country to another
     */
    wxString defName = TEMPLATE_FIELDNAME::GetDefaultFieldName( id );

    if( id >= MANDATORY_FIELDS && !aField->m_name.IsEmpty() && aField->m_name != defName )
        aFormatter.Print( 0, " %s", EscapedUTF8( aField->m_name ).c_str() );

    aFormatter.Print( 0, "\n" );
}


void SCH_LEGACY_PLUGIN_CACHE::savePin( const LIB_PIN* aPin, OUTPUTFORMATTER& aFormatter )
{
    wxCHECK_RET( aPin && aPin->Type() == LIB_PIN_T, "Invalid LIB_PIN object." );

    int      Etype;

    switch( aPin->GetType() )
    {
    default:
    case ELECTRICAL_PINTYPE::PT_INPUT:         Etype = 'I'; break;
    case ELECTRICAL_PINTYPE::PT_OUTPUT:        Etype = 'O'; break;
    case ELECTRICAL_PINTYPE::PT_BIDI:          Etype = 'B'; break;
    case ELECTRICAL_PINTYPE::PT_TRISTATE:      Etype = 'T'; break;
    case ELECTRICAL_PINTYPE::PT_PASSIVE:       Etype = 'P'; break;
    case ELECTRICAL_PINTYPE::PT_UNSPECIFIED:   Etype = 'U'; break;
    case ELECTRICAL_PINTYPE::PT_POWER_IN:      Etype = 'W'; break;
    case ELECTRICAL_PINTYPE::PT_POWER_OUT:     Etype = 'w'; break;
    case ELECTRICAL_PINTYPE::PT_OPENCOLLECTOR: Etype = 'C'; break;
    case ELECTRICAL_PINTYPE::PT_OPENEMITTER:   Etype = 'E'; break;
    case ELECTRICAL_PINTYPE::PT_NC:            Etype = 'N'; break;
    }

    if( !aPin->GetName().IsEmpty() )
        aFormatter.Print( 0, "X %s", TO_UTF8( aPin->GetName() ) );
    else
        aFormatter.Print( 0, "X ~" );

    aFormatter.Print( 0, " %s %d %d %d %c %d %d %d %d %c",
                      aPin->GetNumber().IsEmpty() ? "~" : TO_UTF8( aPin->GetNumber() ),
                      schIUScale.IUToMils( aPin->GetPosition().x ),
                      schIUScale.IUToMils( aPin->GetPosition().y ),
                      schIUScale.IUToMils( (int) aPin->GetLength() ),
                      (int) aPin->GetOrientation(),
                      schIUScale.IUToMils( aPin->GetNumberTextSize() ),
                      schIUScale.IUToMils( aPin->GetNameTextSize() ),
                      aPin->GetUnit(),
                      aPin->GetConvert(),
                      Etype );

    if( aPin->GetShape() != GRAPHIC_PINSHAPE::LINE || !aPin->IsVisible() )
        aFormatter.Print( 0, " " );

    if( !aPin->IsVisible() )
        aFormatter.Print( 0, "N" );

    switch( aPin->GetShape() )
    {
    case GRAPHIC_PINSHAPE::LINE:                                            break;
    case GRAPHIC_PINSHAPE::INVERTED:           aFormatter.Print( 0, "I" );  break;
    case GRAPHIC_PINSHAPE::CLOCK:              aFormatter.Print( 0, "C" );  break;
    case GRAPHIC_PINSHAPE::INVERTED_CLOCK:     aFormatter.Print( 0, "IC" ); break;
    case GRAPHIC_PINSHAPE::INPUT_LOW:          aFormatter.Print( 0, "L" );  break;
    case GRAPHIC_PINSHAPE::CLOCK_LOW:          aFormatter.Print( 0, "CL" ); break;
    case GRAPHIC_PINSHAPE::OUTPUT_LOW:         aFormatter.Print( 0, "V" );  break;
    case GRAPHIC_PINSHAPE::FALLING_EDGE_CLOCK: aFormatter.Print( 0, "F" );  break;
    case GRAPHIC_PINSHAPE::NONLOGIC:           aFormatter.Print( 0, "X" );  break;
    default:                                   wxFAIL_MSG( "Invalid pin shape" );
    }

    aFormatter.Print( 0, "\n" );

    const_cast<LIB_PIN*>( aPin )->ClearFlags( IS_CHANGED );
}


void SCH_LEGACY_PLUGIN_CACHE::savePolyLine( LIB_SHAPE* aPolyLine, OUTPUTFORMATTER& aFormatter )
{
    wxCHECK_RET( aPolyLine && aPolyLine->GetShape() == SHAPE_T::POLY, "Invalid POLY object." );

    aFormatter.Print( 0, "P %d %d %d %d",
                      (int) aPolyLine->GetPolyShape().Outline( 0 ).GetPointCount(),
                      aPolyLine->GetUnit(),
                      aPolyLine->GetConvert(),
                      schIUScale.IUToMils( aPolyLine->GetWidth() ) );

    for( const VECTOR2I& pt : aPolyLine->GetPolyShape().Outline( 0 ).CPoints() )
        aFormatter.Print( 0, " %d %d", schIUScale.IUToMils( pt.x ), schIUScale.IUToMils( pt.y ) );

    aFormatter.Print( 0, " %c\n", fill_tab[ static_cast<int>( aPolyLine->GetFillMode() ) - 1 ] );
}


void SCH_LEGACY_PLUGIN_CACHE::saveRectangle( LIB_SHAPE* aRectangle, OUTPUTFORMATTER& aFormatter )
{
    wxCHECK_RET( aRectangle && aRectangle->GetShape() == SHAPE_T::RECT, "Invalid RECT object." );

    aFormatter.Print( 0, "S %d %d %d %d %d %d %d %c\n",
                      schIUScale.IUToMils( aRectangle->GetPosition().x ),
                      schIUScale.IUToMils( aRectangle->GetPosition().y ),
                      schIUScale.IUToMils( aRectangle->GetEnd().x ),
                      schIUScale.IUToMils( aRectangle->GetEnd().y ),
                      aRectangle->GetUnit(),
                      aRectangle->GetConvert(),
                      schIUScale.IUToMils( aRectangle->GetWidth() ),
                      fill_tab[ static_cast<int>( aRectangle->GetFillMode() ) - 1 ] );
}


void SCH_LEGACY_PLUGIN_CACHE::saveText( const LIB_TEXT* aText, OUTPUTFORMATTER& aFormatter )
{
    wxCHECK_RET( aText && aText->Type() == LIB_TEXT_T, "Invalid LIB_TEXT object." );

    wxString text = aText->GetText();

    if( text.Contains( wxT( " " ) ) || text.Contains( wxT( "~" ) ) || text.Contains( wxT( "\"" ) ) )
    {
        // convert double quote to similar-looking two apostrophes
        text.Replace( wxT( "\"" ), wxT( "''" ) );
        text = wxT( "\"" ) + text + wxT( "\"" );
    }

    aFormatter.Print( 0, "T %g %d %d %d %d %d %d %s",
                      (double) aText->GetTextAngle().AsTenthsOfADegree(),
                      schIUScale.IUToMils( aText->GetTextPos().x ),
                      schIUScale.IUToMils( aText->GetTextPos().y ),
                      schIUScale.IUToMils( aText->GetTextWidth() ),
                      !aText->IsVisible(),
                      aText->GetUnit(),
                      aText->GetConvert(),
                      TO_UTF8( text ) );

    aFormatter.Print( 0, " %s %d", aText->IsItalic() ? "Italic" : "Normal", aText->IsBold() );

    char hjustify = 'C';

    if( aText->GetHorizJustify() == GR_TEXT_H_ALIGN_LEFT )
        hjustify = 'L';
    else if( aText->GetHorizJustify() == GR_TEXT_H_ALIGN_RIGHT )
        hjustify = 'R';

    char vjustify = 'C';

    if( aText->GetVertJustify() == GR_TEXT_V_ALIGN_BOTTOM )
        vjustify = 'B';
    else if( aText->GetVertJustify() == GR_TEXT_V_ALIGN_TOP )
        vjustify = 'T';

    aFormatter.Print( 0, " %c %c\n", hjustify, vjustify );
}


void SCH_LEGACY_PLUGIN_CACHE::saveDocFile()
{
    /*
     * NB:
     * Some of the rescue code still uses the legacy format as an intermediary, so we have
     * to keep this code.
     */

    wxFileName fileName = m_libFileName;

    fileName.SetExt( LegacySymbolDocumentFileExtension );
    FILE_OUTPUTFORMATTER formatter( fileName.GetFullPath() );

    formatter.Print( 0, "%s\n", DOCFILE_IDENT );

    for( LIB_SYMBOL_MAP::iterator it = m_symbols.begin();  it != m_symbols.end();  ++it )
    {
        wxString description =  it->second->GetDescription();
        wxString keyWords = it->second->GetKeyWords();
        wxString docFileName = it->second->GetDatasheetField().GetText();

        if( description.IsEmpty() && keyWords.IsEmpty() && docFileName.IsEmpty() )
            continue;

        formatter.Print( 0, "#\n$CMP %s\n", TO_UTF8( it->second->GetName() ) );

        if( !description.IsEmpty() )
            formatter.Print( 0, "D %s\n", TO_UTF8( description ) );

        if( !keyWords.IsEmpty() )
            formatter.Print( 0, "K %s\n", TO_UTF8( keyWords ) );

        if( !docFileName.IsEmpty() )
            formatter.Print( 0, "F %s\n", TO_UTF8( docFileName ) );

        formatter.Print( 0, "$ENDCMP\n" );
    }

    formatter.Print( 0, "#\n#End Doc Library\n" );
}


void SCH_LEGACY_PLUGIN_CACHE::DeleteSymbol( const wxString& aSymbolName )
{
    LIB_SYMBOL_MAP::iterator it = m_symbols.find( aSymbolName );

    if( it == m_symbols.end() )
        THROW_IO_ERROR( wxString::Format( _( "library %s does not contain a symbol named %s" ),
                                          m_libFileName.GetFullName(), aSymbolName ) );

    LIB_SYMBOL* symbol = it->second;

    if( symbol->IsRoot() )
    {
        LIB_SYMBOL* rootSymbol = symbol;

        // Remove the root symbol and all its children.
        m_symbols.erase( it );

        LIB_SYMBOL_MAP::iterator it1 = m_symbols.begin();

        while( it1 != m_symbols.end() )
        {
            if( it1->second->IsAlias()
              && it1->second->GetParent().lock() == rootSymbol->SharedPtr() )
            {
                delete it1->second;
                it1 = m_symbols.erase( it1 );
            }
            else
            {
                it1++;
            }
        }

        delete rootSymbol;
    }
    else
    {
        // Just remove the alias.
        m_symbols.erase( it );
        delete symbol;
    }

    SCH_LEGACY_PLUGIN_CACHE::IncrementModifyHash();
    m_isModified = true;
}
