#!/usr/bin/ruby
#
#  BuildProject board=... mcu=... -- FILE1 FILE2 FILE3
#
#  AVRsack
#
#  Created by Matthias Neeracher on 11/26/14.
#  Copyright © 2014 Aere Perennius. All rights reserved.
#

require 'fileutils.rb'
require 'rake'

BUILD = {
    'board'   => 'uno',
    'mcu'     => 'atmega328p',
    'f_cpu'   => 16000000,
    'core'    => 'arduino',
    'variant' => 'standard',
    'max_size'=> 32256
}

def parseArguments
    while ARGV.length > 0 do
        param = ARGV.shift
        break if param == '--'
        param =~ /(\S+?)=(\S*)/
        BUILD[$1] = $2
    end
end

def createBuildDirectory
    buildDir     = "build/#{BUILD['board']}"
    sketchDir    = "#{buildDir}/sketch"
    FileUtils::mkdir_p "#{sketchDir}", :verbose => true
    Dir.chdir(buildDir)
end

HEADERMAP  = {}
def buildHeaderMap
    paths       = Rake::FileList.new(BUILD['libs'].split(':').reverse)
    paths.each do |path|
        libs        = Rake::FileList.new(path+"/*")
        libs.each do |lib|
            headers = Rake::FileList.new(lib+"/**/*.h")
            headers.each do |h|
                name    = h[lib.length+1..-1]
                next if name =~ %r|^utility/|
                if !HEADERMAP[name] or HEADERMAP[name].pathmap('%f') != name.pathmap('%X')
                    HEADERMAP[name] = lib
                end
            end
        end
    end
end

LIBRARIES = Rake::FileList[]
CORES     = Rake::FileList[]
SKETCHES  = Rake::FileList[]

