#!/usr/bin/env ruby

require 'ftools'

class Filename
	def initialize(filename)
		@fn = filename.to_str
		normalize
	end

	def normalize
		if @fn[0] == ?/
			loc = []
		else
			loc = Dir.getwd.split('/')
		end
		norm = []
		up = []
		@fn.split('/').each do |n|
			case n
			when '.'
			when '..'
				if norm.empty? || norm[-1] == '..'
					norm << n
				else
					norm.pop
				end
				up << loc.pop
			else
				if up.empty? || up[-1] != n
					norm << n
					up = []
				else
					norm.pop
					up.pop
				end
			end
		end
		@fn = norm.join('/')
	end

	def join(other)
		if other[0] == ?/ || @fn.empty?
			Filename.new(other)
		else
			Filename.new(@fn.to_s + '/' + other.to_s)
		end
	end

	def path
		Filename.new(@fn.split('/')[0..-2].join('/'))
	end

	def filename
		@fn.split('/')[-1]
	end

	def ext
		t = filename.split('.')
		return '' if t.size < 2
		t[-1]
	end

	def ext?(*arg)
		e = ext
		arg.any? {|c| e == c.to_s}
	end

	def change_ext(new)
		raise("Filename '%s' has no extension" % @fn) if ext == ''
		Filename.new((@fn.split('.')[0..-2] << new.to_s).join('.'))
	end

	def exists?
		begin
			return File.ftype(compl)
		rescue Errno::ENOENT
			return nil
		end
	end

	def file?
		exists? == 'file'
	end

	def dir?
		exists? == 'directory'
	end

	def newer_than?(*arg)
		arg.flatten!
		return false unless exists?
		date = File.mtime(compl)
		arg.all? do |f|
			if f.exists?
				date > File.mtime(f.compl)
			else
				true
			end
		end
	end

	def open(mode = 'r')
		File.open(compl, mode) {|f| yield f}
	end

	def read(mode = 'r')
		open {|f| f.read}
	end

	def read_dir
		Dir.entries(compl).reject{|d| d[0] == ?.}.map {|d| join(d)}
	end

	def compl
		return @fn if absolute?
		'./' + @fn
	end

	def cline
		return '"' + @fn + '"' if @fn.index(' ')
		@fn
	end

	def output_name
		p = path.join('.' + $make.version)
		Dir.mkdir(p.compl) unless p.dir?
		return p.join(filename)
	end

	def remove_output_dir
		Filename.new(@fn.sub(Regexp.new(Regexp.escape('.' + $make.version + '/')), ''))
	end

	def absolute?
		@fn[0] == ?/
	end

	def +(other)
		Filename.new(@fn + other.to_s)
	end

	def ==(other)
		@fn == other.to_s
	end

	def to_s
		@fn
	end

	def to_str
		@fn
	end

	def split_parameters
		n, p = @fn.split('!')
		return self, {} unless p
		h = {}
		p.split(',').each do |param|
			k, v = param.split('=')
			h[k] = v
		end
		return Filename.new(n), h
	end

	def hash
		@fn.hash
	end

	def eql?(other)
		@fn == other.to_str
	end
end

