/*
 * This program source code file is part of KiCad, a free EDA CAD application.
 *
 * Copyright (C) 2022 Chetan Subhash Shinde<chetanshinde2001@gmail.com>
 * Copyright (C) 2023 CERN
 * Copyright (C) 2022-2023 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 3 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/>.
 */

/**
 * @brief Loads the asc file and asy files.
 */

#include <sch_io/ltspice/ltspice_schematic.h>
#include <sch_io/ltspice/sch_io_ltspice_parser.h>
#include <sch_screen.h>
#include <wx/log.h>
#include <wx/dir.h>
#include <wildcards_and_files_ext.h>
#include <sch_sheet.h>
#include <schematic.h>


void LTSPICE_SCHEMATIC::Load( SCHEMATIC* aSchematic, SCH_SHEET* aRootSheet,
                              const wxFileName& aLibraryFileName, REPORTER* aReporter )
{
    std::map<wxString, wxString> mapOfAscFiles;
    std::map<wxString, wxString> mapOfAsyFiles;

    // Library paths to search (Give highest priority to files contained in same directory)
    GetAscAndAsyFilePaths( aLibraryFileName.GetPath(), false, mapOfAscFiles, mapOfAsyFiles );

    // TODO: Custom paths go here (non-recursive)

    // Default LTspice libs
    GetAscAndAsyFilePaths( m_ltspiceDataDir.GetPathWithSep() + wxS( "sub" ), true, mapOfAscFiles,
                           mapOfAsyFiles );

    GetAscAndAsyFilePaths( m_ltspiceDataDir.GetPathWithSep() + wxS( "sym" ), true, mapOfAscFiles,
                           mapOfAsyFiles );

    m_schematic = aSchematic;

    std::queue<wxString> ascFileQueue;
    ascFileQueue.push( aLibraryFileName.GetName().Lower() );

    LTSPICE_FILE rootAscFile( ascFileQueue.front(), { 0, 0 } );

    rootAscFile.Sheet = aRootSheet;
    rootAscFile.Screen = new SCH_SCREEN();

    int parentSheetIndex = 0;

    // Asc files who are subschematic in nature
    std::vector<LTSPICE_FILE> ascFiles;

    ascFiles.push_back( rootAscFile );

    while( !ascFileQueue.empty() )
    {
        SCH_SCREEN* screen = nullptr;

        // Reading the .asc file
        wxString ascFilePath = mapOfAscFiles[ ascFileQueue.front() ];
        wxString buffer = SafeReadFile( ascFilePath, "r" );

        std::vector<LTSPICE_FILE> newSubSchematicElements = GetSchematicElements( buffer );

        alg::delete_if( newSubSchematicElements,
                        [&mapOfAscFiles]( const LTSPICE_FILE& ii )
                        {
                            return mapOfAscFiles[ii.ElementName].IsEmpty();
                        } );

        for( LTSPICE_FILE& newSubSchematicElement : newSubSchematicElements )
        {
            wxString asyName = newSubSchematicElement.ElementName;
            auto     it = mapOfAsyFiles.find( asyName );

            if( it == mapOfAsyFiles.end() )
                continue;

            wxString asyBuffer = SafeReadFile( it->second, "r" );

            if( IsAsySubsheet( asyBuffer ) )
            {
                if( !screen )
                    screen = new SCH_SCREEN( m_schematic );

                newSubSchematicElement.ParentIndex = parentSheetIndex;
                newSubSchematicElement.Screen = screen;
                newSubSchematicElement.Sheet = new SCH_SHEET();

                ascFileQueue.push( newSubSchematicElement.ElementName );
                ascFiles.push_back( newSubSchematicElement );
            }
        }

        ascFileQueue.pop();

        parentSheetIndex++;
    }

    for( unsigned int i = 0; i < ascFiles.size(); i++ )
    {
        // Reading the .asc file
        wxString buffer = SafeReadFile( mapOfAscFiles[ascFiles[i].ElementName], wxS( "r" ) );

        // Getting the keywords to read
        {
            std::vector<LTSPICE_FILE> sourceFiles = GetSchematicElements( buffer );

            m_fileCache[ wxS( "asyFiles" ) ] = ReadAsyFiles( sourceFiles, mapOfAsyFiles );
            m_fileCache[ wxS( "ascFiles" ) ][ wxS( "parentFile" ) ] = buffer;

            for( const LTSPICE_FILE& file : sourceFiles )
                wxASSERT( file.Sheet == nullptr && file.Screen == nullptr );
        }

        SCH_SHEET_PATH     curSheetPath;
        SCH_IO_LTSPICE_PARSER parser( this );

        if( i > 0 )
        {
            SCH_SHEET* curSheet = ascFiles[i].Sheet;
            std::map   tempAsyMap = ReadAsyFile( ascFiles[i], mapOfAsyFiles );
            wxString   ascFileName = ascFiles[i].ElementName;
            LT_ASC     dummyAsc;
            LT_SYMBOL  tempSymbol = SymbolBuilder( ascFileName, tempAsyMap[ascFileName], dummyAsc );
            LIB_SYMBOL tempLibSymbol( ascFiles[i].ElementName );

            parser.CreateSymbol( tempSymbol, &tempLibSymbol );

            BOX2I bbox = tempLibSymbol.GetBoundingBox();

            curSheet->SetSize( bbox.GetSize() );
            curSheet->SetPosition( parser.ToKicadCoords( ascFiles[i].Offset ) + bbox.GetOrigin() );
            curSheet->SetParent( ascFiles[ascFiles[i].ParentIndex].Sheet );

            SCH_FIELD& sheetNameField = curSheet->GetFields()[SHEETNAME];
            SCH_FIELD& fileNameSheet = curSheet->GetFields()[SHEETFILENAME];
            wxString   sheetName = wxString::Format( wxS( "%s-subsheet-%d" ),
                                                     ascFiles[i].ElementName,
                                                     i );

            sheetNameField.SetText( sheetName );
            fileNameSheet.SetText( sheetName + ".kicad_sch" );

            curSheet->SetScreen( ascFiles[i].Screen );

            curSheetPath = ascFiles[ascFiles[i].ParentIndex].SheetPath;
            curSheetPath.push_back( curSheet );

            ascFiles[i].SheetPath = curSheetPath;

            ascFiles[ascFiles[i].ParentIndex].Sheet->GetScreen()->Append( curSheet );

            curSheet->GetScreen()->SetFileName( m_schematic->Prj().GetProjectPath() + sheetName
                                                 + ".kicad_sch" );
        }
        else
        {
            SCH_SHEET* curSheet = ascFiles[i].Sheet;

            ascFiles[i].SheetPath.push_back( curSheet );
            curSheetPath = ascFiles[i].SheetPath;
        }

        std::vector<wxString> subSchematicAsyFiles;

        for( const LTSPICE_FILE& ascFile : ascFiles )
            subSchematicAsyFiles.push_back( ascFile.ElementName );

        try
        {
            std::vector<LTSPICE_SCHEMATIC::LT_ASC> lt_ascs = StructureBuilder();
            parser.Parse( &curSheetPath, lt_ascs, subSchematicAsyFiles );
        }
        catch( IO_ERROR& e )
        {
            aReporter->Report( e.What(), RPT_SEVERITY_ERROR );
        }
    }
}


