require_relative 'parser'
require_relative 'assembler'
require_relative 'objectwriter'
require 'optparse'
require 'ostruct'

module Asm
  class CommandLine
    def initialize
      options = OpenStruct.new
      options.output_file = "a.out"
      options.target = :arm

      opts = OptionParser.new do |opts|
        opts.banner = "Usage: as [options] <input file>"

        opts.separator ""
        opts.separator "Options:"

        opts.on("-t", "--target TARGET",
                "Specify target architecture (arm [default], ttk91)") { |o|
          options.target = o.to_sym
          if (not [:arm, :ttk91].include?(options.target))
            puts opts
            exit
          end
        }

        opts.on("-o", "--output FILENAME",
                "Specify output filename for object file") { |o|
          options.output_file = o
        }

        opts.on("-s", "--show-ast",
                "Show parse tree") { |o|
          options.show_ast = true
        }

        opts.on_tail("-h", "--help", "Show this message") {
          puts opts
          exit
        }
      end

      opts.parse!(ARGV)

      options.input_file = ARGV.shift
      if (not options.input_file)
        puts opts
        exit
      end

      @options = options
    end
    attr_reader :options

    def run
      begin
        if (options.input_file == '-')
          code = $stdin.read
        else
          code = File.read(options.input_file)
        end
      rescue => err
        puts 'as: could not read input file: ' + err.message
        exit 2
      end

      begin
        ast = Asm::Parser.parse(code)
      rescue Asm::ParseError => err
        puts 'as: parse error on line %d, column %d' % [err.line+1, err.column+1]
        line = code.split("\n")[err.line]
        puts line.gsub(/\s/, ' ')
        puts ' ' * (err.column-1) + '^'
        puts '  ' + err.message
        exit 3
      end

      if (options.show_ast)
        require 'pp'
        pp ast
        exit 0
      end
    
      case options.target
      when :arm
        require_relative 'arm_assembler.rb'
        as_module = Asm::Arm
        as_target = Elf::Constants::TARGET_ARM
      when :ttk91
        require_relative 'ttk91_assembler.rb'
        as_module = Asm::TTK91
        as_target = Elf::Constants::TARGET_TTK91
      end

      asm = Asm::AstAssembler.new(as_module)
      begin
        asm.load_ast ast
        data = StringIO.new
        asm.assemble(data)
        symbols = asm.symbols
      rescue Asm::AssemblyError => err
        if (err.node)
          puts 'as: assembly error on line %d, column %d' % [
            err.node.line+1, err.node.column+1]
          line = code.split("\n")[err.node.line]
          puts line.gsub(/\s/, ' ')
          puts ' ' * (err.node.column-1) + '^'
          puts '  ' + err.message
        else
          puts 'as: ' + err.message
        end
        exit 4
      end

      writer = Asm::ObjectWriter.new(as_target)
      writer.set_text data.string

      reloc_name_ref = {}

      symbols.each { |symbol|
        label = symbol[:label]
        if (label.extern?)
          reloc_name_ref[label] = symbol[:name]
          writer.add_reloc_symbol symbol[:name]
        else
          writer.add_symbol symbol[:name], symbol[:label].address, symbol[:linkage]
        end
      }

      asm.relocations.each { |reloc|
        writer.add_reloc reloc.position, reloc_name_ref[reloc.label], reloc.type
      }

      begin
        writer.save(options.output_file)
      rescue => err
        puts 'as: cannot save output file: ' + err.message
      end
    end
  end
  
end


if (__FILE__ == $0)
  Asm::CommandLine.new.run
end