class Make
	attr_reader :target, :include_dirs, :lib_dirs, :libs, :cflags, :version
	
	def initialize
		# default config
		@target = Filename.new('main')
		@compile_dirs = [Filename.new('')]
		@include_dirs = []
		@lib_dirs = []
		@libs = ['m']
		@version = 'odebug'
		@makeables = []
		@compile_objects = []
		@obj_hash = {}
		@src_hash = {}
		@uptodate = true
		@to_delete = []
	end

	def start
		read_makefile

		@version = ARGV[0] unless ARGV.empty?

		if @recursive
			subdirs = []
			@compile_dirs.each  {|d| subdirs += find_subdirs(d) }
			@compile_dirs += subdirs
			@include_dirs += subdirs
		end

		case @version
		when 'release'
			@cflags = '-O2 -Wall -fno-common -ffast-math'
		when 'odebug'
			@cflags = '-O2 -DDEBUG -Wall -fno-common -ffast-math'
			@target += '_odebug'
		when 'debug'
			@cflags = '-O0 -DDEBUG -Wall -fno-common -ffast-math'
			@target += '_debug'
		when 'clean'
			clean :release
			clean :odebug
			clean :debug
			return
		else
			raise("Unknown version '#{@version}'")
		end

		@include_dirs.each {|i| @cflags += ' -I' + i.cline}

		@compile_dirs.each do |dir|
			dir.read_dir.each do |source|
				@makeables.each do |m|
					break if m.can_compile?(source)
				end
			end
		end

		@compile_objects.each {|o| o.load_depends}

		@to_delete.each do |o|
			@obj_hash.delete(o.object)
			@src_hash[o.source].delete(o)
			@compile_objects.delete(o)
		end

		@compile_objects.each {|o| make_object(o)}

		unless @uptodate && @target.exists?
			libs = (@lib_dirs.map {|d| '-L' + d.cline} + @libs.map {|l| '-l'+l}).join(' ')
			objs = @compile_objects.select {|o| o.link?}.map {|o| o.object.cline}.join(' ')
			command("g++ -o %s %s %s" % [@target, objs, libs])
		else
			printf "%s is up to date\n", @target
		end
	end

	def make_object(o)
		return if !o || o.compiled
		o.compiled = true
		o.depends.each {|d| make_object(@obj_hash[d])}
		unless o.object.newer_than?(o.depends)
			o.compile
			@uptodate = false
		end
	end

	def read_makefile
		em = Filename.new('eMakefile')
		if em.file?
			@recursive = false
			file = em.read.split(/(.*:)/)
			file = file.map{|l| l.split(/\n|,/).map{|v| v.strip}.reject{|v| v.empty?}}
			until file.empty?
				case option = file.shift[0]
				when 'dir:'
					file.shift.each do |l|
						@compile_dirs << Filename.new(l)
						@include_dirs << Filename.new(l)
					end
				when 'compile_dir:'
					file.shift.each {|l| @compile_dirs << Filename.new(l)}
				when 'include_dir:'
					file.shift.each {|l| @include_dirs << Filename.new(l)}
				when 'lib_dir:'
					file.shift.each {|l| @lib_dirs << Filename.new(l)}
				when 'libs:'
					file.shift.each {|l| @libs << l}
				when 'recursive:'
					@recursive = file.shift[0] == 'true'
				when 'target:'
					@target = Filename.new(file.shift[0])
				when nil
					# ignore
				else
					printf "Warning: Unknown eMakefile option %s.\n", option.dump
				end
			end
		else
			@recursive = true
		end
	end

	def find_subdirs(dir)
		dirs = dir.read_dir.select {|f| f.dir?}
		subdirs = []
		dirs.each {|d| subdirs += find_subdirs(d)}
		return dirs + subdirs
	end

	def can_create?(obj)
		return @obj_hash[obj.to_str] if @obj_hash.has_key?(obj)
		@makeables.each do |m|
			o = m.can_create?(obj)
			return o if o
		end
		nil
	end

	def register(makeable)
		@makeables << makeable
	end

	def add_compile_object(o)
		return if @obj_hash.has_key?(o.object)
		@obj_hash[o.object] = o
		@compile_objects << o
		if @src_hash.has_key?(o.source)
			@src_hash[o.source] << o
		else
			@src_hash[o.source] = [o]
		end
	end

	def find_objects_from_source(src)
		@src_hash[src] || []
	end

	def delete_object(obj)
		@to_delete << obj
	end

	def clean(version)
		version = version.to_s
		@compile_dirs.each do |d|
			command("rm -rf %s" % [d.join('.' + version).cline])
		end
		command("rm -f %s" % [@target + (version == 'release' ? '' : '_' + version)])
	end
end

class Makeable
	attr_reader :depends, :source, :object
	attr_accessor :compiled

	def initialize(src, obj)
		@source = src
		@object = obj.output_name
		@compiled = false
		@include_dirs = []
		$make.add_compile_object(self)
	end


	def Makeable.inherited(new)
		return if new.name == 'MakeableLink'
		$make.register(new)
	end

	def Makeable.can_compile?(file)
		nil
	end

	def Makeable.can_create?(file)
		nil
	end

	def load_depends
		depname = (@source + '.depend').output_name
		new = true
		if depname.exists?
			@depends = depname.read.map {|f| Filename.new(f.strip)}
			new = false if depname.newer_than?(@depends)
		end
		if new
			@depends = depends_on
			depname.open('w') {|f| f.puts @depends}
		end
		@depends.each do |d|
			d = d.remove_output_dir
			next if d.exists?
			o = $make.can_create?(d)
			name = o.object
			@include_dirs << name.path unless @include_dirs.include?(name.path)
		end
	end

	def depends_on
		[@source]
	end

	def find_depends(incre)
		dep = []
		find_depends_rec(@source, incre, dep)
		dep
	end

	def find_depends_rec(src, incre, dep)
		return if dep.include?(src)
		dep << src
		return unless src.exists?
		src.read.scan(incre) do |i|
			i = i[0]
			found = false
			([Filename.new(''), src.path] + $make.include_dirs).uniq.each do |d|
				name = d.join(i)
				if name.exists?
					find_depends_rec(name, incre, dep)
					found = true
					break
				end
			end

			unless found
				([Filename.new(''), src.path] + $make.include_dirs).uniq.each do |d|
					name = d.join(i)
					if o = $make.can_create?(name)
						name = o.object
						dep << name unless dep.include?(name)
						found = true
						break
					end
				end
			end

			raise("Unable to find include '%s'" % [i]) unless found
		end
	end

	def link?
		false
	end