void LTSPICE_SCHEMATIC::GetAscAndAsyFilePaths( const wxDir& aDir, bool aRecursive,
                                               std::map<wxString, wxString>& aMapOfAscFiles,
                                               std::map<wxString, wxString>& aMapOfAsyFiles,
                                               const wxString&               aBase )
{
    wxString filename;

    {
        bool cont = aDir.GetFirst( &filename, wxEmptyString, wxDIR_FILES | wxDIR_HIDDEN );

        while( cont )
        {
            wxFileName path( aDir.GetName(), filename );

            auto logToMap = [&]( std::map<wxString, wxString>& aMapToLogTo, const wxString& aKey )
            {
                if( aMapToLogTo.count( aKey ) )
                {
                    if( m_reporter )
                    {
                        m_reporter->Report( wxString::Format(
                                _( "File at '%s' was ignored. Using previously found "
                                   "file at '%s' instead." ),
                                path.GetFullPath(), aMapToLogTo.at( aKey ) ) );
                    }
                }
                else
                {
                    aMapToLogTo.emplace( aKey, path.GetFullPath() );
                }
            };

            wxString elementName1 = ( aBase + path.GetName() ).Lower();
            wxString elementName2 = path.GetName().Lower();
            wxString extension = path.GetExt().Lower();

            if( extension == wxS( "asc" ) )
            {
                logToMap( aMapOfAscFiles, elementName1 );

                if( !aBase.IsEmpty() )
                    logToMap( aMapOfAscFiles, elementName2 );
            }
            else if( extension == wxS( "asy" ) )
            {
                logToMap( aMapOfAsyFiles, elementName1 );

                if( !aBase.IsEmpty() )
                    logToMap( aMapOfAsyFiles, elementName2 );
            }

            cont = aDir.GetNext( &filename );
        }
    }

    if( aRecursive )
    {
        bool cont = aDir.GetFirst( &filename, wxEmptyString, wxDIR_DIRS | wxDIR_HIDDEN );

        while( cont )
        {
            wxFileName path( aDir.GetName(), filename );
            wxDir      subDir( path.GetFullPath() );

            GetAscAndAsyFilePaths( subDir, true, aMapOfAscFiles, aMapOfAsyFiles,
                                   filename + wxS( "/" ) );

            cont = aDir.GetNext( &filename );
        }
    }
}


std::map<wxString, wxString>
LTSPICE_SCHEMATIC::ReadAsyFile( const LTSPICE_FILE& aSourceFile,
                                const std::map<wxString, wxString>& aAsyFileMap )
{
    std::map<wxString, wxString> resultantMap;

    wxString fileName = aSourceFile.ElementName;

    if( aAsyFileMap.count( fileName ) )
        resultantMap[fileName] = SafeReadFile( aAsyFileMap.at( fileName ), wxS( "r" ) );

    return resultantMap;
}


std::map<wxString, wxString>
LTSPICE_SCHEMATIC::ReadAsyFiles( const std::vector<LTSPICE_FILE>& aSourceFiles,
                                 const std::map<wxString, wxString>& aAsyFileMap )
{
    std::map<wxString, wxString> resultantMap;

    for( const LTSPICE_FILE& source : aSourceFiles )
    {
        wxString fileName = source.ElementName;

        if( aAsyFileMap.count( fileName ) )
            resultantMap[fileName] = SafeReadFile( aAsyFileMap.at( fileName ), wxS( "r" ) );
    }

    return resultantMap;
}