def parseInoFiles
    CORES.add(BUILD['core_path'])
    if BUILD['variant_path']
        CORES.add(BUILD['variant_path'])
    end
    ARGV.each_index do |arg|
        inName = ARGV[arg]
        inName = inName.pathmap("../../%p") unless inName =~ %r|^/|
        if inName =~ /\.ino$/
            inDir       = inName.pathmap("%d")
            if !SKETCHES.include?(inDir)
                SKETCHES.add(inDir)
            end
            outName     = inName.pathmap("sketch/%n.cpp")
            outFile     = File.open(outName, 'w')
            File.open(inName, 'r') do |ino|
                contents = ino.read
                # Find protypes:
                prototypes = contents.dup
                # - Strip comments, quoted strings, and preprocessor directives
                prototypes.gsub!(%r{'(?:\\'|[^'])+'|"(?:\\"|[^"])*"|//.*?$|/\*.*?\*/|^\s*?#.*?$}m, ' ')
                # Collapse braces
                while prototypes.sub!(/(\{)([^{}]+|\{[^{}]*\})/m, '\1') do
                end
                existingProto = {}
                prototypes.scan(/[\w\[\]\*]+\s+[&\[\]\*\w\s]+\([&,\[\]\*\w\s]*\)(?=\s*;)/) {|p|
                    existingProto[smashSpaces(p)] = true
                }
                proto = []
                prototypes.scan(/[\w\[\]\*]+\s+[&\[\]\*\w\s]+\([&,\[\]\*\w\s]*\)(?=\s*{)/) {|p|
                    p = smashSpaces(p)
                    proto << p+";\n" unless existingProto[p]
                }
                contents.each_line do |line|
                    if line =~ %r{^\s*#include\s+[<"](.*)[">]\s*(#.*|/\*.*?\*/\s*|//.*)?$}
                        addLibrary($1)
                    end
                end
                %r{(?<preamble>(?:\s+|//.*?$|/\*.*?\*/)*)(?<rest>.*)}m =~ contents
                outFile.print preamble
                outFile.puts '#include "Arduino.h"'
                outFile.print proto.join('')
                outFile.puts "#line #{1+preamble.count("\n")}"
                outFile.print rest
            end
            outFile.close
            FileUtils.touch outName, :mtime => File.mtime(inName)
            ARGV[arg]   = outName
        else
            ARGV[arg]   = inName
        end
    end
end

def smashSpaces(s)
    return s.gsub(/(\W)\s+(\W)/, '\1\2').gsub(/\s+/, ' ')
end
                                 
def addLibrary(header)
    lib = HEADERMAP[header]
    if lib && !LIBRARIES.include?(lib)
        LIBRARIES.add(lib)
    end
end

parseArguments
createBuildDirectory
buildHeaderMap
parseInoFiles

File.open("Rakefile", 'w') do |rakeFile|
    SOURCES = Rake::FileList.new(ARGV).select {|f| f =~ /\.(c|cpp|cp|cxx|S)$/}
    INCLUDES= (SKETCHES+CORES+LIBRARIES).map {|l| " +\n   \" -I'#{l}'\""}.join('')
    rakeFile.print <<END_RAKE
TOOLCHAIN   = "#{BUILD['toolchain']}"
BIN         = TOOLCHAIN+"/bin/"
USB         = '-DUSB_VID=#{BUILD['usb_vid']} -DUSB_PID=#{BUILD['usb_pid']}'
CC          = BIN+'avr-gcc -c -g -Os -Wall -fno-exceptions -ffunction-sections -fdata-sections -mmcu=#{BUILD['mcu']} -DF_CPU=#{BUILD['f_cpu']} -MMD '+USB+' -DARDUINO=105'#{INCLUDES}
CCP         = BIN+'avr-g++ -c -g -Os -Wall -fno-exceptions -ffunction-sections -fdata-sections -mmcu=#{BUILD['mcu']} -DF_CPU=#{BUILD['f_cpu']} -MMD '+USB+' -DARDUINO=105'#{INCLUDES}
LD          = BIN+'avr-g++ -Os -Wl,--gc-sections -mmcu=#{BUILD['mcu']}'
AR          = BIN+'avr-ar crs'
EEP         = BIN+'avr-objcopy -O ihex -j .eeprom --set-section-flags=.eeprom=alloc,load --no-change-warnings --change-section-lma .eeprom=0'
HEX         = BIN+'avr-objcopy -O ihex -R .eeprom'
SIZE        = BIN+'avr-size'

def compile(dest, extrainc, *src)
    directory dest
    archive = dest.ext('.a')
    sources = Rake::FileList.new(src)
    objects = sources.pathmap(dest+"/%n.o")
    objects.each_index do |i|
        file objects[i] => [sources[i],dest] do
             sh "%s %s '%s' -o '%s'" % [sources[i] =~ /.c$/ ? CC : CCP,
                extrainc ? "-I '"+extrainc+"'" : '', sources[i], objects[i]]
        end
    end
    file archive => objects do
        sh ("%s '%s' " % [AR, archive])+objects.map {|o| "'"+o+"'"}.join(" ")
    end
end
                                 
def compile_library(lib)
    extrainc = nil
    utility  = lib+"/utility"
    if File::exists?(utility)
        extrainc = utility
    end
    compile(lib.pathmap('lib/%f'), extrainc, *Rake::FileList[lib+"/*.{c,cpp,cp,cxx,S}", lib+"/utility/*.{c,cpp,cp,cxx,S}"])
end
                            
def compile_core(core,variant=nil)
    list = Rake::FileList[core+"/*.{c,cpp,cp,cxx,S}"]
    list.add(variant+"/*.{c,cpp,cp,cxx,S}") if variant
    compile('core', nil, list)
end

compile('sketch', nil, #{SOURCES.map{|f| "'"+f+"'"}.join(', ')})
compile_core(#{CORES.map {|c| "'"+c+"'"}.join(", ")})
#{LIBRARIES.pathmap("compile_library('%p')\n").join('')}
file '#{BUILD['project']}.elf' => %w[sketch.a #{LIBRARIES.pathmap("lib/%f.a").join(" ")} core.a] do |task|
    sh ("%s -o %s -Wl,'-(' " % [LD, task.name])+task.prerequisites.map {|a| "'"+a+"'"}.join(" ")+" -Wl,'-)' -lm"
end
file '#{BUILD['project']}.eep' => '#{BUILD['project']}.elf' do |task|
    sh "%s '%s' '%s'" % [EEP, task.prerequisites[0], task.name]
end
file '#{BUILD['project']}.hex' => '#{BUILD['project']}.elf' do |task|
    sh "%s '%s' '%s'" % [HEX, task.prerequisites[0], task.name]
end
task :default => ['#{BUILD['project']}.eep', '#{BUILD['project']}.hex'] do
    szCmd = "%s '%s' | tail -1 | awk '{print $4}'" % [SIZE, '#{BUILD['project']}.hex']
    sz = %x{#{'#'}{szCmd}}.chomp
    puts "Binary sketch size: "+sz+" bytes (of a #{BUILD['max_size']} byte maximum)"
end
                                 
END_RAKE
end
                                 
sh 'rake >& ../build.log'