#!/usr/bin/perl -w

#A program to generate a nicely formatted BOM from a TinyCAD BOM.

#--------------------------------------------------------------------------------------
#Field locations of the various part specific description fields in the TinyCAD text formatted BOM
$Quantity_Field = 0;
$PartName_Field = 1;
$PartDescription_Field = 2;

#A map of the fields that will be output
@FieldMap = ($Quantity_Field,$PartName_Field,$PartDescription_Field);

#The DigiKey and Jameco part number fields are determined farther down in the program and will be
#added to the @FieldMap list at that time.

#Starting positions for the outputting of the various BOM fields
# Note: The $PartName and the $PartDescription use the same position value so that
#  the $PartDescription follows immediately after the $PartName text.
@FieldPos = (30,36,36,90,112);

#--------------------------------------------------------------------------------------

#Ask which TinyCAD BOM to process
print "TinyCAD BOM file to process: ";
$InputBOM = <STDIN>;
chomp($InputBOM);

#First determine if the input is a "comma separated value" or a "text" type of BOM file
    if(length($InputBOM) >=5){
        if(rindex(uc($InputBOM),".TXT") == (length($InputBOM) - 4)){
            goto TEXTBOM;
        }
        if(rindex(uc($InputBOM),".CSV") == (length($InputBOM) - 4)){
            goto CSVBOM;
        }
    }
    else{
        die "Invalid file name\!  File must be a \"filename.txt\" or \"filename.csv\" type of file.\n";
    }

#======================================================================================================
TEXTBOM:

#Position of the various data elements in a TinyCAD text formatted BOM file
$StartRefDes = 0;        #The starting position of the Reference Designators
$EndRefDes = 25;         #The ending position of the Reference Designators
$StartDescription = 34;  #The starting position of the Part Description fields

#Open the to be processed TinyCAD text formatted BOM file as Read Only
open(INPUTBOM, '<', $InputBOM) or die "File $InputBOM could not be found!\n";

#Create a name for, and Open that file name for, writing out the new BOM
$OutputFileName = ">" . substr($InputBOM,0,length($InputBOM) - 4) . "_BOM.txt";
open(OUTPUTBOM, $OutputFileName) or die "Couldn't open the BOM_output.txt file for writing.\n";

#Read the file's header line and make sure it is a TinyCAD BOM "text" type of file
$HeaderLine = <INPUTBOM>;
if (substr($HeaderLine, 0, 22) ne '====+  Parts List for '){
    die "File " . \"$InputBOM\" . \"is not a TinyCAD \"text\" BOM file!\n";
}  
#Simplify the header line info presented
print OUTPUTBOM substr($HeaderLine, 0, 22) . substr($HeaderLine, rindex($HeaderLine, "\\")+1) . "\n";
#Setup the column headings
print OUTPUTBOM "Reference_Designator(s)       Qty   Description                                           PN_Digi-Key           PN_Jameco       \n";
print OUTPUTBOM "---------------------------   ---   ---------------------------------------------------   -------------------   ----------------\n";