std::vector<LTSPICE_FILE> LTSPICE_SCHEMATIC::GetSchematicElements( const wxString& aAscFile )
{
    std::vector<LTSPICE_FILE> resultantArray;
    wxArrayString             lines = wxSplit( aAscFile, '\n' );

    for( const wxString& line : lines )
    {
        wxArrayString tokens = wxSplit( line, ' ' );

        if( !tokens.IsEmpty() && tokens[0].Upper() == wxS( "SYMBOL" ) )
        {
            wxString elementName( tokens[1] );
            long     posX, posY;

            tokens[2].ToLong( &posX );
            tokens[3].ToLong( &posY );

            elementName.Replace( '\\', '/' );

            LTSPICE_FILE asyFile( elementName, VECTOR2I( (int) posX, (int) posY ) );

            resultantArray.push_back( asyFile );
        }
    }

    return resultantArray;
}


bool LTSPICE_SCHEMATIC::IsAsySubsheet( const wxString& aAsyFile )
{
    wxStringTokenizer lines( aAsyFile, "\n" );

    while( lines.HasMoreTokens() )
    {
        wxStringTokenizer parts( lines.GetNextToken(), " " );

        if( parts.GetNextToken().IsSameAs( wxS( "SYMATTR" ), false )
            && parts.GetNextToken().IsSameAs( wxS( "Prefix" ), false ) )
        {
            return false;
        }
    }

    return true;
}


int LTSPICE_SCHEMATIC::integerCheck( const wxString& aToken, int aLineNumber,
                                     const wxString& aFileName )
{
    long result;

    if( !aToken.ToLong( &result ) )
    {
        THROW_IO_ERROR( wxString::Format( _( "Expecting integer at line %d in file %s" ),
                                          aLineNumber,
                                          aFileName ) );
    }

    return (int) result;
}


VECTOR2I LTSPICE_SCHEMATIC::pointCheck( const wxString& aTokenX, const wxString& aTokenY,
                                        int aLineNumber, const wxString& aFileName )
{
    return VECTOR2I( integerCheck( aTokenX, aLineNumber, aFileName ),
                     integerCheck( aTokenY, aLineNumber, aFileName ) );
}


void LTSPICE_SCHEMATIC::tokensSizeRangeCheck( size_t aActualSize, int aExpectedMin,
                                              int aExpectedMax, int aLineNumber,
                                              const wxString& aFileName )
{
    if( (int) aActualSize < aExpectedMin )
    {
        THROW_IO_ERROR( wxString::Format( _( "Expected data missing on line %d in file %s" ),
                                          aLineNumber,
                                          aFileName ) );
    }
    else if( (int) aActualSize > aExpectedMax )
    {
        THROW_IO_ERROR( wxString::Format( _( "Extra data found on line %d in file %s" ),
                                          aLineNumber,
                                          aFileName ) );
    }
}


void LTSPICE_SCHEMATIC::aggregateAttributeValue( wxArrayString& aTokens, int aIndex )
{
    // Merges a value which is across multiple tokens into one token with spaces in between.
    for( int i = aIndex + 1; i < (int) aTokens.GetCount(); i++ )
        aTokens[ aIndex ] += " " + aTokens[i];
}


LTSPICE_SCHEMATIC::LINESTYLE LTSPICE_SCHEMATIC::getLineStyle( int aValue )
{
    std::map<int, LINESTYLE> lineStyleMap;

    lineStyleMap[0] = LINESTYLE::SOLID;
    lineStyleMap[1] = LINESTYLE::DASH;
    lineStyleMap[2] = LINESTYLE::DOT;
    lineStyleMap[3] = LINESTYLE::DASHDOT;
    lineStyleMap[4] = LINESTYLE::DASHDOTDOT;

    if( lineStyleMap.find( aValue ) == lineStyleMap.end() )
        THROW_IO_ERROR( _( "Expecting 0, 1, 2, 3 or 4" ) );

    return lineStyleMap[ aValue ];
}


LTSPICE_SCHEMATIC::LINEWIDTH LTSPICE_SCHEMATIC::getLineWidth( const wxString& aValue )
{
    std::map<wxString, LINEWIDTH> lineWidthMap;

    lineWidthMap["NORMAL"] = LINEWIDTH::Normal;
    lineWidthMap["WIDE"] = LINEWIDTH::Wide;

    if( lineWidthMap.find( aValue.Upper() ) == lineWidthMap.end() )
        THROW_IO_ERROR( _( "Expecting NORMAL or WIDE" ) );

    return lineWidthMap[ aValue.Upper() ];
}


LTSPICE_SCHEMATIC::POLARITY LTSPICE_SCHEMATIC::getPolarity( const wxString& aValue )
{
    std::map<wxString, POLARITY> polarityMap;

    polarityMap["I"] = POLARITY::INPUT;
    polarityMap["O"] = POLARITY::OUTPUT;
    polarityMap["B"] = POLARITY::BIDIR;
    polarityMap["IN"] = POLARITY::INPUT;
    polarityMap["OUT"] = POLARITY::OUTPUT;
    polarityMap["BIDIR"] = POLARITY::BIDIR;

    if( polarityMap.find( aValue.Upper() ) == polarityMap.end() )
        THROW_IO_ERROR( _( "Expecting I, O, B, IN, OUT or BIDIR" ) );

    return polarityMap[ aValue.Upper() ];
}