end

class MakeableLink < Makeable
	def link?
		true
	end
end

def command(c)
	puts c
	unless system c
		puts "Command returned with an error!"
		exit 1
	end
end

$make = Make.new

class MakeC < MakeableLink
	def MakeC.can_compile?(file)
		return nil unless file.ext?(:c, :cpp)
		MakeC.new(file, file.change_ext(:o))
	end

	def depends_on
		find_depends(/#include "([^"]*)"/)
	end

	def compile
		cflags = $make.cflags
		@include_dirs.each {|d| cflags += ' -I' + d.cline}
		command("gcc %s -c %s -o %s" % [cflags, @source, @object])
	end
end

class MakeHFromI < Makeable
	def MakeHFromI.can_create?(file)
		return nil unless file.ext?(:h) && file.change_ext(:i).exists?
		MakeHFromI.new(file.change_ext(:i), file)
	end

	def compile
		printf "Creating %s from %s\n", @object.cline, @source.cline
		@source.open do |i|
			@object.open('w') do |o|
				i.each do |l|
					o.puts(l.sub(/\A(\w+)\s+\.equ\s+(.*)/i, '#define \1 \2').sub(/;/, '//').sub(/H'/, '0x'))
				end
			end
		end
	end
end

def bin2as(name, src, dest)
	src.open('rb') do |i|
		dest.open('w') do |o|
			o.printf("\t.text\n\t.balign 16\n\t.global %s\n.word %s_end - %s, 0, 0, 0\n%s:\n",
					name, name, name, name)
			until i.eof?
				o.printf("\t.byte %s\n",
						i.read(16).split(//).map{|c| "0x%02x" % [c[0]]}.join(', '))
			end
			o.printf("%s_end:\n", name)
		end
	end			
end

class MakeOFromVcl < MakeableLink
	def MakeOFromVcl.can_compile?(file)
		return nil unless file.ext? :vcl
		MakeOFromVcl.new(file, file.change_ext(:o))
	end

	def depends_on
		find_depends(/\.include "([^"]*)"/)
	end

	def compile
		command("vcl -o.tmp -g %s" % [@source])
		command("ee-dvp-as -o .tmp2 .tmp")
		command("objcopy -Obinary .tmp2 .tmp")
		bin2as("%s_vu" % [source.filename.sub(/\.[^.]*\Z/, '')], Filename.new('.tmp'), Filename.new('.tmp2'))
		command("as -KPIC -o %s .tmp2" % [@object])
		command("rm .tmp .tmp2")
	end
end

class MakeVuFromVcl < Makeable
	def MakeVuFromVcl.can_create?(file)
		fname, params = file.split_parameters
		src = fname.change_ext :vcl
		return nil unless fname.ext?(:vu) && src.exists?
		p fname, params
		$make.find_objects_from_source(src).each do |o|
			$make.delete_object(o) if o.is_a? MakeOFromVcl
		end
		MakeVuFromVcl.new(src, file, params)
	end

	def initialize(src, file, params)
		super src, file
		@params = params
	end

	def depends_on
		find_depends(/\.include "([^"]*)"/)
	end

	def compile
		if @params.empty?
			command("vcl -o%s -g %s" % [@object, @source])
		else
			File.open('.tmp', 'w') do |f|
				@params.each_pair do |k, v|
					f.printf "%s .equ %s\n", k, v
				end
				f.printf ".include \"%s\"\n", @source
			end
			command("vcl -o%s -g .tmp" % [@object])
			command("rm .tmp")
		end
	end
end

class MakeOFromVsm < MakeableLink
	def MakeOFromVsm.can_compile?(file)
		return nil unless file.ext? :vsm
		MakeOFromVsm.new(file, file.change_ext(:o))
	end

	def depends_on
		find_depends(/\.include "([^"]*)"/)
	end

	def compile
		command("ee-dvp-as %s -o .tmp2 %s" %
			[@include_dirs.map{|d| '-I' + d.cline}.join(' '), @source])
		command("objcopy -Obinary .tmp2 .tmp")
		bin2as("%s_vu" % [source.filename.sub(/\.[^.]*\Z/, '')], Filename.new('.tmp'), Filename.new('.tmp2'))
		command("as -KPIC -o %s .tmp2" % [@object])
		command("rm .tmp .tmp2")
	end
end

$make.start