#Now determine the total number of fields in the BOM by scanning through the rest of the BOM.
#Also, at the same time check for a DigiKey and Jameco part number and capture their field positions.
$NumberOfFields = 0;
$PN_DigiKey_Field = -1;
$PN_Jameco_Field = -1;
while (<INPUTBOM>){
    $BomLine = $_;
    chomp($BomLine);
    if($BomLine eq ""){next};
    if(length($BomLine) < $StartDescription){next};
    $CheckPosition = $StartDescription;
    $NumberOfFieldsInLine = 1;
    while($CheckPosition < length($BomLine)){
        if(substr($BomLine, $CheckPosition, 1) eq ","){$NumberOfFieldsInLine++};

#Here is the check for a DigiKey Part Number (we assume all DigiKey part numbers end in "-ND" (which is not always the case))
        if(($CheckPosition >= ($StartDescription + 2)) && (substr($BomLine, ($CheckPosition - 2), 3) eq "-ND")){
            $PN_DigiKey_Field = $NumberOfFieldsInLine;
        }
#Here is the check for a Jameco Part Number (Jameco part numbers are 10000 to 9999999)
        if(($CheckPosition >= ($StartDescription + 4)) && (substr($BomLine, ($CheckPosition - 4), 5) =~ /\d{5}/) && ($NumberOfFieldsInLine != ($PN_DigiKey_Field || Quantity_Field || PartName_Field || PartDescription_Field))){
            $PN_Jameco_Field = $NumberOfFieldsInLine;
        }
        if(($CheckPosition >= ($StartDescription + 5)) && (substr($BomLine, ($CheckPosition - 5), 6) =~ /\d{6}/) && ($NumberOfFieldsInLine != ($PN_DigiKey_Field || Quantity_Field || PartName_Field || PartDescription_Field))){
            $PN_Jameco_Field = $NumberOfFieldsInLine;
        }
        if(($CheckPosition >= ($StartDescription + 6)) && (substr($BomLine, ($CheckPosition - 6), 7) =~ /\d{7}/) && ($NumberOfFieldsInLine != ($PN_DigiKey_Field || Quantity_Field || PartName_Field || PartDescription_Field))){
            $PN_Jameco_Field = $NumberOfFieldsInLine;
        }

        $CheckPosition++;
    }
    if($NumberOfFields < $NumberOfFieldsInLine){$NumberOfFields = $NumberOfFieldsInLine};
}
push(@FieldMap, $PN_DigiKey_Field);
push(@FieldMap, $PN_Jameco_Field);

#Now, close the BOM and reopen it for actually processing the BOM elements
close(INPUTBOM);
open(INPUTBOM, '<', $InputBOM);
#And, flush/ignore the header line in the BOM
$DummyRead = <INPUTBOM>;
$DummyRead = $DummyRead; #done just to eliminate "only used once" compile error