LTSPICE_SCHEMATIC::ORIENTATION LTSPICE_SCHEMATIC::getSymbolRotationOrMirror( const wxString& aValue )
{
    std::map<wxString, ORIENTATION> rotationMirrorMap;

    rotationMirrorMap["R0"] = ORIENTATION::R0;
    rotationMirrorMap["R90"] = ORIENTATION::R90;
    rotationMirrorMap["R180"] = ORIENTATION::R180;
    rotationMirrorMap["R270"] = ORIENTATION::R270;

    rotationMirrorMap["M0"] = ORIENTATION::M0;
    rotationMirrorMap["M90"] = ORIENTATION::M90;
    rotationMirrorMap["M180"] = ORIENTATION::M180;
    rotationMirrorMap["M270"] = ORIENTATION::M270;

    if( rotationMirrorMap.find( aValue.Upper() ) == rotationMirrorMap.end() )
        THROW_IO_ERROR( _( "Expecting R0, R90, R18, R270, M0, M90, M180 or M270" ) );

    return rotationMirrorMap[ aValue.Upper() ];
}


LTSPICE_SCHEMATIC::JUSTIFICATION LTSPICE_SCHEMATIC::getTextJustification( const wxString& aValue )
{
    std::map<wxString, JUSTIFICATION> justificationMap;

    justificationMap["LEFT"] = JUSTIFICATION::LEFT;
    justificationMap["CENTER"] = JUSTIFICATION::CENTER;
    justificationMap["RIGHT"] = JUSTIFICATION::RIGHT;
    justificationMap["VLEFT"] = JUSTIFICATION::VLEFT;
    justificationMap["VRIGHT"] = JUSTIFICATION::VRIGHT;
    justificationMap["VCENTER"] = JUSTIFICATION::VCENTER;
    justificationMap["BOTTOM"] = JUSTIFICATION::BOTTOM;
    justificationMap["TOP"] = JUSTIFICATION::TOP;
    justificationMap["VBOTTOM"] = JUSTIFICATION::VBOTTOM;
    justificationMap["VTOP"] = JUSTIFICATION::VTOP;

    if( justificationMap.find( aValue.Upper() ) == justificationMap.end() )
        THROW_IO_ERROR( _( "Expecting LEFT, CENTER, RIGHT, TOP, BOTTOM, VLEFT, VRIGHT, VCENTER, VTOP or VBOTTOM" ) );

    return justificationMap[ aValue.Upper() ];
}


LTSPICE_SCHEMATIC::JUSTIFICATION LTSPICE_SCHEMATIC::getPinJustification( const wxString& aValue )
{
    std::map<wxString, JUSTIFICATION> pinJustificationMap;

    pinJustificationMap["BOTTOM"] = JUSTIFICATION::BOTTOM;
    pinJustificationMap["NONE"] = JUSTIFICATION::NONE;
    pinJustificationMap["LEFT"] = JUSTIFICATION::LEFT;
    pinJustificationMap["RIGHT"] = JUSTIFICATION::RIGHT;
    pinJustificationMap["TOP"] = JUSTIFICATION::TOP;
    pinJustificationMap["VBOTTOM"] = JUSTIFICATION::VBOTTOM;
    pinJustificationMap["VLEFT"] = JUSTIFICATION::VLEFT;
    pinJustificationMap["VRIGHT"] = JUSTIFICATION::VRIGHT;
    pinJustificationMap["VCENTER"] = JUSTIFICATION::VCENTER;
    pinJustificationMap["VTOP"] = JUSTIFICATION::VTOP;

    if( pinJustificationMap.find( aValue.Upper() ) == pinJustificationMap.end() )
        THROW_IO_ERROR( _( "Expecting NONE, BOTTOM, TOP, LEFT, RIGHT, VBOTTOM, VTOP, VCENTER, VLEFT or VRIGHT" ) );

    return pinJustificationMap[ aValue.Upper() ];
}


LTSPICE_SCHEMATIC::SYMBOLTYPE LTSPICE_SCHEMATIC::getSymbolType( const wxString& aValue )
{
    std::map<wxString, SYMBOLTYPE> symbolTypeMap;

    symbolTypeMap["CELL"] = SYMBOLTYPE::CELL;
    symbolTypeMap["BLOCK"] = SYMBOLTYPE::BLOCK;

    if( symbolTypeMap.find( aValue.Upper() ) == symbolTypeMap.end() )
        THROW_IO_ERROR( _( "Expecting CELL or BLOCK" ) );

    return symbolTypeMap[ aValue.Upper() ];
}


void LTSPICE_SCHEMATIC::removeCarriageReturn( wxString& elementFromLine )
{
    if( elementFromLine.EndsWith( '\r' ) )
        elementFromLine = elementFromLine.BeforeLast( '\r' );
}


LTSPICE_SCHEMATIC::LT_SYMBOL LTSPICE_SCHEMATIC::SymbolBuilder( const wxString& aAscFileName,
                                                               LT_ASC& aAscFile )
{
    const std::map<wxString, wxString>& asyFiles = m_fileCache[ wxS( "asyFiles" ) ];

    if( !asyFiles.count( aAscFileName.Lower() ) )
        THROW_IO_ERROR( wxString::Format( _( "Symbol '%s.asy' not found" ), aAscFileName ) );

    return SymbolBuilder( aAscFileName, asyFiles.at( aAscFileName.Lower() ), aAscFile );
}

LTSPICE_SCHEMATIC::LT_SYMBOL LTSPICE_SCHEMATIC::SymbolBuilder( const wxString& aAscFileName,
                                                               const wxString& aAsyFileContent,
                                                               LT_ASC& aAscFile )
{
    LT_SYMBOL lt_symbol;
    int       lineNumber = 1;

    lt_symbol.Name = aAscFileName;
    lt_symbol.SymbolType = LTSPICE_SCHEMATIC::SYMBOLTYPE::CELL;
    lt_symbol.SymbolOrientation = LTSPICE_SCHEMATIC::ORIENTATION::R0;

    for( wxString line : wxSplit( aAsyFileContent, '\n' ) )
    {
        removeCarriageReturn( line );

        wxArrayString tokens = wxSplit( line, ' ' );

        if( tokens.IsEmpty() )
            continue;

        wxString element = tokens[0].Upper();

        if( element == "LINE" )
        {
            tokensSizeRangeCheck( tokens.size(), 6, 7, lineNumber, aAscFileName );

            wxString lineWidth = tokens[1];
            wxString startPointX = tokens[2];
            wxString startPointY = tokens[3];
            wxString endPointX = tokens[4];
            wxString endPointY = tokens[5];

            LINE lt_line;
            lt_line.LineWidth = getLineWidth( lineWidth );
            lt_line.Start = pointCheck( startPointX, startPointY, lineNumber, aAscFileName );
            lt_line.End = pointCheck( endPointX, endPointY, lineNumber, aAscFileName );

            int lineStyleNumber = 0;  // default

            if( tokens.size() == 7 )
            {
                wxString lineStyle = tokens[6];
                lineStyleNumber = integerCheck( lineStyle, lineNumber, aAscFileName );
            }

            lt_line.LineStyle = getLineStyle( lineStyleNumber );

            lt_symbol.Lines.push_back( lt_line );
        }
        else if( element == "RECTANGLE" )
        {
            tokensSizeRangeCheck( tokens.size(), 6, 7, lineNumber, aAscFileName );

            wxString lineWidth = tokens[1];
            wxString botRightX = tokens[2];
            wxString botRightY = tokens[3];
            wxString topLeftX = tokens[4];
            wxString topRightY = tokens[5];

            RECTANGLE rect;
            rect.LineWidth = getLineWidth( lineWidth );
            rect.BotRight = pointCheck( botRightX, botRightY, lineNumber, aAscFileName );
            rect.TopLeft = pointCheck( topLeftX, topRightY, lineNumber, aAscFileName );

            int lineStyleNumber = 0;  // default

            if( tokens.size() == 7 )
            {
                wxString lineStyle = tokens[6];
                lineStyleNumber = integerCheck( lineStyle, lineNumber, aAscFileName );
            }

            rect.LineStyle = getLineStyle( lineStyleNumber );

            lt_symbol.Rectangles.push_back( rect );
        }
        else if( element == "CIRCLE" )
        {
            /**
             *  The Circle is enclosed in the square which is represented by bottomRight and
             *  topLeft coordinates.
             */
            tokensSizeRangeCheck( tokens.size(), 6, 7, lineNumber, aAscFileName );

            wxString lineWidth = tokens[1];
            wxString botRightX = tokens[2];
            wxString botRightY = tokens[3];
            wxString topLeftX = tokens[4];
            wxString topRightY = tokens[5];

            CIRCLE circle;
            circle.LineWidth = getLineWidth( lineWidth );
            circle.BotRight = pointCheck( botRightX, botRightY, lineNumber, aAscFileName );
            circle.TopLeft = pointCheck( topLeftX, topRightY, lineNumber, aAscFileName );

            int lineStyleNumber = 0;  // default

            if( tokens.size() == 7 )
            {
                wxString lineStyle = tokens[6];
                lineStyleNumber = integerCheck( lineStyle, lineNumber, aAscFileName );
            }

            circle.LineStyle = getLineStyle( lineStyleNumber );

            lt_symbol.Circles.push_back( circle );
        }
        else if( element == "ARC" )
        {
            /**
             *  The Arc is enclosed in the square given by above coordinates and its start and end
             *  coordinates are given.
             *  The arc is drawn counterclockwise from the starting point to the ending point.
             */
            tokensSizeRangeCheck( tokens.size(), 10, 11, lineNumber, aAscFileName );

            wxString lineWidth = tokens[1];
            wxString botRightX = tokens[2];
            wxString botRightY = tokens[3];
            wxString topLeftX = tokens[4];
            wxString topRightY = tokens[5];
            wxString arcStartPointX = tokens[6];
            wxString arcStartPointY = tokens[7];
            wxString arcEndPointX = tokens[8];
            wxString arcEndPointY = tokens[9];

            ARC arc;
            arc.LineWidth = getLineWidth( lineWidth );
            arc.BotRight = pointCheck( botRightX, botRightY, lineNumber, aAscFileName );
            arc.TopLeft = pointCheck( topLeftX, topRightY, lineNumber, aAscFileName );
            arc.ArcStart = pointCheck( arcStartPointX, arcStartPointY, lineNumber, aAscFileName );
            arc.ArcEnd = pointCheck( arcEndPointX, arcEndPointY, lineNumber, aAscFileName );

            int lineStyleNumber = 0;  // default

            if( tokens.size() == 11 )
            {
                wxString lineStyle = tokens[10];
                lineStyleNumber = integerCheck( lineStyle, lineNumber, aAscFileName );
            }

            arc.LineStyle = getLineStyle( lineStyleNumber );

            lt_symbol.Arcs.push_back( arc );
        }
        else if( element == "WINDOW" )
        {
            tokensSizeRangeCheck( tokens.size(), 6, 6, lineNumber, aAscFileName );

            wxString number = tokens[1];
            wxString windowPosX = tokens[2];
            wxString windowPosY = tokens[3];
            wxString justification = tokens[4];
            wxString fontSize = tokens[5];

            LT_WINDOW window;
            window.WindowNumber = integerCheck( number, lineNumber, aAscFileName );
            window.Position = pointCheck( windowPosX, windowPosY, lineNumber, aAscFileName );
            window.Justification = getTextJustification( justification );
            window.FontSize = integerCheck( fontSize, lineNumber, aAscFileName );

            // LTSpice appears to ignore hidden property from .asy files
            if( window.FontSize == 0 )
                window.FontSize = 2;

            lt_symbol.Windows.push_back( window );
        }
        else if( element == "SYMATTR" )
        {
            aggregateAttributeValue( tokens, 2 );

            tokensSizeRangeCheck( tokens.size(), 3, INT_MAX, lineNumber, aAscFileName );

            wxString key = tokens[1];
            wxString value = tokens[2];

            if( value == wxS( "\"\"" ) )
                value = wxEmptyString;

            lt_symbol.SymAttributes[ key.Upper() ] = value;
        }
        else if( element == "PIN" )
        {
            tokensSizeRangeCheck( tokens.size(), 5, 5, lineNumber, aAscFileName );

            wxString pinLocationX = tokens[1];
            wxString pinLocationY = tokens[2];
            wxString Justification = tokens[3];
            wxString nameOffSet = tokens[4];

            LT_PIN pin;
            pin.PinLocation = pointCheck( pinLocationX, pinLocationY, lineNumber,aAscFileName );
            pin.PinJustification = getPinJustification( Justification );
            pin.NameOffSet = integerCheck( nameOffSet, lineNumber, aAscFileName );

            lt_symbol.Pins.push_back( pin );
        }
        else if( element == "PINATTR" )
        {
            aggregateAttributeValue( tokens, 2 );

            tokensSizeRangeCheck( tokens.size(), 3, INT_MAX, lineNumber, aAscFileName );

            wxString name = tokens[1];
            wxString Value = tokens[2];

            lt_symbol.Pins.back().PinAttribute.insert( { name, Value } );
        }
        else if( element == "SYMBOLTYPE" )
        {
            tokensSizeRangeCheck( tokens.size(), 2, 2, lineNumber, aAscFileName );

            wxString symbolType = tokens[1];

            lt_symbol.SymbolType = getSymbolType( symbolType );
        }

        lineNumber++;
    }

    return lt_symbol;
}