#Now process each individual BOM line in the file
@RefDes = ();
%BomHash = ();
while (<INPUTBOM>){
    $BomLine = $_;
    chomp($BomLine);
    if($BomLine eq ""){
        print OUTPUTBOM "\n";
        next;
    }
#If the line only contains reference designators make sure it has a comma at the end of it
    if((length($BomLine) <= ($EndRefDes + 1)) && (rindex($BomLine,",") != (length($BomLine) - 1))){
        $BomLine = $BomLine . ",";
    }

#Gather up all of the this particular part's reference designators
    if((length($BomLine) <= ($EndRefDes + 1)) && (rindex($BomLine,",") == (length($BomLine) - 1))){
        push(@RefDes, split(/,/,$BomLine));
        next;
    }
    else{
        push(@RefDes, split(/,/,substr($BomLine,$StartRefDes,($EndRefDes + 1))));
    }

#Cleanup the last reference designator element read (remove any trailing space characters)
    $RefDes[$#RefDes] = substr($RefDes[$#RefDes],0,index($RefDes[$#RefDes]," "));

#Put the reference designators in accending order for this particular part type
    @RefDes = sort @RefDes;
    @RefDes = sort{length($a)<=>length($b)} @RefDes;

#Get the remaining part info fields
    @PartInfo = split(/,/,substr($BomLine,$StartDescription));
#Normalize the number of part info fields by padding shortages
    while (($#PartInfo +1) < $NumberOfFields){
      push(@PartInfo, "");
    }

#Attach the count of the RefDes field for later usage... it will be stripped off later
    push(@PartInfo, $#RefDes + 1);

#Convert the list to a string
    $ABomLine = join(',',@RefDes) . "," . join(',',@PartInfo);

#Now add the ABomLine as a record to the hash using a modified first reference designator as the key
    @dummy = split(/,/,$ABomLine);
    while (length($dummy[0]) < 10){
        $dummy[0] =~ s/(\d+)/"0" . $1 /e;
    }
    $BomHash{$dummy[0]} = $ABomLine;

    @RefDes = ();
    @PartInfo = ();
}

#Sort the BOM
@AllKeys = sort(keys(%BomHash));

#Get ready to print a BOM line
foreach $TheKey(@AllKeys){
    @ThisLine = split(/,/,$BomHash{$TheKey});
    $RefDesCount = pop(@ThisLine);
    for($i = 0; $i < $RefDesCount; $i++){
        push(@RefDes, shift(@ThisLine));
    }
#Now that the reference designators are removed from @ThisLine, put the
# part's Quantity at the beginning of the @ThisLine list
    if ($RefDesCount < 10){$RefDesCount = " " . $RefDesCount};
    if ($RefDesCount < 100){$RefDesCount = " " . $RefDesCount};
#   if ($RefDesCount < 1000){$RefDesCount = " " . $RefDesCount};
    unshift(@ThisLine, $RefDesCount);

#Now grab the selected fields to be output and also pad with needed spaces
    $fieldposition = $FieldPos[0];
    $ThePartsInfo = "";
    for ($i = 0; $i <= $#FieldMap; $i++){
        while($fieldposition < $FieldPos[$i]){
            $ThePartsInfo = $ThePartsInfo . " ";
            $fieldposition++;
        }
        $ThePartsInfo = $ThePartsInfo . $ThisLine[$FieldMap[$i]] . " ";
        $fieldposition = $fieldposition + length($ThisLine[$FieldMap[$i]]) + 1;
    }

#And, output the BOM line
    $CharCount = 0;
    $RefDesLine = "";
    while($#RefDes > -1){
        $TheDes = shift(@RefDes);
        if(($#RefDes != -1) && (($CharCount + length($TheDes) + 1) < ($FieldPos[0] - 3))){
            $RefDesLine = $RefDesLine . $TheDes . ",";
            $CharCount = $CharCount + length($TheDes) + 1;
            next;
        }
        if(($#RefDes == -1) && (($CharCount + length($TheDes)) < ($FieldPos[0] - 3))){
            $RefDesLine = $RefDesLine . $TheDes;
            while(length($RefDesLine) < ($FieldPos[0])){
                $RefDesLine = $RefDesLine . " ";
            }
            print OUTPUTBOM "$RefDesLine" . "$ThePartsInfo" . "\n";
            next;
        }
        if(($#RefDes != -1) && (($CharCount + length($TheDes) + 1) >= ($FieldPos[0] - 3))){
            while(length($RefDesLine) < ($FieldPos[0])){
                $RefDesLine = $RefDesLine . " ";
            }
            print OUTPUTBOM "$RefDesLine" . "$ThePartsInfo" . "\n";
            $ThePartsInfo = "";
            $RefDesLine = $TheDes . ",";
            $CharCount = length($TheDes) + 1;
            next;
        }
        if(($#RefDes == -1) && (($CharCount + length($TheDes)) >= ($FieldPos[0] - 3))){
            while(length($RefDesLine) < ($FieldPos[0])){
                $RefDesLine = $RefDesLine . " ";
            }
            print OUTPUTBOM "$RefDesLine" . "$ThePartsInfo" . "\n";
            $RefDesLine = $TheDes;
            print OUTPUTBOM "$RefDesLine" . "\n";
            next;
        }

    }
}
print OUTPUTBOM "\n\n" . substr($HeaderLine, 0, 7) . "End of " . substr($HeaderLine, 7, 15) .
    substr($HeaderLine, rindex($HeaderLine, "\\")+1) . "\n";
goto LETSQUIT;




#======================================================================================================
CSVBOM:

#Open the to be processed comma separated value (CSV) BOM file as Read Only
open(INPUTBOM, '<', $InputBOM) or die "File $InputBOM could not be found!\n";

#Create a name for, and Open that file name for, writing out the new text formatted BOM
$OutputFileName = ">" . substr($InputBOM,0,length($InputBOM) - 4) . "_BOM.txt";
open(OUTPUTBOM, $OutputFileName) or die "Couldn't open the " . substr($OutputFileName,1,length($OutputFileName)) . " file for writing.\n";

#Read the file's header line to make sure it is a TinyCAD BOM -and- whether it is a "comma separated value"
# type of file or if it is a "semicolon separated value" type of file
$HeaderLine = <INPUTBOM>;
chomp ($HeaderLine);
if ((substr($HeaderLine,0,36) ne "Reference,Quantity,Name,Description,") && (substr($HeaderLine,0,36) ne "Reference;Quantity;Name;Description;")){
        die "File " . \"$InputBOM\" . \"is not a TinyCAD \"comma separated value\" BOM file!\n";
    }
if (substr($HeaderLine,0,36) eq "Reference,Quantity,Name,Description,"){$CSV_TYPE = "COMMA"};
if (substr($HeaderLine,0,36) eq "Reference;Quantity;Name;Description;"){$CSV_TYPE = "SEMICOLON"};

#Determine which fields contain the DigiKey and Jameco part numbers by scanning through the HeaderLine
$FieldNumber = -1;
$PN_DigiKey_Field = -1;
$PN_Jameco_Field = -1;
$CheckPosition = 0;
while($CheckPosition < length($HeaderLine)){
    if(($CSV_TYPE eq "COMMA") && (substr($HeaderLine, $CheckPosition, 1) eq ",")){$FieldNumber++};
    if(($CSV_TYPE eq "SEMICOLON") && (substr($HeaderLine, $CheckPosition, 1) eq ";")){$FieldNumber++};
    if(substr($HeaderLine, $CheckPosition, 10) eq "PN_DigiKey"){$PN_DigiKey_Field = $FieldNumber};
    if(substr($HeaderLine, $CheckPosition, 9) eq "PN_Jameco"){$PN_Jameco_Field = $FieldNumber};
    $CheckPosition++;
}
$NumberOfFieldsInLine = $FieldNumber + 1;
push(@FieldMap, $PN_DigiKey_Field);
push(@FieldMap, $PN_Jameco_Field);

#Print out a title line for the BOM
print OUTPUTBOM "====+  Parts List for " . substr($InputBOM,0,length($InputBOM) -4) . ".dsn  +====\n\n";

#Setup the column headings
print OUTPUTBOM "Reference_Designator(s)       Qty   Description                                           PN_Digi-Key           PN_Jameco       \n";
print OUTPUTBOM "---------------------------   ---   ---------------------------------------------------   -------------------   ----------------\n\n";

#Now process each individual BOM line in the file
@RefDes = ();
%BomHash = ();
while (<INPUTBOM>){
    $BomLine = $_;
    chomp($BomLine);
    if($BomLine eq ""){
        print OUTPUTBOM "\n";
        next;
    }
#Gather up all of the this particular part's reference designators
    if($CSV_TYPE eq "COMMA"){$StartRefDes = index($BomLine, "\"", 0) + 1};
    if($CSV_TYPE eq "SEMICOLON"){$StartRefDes = 0};
    if($CSV_TYPE eq "COMMA"){$EndRefDes = index($BomLine, "\"", $StartRefDes) - 1};
    if($CSV_TYPE eq "SEMICOLON"){$EndRefDes = index($BomLine, "\;", $StartRefDes)};

    if(($StartRefDes == $EndRefDes) || ($StartRefDes - 1 == $EndRefDes + 1)){
        next;
    }
    else{
        push(@RefDes, split(/,/,substr($BomLine,$StartRefDes,$EndRefDes)));
    }

#Put the reference designators in accending order for this particular part type
    @RefDes = sort @RefDes;
    @RefDes = sort{length($a)<=>length($b)} @RefDes;

#Get the remaining part info fields
    if($CSV_TYPE eq "COMMA"){@PartInfo = split(/,/,substr($BomLine,$EndRefDes + 3))};
    if($CSV_TYPE eq "SEMICOLON"){@PartInfo = split(/;/,substr($BomLine,$EndRefDes + 2))};
    shift(@PartInfo);

#Normalize the number of part info fields by padding shortages
    while ($#PartInfo < $NumberOfFieldsInLine){
      push(@PartInfo, "");
    }

#Attach the count of the RefDes field for later usage... it will be stripped off later
    push(@PartInfo, $#RefDes + 1);

#Convert the list to a string
    $ABomLine = join(',',@RefDes) . "," . join(',',@PartInfo);

#Now add the ABomLine as a record to the hash using a modified first reference designator as the key
    @dummy = split(/,/,$ABomLine);
    while (length($dummy[0]) < 10){
        $dummy[0] =~ s/(\d+)/"0" . $1 /e;
    }
    $BomHash{$dummy[0]} = $ABomLine;

    @RefDes = ();
    @PartInfo = ();
}

#Sort the BOM
@AllKeys = sort(keys(%BomHash));

#Get ready to print a BOM line
foreach $TheKey(@AllKeys){
    @ThisLine = split(/,/,$BomHash{$TheKey});
    $RefDesCount = pop(@ThisLine);
    for($i = 0; $i < $RefDesCount; $i++){
        push(@RefDes, shift(@ThisLine));
    }
#Now that the reference designators are removed from @ThisLine, put the
# part's Quantity at the beginning of the @ThisLine list
    if ($RefDesCount < 10){$RefDesCount = " " . $RefDesCount};
    if ($RefDesCount < 100){$RefDesCount = " " . $RefDesCount};
#   if ($RefDesCount < 1000){$RefDesCount = " " . $RefDesCount};
    unshift(@ThisLine, $RefDesCount);

#Now grab the selected fields to be output
    $fieldposition = $FieldPos[0];
    $ThePartsInfo = "";
    for ($i = 0; $i <= $#FieldMap; $i++){
        while($fieldposition < $FieldPos[$i]){
            $ThePartsInfo = $ThePartsInfo . " ";
            $fieldposition++;
        }
        $ThePartsInfo = $ThePartsInfo . $ThisLine[$FieldMap[$i]] . " ";
        $fieldposition = $fieldposition + length($ThisLine[$FieldMap[$i]]) + 1;
    }

#And, output the BOM line
    $CharCount = 0;
    $RefDesLine = "";
    while($#RefDes > -1){
        $TheDes = shift(@RefDes);
        if(($#RefDes != -1) && (($CharCount + length($TheDes) + 1) < ($FieldPos[0] - 3))){
            $RefDesLine = $RefDesLine . $TheDes . ",";
            $CharCount = $CharCount + length($TheDes) + 1;
            next;
        }
        if(($#RefDes == -1) && (($CharCount + length($TheDes)) < ($FieldPos[0] - 3))){
            $RefDesLine = $RefDesLine . $TheDes;
            while(length($RefDesLine) < ($FieldPos[0])){
                $RefDesLine = $RefDesLine . " ";
            }
            print OUTPUTBOM "$RefDesLine" . "$ThePartsInfo" . "\n";
            next;
        }
        if(($#RefDes != -1) && (($CharCount + length($TheDes) + 1) >= ($FieldPos[0] - 3))){
            while(length($RefDesLine) < ($FieldPos[0])){
                $RefDesLine = $RefDesLine . " ";
            }
            print OUTPUTBOM "$RefDesLine" . "$ThePartsInfo" . "\n";
            $ThePartsInfo = "";
            $RefDesLine = $TheDes . ",";
            $CharCount = length($TheDes) + 1;
            next;
        }
        if(($#RefDes == -1) && (($CharCount + length($TheDes)) >= ($FieldPos[0] - 3))){
            while(length($RefDesLine) < ($FieldPos[0])){
                $RefDesLine = $RefDesLine . " ";
            }
            print OUTPUTBOM "$RefDesLine" . "$ThePartsInfo" . "\n";
            $RefDesLine = $TheDes;
            print OUTPUTBOM "$RefDesLine" . "\n";
            next;
        }

    }
}
print OUTPUTBOM "\n\n====+  End of Parts List for " . substr($InputBOM,0,length($InputBOM) -4) .
    ".dsn  +====\n\n";
goto LETSQUIT;



#======================================================================================================
LETSQUIT:
close(INPUTBOM);

print "BOM conversion completed!\n";