std::vector<LTSPICE_SCHEMATIC::LT_ASC> LTSPICE_SCHEMATIC::StructureBuilder()
{
    // Initialising Symbol Struct

    std::vector<LT_ASC> ascFiles;

    for( const auto& [ fileName, contents ] : m_fileCache[ wxS( "ascFiles" ) ] )
    {
        LT_ASC                 ascFile;
        std::vector<LT_SYMBOL> symbolArray;

        ascFile.SheetSize = VECTOR2I( 0, 0 );
        ascFile.Symbols = symbolArray;
        ascFile.Version = 0;
        ascFile.SheetNumber = 0;

        int lineNumber = 1;

        for( wxString line : wxSplit( contents, '\n' ) )
        {
            removeCarriageReturn( line );

            wxArrayString tokens = wxSplit( line, ' ' );

            if( tokens.IsEmpty() )
                continue;

            wxString element = tokens[0].Upper();

            if( element == "SHEET" )
            {
                tokensSizeRangeCheck( tokens.size(), 4, 4, lineNumber, fileName );

                wxString sheetNumber = tokens[1];
                wxString sheetWidth = tokens[2];
                wxString sheetHeight = tokens[3];

                ascFile.SheetNumber = integerCheck( sheetNumber, lineNumber, fileName );
                ascFile.SheetSize = pointCheck( sheetWidth, sheetHeight, lineNumber, fileName );
            }
            else if( element == "SYMBOL" )
            {
                tokensSizeRangeCheck( tokens.size(), 5, 5, lineNumber, fileName );

                wxString symbolName = tokens[1];
                wxString posX = tokens[2];
                wxString posY = tokens[3];
                wxString rotate_mirror_option = tokens[4];

                symbolName.Replace( '\\', '/' );

                LT_SYMBOL lt_symbol = SymbolBuilder( symbolName, ascFile );
                lt_symbol.Offset = pointCheck( posX, posY, lineNumber, fileName );
                lt_symbol.SymbolOrientation = getSymbolRotationOrMirror( rotate_mirror_option );

                ascFile.Symbols.push_back( lt_symbol );
                ascFile.BoundingBox.Merge( lt_symbol.Offset );
            }
            else if( element == "WIRE" )
            {
                tokensSizeRangeCheck( tokens.size(), 5, 5, lineNumber, fileName );

                wxString startPointX = tokens[1];
                wxString startPointY = tokens[2];
                wxString endPointX = tokens[3];
                wxString endPointY = tokens[4];

                WIRE wire;
                wire.Start = pointCheck( startPointX, startPointY, lineNumber, fileName );
                wire.End = pointCheck( endPointX, endPointY, lineNumber, fileName );

                ascFile.Wires.push_back( wire );
                ascFile.BoundingBox.Merge( wire.Start );
                ascFile.BoundingBox.Merge( wire.End );
            }
            else if( element == "FLAG" )
            {
                tokensSizeRangeCheck( tokens.size(), 4, 4, lineNumber, fileName );

                wxString posX = tokens[1];
                wxString posY = tokens[2];
                wxString name = tokens[3];

                FLAG flag;
                flag.Offset = pointCheck( posX, posY, lineNumber, fileName );
                flag.Value = name;
                flag.FontSize = 2;

                ascFile.Flags.push_back( flag );
                ascFile.BoundingBox.Merge( flag.Offset );
            }
            else if( element == "DATAFLAG" )
            {
                tokensSizeRangeCheck( tokens.size(), 4, 4, lineNumber, fileName );

                wxString posX = tokens[1];
                wxString posY = tokens[2];
                wxString expression = tokens[3];

                DATAFLAG flag;
                flag.Offset = pointCheck( posX, posY, lineNumber, fileName );
                flag.Expression = expression;
                flag.FontSize = 2;

                ascFile.DataFlags.push_back( flag );
                ascFile.BoundingBox.Merge( flag.Offset );
            }
            else if( element == "WINDOW" )
            {
                tokensSizeRangeCheck( tokens.size(), 6, 6, lineNumber, fileName );

                wxString number = tokens[1];
                wxString windowPosX = tokens[2];
                wxString windowPosY = tokens[3];
                wxString justification = tokens[4];
                wxString fontSize = tokens[5];

                int        windowNumber = integerCheck( number, lineNumber, fileName );
                LT_WINDOW* window = nullptr;

                // Overwrite an existing window from the symbol definition
                for( LT_WINDOW& candidate : ascFile.Symbols.back().Windows )
                {
                    if( candidate.WindowNumber == windowNumber )
                    {
                        window = &candidate;
                        break;
                    }
                }

                if( !window )
                {
                    ascFile.Symbols.back().Windows.emplace_back( LT_WINDOW() );
                    window = &ascFile.Symbols.back().Windows.back();
                }

                window->WindowNumber = windowNumber;
                window->Position = pointCheck( windowPosX, windowPosY, lineNumber, fileName );
                window->Justification = getTextJustification( justification );
                window->FontSize = integerCheck( fontSize, lineNumber, fileName );

                ascFile.BoundingBox.Merge( window->Position );
            }
            else if( element == "SYMATTR" )
            {
                tokensSizeRangeCheck( tokens.size(), 3, INT_MAX, lineNumber, fileName );

                aggregateAttributeValue( tokens, 2 );

                wxString name = tokens[1];
                wxString value = tokens[2];

                if( value == wxS( "\"\"" ) )
                    value = wxEmptyString;

                ascFile.Symbols.back().SymAttributes[ name.Upper() ] = value;
            }
            else if( element == "LINE" )
            {
                tokensSizeRangeCheck( tokens.size(), 6, 7, lineNumber, fileName );

                wxString lineWidth = tokens[1];
                wxString startPointX = tokens[2];
                wxString startPointY = tokens[3];
                wxString endPointX = tokens[4];
                wxString endPointY = tokens[5];

                LINE lt_line;
                lt_line.LineWidth = getLineWidth( lineWidth );
                lt_line.Start = pointCheck( startPointX, startPointY, lineNumber, fileName );
                lt_line.End = pointCheck( endPointX, endPointY, lineNumber, fileName );

                int lineStyleNumber = 0;  // default

                if( tokens.size() == 7 )
                {
                    wxString lineStyle = tokens[6];
                    lineStyleNumber = integerCheck( lineStyle, lineNumber, fileName );
                }

                lt_line.LineStyle = getLineStyle( lineStyleNumber );

                ascFile.Lines.push_back( lt_line );
                ascFile.BoundingBox.Merge( lt_line.Start );
                ascFile.BoundingBox.Merge( lt_line.End );
            }
            else if( element == "RECTANGLE" )
            {
                tokensSizeRangeCheck( tokens.size(), 6, 7, lineNumber, fileName );

                wxString lineWidth = tokens[1];
                wxString botRightX = tokens[2];
                wxString botRightY = tokens[3];
                wxString topLeftX = tokens[4];
                wxString topRightY = tokens[5];

                RECTANGLE rect;
                rect.LineWidth = getLineWidth( tokens[1] );
                rect.BotRight = pointCheck( botRightX, botRightY, lineNumber, fileName );
                rect.TopLeft = pointCheck( topLeftX, topRightY, lineNumber, fileName );

                int lineStyleNumber = 0;  // default

                if( tokens.size() == 7 )
                {
                    wxString lineStyle = tokens[6];
                    lineStyleNumber = integerCheck( lineStyle, lineNumber, fileName );
                }

                rect.LineStyle = getLineStyle( lineStyleNumber );

                ascFile.Rectangles.push_back( rect );
                ascFile.BoundingBox.Merge( rect.TopLeft );
                ascFile.BoundingBox.Merge( rect.BotRight );
            }
            else if( element == "CIRCLE" )
            {
                tokensSizeRangeCheck( tokens.size(), 6, 7, lineNumber, fileName );

                wxString lineWidth = tokens[1];
                wxString botRightX = tokens[2];
                wxString botRightY = tokens[3];
                wxString topLeftX = tokens[4];
                wxString topRightY = tokens[5];

                CIRCLE circle;
                circle.LineWidth = getLineWidth( lineWidth );
                circle.BotRight = pointCheck( botRightX, botRightY, lineNumber, fileName );
                circle.TopLeft = pointCheck( topLeftX, topRightY, lineNumber, fileName );

                int lineStyleNumber = 0;  // default

                if( tokens.size() == 7 )
                {
                    wxString lineStyle = tokens[6];
                    lineStyleNumber = integerCheck( lineStyle, lineNumber, fileName );
                }

                circle.LineStyle = getLineStyle( lineStyleNumber );

                ascFile.Circles.push_back( circle );
                ascFile.BoundingBox.Merge( circle.TopLeft );
                ascFile.BoundingBox.Merge( circle.BotRight );
            }
            else if( element == "ARC" )
            {
                /**
                 *  The Arc is enclosed in the square given by above coordinates and its start and
                 *  end coordinates are given.
                 *  The arc is drawn counterclockwise from the starting point to the ending point.
                 */
                tokensSizeRangeCheck( tokens.size(), 10, 11, lineNumber, fileName );

                wxString lineWidth = tokens[1];
                wxString botRightX = tokens[2];
                wxString botRightY = tokens[3];
                wxString topLeftX = tokens[4];
                wxString topRightY = tokens[5];
                wxString arcStartPointX = tokens[6];
                wxString arcStartPointY = tokens[7];
                wxString arcEndPointX = tokens[8];
                wxString arcEndPointY = tokens[9];

                ARC arc;
                arc.LineWidth = getLineWidth( lineWidth );
                arc.BotRight = pointCheck( botRightX, botRightY, lineNumber, fileName );
                arc.TopLeft = pointCheck( topLeftX, topRightY, lineNumber, fileName );
                arc.ArcEnd = pointCheck( arcStartPointX, arcStartPointY, lineNumber, fileName );
                arc.ArcStart = pointCheck( arcEndPointX, arcEndPointY, lineNumber, fileName );

                int lineStyleNumber = 0;  // default

                if( tokens.size() == 11 )
                {
                    wxString lineStyle = tokens[10];
                    lineStyleNumber = integerCheck( lineStyle, lineNumber, fileName );
                }

                arc.LineStyle = getLineStyle( lineStyleNumber );

                ascFile.Arcs.push_back( arc );
                ascFile.BoundingBox.Merge( arc.TopLeft );
                ascFile.BoundingBox.Merge( arc.BotRight );
            }
            else if( element == "IOPIN" )
            {
                tokensSizeRangeCheck( tokens.size(), 4, 4, lineNumber, fileName );

                wxString pinLocationX = tokens[1];
                wxString pinLocationY = tokens[2];
                wxString pinPolarity = tokens[3];

                IOPIN iopin;
                iopin.Location = pointCheck( pinLocationX, pinLocationY, lineNumber, fileName );
                iopin.Polarity = getPolarity( pinPolarity );

                ascFile.Iopins.push_back( iopin );
                ascFile.BoundingBox.Merge( iopin.Location );
            }
            else if( element == "TEXT" )
            {
                aggregateAttributeValue( tokens, 5 );

                tokensSizeRangeCheck( tokens.size(), 6, INT_MAX, lineNumber, fileName );

                wxString positionX = tokens[1];
                wxString positionY = tokens[2];
                wxString justification = tokens[3];
                wxString fontSize = tokens[4];
                wxString value = tokens[5];

                TEXT text;
                text.Offset = pointCheck( positionX, positionY, lineNumber, fileName );
                text.Justification = getTextJustification( justification );
                text.FontSize = integerCheck( fontSize, lineNumber, fileName );

                if( value.StartsWith( wxS( "!" ), &text.Value ) )
                    text.Value.Replace( wxS( "! " ), wxS( "\n" ) );  // replace subsequent ! with \n
                else if( value.StartsWith( wxS( ";" ), &text.Value ) )
                    text.Value.Replace( wxS( "; " ), wxS( "\n" ) );  // replace subsequent ; with \n
                else
                    text.Value = value;

                text.Value.Replace( wxS( "\\n" ), wxS( "\n" ) );

                ascFile.Texts.push_back( text );
                ascFile.BoundingBox.Merge( text.Offset );
            }
            else if( element == "BUSTAP" )
            {
                tokensSizeRangeCheck( tokens.size(), 5, 5, lineNumber, fileName );

                wxString startPointX = tokens[1];
                wxString startPointY = tokens[2];
                wxString endPointX = tokens[3];
                wxString endPointY = tokens[4];

                BUSTAP bustap;
                bustap.Start = pointCheck( startPointX, startPointY, lineNumber, fileName );
                bustap.End = pointCheck( endPointX, endPointY, lineNumber, fileName );

                ascFile.Bustap.push_back( bustap );
                ascFile.BoundingBox.Merge( bustap.Start );
                ascFile.BoundingBox.Merge( bustap.End );
            }
            else if( element == "VERSION" )
            {
                wxString versionNumber = tokens[1];
                ascFile.Version = integerCheck( versionNumber, lineNumber, fileName );
            }

            lineNumber++;
        }

        ascFiles.push_back( ascFile );
    }

    return ascFiles;
}


